Skip to main content

Lesson 8 · Implementing the Navigator Class

Module 4 · Manhattan Navigation

Lesson 8 · Implementing the Navigator Class

60 minPython + robotReusing LineTrack
👩‍🏫

Teacher mode is on. Toggle it off (bottom-right) to preview the student view.

Time to make the robot move. You'll package the turning logic from Lesson 7 into a Navigator class that drives any Manhattan path on the grid. The best part: it does the actual driving by reusing the LineTrack class you built in Module 2 — no motor code to rewrite. That's the payoff of building reusable classes.

Learning Objectives

By the end of this lesson you will be able to:

  • Design a Navigator with position, heading, and a line_track object
  • Implement desired_heading(), turn_to(), and drive_path() as methods
  • Explain delegation: the Navigator asks LineTrack to turn and follow lines
  • Explain why the robot must "clear the intersection" when driving straight
  • Integrate Manhattan and Navigator to drive a computed path on the robot

What the Navigator needs

Manhattan plans; Navigator drives. When you create one, it needs to know its starting position, its starting heading, and it builds a LineTrack to do the physical work:

HEADING_NAMES = ["N", "E", "S", "W"]

class Navigator:
def __init__(self, start, heading):
self.position = start
self.heading = heading # 0=N, 1=E, 2=S, 3=W
self.line_track = LineTrack()

Notice the Navigator does not create a DifferentialDriveLineTrack already has one inside it. The Navigator talks to LineTrack; LineTrack talks to the motors. Each class has one job. That handing-off is called delegation.

Knowledge Check

How does the Navigator control the robot's motors?

The two logic methods

desired_heading() is the Lesson 7 logic, now a method that reads self.position instead of taking a current parameter — the object already knows where it is:

def desired_heading(self, next_pos):
row_diff = next_pos[0] - self.position[0]
col_diff = next_pos[1] - self.position[1]
if row_diff == -1:
return 0 # North
elif col_diff == 1:
return 1 # East
elif row_diff == 1:
return 2 # South
elif col_diff == -1:
return 3 # West

And turn_to() is the while loop from Lesson 7 — but now each turn is a real turn, self.line_track.turn_right(), which physically spins the robot:

def turn_to(self, desired):
while self.heading != desired:
self.line_track.turn_right()
self.heading = self.heading + 1
if self.heading == 4:
self.heading = 0
Knowledge Check

Why does desired_heading() use self.position instead of a 'current' parameter?

drive_path and clearing the intersection

drive_path loops the path: work out the heading, turn to it, follow the line to the next intersection, update position. There's one subtle wrinkle:

def drive_path(self, path):
for next_pos in path:
needed = self.desired_heading(next_pos)
if self.heading == needed:
self.line_track.drivetrain.straight(8) # clear the intersection
self.turn_to(needed)
self.line_track.track_until_cross()
self.position = next_pos

When the robot needs to turn, turn_right() already drives it off the current intersection as part of the turn. But when it's going straight (no turn needed), it's still sitting on the cross it just arrived at — and track_until_cross() would instantly detect that cross and stop. So for the straight-ahead case only, we drive forward 8 cm first to clear it. track_until_cross() then line-follows until the next intersection — no distance measurement, the sensors say when it's arrived.

Knowledge Check

Why drive straight(8) before track_until_cross() only when going straight ahead?

Activity · Trace it, then integrate

Before running anything, trace [(1,0), (1,1)] from (0,0) heading 0 (N) on paper. For each step write down: the next intersection, the heading the robot needs, and how many right turns get it there. Watch what happens when the count passes 3.

Now connect the two classes — Manhattan computes, Navigator drives:

manhattan = Manhattan((0, 0))
navigator = Navigator((0, 0), 0) # start heading North

path = manhattan.compute_path((2, 3))
print("Path:", path)
navigator.drive_path(path)
print("Arrived at:", navigator.position)
print("Heading:", HEADING_NAMES[navigator.heading])

Test it in stages: Manhattan alone (prints), then one short leg on the robot, then a longer path. Watch the straight-through cells especially — does the robot clear each intersection cleanly?

Knowledge Check

The path from Manhattan is [(1,0),(2,0),(2,1)]. How many times does drive_path call track_until_cross()?

Real-world connections

Delegating hard work to a well-built component is how real systems scale:

Software

Libraries

You call a graphics or networking library instead of rewriting it — exactly how Navigator calls LineTrack.

Robotics

Motor controllers

High-level planners send "go to X"; a lower-level controller handles the motors, just like this split.

Cars

Drive-by-wire

The steering software sets a target; a separate actuator system does the physical turning.

Wrap-up

  • What three things does the Navigator store, and what does each do? (position, heading, line_track — where it is, which way it faces, and the driver.)
  • What is delegation here? (Navigator asks LineTrack to move; it never touches motors itself.)
  • Why the straight(8) clear? (To leave the current cross so the sensors find the next one.)

Resources