▚ BATTLE CODE ARENA // BCAS REFERENCE PLAY ▶

Hitting a moving target

Missiles are unguided and take time to arrive. Aim straight at a moving opponent and it has moved on by the time the missile gets there. Aim where it will be instead.

Why aiming straight at it misses

A basic missile flies at 140 metres a second. At 140 metres that's a second in the air, and a vehicle crossing your path at 15 metres a second has moved 15 metres by then: a clean miss. The fix is called leading the target, and it needs two things: how fast the target is moving, and how long the missile will take.

Measuring the target's speed

A contact gives the target's position (contact.x, contact.y) but not its velocity. Work it out yourself: remember where it was, and a moment later divide how far it went by the time it took. memory keeps the mark between updates.

Speed from two sightings
if memory.mark_t == None or state.time - memory.mark_t > 1.5:
    # No mark yet, or an old one: start again from here.
    memory.mark_x = contact.x
    memory.mark_y = contact.y
    memory.mark_t = state.time
    memory.vx = 0
    memory.vy = 0
elif state.time - memory.mark_t >= 0.5:
    passed = state.time - memory.mark_t
    memory.vx = (contact.x - memory.mark_x) / passed
    memory.vy = (contact.y - memory.mark_y) / passed
    memory.mark_x = contact.x
    memory.mark_y = contact.y
    memory.mark_t = state.time

Half a second between sightings smooths out the radar's rounding (a basic radar reports range to the nearest few metres). A mark more than 1.5 seconds old means the target was lost for a while, so it starts afresh rather than trusting a stale speed.

Where it will be

The missile's flight time is roughly the range divided by its speed. Move the target on by its velocity for that long, and aim at that point with bearing_to():

The lead
flight = contact.range / 140
aim = bearing_to(state.x, state.y, contact.x + memory.vx * flight, contact.y + memory.vy * flight)
missiles.aim(aim)
Other launchers fly at other speeds. 140 is the basic launcher's. Check yours on the equipment page, and change the number to match.

Fire when the turret is there

The turret turns at a limited rate, so aim every update and fire only once it has come round. A few degrees of tolerance is enough at most ranges; firing from too far away wastes missiles on a target that has time to turn.

Only good shots
on_target = abs(angle_diff(aim, missiles.turret())) <= 4

if on_target and contact.range < 170 and missiles.ready():
    missiles.fire()

The whole program

leading.bcas
from vehicle import drive, radar, missiles

def update(state, memory):
    if memory.sweep == None:
        memory.sweep = 0

    contact = radar.contact()

    if contact and state.time - contact.seen_at <= 0.2:
        radar.scan(contact.bearing)

        # The target's speed, from where it was half a second ago.
        if memory.mark_t == None or state.time - memory.mark_t > 1.5:
            memory.mark_x = contact.x
            memory.mark_y = contact.y
            memory.mark_t = state.time
            memory.vx = 0
            memory.vy = 0
        elif state.time - memory.mark_t >= 0.5:
            passed = state.time - memory.mark_t
            memory.vx = (contact.x - memory.mark_x) / passed
            memory.vy = (contact.y - memory.mark_y) / passed
            memory.mark_x = contact.x
            memory.mark_y = contact.y
            memory.mark_t = state.time

        # Aim where it will be when the missile gets there.
        flight = contact.range / 140
        aim = bearing_to(state.x, state.y, contact.x + memory.vx * flight, contact.y + memory.vy * flight)
        missiles.aim(aim)

        # Close in at an angle, which makes this vehicle harder to hit too.
        course = contact.bearing + 35
        turn = clamp(angle_diff(course, state.heading) / 45, -1, 1)
        drive.set(throttle=1, turn=turn)

        on_target = abs(angle_diff(aim, missiles.turret())) <= 4

        if on_target and contact.range < 170 and missiles.ready():
            missiles.fire()
    else:
        memory.sweep = wrap(memory.sweep + 15)
        radar.scan(memory.sweep)
        drive.set(throttle=0.5, turn=0.2)

Approaching at an angle (contact.bearing + 35) rather than head on matters as much as the lead: a vehicle coming straight at its opponent is the easiest target there is.

Going further