Learning Goals
3 minBy the end of this lesson you can:
- Bind a keyboard key to a Python function with
screen.onkey(). - Use
screen.listen()+screen.mainloop()to make the window respond to events. - Define tiny "move up/down/left/right" functions and wire each to an arrow key.
- Detect when the player reaches the goal — distance check from the goal's coordinates.
Warm-Up · A Key Press is a Function Call
5 minIn turtle, pressing a key just calls a function you wrote. Try this:
import turtle player = turtle.Turtle(shape="turtle") screen = turtle.Screen() def up(): player.setheading(90) player.forward(20) screen.listen() screen.onkey(up, "Up") # arrow keys: "Up", "Down", "Left", "Right" screen.mainloop()
Click the turtle window, then press ↑. The turtle moves up 20 pixels. Press it again — moves again. The key is wired to your function.
Until now your code ran top-to-bottom. Now it waits — and reacts to events. That's the shape of every interactive program: bind handlers, then sit in an event loop.
New Concept · The Event Loop Pattern
14 minThe four-step shape
1. Define handler functions (def up(): ...) 2. Tell screen to listen (screen.listen()) 3. Bind keys to handlers (screen.onkey(up, "Up")) 4. Enter the event loop (screen.mainloop())
After step 4, your script doesn't run any more lines. It just sits waiting for key presses. Each press fires the matching handler.
The four direction handlers
STEP = 20 def up(): player.setheading(90); player.forward(STEP) def down(): player.setheading(270); player.forward(STEP) def left(): player.setheading(180); player.forward(STEP) def right(): player.setheading(0); player.forward(STEP) screen.listen() screen.onkey(up, "Up") screen.onkey(down, "Down") screen.onkey(left, "Left") screen.onkey(right, "Right")
Setting the heading before moving means the turtle always points the right way — handy for a turtle-shaped cursor that visibly turns.
Key names
Up Down Left Right arrow keys space spacebar Return enter Escape esc a b c ... z single letters (case-sensitive on some systems) 1 2 3 4 ... 0 number keys
The goal · distance check
Pick a goal position. After each move, check if the player's position is close enough.
GOAL = (200, 150) def at_goal(): gx, gy = GOAL return abs(player.xcor() - gx) < 15 and abs(player.ycor() - gy) < 15
Why < 15 instead of ==? Because the player moves in 20-pixel steps and almost never lands exactly on the goal. A 15-pixel tolerance feels generous and forgiving.
Reacting to reaching the goal
At the end of every direction handler, check the goal and announce:
def check_win(): if at_goal(): win_label.write("YOU WIN!", font=("Arial", 36, "bold")) def up(): player.setheading(90); player.forward(STEP); check_win()
Drawing walls
Walls are static — just lines drawn by a helper turtle. Use hideturtle and speed(0) so they appear instantly.
def draw_wall(x1, y1, x2, y2): walls.penup(); walls.goto(x1, y1); walls.pendown() walls.goto(x2, y2) walls = turtle.Turtle() walls.hideturtle(); walls.speed(0); walls.color("dimgray"); walls.pensize(4) draw_wall(-200, 100, 200, 100) draw_wall(100, 100, 100, -100) # ... more walls ...
Detecting collisions with walls is harder — really, you check if the next position would cross a line. We'll keep walls visual-only today; the maze just sets expectations. Walking through walls is rude but allowed in v1.
Worked Example · The Full Maze
12 minSave as maze.py:
# maze.py — arrow-key maze walker import turtle screen = turtle.Screen() screen.setup(width=500, height=500) screen.title("Maze Walker") screen.bgcolor("white") # Walls walls = turtle.Turtle() walls.hideturtle(); walls.speed(0) walls.color("dimgray"); walls.pensize(4) def line(x1, y1, x2, y2): walls.penup(); walls.goto(x1, y1); walls.pendown(); walls.goto(x2, y2) line(-200, -200, 200, -200) # bottom line(200, -200, 200, 200) # right line(200, 200, -200, 200) # top line(-200, 200, -200, -200) # left line(-200, 100, 100, 100) # ledge line(100, 100, 100, -50) # vertical line(-100, -50, 100, -50) # lower ledge # Goal marker GOAL = (170, 170) gx, gy = GOAL flag = turtle.Turtle() flag.hideturtle(); flag.penup(); flag.goto(gx - 12, gy - 12); flag.pendown() flag.color("green"); flag.begin_fill() for _ in range(4): flag.forward(24); flag.right(90) flag.end_fill() # Player player = turtle.Turtle(shape="turtle") player.color("steelblue"); player.penup(); player.goto(-170, -170); player.pendown() player.pensize(1) # Win message win = turtle.Turtle() win.hideturtle(); win.penup(); win.goto(-100, 0) STEP = 20 def at_goal(): return abs(player.xcor() - gx) < 15 and abs(player.ycor() - gy) < 15 def check_win(): if at_goal(): win.color("green") win.write("YOU WIN!", font=("Arial", 36, "bold")) screen.onkey(None, "Up") screen.onkey(None, "Down") screen.onkey(None, "Left") screen.onkey(None, "Right") def up(): player.setheading(90); player.forward(STEP); check_win() def down(): player.setheading(270); player.forward(STEP); check_win() def go_left(): player.setheading(180); player.forward(STEP); check_win() def go_right(): player.setheading(0); player.forward(STEP); check_win() screen.listen() screen.onkey(up, "Up") screen.onkey(down, "Down") screen.onkey(go_left, "Left") screen.onkey(go_right, "Right") screen.mainloop()
What you'll see
A 500×500 square room with a few interior walls. A green flag in the top-right, a steel-blue turtle starting in the bottom-left. Click the window, then arrow-key the turtle through the gaps to the flag. When you reach it, a big "YOU WIN!" appears and the arrow keys are unbound.
Read the diff
Three new shapes. (1) Many small helper turtles — one for walls, one for the flag, one for the win message, plus the player itself. Each has its own job. (2) Functions wired to keys — up, down, go_left, go_right. (Note: we couldn't use left and right as function names because they shadow built-in turtle methods.) (3) The event loop — screen.mainloop() at the end. Your script ends there; the program is now driven entirely by the user.
Try It Yourself
13 minChange STEP to 40. Try the maze. Is it easier or harder?
Hint
Bigger steps = fewer presses but coarser control. Hard to thread through narrow gaps.
Add a global counter that increases by 1 every direction press. At win time, also print Steps used: N.
Hint
steps = 0 def up(): global steps steps += 1 player.setheading(90); player.forward(STEP); check_win() # Repeat the global+increment in every direction. def check_win(): if at_goal(): win.write(f"YOU WIN!\nSteps used: {steps}", font=("Arial", 24, "bold")) ...
global steps lets the function modify the top-level counter. We'll meet a tidier alternative (class state) in Level 3.
Bind r to a function that resets the player to the starting position and clears the win message.
Hint
def reset(): player.penup(); player.goto(-170, -170); player.pendown() win.clear() screen.onkey(up, "Up") screen.onkey(down, "Down") screen.onkey(go_left, "Left") screen.onkey(go_right, "Right") screen.onkey(reset, "r")
Rebinding the arrow keys re-enables movement after the win-state unbound them. win.clear() wipes the message.
Mini-Challenge · Wall Collision Detection
8 minAdd collision detection to your maze. When a move would put the turtle into a wall, refuse it — undo the move and play a system bell or just print a message.
The simplest approach: walls are rectangles. After each move, check if the player is inside any rectangle; if so, undo.
Show one possible solution
# Walls as rectangles: (x_min, y_min, x_max, y_max) WALL_BOXES = [ (-200, 95, 100, 105), # horizontal ledge — thin band ( 95, -50, 105, 105), # vertical wall (-100, -55, 100, -45), # lower ledge ] def inside_a_wall(): x, y = player.xcor(), player.ycor() for x1, y1, x2, y2 in WALL_BOXES: if x1 < x < x2 and y1 < y < y2: return True return False def safe_move(heading): # Remember where we were px, py = player.xcor(), player.ycor() player.setheading(heading) player.forward(STEP) if inside_a_wall(): player.penup(); player.goto(px, py); player.pendown() check_win() def up(): safe_move(90) def down(): safe_move(270) def go_left(): safe_move(180) def go_right(): safe_move(0)
Non-negotiables: a list of wall rectangles, a safe_move wrapper that undoes when the new position is inside a wall, and re-checking the win condition. This is the first time your game has real rules beyond just "move".
Recap
3 minInteractive games follow a fixed shape. Define handler functions for each player action. Tell the screen to listen(). Bind keys to handlers with onkey(). Enter the event loop with mainloop(). The script then sits and reacts. Check the win condition at the end of each handler. Collision detection is just "is the new position somewhere it shouldn't be?" — undo if yes.
Vocabulary Card
- screen.listen()
- Tell the window to start accepting keyboard input. Must come before binds.
- screen.onkey(handler, key)
- Bind a key to a function. Pass
Noneas handler to unbind. - screen.mainloop()
- Enter the event loop. The script doesn't run any more lines after this — it just reacts.
- collision detection
- Asking "is the new position inside a wall?" after every move. Undo if yes.
Homework
4 minDesign and build your own maze. Requirements:
- At least 6 wall segments. Build a maze with at least one dead-end.
- The goal must require turning at least 4 corners to reach from the start.
- Step counter + win message.
- A reset key.
Stretch. Add collision detection from the mini-challenge.
Sample · designing the maze
The easiest way to design is on paper first. Draw a 500×500 grid. Sketch the walls. Mark the start (S) and the goal (G). Count corners on the shortest path — should be at least 4.
Then translate each wall into line(x1, y1, x2, y2) calls in your code. Position the player at S, the flag at G. Run, walk through, adjust. Most maze design is the test-and-tune cycle.
There's no canonical answer — every maze is yours. Hand in a screenshot of yours plus the code that drew it.