Repository navigation
Expand file tree
/
Copy pathbot.py
More file actions
132 lines (123 loc) · 7.13 KB
/
Copy pathbot.py
File metadata and controls
132 lines (123 loc) · 7.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# A starter robot for rules v2, on the Clunkyard Python binding (the
# clunkyard package, which the arena provides). It sweeps a medium scan to
# find a robot, or points it where a passive contact was heard, narrows the
# scan to hold it, leads it with shots whose power suits the range, fires
# only when the turret is on target, keeps battery back for driving and
# keeps out of the storm.
#
# The trade-offs (self.gun, self.scanner and self.battery have the numbers):
# scan a narrow arc reaches further; a wide one finds robots nearby
# power heavy shots hit harder for less battery, but fly slowly
# battery shots and driving share it, and it refills slowly
# turret it turns at a limited rate; self.gun.aim_now is where it points now
# cues passive contacts (a ping, a scan beam, a shot heard) say which
# way to look, not where to shoot: they have no id and a noisy
# bearing, so self.sensors.nearest leaves them out
# Clunkyard robot API 2.0.0: one core under every language, the same
# answers everywhere. Facts (a call shows its arguments):
# match: mode ruleset control robots is_3d tick_rate max_ticks
# robot_id(slot)
# clock: tick seconds remaining_ticks
# arena: size gravity terrain_kind ground_height(x y)
# me: health position velocity heading pitch roll angular_velocity
# attitude grounded altitude applied
# battery: charge capacity regen sustainable_drive compute
# drive_cost(fraction)
# drive: hover_lift
# gun: aim_now cooldown_ticks spread max_power damage_scale
# can_fire(power reserve) shot_speed(power) shot_cost(power)
# reload_ticks(power) shot_damage(power) power_for_speed(speed)
# power_for_flight_time(range seconds) lead(contact power)
# lead_point(position velocity power)
# scanner: last proximity_range reach(arc) arc_for_reach(range)
# sensors: contacts nearest cues contact(slot) position_of(contact)
# tracker: cue nearest_bearing_rate velocity(slot) position(slot)
# last_seen(slot)
# nav: zone zone.margin(point) zone.contains(point margin)
# zone.distance_from(point) storm storm.margin(point)
# storm.contains(point margin) storm.distance_from(point) hill
# hill.margin(point) hill.contains(point margin)
# hill.distance_from(point) pickups nearest_pickup(kind)
# field_distance(a b)
# score: me best armour
# angles: normalize(degrees) angle_diff(target current)
# bearing_to(from to) elevation_to(from to) direction(heading pitch)
# distance(a b) clamp(value low high)
# raw: number(path) text(path) has(path) flag(path) count(path) line()
# Controls: drive.set(throttle turn pitch_turn)
# drive.forces(thrust strafe lift yaw_torque pitch_torque roll_torque brake)
# gun.aim(heading pitch) gun.fire(power) scanner.set(heading arc pitch)
# tracker.configure(smoothing forget_ticks cue_ticks)
# status.set(text value) raw.extra(json)
# Every control argument is optional; a second call keeps what the first
# set. Absent facts are your language's none. Names are snake_case here;
# your language cases them its own way. Angles are degrees from the +x
# axis; pitch is positive up.
# Manual: https://clunkyard.com/manual#sdk
import math
import clunkyard as cy
SEARCH_TURN = 0.33 # enough to turn against the brake in the force rulesets
def steer(towards, current):
"""A turn or pitch-turn control towards an angle, full at 45 degrees off."""
return cy.angles.clamp(cy.angles.angle_diff(towards, current) / 45)
class Starter(cy.Robot):
def on_start(self):
# Flying free, robots may be above or below.
self.free = self.match.is_3d and self.arena.gravity.z == 0
def on_turn(self):
me, gun, scanner = self.me, self.gun, self.scanner
self.drive.forces(brake=0.2)
target = self.sensors.nearest
cue = self.tracker.cue # the freshest passive contact, for a few seconds
if target:
throttle = 0.7 if target.range > 350 else -0.4 if target.range < 200 else 0.2
self.drive.set(throttle=throttle, turn=steer(target.bearing, me.heading),
pitch_turn=steer(target.elevation, me.pitch))
scanner.set(heading=target.bearing, arc=20, pitch=target.elevation) # narrow: holds it, reaching about 735
# The heaviest shot that still covers the range in about a second,
# aimed where the target will be, lobbed under gravity.
power = gun.power_for_flight_time(target.range, 1)
aim = gun.lead(target, power)
heading, pitch = (aim.heading, aim.pitch) if aim else (target.bearing, target.elevation)
gun.aim(heading, pitch)
now = gun.aim_now
on_target = abs(cy.angles.angle_diff(heading, now.heading)) < 3 and abs(pitch - now.pitch) < 3
if on_target and gun.can_fire(power, 10):
gun.fire(power)
self.status.set("TRACK", target.range)
elif cue:
# Head for where it was heard, sweeping an arc that still reaches
# it across the bearing's error (and, flying free, the elevation's).
bearing = cy.angles.bearing_to(me.position, cue.position)
distance = cy.angles.distance(me.position, cue.position)
elevation = cy.angles.elevation_to(me.position, cue.position) if self.free else 0
error = min(cue.bearing_error, 30)
tick = self.clock.tick
across = (tick % 5 - 2) / 2 * error
up = ((tick // 5) % 3 - 1) * error / 2 if self.free else 0
self.drive.set(throttle=0.7, turn=steer(bearing, me.heading), pitch_turn=steer(elevation, me.pitch))
scanner.set(heading=bearing + across, arc=scanner.arc_for_reach(distance + 100), pitch=elevation + up)
self.status.set("CUE", distance)
else:
self.drive.set(throttle=0.5, turn=SEARCH_TURN, pitch_turn=steer(0, me.pitch))
# Sweeps the circle in half a second, flying free at three heights in turn.
pitch = ((self.clock.tick // 10) % 3 - 1) * 35 if self.free else 0
scanner.set(heading=cy.angles.normalize(scanner.last.heading + 36), arc=40, pitch=pitch)
self.status.set("SEARCH")
# Low battery: drive gently so the shots can still be paid for.
if self.battery.charge < self.battery.capacity / 4:
self.drive.set(throttle=self.drive.get("throttle") * 0.5)
# The storm hurts inside: near its edge, drive straight away from its centre.
storm = self.nav.storm
if storm and storm.margin(me.position) > -60:
away = cy.angles.bearing_to(storm.center, me.position)
self.drive.set(throttle=0.8, turn=cy.angles.clamp(cy.angles.angle_diff(away, me.heading) / 30))
# Sudden death: head back inside the safe zone.
zone = self.nav.zone
if zone and zone.margin(me.position) < 60:
climb = cy.angles.elevation_to(me.position, zone.center) if zone.shape == "sphere" else 0
self.drive.set(throttle=0.8,
turn=cy.angles.clamp(cy.angles.angle_diff(cy.angles.bearing_to(me.position, zone.center), me.heading) / 30),
pitch_turn=cy.angles.clamp((climb - me.pitch) / 30))
if __name__ == "__main__":
cy.run(Starter())