Project: the behaviour controller

Get past a wall to the dock with prioritised behaviour classes, a controller that aggregates them, and an exception to finish.

A1.10Programming techniques and object-oriented programmingA level35 min

Do this lesson in the simulator

Real robots are rarely programmed as one long sequence of moves. They are built from behaviours, each a small rule of the form "when this is true, do that", and a controller that decides, many times a second, which behaviour gets to act. When two behaviours both want to act, the one with higher priority wins: avoiding a wall matters more than cruising forward. This project builds that controller with everything in the module: constants, subroutines, exception handling, classes, inheritance, polymorphism and aggregation.

The job

The robot starts near the bottom left of the mat, facing north. A wall runs from the left edge part of the way across, in its path. The dock is in the top right corner. The robot must reach the dock without touching anything, turn its LED green and play a note, and print a log of which behaviour acted at each step.

It does not know where the wall ends. It finds out by behaving: cruise forward while the way is clear, sidestep right while it is not, and dock when it has come far enough north.

The design

Class diagram for the behaviour controllerController- behaviours : List- steps : Integer+ step()+ run(max_steps : Integer)Behaviour+ name : String+ wants() : Boolean+ act()1..*Cruise+ wants() : Boolean+ act()Sidestep+ wants() : Boolean+ act()Dock+ wants() : Boolean+ act()
Controller aggregates one or more Behaviours; the abstract Behaviour (in italics) has three subclasses
  • Behaviour is an abstract class. Its methods wants() and act() raise NotImplementedError, so a subclass that forgets to override one fails loudly.
  • Cruise, Sidestep and Dock inherit from it and override both methods.
  • Controller aggregates a list of behaviours, handed to its constructor in priority order. It never asks which class a behaviour is: it calls wants() and act() on each, and polymorphism runs the right code.
  • Docking ends the run by raising an exception, Docked, which the controller catches. This is a legitimate use of an exception: the event is detected deep inside one behaviour's method and must stop a loop two calls further out.
  • The controller's finally block stops the motors however the run ends: docked, given up, or crashed.

Where each idea comes in

Idea Lesson In the project
Named constants A1.1 SAFE_CM, STEP_CM, DOCK_Y
Relational and Boolean operations A1.2 Each wants() returns a Boolean
Exception handling, raising your own A1.3 Docked, NotImplementedError, finally
Subroutines with interfaces A1.4 Every behaviour has the same wants() and act() interface
Local variables, no globals A1.5 The step count is a private attribute, not a global
Encapsulation A1.7 Controller keeps its behaviours and count private
Inheritance, overriding, polymorphism A1.8 Three subclasses of Behaviour, called through one loop
Aggregation A1.9 The controller is given its behaviours

Build it in this order

  1. Write Docked and the abstract Behaviour, then Cruise alone. Test it with c = Cruise() and a loop of if c.wants(): c.act().
  2. Add Sidestep, and test the two together with the same kind of loop.
  3. Write Controller.step() and test one step at a time.
  4. Add Dock and run, with the exception handling, and try the whole task.

Task: the behaviour controller

The robot starts at the bottom left, facing north (heading 0). The starter declares the constants SAFE_CM = 25, STEP_CM = 10 and DOCK_Y = 60, the exception class Docked and the abstract class Behaviour. Write:

  • Cruise(Behaviour): name cruise. wants() returns True when distance() is more than SAFE_CM. act() drives forward STEP_CM cm.
  • Sidestep(Behaviour): name sidestep. wants() returns True when distance() is SAFE_CM or less. act() moves sideways to the right, with right, by STEP_CM + 5 cm.
  • Dock(Behaviour): name dock. wants() returns True when the y part of position() (cm north of the start) is DOCK_Y or more. act() turns the LED green, plays one note, and raises Docked.
  • Controller: its constructor takes a list of behaviours in priority order and keeps them, and a step count starting at 0, in private attributes. step() goes through the behaviours in order, and for the first one whose wants() is True it adds 1 to the count, prints step <n>: <name> and calls its act(), then returns. run(max_steps) calls step() up to max_steps times inside a try block. It catches Docked and prints docked after <n> steps, and in a finally block calls stop() and prints controller stopped.

The main program makes Controller([Dock(), Sidestep(), Cruise()]) and calls run(30). Do not use global or isinstance.

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

SAFE_CM = 25
STEP_CM = 10
DOCK_Y = 60

class Docked(Exception):
    pass

class Behaviour:
    def __init__(self, name):
        self.name = name

    def wants(self):
        raise NotImplementedError

    def act(self):
        raise NotImplementedError

# write Cruise, Sidestep, Dock and Controller

forward(50, distance=STEP_CM)

Challenges

  1. Swap the order to Controller([Cruise(), Sidestep(), Dock()]). Predict what happens before you run it, then explain what you see.
  2. Add a Reverse behaviour with the highest priority that backs away when distance() is under 10 cm. Does anything else need to change?
  3. Change run so that giving up after max_steps raises an exception of your own instead of printing, and handle it in the main program.
  4. Draw the class diagram again with Docked on it. What relationship does it have with Exception?