▚ BATTLE CODE ARENA // BCAS REFERENCE PLAY ▶

Memory, timers and modes

BCAS has no loops: your program runs once an update and starts afresh each time. Anything that takes longer than an update, like waiting, counting or a plan with steps, lives in memory.

What memory is

memory is a record you write fields onto. A field you set stays set for the rest of the battle; one you've never set reads as None. Local variables, by contrast, are gone when update ends.

Setting up on the first update
if memory.started == None:
    memory.started = state.time
    memory.shots = 0

A field holds a number, a string, True, False or None. There are limits: 64 fields and 16 KiB in all. A record such as a contact can't be kept in memory; keep the numbers you need from it.

Timers

state.time is the battle clock, in seconds. To do something for a while, remember when it should stop; to do it every so often, remember when it last happened.

For a while, and every so often
# Reverse for 0.7 seconds after running into something.
if state.blocked:
    memory.back_until = state.time + 0.7

if memory.back_until != None and state.time < memory.back_until:
    drive.set(throttle=-1, turn=0.5)

# Log the energy once every five seconds.
if memory.logged == None or state.time - memory.logged >= 5:
    memory.logged = state.time
    debug.log(state.energy)

Counting

Where another language would loop, BCAS counts across updates. To sweep the radar all the way round once, a step an update:

A count across updates
if memory.step == None:
    memory.step = 0

if memory.step < 24:
    radar.scan(memory.step * 15)
    memory.step = memory.step + 1

Modes

As a program grows, it helps to give it modes: one thing it's doing at a time, with clear rules for changing. Keep the mode's name in memory as a string, decide whether to change it, then act on it.

modes.bcas
from vehicle import drive, radar, missiles, debug

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

    contact = radar.contact()
    seen = contact and state.time - contact.seen_at <= 0.2

    # Change mode: each rule says when, and logs it for the replay.
    if memory.mode == "search" and seen:
        memory.mode = "attack"
        debug.log("Found it: attacking")
    elif memory.mode == "attack" and not seen:
        memory.mode = "search"
        debug.log("Lost it: searching")
    elif memory.mode == "attack" and missiles.ammo() == 0:
        memory.mode = "retreat"
        memory.retreat_until = state.time + 4
        debug.log("Out of missiles: falling back")
    elif memory.mode == "retreat" and state.time >= memory.retreat_until:
        memory.mode = "search"

    # Act on the mode.
    if memory.mode == "search":
        memory.sweep = wrap(memory.sweep + 15)
        radar.scan(memory.sweep)
        drive.set(throttle=0.5, turn=0.2)
    elif memory.mode == "attack":
        radar.scan(contact.bearing)
        missiles.aim(contact.bearing)
        turn = clamp(angle_diff(contact.bearing + 35, state.heading) / 45, -1, 1)
        drive.set(throttle=1, turn=turn)

        if abs(angle_diff(contact.bearing, missiles.turret())) <= 3 and missiles.ready():
            missiles.fire()
    else:
        # Retreat: away from where it was last seen.
        if contact:
            away = wrap(contact.bearing + 180)
            drive.set(throttle=1, turn=clamp(angle_diff(away, state.heading) / 45, -1, 1))

Each mode's code only runs in that mode, so it's easy to follow, and the log lines say in the replay exactly when the program changed its mind and why.

When an update goes wrong

If an update fails (a sum with None in it, say, or running over the 2000 operations an update may use), everything it did is thrown away: its changes to memory, its orders and its logs. The fault goes on the timeline and the program runs again at the next update. A fault that happens every update means a vehicle that does nothing, so the editor's checks and the timeline are worth watching.

The commonest cause is reading a memory field before it's been set. Set everything on the first update, as above, or check for None before using it.