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.
Do this lesson in the simulatorReal 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
Behaviouris an abstract class. Its methodswants()andact()raiseNotImplementedError, so a subclass that forgets to override one fails loudly.Cruise,SidestepandDockinherit from it and override both methods.Controlleraggregates a list of behaviours, handed to its constructor in priority order. It never asks which class a behaviour is: it callswants()andact()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
finallyblock 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
- Write
Dockedand the abstractBehaviour, thenCruisealone. Test it withc = Cruise()and a loop ofif c.wants(): c.act(). - Add
Sidestep, and test the two together with the same kind of loop. - Write
Controller.step()and test one step at a time. - Add
Dockandrun, 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): namecruise.wants()returnsTruewhendistance()is more thanSAFE_CM.act()drives forwardSTEP_CMcm.Sidestep(Behaviour): namesidestep.wants()returnsTruewhendistance()isSAFE_CMor less.act()moves sideways to the right, withright, bySTEP_CM + 5cm.Dock(Behaviour): namedock.wants()returnsTruewhen the y part ofposition()(cm north of the start) isDOCK_Yor more.act()turns the LED green, plays one note, and raisesDocked.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 whosewants()isTrueit adds 1 to the count, printsstep <n>: <name>and calls itsact(), then returns.run(max_steps)callsstep()up tomax_stepstimes inside atryblock. It catchesDockedand printsdocked after <n> steps, and in afinallyblock callsstop()and printscontroller 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
- Swap the order to
Controller([Cruise(), Sidestep(), Dock()]). Predict what happens before you run it, then explain what you see. - Add a
Reversebehaviour with the highest priority that backs away whendistance()is under 10 cm. Does anything else need to change? - Change
runso that giving up aftermax_stepsraises an exception of your own instead of printing, and handle it in the main program. - Draw the class diagram again with
Dockedon it. What relationship does it have withException?