▚ BATTLE CODE ARENA // BCAS REFERENCE PLAY ▶

Missiles, energy and repairs

A launcher holds only a few missiles, energy only comes back at a charging point, and a damaged hull is only patched at a repair depot. A vehicle that can find and use them keeps fighting long after one that can't.

What's in the arena

Supply crates2 in each arena. stopped within 8 m of a stocked crate, for 2 s. A crate restocks after 20 s.
Charging points4 in each arena. 80 energy / s, stopped within 6 m of one.
Repair depots2 in each arena. 25 % of full health over 4 s, stopped within 6 m of a stocked depot. A depot restocks after 30 s.

Your program can't see them in state. It finds them with the radar.

Finding things with the echo

Every scan reports what it ran into along its bearing, apart from the opponent: radar.echo(). An echo has x and y, and says what it was: wall, supply, charger or repair (see the echo record). It comes free with every scan, so a sweeping radar maps the arena as it goes. Keep the nearest of each kind in memory:

Remembering the nearest crate
echo = radar.echo()

if echo and echo.supply:
    if memory.crate_x == None or echo.range < memory.crate_range:
        memory.crate_x = echo.x
        memory.crate_y = echo.y
        memory.crate_range = echo.range

(memory.crate_range is the range when it was seen; the full program below compares with how far away the remembered crate is now.)

Getting there, and stopping

Crates, charging points and depots all work the same way: drive to it, stop within reach, then ask. A vehicle still rolling doesn't count as stopped, so ease off as you arrive and wait until state.speed is nearly nothing.

Driving to a point and stopping on it
dx = memory.crate_x - state.x
dy = memory.crate_y - state.y
near = dx * dx + dy * dy

if near < 5 * 5:
    drive.set(throttle=0, turn=0)

    if abs(state.speed) < 1:
        missiles.reload()
else:
    course = bearing_to(state.x, state.y, memory.crate_x, memory.crate_y)
    turn = clamp(angle_diff(course, state.heading) / 45, -1, 1)
    throttle = 1

    if near < 30 * 30:
        throttle = 0.4

    drive.set(throttle=throttle, turn=turn)

Comparing squared distances (dx * dx + dy * dy against 5 * 5) avoids needing a square root, which BCAS doesn't have. The same pattern works for charging, with drive.charge(), and repairs, with hull.repair().

Someone else may get there first. A crate emptied by the opponent stays empty until it restocks. If reloading hasn't worked after a few seconds there, forget it and look for another.

When to go

The whole program

It fights as the earlier tutorials do, maps the arena with every scan, and goes for missiles, energy or repairs when it needs them. The trips come last, so they win over fighting.

supplied.bcas
from vehicle import drive, radar, missiles, shield, hull

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

    course = state.heading
    throttle = 0.5
    shield.set_level(0)
    contact = radar.contact()

    if contact and state.time - contact.seen_at <= 0.2:
        radar.scan(contact.bearing)
        missiles.aim(contact.bearing)
        course = contact.bearing + 35
        throttle = 1

        if contact.range < 200 and state.energy > 30:
            shield.set_level(0.5)

        on_target = abs(angle_diff(contact.bearing, missiles.turret())) <= 3

        if on_target and contact.range < 170 and missiles.ready():
            missiles.fire()
    else:
        memory.sweep = wrap(memory.sweep + 15)
        radar.scan(memory.sweep)

    turn = clamp(angle_diff(course, state.heading) / 45, -1, 1)
    drive.set(throttle=throttle, turn=turn)

    # Map the arena: every scan's echo, the nearest of each kind kept.
    echo = radar.echo()

    if echo and echo.supply:
        memory.crate_x = echo.x
        memory.crate_y = echo.y

    if echo and echo.charger:
        memory.charger_x = echo.x
        memory.charger_y = echo.y

    if echo and echo.repair:
        memory.depot_x = echo.x
        memory.depot_y = echo.y

    # What it needs most, if anything: energy, then missiles, then repairs.
    goal = None

    if state.energy < state.energy_max * 0.3 and memory.charger_x != None:
        goal = "charge"
        gx = memory.charger_x
        gy = memory.charger_y
    elif missiles.ammo() == 0 and memory.crate_x != None:
        goal = "reload"
        gx = memory.crate_x
        gy = memory.crate_y
    elif state.health < 40 and memory.depot_x != None and not contact:
        goal = "repair"
        gx = memory.depot_x
        gy = memory.depot_y

    if goal != None:
        shield.set_level(0)
        dx = gx - state.x
        dy = gy - state.y
        near = dx * dx + dy * dy

        if near < 5 * 5:
            drive.set(throttle=0, turn=0)

            if abs(state.speed) < 1:
                if goal == "charge":
                    drive.charge()
                elif goal == "reload":
                    missiles.reload()
                else:
                    hull.repair()
        else:
            course = bearing_to(state.x, state.y, gx, gy)
            turn = clamp(angle_diff(course, state.heading) / 45, -1, 1)
            throttle = 1

            if near < 30 * 30:
                throttle = 0.4

            drive.set(throttle=throttle, turn=turn)

Two simplifications to improve on: it remembers the last of each kind it saw rather than the nearest, and it stays on an empty crate for ever. The Starter's program does both properly.