Learning Goals
3 minBy the end of this lesson you can:
- Store a level as a list of strings where each character maps to a tile type.
- Use nested
for-loops withenumerateto draw every tile atcol * SIZE, row * SIZE. - Run a complete tile-map scene and change the level layout by editing one string.
Warm-Up · Nested Loop Coordinates
5 minTile drawing relies on nested loops. Predict what this prints:
SIZE = 32 grid = ["AB", "CD"] for row, line in enumerate(grid): for col, char in enumerate(line): print(char, col * SIZE, row * SIZE)
Show the answer
Output
A 0 0 B 32 0 C 0 32 D 32 32
Each character becomes a tile at pixel position (col * SIZE, row * SIZE). That is the complete formula for a tile map.
New Concept · The Tile Map
12 minThink of a tile map like graph paper. Each square of the paper is one tile. You label each square with a letter — G for grass, W for water, . for sky — and then a drawing routine colours each square based on its label.
The level data
LEVEL = [ "...........", "...........", "...GGG.....", "GGGGGGGGGGG", "WWWWWWWWWWW", ]
Each string is one row. Each character is one tile. The map is easy to read and easy to edit.
Tile palette
A dictionary maps each character to a colour (or later, an image name):
TILE_COLOURS = { "G": "forestgreen", "W": "dodgerblue", ".": "skyblue", }
Drawing the map
SIZE = 64 # pixels per tile def draw(): for row, line in enumerate(LEVEL): for col, char in enumerate(line): colour = TILE_COLOURS.get(char, "black") screen.draw.filled_rect( Rect((col * SIZE, row * SIZE), (SIZE, SIZE)), colour, )
Worked Example · Kampung Platform Scene
12 minNurul is building a side-scroller set in a kampung. She designs the tile map first. Save as tilemap.py:
Part A — data and palette
# tilemap.py — tile-based kampung level import pgzrun SIZE = 48 WIDTH = SIZE * 10 # 10 tiles wide = 480 px HEIGHT = SIZE * 7 # 7 tiles tall = 336 px LEVEL = [ "..........", "..........", "...PP.....", "..PPPP....", "GGGGGGGGGG", "SSSSSSSSSS", "SSSSSSSSSS", ] # P = platform, G = grass, S = soil, . = sky TILE_COLOURS = { ".": (135, 206, 235), # sky blue "G": (34, 139, 34), # forest green "S": (139, 90, 43), # brown soil "P": (160, 120, 60), # wooden platform }
Part B — draw
def draw(): for row, line in enumerate(LEVEL): for col, char in enumerate(line): colour = TILE_COLOURS.get(char, (0, 0, 0)) screen.draw.filled_rect( Rect((col * SIZE, row * SIZE), (SIZE, SIZE)), colour, ) pgzrun.go()
What you will see
LEVEL list.Try It Yourself
13 minAdd a new tile type W (water, colour "dodgerblue") to the palette and draw a small pond by placing W characters in the LEVEL grid.
TILE_COLOURS["W"] = "dodgerblue" # then edit LEVEL row 4 e.g.: # "GGGWWWWGGG"
After drawing each filled rect, draw a 1-pixel outline on top using screen.draw.rect with colour "black". The grid lines make the tile structure visible.
screen.draw.rect( Rect((col * SIZE, row * SIZE), (SIZE, SIZE)), "black", )
Mini-Challenge · Daniel's Invisible Tiles
8 minDaniel's tile map compiles but nothing appears. Spot both bugs.
# daniel_tiles.py — buggy
import pgzrun
SIZE = 40
WIDTH = 400
HEIGHT = 240
LEVEL = ["GGGGGGGGGG", "SSSSSSSSSS"]
COLOURS = {"G": "green", "S": "brown"}
def draw():
for row in LEVEL:
for col, char in enumerate(row):
colour = COLOURS.get(char, "black")
screen.draw.filled_rect( # bug 1
(col * SIZE, SIZE, SIZE, SIZE), colour
)
pgzrun.go()It works if…
a green row and a brown row appear at the correct positions
Show the fix
# daniel_tiles.py — fixed import pgzrun SIZE = 40 WIDTH = 400 HEIGHT = 240 LEVEL = ["GGGGGGGGGG", "SSSSSSSSSS"] COLOURS = {"G": "green", "S": "brown"} def draw(): for row_idx, row in enumerate(LEVEL): # fix 1: track row index for col, char in enumerate(row): colour = COLOURS.get(char, "black") screen.draw.filled_rect( Rect((col * SIZE, row_idx * SIZE), (SIZE, SIZE)), # fix 2: use Rect() colour, ) pgzrun.go()
Bug 1: for row in LEVEL gives the string, not its index — use enumerate. Bug 2: filled_rect requires a Rect object, not a plain tuple.
Recap
3 minA tile map stores a level as a list of strings. Nested for-loops with enumerate give you a row index and a column index. Multiplying by SIZE converts those indices into pixel coordinates. A palette dictionary maps each character to a colour or image.
Vocabulary Card
- tile
- A fixed-size square image or coloured rectangle that is repeated to fill a level.
- tile map
- A 2D grid (list of strings) where each character represents a tile type.
- tile palette
- A dictionary mapping a character key to a colour or image filename.
- col * SIZE, row * SIZE
- The formula that converts a grid position into a pixel coordinate on the canvas.
Homework
4 minDesign your own 10-column × 8-row level using at least four different tile types. Include at least one "ladder" or "door" tile drawn in a distinct colour. Print the level layout to the terminal with print statements before the game window opens, so you can see the grid as text. Bring a screenshot of both the terminal output and the window.
Sample · level2.py
# level2.py — 10×8 custom level import pgzrun SIZE = 48 WIDTH = SIZE * 10 HEIGHT = SIZE * 8 LEVEL = [ "..........", "....D.....", "....L.....", "..PPPPPP..", "....L.....", "GGGGGGGGGG", "SSSSSSSSSS", "SSSSSSSSSS", ] TILE_COLOURS = { ".": (135, 206, 235), "G": (34, 139, 34), "S": (139, 90, 43), "P": (160, 120, 60), "L": (180, 100, 40), "D": (255, 200, 0), } for line in LEVEL: print(line) def draw(): for row_idx, row in enumerate(LEVEL): for col, char in enumerate(row): colour = TILE_COLOURS.get(char, (0, 0, 0)) screen.draw.filled_rect( Rect((col * SIZE, row_idx * SIZE), (SIZE, SIZE)), colour ) pgzrun.go()
The print loop before pgzrun.go() runs once in the terminal before the window opens. Your tile types and colours will differ.