Project: plan the route, then drive it
Choose and justify a route planner, build the mat's graph, find the shortest route with Dijkstra's algorithm and drive it.
Do this lesson in the simulatorThis project puts the module together on the mat. BugBot starts at waypoint S and has to reach waypoint G along the roads between waypoints, without touching a wall. You will choose a planning algorithm and justify it, build the graph from the map, run Dijkstra's algorithm, and drive the route it finds.
The map as data
The robot's map is two pieces of data. WAYPOINTS gives each waypoint's position on the mat in cm, with x across and y up the mat from the bottom-left corner. ROADS lists which pairs of waypoints are joined by a straight road the robot can drive along safely. Every road is either along x or along y, so the robot never has to turn: it drives forward, backward, left and right.
import math
WAYPOINTS = {"S": (10, 10), "A": (10, 90), "B": (90, 90), "C": (35, 10), "D": (35, 35),
"E": (65, 35), "F": (65, 60), "G": (90, 60), "J": (80, 35)}
ROADS = [("S", "A"), ("A", "B"), ("B", "G"), ("S", "C"), ("C", "D"),
("D", "E"), ("E", "F"), ("F", "G"), ("E", "J")]
for a, b in ROADS:
print(a, "to", b, math.dist(WAYPOINTS[a], WAYPOINTS[b]), "cm")
A road's weight is its length, the straight-line distance between its ends, which math.dist works out. Roads can be driven both ways, so the graph is undirected: each road goes into the adjacency list of both its ends.
Choosing the algorithm
Before writing code, think about what each approach you have met would do on this map.
Try every route. Listing every route from S to G and taking the shortest always works, but the number of routes can grow factorially with the number of waypoints (A5.1). Nine waypoints is fine; a real building with a few hundred is not.
Greedy: always take the nearest next waypoint. From S the nearest is C (25 cm), then D (25), then E (30). From E the nearest is J, only 15 cm away, and J is a dead end. A greedy rule is fast but can walk into a trap, because it never looks further than one road ahead.
Breadth-first search (A4). It finds the route with the fewest roads: S, A, B, G, only 3 roads but 80 + 80 + 30 = 190 cm. The route through the middle uses 5 roads and is shorter. Fewest roads is not shortest distance when roads have different lengths.
Dijkstra's algorithm (A5.7). Finds the shortest total distance, and works because no road has a negative length. With V waypoints and E roads it is O((V + E) log V) with a priority queue, so it would still be fast on a map of thousands of waypoints.
A* (A5.8). Also correct here, with straight-line distance to G as an admissible heuristic, and it would expand fewer waypoints. On a map this small the saving is a handful of vertices, and Dijkstra's algorithm also gives the distance to every waypoint, which is handy if the robot is later sent somewhere else. Either is a good answer if you justify it.
This project uses Dijkstra's algorithm.
Driving a route accurately
The robot drifts a little on every move (module 2). Driving five roads in a row, the errors add up. The starter includes a helper, go_to(x, y), that reads position() before each move and drives the difference, so each road corrects the error left by the one before. position() measures from where the robot started, so the helper adds the start's mat position to it. This cell runs on the project's mat: it drives from S to D and back.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
START = (10, 10) # the robot starts at waypoint S
def go_to(x, y):
"""Drive to the mat point (x, y): along y, then along x, twice so the second pass corrects any drift."""
for attempt in range(2):
px, py = position()
dy = y - (START[1] + py)
if dy > 1:
forward(60, distance=dy)
elif dy < -1:
backward(60, distance=-dy)
px, py = position()
dx = x - (START[0] + px)
if dx > 1:
right(60, distance=dx)
elif dx < -1:
left(60, distance=-dx)
for point in [(35, 10), (35, 35), (35, 10), (10, 10)]: # S to C to D, and back
go_to(*point)
px, py = position()
print("aimed for", point, "reached", (START[0] + px, START[1] + py))
Putting it together
Your program has four parts, each one a separate job:
- Build the graph: a dictionary from each waypoint to a dictionary of its neighbours and road lengths.
- Plan:
dijkstra(graph, start)returns the distances and previous waypoints. - Read the route back from G to S through the previous waypoints, and reverse it.
- Drive:
go_toeach waypoint on the route in order, then signal arrival.
Keeping planning and driving apart is good design. The planner can be tested with no robot at all, and the same driving code will follow whatever route any planner produces.
Task: plan the route, then drive it
The starter has the map and the go_to helper. Write the rest:
- Build an undirected weighted graph from
WAYPOINTSandROADS, where each road's weight is its length in cm (usemath.dist; do not type the lengths). - Write
dijkstra(graph, start)and use it to find the shortest route fromStoG. The program must find the route: do not type it. - Print
route: <waypoints joined with ->, such asroute: S-X-Y-G, andlength: <n> cm, with n a whole number. - Drive the route by calling
go_tofor each waypoint afterS, in order. Stay on the roads: do not go near waypoints that are not on your route, and do not touch a wall. - When the robot reaches
G, turn the LED green.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
import math
import heapq
WAYPOINTS = {"S": (10, 10), "A": (10, 90), "B": (90, 90), "C": (35, 10), "D": (35, 35),
"E": (65, 35), "F": (65, 60), "G": (90, 60), "J": (80, 35)}
ROADS = [("S", "A"), ("A", "B"), ("B", "G"), ("S", "C"), ("C", "D"),
("D", "E"), ("E", "F"), ("F", "G"), ("E", "J")]
START = WAYPOINTS["S"] # the robot starts at S
def go_to(x, y):
"""Drive to the mat point (x, y): along y, then along x, twice so the second pass corrects any drift."""
for attempt in range(2):
px, py = position()
dy = y - (START[1] + py)
if dy > 1:
forward(60, distance=dy)
elif dy < -1:
backward(60, distance=-dy)
px, py = position()
dx = x - (START[0] + px)
if dx > 1:
right(60, distance=dx)
elif dx < -1:
left(60, distance=-dx)
Challenges
- The road from E to F is closed for repairs. Remove it from
ROADSand plan again. What route and length do you get, and would the robot hit a wall driving it? - Add A* with straight-line distance as the heuristic, and count how many waypoints each algorithm expands before G.
- Add a second destination and use the distances from one run of
dijkstrato decide which destination is nearer, without planning twice.