Brains

From a loop to think(me) and me.memory, with a practice arena.

5.5BehavioursRobot club20 min

Do this lesson in the simulator

The built-in bots in the games are written as a brain: def think(me), a function the arena calls ten times a second. (Your own game programs can be plain scripts like every lesson, or brains; both run in the arena.) You have been writing the same thing all module with a for tick loop around it. This lesson turns a state machine into a brain, and puts it in an arena against other robots.

The loop is outside

A behaviour loop:

state = "look"
for tick in range(300):
    if state == "look":
        ...
    wait(0.1)

The same as a brain:

def think(me):
    state = me.memory.get("state", "look")
    if state == "look":
        ...
    me.memory["state"] = state

The arena owns the loop and the waiting. Your function is the body of the loop. Anything that has to survive from one tick to the next goes in me.memory, because local variables are forgotten when the function returns.

Escape, as a loop

The robot starts inside three walls. Look around, find the gap, drive out:

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

def wrapped(h):
    return (h + 180) % 360 - 180

state = "look"
samples = []
# do this 290 times (tick counts from 0)
for tick in range(290):
    if state == "look":
        # a full turn of readings
        samples.append((distance(), heading()))
        # spin clockwise on the spot at 50
        turn_right(50)
        if len(samples) >= 65:
            # headings with room: the gap
            opens = [h for d, h in samples if d > 40]
            # the middle of the gap, not the longest ray
            target = opens[len(opens) // 2]
            state = "turn"
            print("gap is around heading", round(target))
    elif state == "turn":
        error = wrapped(target - heading())
        if abs(error) < 4:
            state = "go"
        else:
            # forward, sideways, rotation: -100 to 100 each, until the next command
            drive(0, 0, max(-40, min(40, error * 3)))
    elif state == "go":
        # where am I? (cm from where I started)
        x, y = position()
        if y < -32:
            # leave the loop
            break
        # forward, sideways, rotation: -100 to 100 each, until the next command
        drive(60, 0, wrapped(target - heading()) * 3)
    # pause 0.1 s (the robot keeps doing what it was told)
    wait(0.1)
# all motors off
stop()
print("out, at", position())

Run this in the simulator

Note the gap logic: the longest single reading points at a corner of the opening, so the robot aims at the middle of all the headings that had room.

The same thing as a brain

The Control Points game below has no walls, so the brain is simpler: go to a zone, then hold it, stepping away from anyone who comes close (driving into an opponent is a foul) and going back in if you drift out. A state machine with two states and me.memory:

def think(me):
    state = me.memory.get("state", "go")
    zone = me.info["points"][1]
    cx, cy = zone["x"] + zone["w"] / 2, zone["y"] + zone["h"] / 2
    if state == "go":
        ...drive towards (cx, cy)...
        if close enough:
            state = "hold"
    elif state == "hold":
        if not in the zone any more:
            state = "go"
        ...else step away from the nearest robot, or drift back to the middle...
    me.memory["state"] = state

Try it

This is the real game arena with the built-in bots: you are on the red team. Your brain (a plain script would work here too), no room:

# maths: atan2, hypot, sin, cos, radians
import math

def wrapped(h):
    return (h + 180) % 360 - 180

def go_towards(me, x, y, speed):
    bearing = math.degrees(math.atan2(x - me.x, y - me.y))
    a = math.radians(wrapped(bearing - me.heading))
    me.drive(speed * math.cos(a), speed * math.sin(a), 0)

def think(me):
    state = me.memory.get("state", "go")
    # the middle one of the three zones
    zone = me.info["points"][1]
    cx, cy = zone["x"] + zone["w"] / 2, zone["y"] + zone["h"] / 2
    in_zone = zone["x"] <= me.x <= zone["x"] + zone["w"] and zone["y"] <= me.y <= zone["y"] + zone["h"]
    near = [o for o in me.others if math.hypot(o['x'] - me.x, o['y'] - me.y) < 12]
    if state == "go":
        go_towards(me, cx, cy, 100)
        if math.hypot(me.x - cx, me.y - cy) < 4:
            state = "hold"
    elif state == "hold":
        if not in_zone:
            # drifted out: get back in
            state = "go"
        elif near:
            o = near[0]
            # too close: step straight away from them (driving into an opponent freezes you for 3 s)
            go_towards(me, 2 * me.x - o['x'], 2 * me.y - o['y'], 60)
        elif math.hypot(me.x - cx, me.y - cy) > 3:
            # drift back to the middle
            go_towards(me, cx, cy, 60)
        else:
            me.stop()
    me.memory["state"] = state

Task: escape the box

Three walls around you. Find the gap with the distance sensor, drive out and into the green zone, touching nothing.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

# as long as the thing ahead is further than 15 cm
while distance() > 15:
    # drive forward at 50 (keeps going until the next command)
    forward(50)
    # pause 0.1 s (the robot keeps doing what it was told)
    wait(0.1)
# all motors off
stop()
print("wall at", distance())

Challenges

  1. Escape faster: stop looking as soon as you have seen a reading over 40 and the readings are falling again.
  2. In the arena, add a third state: "shove", entered when another robot is within 10 cm, that drives at it for five ticks.
  3. Write the escape as a brain (think(me) with me.memory) on paper. What changes?