Sessions
Pick a series below. The Python sessions build games with Pygame. The Git sessions teach you version control: from your first commit to pull requests.
Pick a series below. The Python sessions build games with Pygame. The Git sessions teach you version control: from your first commit to pull requests.
Ten sessions in which you build a complete Pygame game every month. Every session has a worksheet, a cheatsheet and starter code to download.
draw()/update() loop and move a sprite with the arrow keys.pygame.Rect, make the ball bounce by reversing its speed and keep the score for two players.pygame.mixer and build a win and lose screen.vel_y variable and detect platform collisions with pygame.Rect.__init__ and make a subclass through inheritance.In this session you build a mini game: a star falls down and you catch it with a basket. By the end of the session everything moves, the game keeps track of how many stars you catch, and you have your first game of your own.
draw() function: what you draw every frameupdate() function: what changes every framemain.py in Thonny.The update() function runs 60 times per second. So everything you put in it runs 60 times per second. That is how movement works in a game.
Right now there is only pass in update(). That means “do nothing”. Remove pass and add this:
def update():
star.y = star.y + 3Click Run. The star now falls from top to bottom. When it falls off the screen, it disappears. We’ll fix that in a moment.
What happens here? star.y is the vertical position of the star. The bigger y, the lower on the screen. By adding + 3 every frame, the star drops a little each time.
The coordinate system: (0, 0) is at the top left, not at the bottom left like in maths. x goes to the right, y goes down. The star starts at y = 30 (almost at the top), the basket sits at y = 370 (almost at the bottom).
Move your mouse over the screen below to see the coordinates:
Move your mouse over the screen to see the coordinates.
Want to know more about how the screen works? → Coordinates explained
Adjust the sliders and watch how actor.x and actor.y change as the actor moves:
Time to steer the basket. Pygame Zero has a handy keyboard variable that keeps track of which keys are pressed.
Add this to update(), after the line about the star:
def update():
star.y = star.y + 3
if keyboard.left:
basket.x = basket.x - 5
if keyboard.right:
basket.x = basket.x + 5Click Run and use the arrow keys. The basket moves along now!
Tip (optional, counts as Stretch): If you move the basket too far left or right, it disappears off the screen. Can you add a check that stops that? Hint: use WIDTH and compare it with basket.x.
Now you want something to happen when the basket touches the star. For that you use colliderect. It checks whether two rectangles overlap.
Add this to update():
if star.colliderect(basket):
star.y = 0The star jumps back to the top when you catch it. But what if you miss it? Add this too:
if star.y > HEIGHT:
star.y = 0Bonus: Make the star come back at a random spot every time. Add this at the top of the file:
import randomThen replace star.y = 0 (in both places) with:
star.x = random.randint(20, WIDTH - 20)
star.y = 0Now the star falls at a different spot every time. Much harder to catch!
A real game keeps track of what you score. For that you need a variable that lives outside the functions: a global variable.
Click the button below to see how score changes every time you catch a star:
Add this at the top, outside all functions (after the lines about basket and star):
score = 0In draw(), add:
screen.draw.text(f"Score: {score}", topleft=(10, 10), color="white")In update(), change the catch code so the score goes up:
if star.colliderect(basket):
global score
score = score + 1
star.x = random.randint(20, WIDTH - 20)
star.y = 0global score tells Python: “I mean the score from outside this function, not a new local variable.” Without that line you get an error message.
Click Run. The score sits at the top left and goes up every time you catch a star!
Show your game to a coach and a buddy. Tell them what you made: what works, and what extras you added.
“Next time: things that FIGHT BACK. And they go BOOM, with real sound.”
Try one of these extensions at home. Each one comes with a code hint to get you going.
Limit lives — Add levens = 3. On a miss (star.y > HEIGHT): levens -= 1. Stop the game when levens == 0 with game_over = True and show “Game Over” in draw().
Random star colour — On every catch: give the star a new colour with random.randint(50, 255) for red, green and blue. Start at 50, not at 0 — otherwise the star is sometimes black and invisible.
Background — Draw fixed stars in the background in draw() before the score. Use screen.draw.filled_circle(x, y, 2, "white") at 20 random positions. Seed random.seed(42) so they don’t flicker.
Second star — Make sterren = [star, Actor("star")]. Give every star its own x. Update both of them in update() and draw them in draw().
High score — Keep track of high_score. If score > high_score: high_score = score. Show f"Best: {high_score}" next to the score.
Smaller basket — After every 5 points: basket.width = max(30, basket.width - 5). Reset basket.width on a restart.
Bomb — Add a red circle that falls now and then. Use a timer bom_timer and random.randint to decide when it shows up. If the basket hits the bomb: score -= 2.
Difficulty — After every 5 stars: raise the falling speed by 1. Build in a maximum: max(3, star_snelheid).
Your spaceship drifts in the dark of space while meteors come down from above. Dodge them, or lose a life. The longer you survive, the faster they go. And this time you also build Dojo Defender: the bigger project that grows every session.
main.py.Fill in the spawn_meteor() function. A meteor is an Actor that starts at the top of the screen at a random x position:
import random
def spawn_meteor():
rock = Actor('meteor')
rock.x = 0
rock.y = random.randint(30, WIDTH - 30)
meteors.append(rock)Call the function in update(), but not every frame, otherwise the screen fills up straight away. The update() function runs 60 times per second. Use that to keep a counter:
See below how fast a meteor moves at different speeds and how many pixels that is per second:
Use a counter:
# at the top, outside the functions:
timer = 0
def update():
global timer
timer += 1
if timer % 60 == 0: # every second (60 frames)
spawn_meteor()Click Run. After a second the first meteor appears, but it doesn’t move yet.
Let all the meteors in the list drop, and remove them when they fall off the screen:
def update():
global timer
timer += 1
if timer % 60 == 0:
spawn_meteor()
for rock in meteors[:]: # [:] = copy so you can remove safely
rock.y += 4
if rock.y > HEIGHT + 30:
meteors.remove(rock)Detecting collisions: If a meteor hits the ship, you lose a life. Add this in the loop:
if rock.colliderect(ship):
meteors.remove(rock)
global lives
lives -= 1
if lives <= 0:
# TODO: game over (step 3)
passGame over: Add a variable game_over = False at the top. If lives == 0, set game_over = True. In draw(), show text when game_over is True:
if game_over:
screen.draw.text(
"GAME OVER",
center=(WIDTH / 2, HEIGHT / 2),
color="red",
fontsize=60,
)Stop the movement in update() when game_over is True:
def update():
if game_over:
return
...Raising the speed: Make meteors go faster as the score rises:
snelheid = 3 + score // 5 # every 5 points, +1 speed
rock.y += snelheidScore: Every second you survive, +1 point. Add global score and raise it in update():
if timer % 60 == 0:
global score
score += 1
spawn_meteor()Several sizes: Make small, medium and large meteors. Use a schaal variable and adjust the hitbox:
def spawn_meteor():
rock = Actor('meteor')
rock.x = random.randint(30, WIDTH - 30)
rock.y = 0
rock.schaal = random.choice([0.5, 1.0, 1.5])
meteors.append(rock)Smaller meteors move faster; bigger ones are harder to dodge but give more points.
Download the Dojo Defender starter for today. This is the base of the project that grows every session. Today you add your spaceship and make it move with the arrow keys.
Were you not there last time? No problem — download the solution from last time and you can join in straight away.
The complete Dojo Defender worksheet is on the Dojo Defender page.
Show your game to a coach and a buddy. How long do you survive? Who gets the highest score?
“Next time: your own spaceship SHOOTS BACK. Enemies in formation, and you blast them away.”
sounds/hit.wav).Enemies stand in formation at the top of the screen. You’re at the bottom with a spaceship and one goal: knock them all out before they reach the bottom. Press SPACE to shoot, and don’t miss.
main.py.The ship already moves with the arrow keys. Check the code in update(). Make sure the ship doesn’t go off the screen:
if keyboard.left and ship.x > 30:
ship.x -= 5
if keyboard.right and ship.x < WIDTH - 30:
ship.x += 5Fill in the shoot() function. A bullet is an Actor that starts at the position of the ship:
def shoot():
kogel = Actor('bullet')
kogel.x = ship.x
kogel.y = ship.y - 20
bullets.append(kogel)Call shoot() when the space bar is pressed. Use on_key_down (Pgzero style):
def on_key_down(key):
if key == keys.SPACE:
shoot()Make the bullets move up in update() and remove them when they go off the screen:
for kogel in bullets[:]:
kogel.y -= 8
if kogel.y < -10:
bullets.remove(kogel)colliderect checks whether the rectangular hitbox of two actors overlaps. Move your mouse over the screen below to see when that happens:
Check in update() whether a bullet hits an enemy:
for kogel in bullets[:]:
kogel.y -= 8
if kogel.y < -10:
bullets.remove(kogel)
continue
for vijand in enemies[:]:
if kogel.colliderect(vijand):
bullets.remove(kogel)
enemies.remove(vijand)
global score
score += 10
breakWin condition: If enemies is empty, you’ve won:
if not enemies:
screen.draw.text("YOU WIN!", center=(WIDTH/2, HEIGHT/2), color="yellow", fontsize=60)Make the enemies come down slowly. Use a timer:
vijand_timer = 0
def update():
global vijand_timer
vijand_timer += 1
if vijand_timer % 90 == 0: # every 1.5 seconds
for vijand in enemies:
vijand.y += 20Game over when an enemy reaches the bottom:
for vijand in enemies:
if vijand.y > HEIGHT - 30:
global game_over
game_over = TrueFaster shooting rate: Build in a cooldown so the ninja can’t shoot endlessly fast:
shoot_cooldown = 0
def update():
global shoot_cooldown
if shoot_cooldown > 0:
shoot_cooldown -= 1
def on_key_down(key):
global shoot_cooldown
if key == keys.SPACE and shoot_cooldown == 0:
shoot()
shoot_cooldown = 15Show your game to a coach. How fast do you clear all the enemies? Compare scores with a buddy.
“Next time: two players, one ball: PONG. And you learn real pygame without pgzrun.”
View the cheatsheet for this session
Download the Dojo Defender starter for today.
Were you not there last time? Download the solution from session 1 and start from there.
The complete worksheet: Dojo Defender Session 2.
Two paddles, one ball: the classic game of Pong. But there’s a surprise: this session you write the game in real pygame, without pgzrun. You learn to set up the game loop yourself. That’s how most real games work.
pygame.Rect for rectangles and collisionsmain.py.Difference with pgzrun: There are no draw() and update() functions any more. Everything sits in one while running: loop.
Find the lines:
# bal.x += ball_dx # turn this on when you're done
# ball.y += ball_dyRemove the # in front of both lines. But first set a starting speed. Find ball_dx, ball_dy = 0, 0 and change it to:
ball_dx, ball_dy = 4, 4Click Run. The ball moves now, but it disappears off the edge. We’ll fix that in step 2.
Add the following after ball.x += ball_dx and ball.y += ball_dy:
# Bounce off the top and bottom edge
if ball.top <= 0 or ball.bottom >= HEIGHT:
ball_dy = -ball_dy
# Bounce off the paddles
if ball.colliderect(left_paddle) or ball.colliderect(right_paddle):
ball_dx = -ball_dxThe ball now bounces forever. Also add limits for the paddles, so they don’t go off the screen. Have a look at the code that’s already in the starter.
If the ball leaves the screen on the left, the right-hand player scores. If it goes out on the right, the left-hand player scores:
# Ball out on the left → right scores
if ball.left <= 0:
score_right += 1
ball.center = (WIDTH // 2, HEIGHT // 2)
ball_dx = 4
# Ball out on the right → left scores
if ball.right >= WIDTH:
score_left += 1
ball.center = (WIDTH // 2, HEIGHT // 2)
ball_dx = -4The score is already drawn in draw(). Check that the variables are right.
Win at 5 points: Add a variable won. When a player reaches 5, stop the game:
if score_left >= 5:
won = "Left wins!"
if score_right >= 5:
won = "Right wins!"Show the winner on the screen and stop the movement.
Speeding up: After every rally the ball gets a bit faster:
if ball.colliderect(left_paddle) or ball.colliderect(right_paddle):
ball_dx = -ball_dx * 1.05 # 5% faster
ball_dy = ball_dy * 1.05Build in a maximum: ball_dx = max(-12, min(12, ball_dx)).
Play a match against a buddy. First to 5 points wins. Show it to a coach.
“Next time: Breakout: the ball smashes bricks to pieces. And there’s SOUND.”
Today Dojo Defender moves to real Pygame! Download the starter. Were you not there? Download the solution from session 2. Worksheet: Dojo Defender Session 3.
A ball bounces back and forth and smashes rows of coloured bricks to pieces. You steer the paddle to keep the ball in the air. And this time you hear it too: there are sound effects on every collision.
pygame.mixermain.py.Find this line in the starter:
ball_dx, ball_dy = 4, 0
# ball_dy = -4 ← remove the # to make the ball move!Remove the # in front of ball_dy = -4. Now the ball has a vertical speed. Add movement in the game loop (look for the update section):
ball.x += ball_dx
ball.y += ball_dyClick Run. The ball moves diagonally but disappears off the edges.
Add bouncing:
# Top, left and right edge
if ball.left <= 0 or ball.right >= WIDTH:
ball_dx = -ball_dx
if ball.top <= 0:
ball_dy = -ball_dy
# Paddle
if ball.colliderect(paddle):
ball_dy = -ball_dy
if snd_hit:
snd_hit.play()Lives: If the ball reaches the bottom, you lose a life:
if ball.top > HEIGHT:
levens -= 1
# Reset the ball on top of the paddle
ball.center = (paddle.centerx, paddle.top - BALL_SIZE)
ball_dy = -4Check the collision with every brick in the loop:
for steen in stenen[:]:
if ball.colliderect(steen['rect']):
stenen.remove(steen)
ball_dy = -ball_dy
score += 10
if snd_hit:
snd_hit.play()
break # break only one brick per frameWin condition: If all the bricks are gone, the ninja has won. Show a message:
if not stenen:
# show win screenExplosion sound when a brick breaks (use snd_explode from the starter):
if snd_explode:
snd_explode.play()Game over at 0 lives: show a screen and stop the ball.
Speeding up as fewer bricks are left:
snelheid_factor = 1 + (1 - len(stenen) / totaal_stenen) * 0.5
ball_dx_huidig = ball_dx * snelheid_factorA new row of bricks when everything is gone: call stenen = maak_stenen() again and raise the difficulty.
Who clears the most bricks? Compare scores and show your coach the longest streak.
“Next time: gravity. Your character falls, and you have to make it jump on platforms.”
View the cheatsheet for this session
Dojo Defender gets a menu and new enemy types! Download the starter. Were you not there? Download the solution from session 3. Worksheet: Dojo Defender Session 4.
In this session you build a micro-platformer. Your player has gravity, can jump, and collects stars on platforms. For the first time you use pygame directly, without pgzrun, and you add sound effects with pygame.mixer.
vel_y variablevel_y negativepygame.Rectpygame.mixermain.py in Thonny.In a platformer the player falls down all the time, unless they’re standing on a platform. We simulate that with vel_y: a vertical speed that grows every frame because of gravity.
Find the # STEP 1 comment in main.py and replace the pass with:
# Apply gravity
vel_y += GRAVITY
player_rect.y += vel_y
# Check whether the player is standing on a platform
on_ground = False
for platform in platforms:
if (player_rect.colliderect(platform) and vel_y > 0):
player_rect.bottom = platform.top
vel_y = 0
on_ground = TrueGRAVITY = 0.5 is already at the top of the file. Click Run. The player now falls onto the nearest platform and stays there.
What happens here? Every frame vel_y gets a bit bigger (the player accelerates downwards). If the player touches a platform while falling down (vel_y > 0), you put their bottom edge (bottom) exactly on top of the platform and stop the fall (vel_y = 0).
A platformer without jumping is only half finished. Press SPACE to jump: that’s nothing more than suddenly making vel_y negative.
Find the # STEP 2 comment and add:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player_rect.x -= SPEED
if keys[pygame.K_RIGHT]:
player_rect.x += SPEED
# Jumping — only when you're standing on the ground
if keys[pygame.K_SPACE] and on_ground:
vel_y = -12Click Run and test:
Tip: If the jump feels too high or too low, change -12 to -10 (jump lower) or -15 (jump higher).
Let’s add something to collect. The starter already has a stars list with pygame.Rect objects. You write the collision detection.
Find the # STEP 3 comment:
for star in stars[:]: # [:] so you can remove safely while looping
if player_rect.colliderect(star):
stars.remove(star)
score += 1And in the drawing function (look for # DRAW STARS):
for star in stars:
pygame.draw.polygon(screen, YELLOW, star_points(star.centerx, star.centery, 12, 5))star_points() is already defined in the starter. It works out the points of a 5-pointed star.
At the top of the game there’s score = 0. Draw the score on the screen (look for # DRAW SCORE):
score_surf = font.render(f"Stars: {score}", True, WHITE)
screen.blit(score_surf, (10, 10))Now you add a jump sound and a win screen for when all the stars have been collected.
Loading sound (this is already in the starter; check that jump.wav is in the sounds/ folder):
pygame.mixer.init()
jump_snd = pygame.mixer.Sound("sounds/jump.wav")Playing the sound when you jump: add it to the jump code:
if keys[pygame.K_SPACE] and on_ground:
vel_y = -12
jump_snd.play()Win screen: add it after the star loop:
if len(stars) == 0:
win_surf = font.render("You win! All stars collected!", True, YELLOW)
screen.blit(win_surf, (WIDTH // 2 - win_surf.get_width() // 2, HEIGHT // 2))
pygame.display.flip()
pygame.time.wait(3000)
running = FalseShow your game to a coach and a buddy. Can you collect all the stars before the time runs out?
Today’s Dojo Defender snapshot builds on session 5:
Download the Dojo Defender starter or solution.
“Next time: click on bugs and ZAP them away, with your own classes.”
Try one of these extensions at home:
Bugs fall down from the top. You shoot up to hit them. In this session you learn OOP: you write your first class of your own (Bug), and then two subclasses: one that swings and one that dives at you. Classes are the building blocks of almost every big program.
__init__ and your own methodsmain.py in Thonny.The Bug class
Find the # STEP 1 — BUG CLASS comment and write:
class Bug:
def __init__(self, x, y):
self.x = x
self.y = y
self.alive = True
def update(self):
self.y += 2 # falls downwards
def draw(self, screen):
pygame.draw.circle(screen, GREEN, (int(self.x), int(self.y)), 15)
# Eyes
pygame.draw.circle(screen, BLACK, (int(self.x) - 5, int(self.y) - 4), 3)
pygame.draw.circle(screen, BLACK, (int(self.x) + 5, int(self.y) - 4), 3)Spawning bugs: the starter already has a timer. Find # SPAWN BUG:
bugs.append(Bug(random.randint(30, WIDTH - 30), -20))Shooting: find # SHOOT (this is in the event loop at K_SPACE):
bullets.append(pygame.Rect(player_x - 3, player_y - 20, 6, 12))Update the bullets every frame (find # UPDATE BULLETS):
for b in bullets[:]:
b.y -= 10
if b.y < 0:
bullets.remove(b)
pygame.draw.rect(screen, YELLOW, b) # this is already thereNow you write the collision detection: when a bullet hits a bug, the bug dies.
Find # STEP 2 — COLLISION:
for bug in bugs[:]:
bug.update()
bug.draw(screen)
if bug.y > HEIGHT + 20:
bugs.remove(bug)
continue
bug_rect = pygame.Rect(bug.x - 15, bug.y - 15, 30, 30)
for bullet in bullets[:]:
if bullet.colliderect(bug_rect):
bug.alive = False
bullets.remove(bullet)
score += 1
break
if not bug.alive:
bugs.remove(bug)Click Run. Shoot at the bugs: they disappear when you hit them!
Now you make a subclass: a Bug that swings back and forth.
import math
class SlingerBug(Bug):
def __init__(self, x, y):
super().__init__(x, y)
self.start_x = x
self.timer = 0
def update(self):
self.timer += 0.05
self.x = self.start_x + math.sin(self.timer) * 60
self.y += 1.5 # a bit slower, but harder to hit
def draw(self, screen):
super().draw(screen)
# Orange circle around it as a visual cue
pygame.draw.circle(screen, ORANGE, (int(self.x), int(self.y)), 18, 2)Spawning SlingerBugs: add this to the spawn code (for example every 3rd bug):
if random.random() < 0.3:
bugs.append(SlingerBug(random.randint(30, WIDTH - 30), -20))
else:
bugs.append(Bug(random.randint(30, WIDTH - 30), -20))super().__init__(x, y) calls the __init__ of the parent class (Bug), so you don’t have to write self.x, self.y and self.alive again.
The DuikBug detects the position of the player and dives straight at it.
class DuikBug(Bug):
def __init__(self, x, y, player_x_ref):
super().__init__(x, y)
self.player_x_ref = player_x_ref # function that gives the current player x
self.speed_x = 0
self.speed_y = 1
def update(self):
target_x = self.player_x_ref()
dx = target_x - self.x
dist = max(abs(dx), 1)
self.x += dx / dist * 2
self.y += 2.5
def draw(self, screen):
super().draw(screen)
pygame.draw.circle(screen, RED, (int(self.x), int(self.y)), 18, 2)Spawning: use a lambda to pass the player x:
bugs.append(DuikBug(random.randint(30, WIDTH - 30), -20, lambda: player_x))Bonus: Draw an HP bar. Start with lives = 3 and take one off when a bug reaches the bottom.
Show your game to a coach and a buddy. How many bugs can you zap before your lives run out?
“Next time: an endless highway in the sky, and everything SCROLLS past.”
Try one of these extensions at home:
lives system with 3 lives, and when a bug reaches the bottom you lose one.PantserbBug that needs 3 hits before it dies (use an hp attribute).In this session you also keep building on Dojo Defender. You add a boss that shows up every 5 waves with 3 phases.
View the Dojo Defender session 6 for the instructions.
The starter and solution are ready for you.
Your ship flies through an endless highway in the sky. The background scrolls past you, red obstacles come from the right, and stars give points. The longer you survive, the faster it gets!
scroll_x variablemain.py in Thonny.A scrolling background gives the illusion of movement. We draw the background twice next to each other and shift the position every frame.
Find # STEP 1 and add:
# Move the background to the left
scroll_x -= speed
if scroll_x <= -WIDTH:
scroll_x = 0
# Draw the background twice
screen.blit(bg_image, (int(scroll_x), 0))
screen.blit(bg_image, (int(scroll_x) + WIDTH, 0))scroll_x starts at 0 and gets smaller every frame. When it reaches -WIDTH we jump back to 0: a seamless loop.
Click Run. The sky now scrolls past your ship!
Obstacles come from the right and move to the left. If you hit them, you lose a life.
Updating obstacles (find # STEP 2):
for obs in obstacles[:]:
obs.x -= speed
if obs.x < -obs.width:
obstacles.remove(obs)
pygame.draw.rect(screen, RED, obs)
# Collision
if ship_rect.colliderect(obs):
obstacles.remove(obs)
lives -= 1
if lives <= 0:
running = FalseDrawing lives:
for i in range(lives):
pygame.draw.rect(screen, RED, (10 + i * 25, 10, 18, 18))Spawning obstacles (in the spawn timer event):
h = random.randint(40, 120)
obstacles.append(pygame.Rect(WIDTH, random.randint(50, HEIGHT - h - 50), 30, h))Stars give points and disappear when you hit them.
Find # STEP 3:
for star in stars[:]:
star.x -= speed
if star.x < -20:
stars.remove(star)
continue
pygame.draw.circle(screen, YELLOW, star.center, 10)
if ship_rect.colliderect(star):
stars.remove(star)
score += 1
# Drawing the score:
score_surf = font.render(f"Score: {score}", True, WHITE)
screen.blit(score_surf, (WIDTH - 120, 10))Spawning stars (in the same spawn timer, but less often):
if random.random() < 0.4:
stars.append(pygame.Rect(WIDTH, random.randint(40, HEIGHT - 40), 20, 20))Now you make the game harder and harder as the score climbs.
Scaling the speed (find # STEP 4):
speed = BASE_SPEED + score * 0.1BASE_SPEED = 4 is already at the top. At score 20 everything goes twice as fast!
Adjusting the spawn interval: the higher the score, the more often new obstacles come:
spawn_interval = max(500, 1800 - score * 30) # min. 500ms
pygame.time.set_timer(SPAWN_EVENT, spawn_interval)Game over screen:
if not running:
go_surf = font.render("GAME OVER", True, RED)
screen.blit(go_surf, (WIDTH // 2 - go_surf.get_width() // 2, HEIGHT // 2 - 30))
sc_surf = font.render(f"Score: {score}", True, WHITE)
screen.blit(sc_surf, (WIDTH // 2 - sc_surf.get_width() // 2, HEIGHT // 2 + 10))
pygame.display.flip()
pygame.time.wait(3000)Show your game to a coach and a buddy. Who gets the highest score?
“Next time: a boss that fights back, in THREE phases.”
Try one of these extensions at home:
Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.
Today you also keep building on Dojo Defender — but not with step-by-step instructions. You decide what you build.
Download the Dojo Defender starter — this is the complete game with everything in it (ship, enemies, asteroids, waves, boss). Pick a feature from the list, plan it briefly with a coach, and get started.
👉 Everything about Dojo Defender is on the Dojo Defender page.
The boss is waiting for you. It moves back and forth, shoots bullets and gets more dangerous as its HP drops. You shoot back, keep track of its HP on a bar, and survive three phases. This is the hardest session, and the most satisfying one if you make it through.
main.py in Thonny.The boss moves back and forth across the screen. When it hits the edge, it turns around.
Find # STEP 1 and add:
boss_rect.x += boss_speed
# Turn around at the edge
if boss_rect.right >= WIDTH or boss_rect.left <= 0:
boss_speed = -boss_speedboss_speed = 3 is already at the top. Click Run: the boss thunders from left to right!
Moving the player and shooting (find # PLAYER):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] and player_rect.left > 0:
player_rect.x -= PLAYER_SPEED
if keys[pygame.K_RIGHT] and player_rect.right < WIDTH:
player_rect.x += PLAYER_SPEED
# Shoot (find # SHOOT in the event loop):
player_bullets.append(pygame.Rect(player_rect.centerx - 3, player_rect.top - 15, 6, 15))Collision player bullet with boss:
Find # STEP 2:
for b in player_bullets[:]:
b.y -= 8
if b.y < 0:
player_bullets.remove(b)
continue
pygame.draw.rect(screen, YELLOW, b)
if b.colliderect(boss_rect):
player_bullets.remove(b)
boss_hp -= 1
if boss_hp <= 0:
running = False # player wins!Drawing the HP bar (find # HP BAR):
# Background of the bar
pygame.draw.rect(screen, DARK_RED, (10, 10, 300, 20))
# Filled bar based on the current HP
breedte = int(300 * boss_hp / BOSS_MAX_HP)
pygame.draw.rect(screen, RED, (10, 10, breedte, 20))
# Label
hp_surf = font.render(f"Boss HP: {boss_hp}", True, WHITE)
screen.blit(hp_surf, (320, 10))Click Run. Shoot at the boss: the HP bar shrinks!
The boss fires a bullet downwards every N frames.
Find # STEP 3:
boss_shoot_timer += 1
if boss_shoot_timer >= boss_shoot_interval:
boss_shoot_timer = 0
boss_bullets.append(pygame.Rect(boss_rect.centerx - 4, boss_rect.bottom, 8, 16))Updating boss bullets:
for b in boss_bullets[:]:
b.y += 6
if b.y > HEIGHT:
boss_bullets.remove(b)
continue
pygame.draw.rect(screen, ORANGE, b)
if b.colliderect(player_rect):
boss_bullets.remove(b)
player_lives -= 1
if player_lives <= 0:
running = FalseDraw player_lives as hearts or red blocks at the bottom of the screen.
Now the boss reacts to its HP:
Find # STEP 4:
if boss_hp > 50:
boss_phase = 1
boss_speed_abs = 3
boss_shoot_interval = 60
elif boss_hp > 25:
boss_phase = 2
boss_speed_abs = 5
boss_shoot_interval = 35
else:
boss_phase = 3
boss_speed_abs = 7
boss_shoot_interval = 20
# Adjust the speed without changing direction:
boss_speed = boss_speed_abs * (1 if boss_speed > 0 else -1)Phase 3: double bullets (add this in the shoot code):
if boss_phase == 3:
boss_bullets.append(pygame.Rect(boss_rect.left + 10, boss_rect.bottom, 8, 16))
boss_bullets.append(pygame.Rect(boss_rect.right - 18, boss_rect.bottom, 8, 16))
else:
boss_bullets.append(pygame.Rect(boss_rect.centerx - 4, boss_rect.bottom, 8, 16))Win screen at boss_hp <= 0:
screen.fill(BLACK)
win = font.render("BOSS DEFEATED!", True, YELLOW)
screen.blit(win, (WIDTH // 2 - win.get_width() // 2, HEIGHT // 2))
pygame.display.flip()
pygame.time.wait(3000)Show your game to a coach and a buddy. Who reaches phase 3 and survives?
“Next time: your own game, from scratch, completely your own idea.”
Try one of these extensions at home:
This is your session. No fixed steps, no compulsory mechanics. You decide what you build, and today you make it.
In nine sessions you learned:
All those building blocks are ready. Today you combine them in your own game.
No idea yet? Here are a few genres to start from:
Dodge game: dodge objects that come faster and faster. Simple but addictive.
Platformer: jump from platform to platform, collect items, avoid enemies.
Shooter: shoot enemies that come from above or from the side.
Puzzle game: slide blocks, connect dots, or solve a pattern.
Racing game: design a course and get to the finish as fast as you can.
Tell a story: no score, no lives. Let the player click and make choices.
You can also combine things: a shooter with puzzle elements, a platformer with a boss at the end. Anything goes.
Before you write code, answer these three questions:
Write your answers on a piece of paper or in a comment at the top of your code. That helps you stay focused when you get stuck.
Do you want a loop animation, an explosion or a blinking object? Make a list of image names and switch actor.image every N milliseconds. The demo below shows how that works: use the slider to change the speed and the button to pause:
In Python you then use a list of image names and a timer:
frames = ['lopen_1', 'lopen_2', 'lopen_3', 'lopen_4']
anim_timer = 0
FRAME_DUUR = 10 # frames (at 60 fps = ~167 ms)
def update():
global anim_timer
anim_timer += 1
speler.image = frames[(anim_timer // FRAME_DUUR) % len(frames)]Download the starter via this link. The starter has:
draw() and update() sectionOpen main.py and start with Step 1 of your own plan. The cheatsheet on this page has all the patterns from the previous sessions at hand.
Stuck? Ask a coach. Describe what you want to achieve, not just what isn’t working. Then we can work out the best approach together.
Finished too fast? Add sound, make a start screen, or add a second player.
At the end of the session everyone presents their game. You don’t have to finish it. Show what you made and tell:
CoderDojo stops after ten sessions. Building things yourself doesn’t stop.
Keep building. Small projects teach you more than big courses. Make a game for your brother or sister, your pet, or just for yourself.
Need inspiration?
CoderDojo will be back. Will you?
Five sessions about Git and GitHub, from your first commit to pull requests. Plus a bonus session about Markdown, the language you write notes and documentation in.
.gitignore and workflow tips.Imagine: you work on your game for three hours. You add a cool new weapon. But… now the rest of your game doesn’t work anymore. And you no longer know what exactly you changed. Three hours of work gone.
Git is the solution. Git remembers every version of your files. Like an endless “undo” button that never forgets what you did.
In this session you install Git, you make your first repository and you record what you do for good: step by step, version by version. And the fun part: Git doesn’t only work for code. Also for notes, school reports and stories.
Git is a version control system. It keeps track of what you change in your files, when you did it, and why. Every time you finish a piece of work, you make a commit: a snapshot of all your files at that moment.
What makes Git special:
Git was made by Linus Torvalds, the same person who built Linux. He needed something to work on the Linux kernel with thousands of programmers at the same time. In 2005 he built Git in a weekend. Today almost every developer in the world uses it.
Git was built for source code, but it works with every text file. A text file is any file you can open and read with Notepad or a simple editor: .py, .html, .css, .json, .txt, .csv, .md (Markdown), .yml, .toml, you name it.
What Git is not good at: binary files. Those are files that don’t consist of readable text: images (.png, .jpg), videos (.mp4), compiled programs (.exe), Word documents (.docx). Git can track them, but it is slower and you can’t see what exactly changed. With text files git diff shows every letter you added or removed. With a .png Git only says “this file has changed”, but not what changed about the image.
Many note-taking apps these days use Markdown (.md files) to store your documents. Think of:
.md files in a folder on your computerBecause Markdown files are ordinary text files, you can let Git loose on them. You keep the history of your notes, you can go back to a previous version of a document, and you can share your notes via GitHub, exactly like you do with code.
Putting a folder of Markdown notes under Git is just as simple as a Python project:
mkdir mijn-notities
cd mijn-notities
git init
echo "# My Notes" > start.md
git add start.md
git commit -m "First note: start page"From that moment on Git keeps every change to your notes. Writing a report for school in Markdown? Commit every paragraph. Working on worldbuilding for a story? Git remembers every version of your plot.
| Do use Git | Better use something else |
|---|---|
Notes in Markdown (.md) | Notes in Word (.docx) |
Configuration files (.json, .yml) | Large images or videos with your notes |
| Writing: reports, stories, blogs | Real-time collaboration like Google Docs (Git is too slow for that) |
| Your CoderDojo cheatsheets | Passwords or personal diaries (unless private repo) |
In short: anything that is text you can put in Git. Anything that is binary, better not.
Without Git your workflow looks like this:
game.py
game_backup.py
game_backup2.py
game_final.py
game_final_echt.py
game_final_echt_nieuwe_wapens.pyRecognise that? With Git you only need one file: game.py. Git remembers the rest.
What version control gives you:
Git is made for text files: files you write yourself: code, notes, configuration. There are things you’d better keep out of Git:
| Do put in Git | Do NOT put in Git |
|---|---|
Your Python files (.py) | Passwords, API keys, tokens |
| Images your game uses | Files your program creates itself (.exe, .pyc) |
Sound files (.wav, .mp3) | The __pycache__ folder |
| Text files, configuration | Large files (>50 MB) like videos |
Your README.md | Settings of your own specific computer |
Why no passwords in Git? Because Git remembers everything, forever. Even if you delete a password later, it is still in the history. Anyone who ever sees your code can scroll back to that commit. And if your code ever ends up on GitHub, your password is on the internet. Forever.
Use a .gitignore file to tell Git which files it should ignore. You’ll find out about that in a moment.
Open Terminal (search for “Terminal” in Spotlight) and type:
git --versionIf Git is not installed, macOS asks you to install the developer tools. Click “Install”. Done.
sudo apt install gitOpen a terminal (Git Bash on Windows) and type:
git --versionYou see something like git version 2.47.0. That means: Git is ready.
Git wants to know who you are, so it can put your name on every commit. You do this once, after that Git remembers it.
git config --global user.name "Your Name"
git config --global user.email "your@email.com"Use the same email address you will use for GitHub later. That makes linking easier.
Tip: --global means: this setting applies to all your Git projects. You only have to do this once.
A repository (or “repo”) is a folder where Git keeps track of the versions. Let’s make one.
First make a new folder for your project:
mkdir mijn-eerste-git-project
cd mijn-eerste-git-projectNow you tell Git: “This folder is a repository.”
git initYou see: Initialized empty Git repository in .../mijn-eerste-git-project/.git/
Git has created a hidden .git folder. That is where it keeps everything: the history, the commits, the configuration. Never touch that folder by hand, because Git manages it itself.
Create a file. For example a Python file with one line:
echo 'print("Hello Git!")' > hallo.pyNow you tell Git: “Keep an eye on this file.”
git add hallo.pyWith git add you put files in the staging area: a waiting room. You say to Git: “I want these changes in my next commit.”
Check what is ready:
git statusGit shows:
On branch master
Changes to be committed:
new file: hallo.pyNow comes the commit itself:
git commit -m "My first commit: added hallo.py"What happens here? -m stands for “message”. The text between quotes is your commit message: a short explanation of what you changed. Git has now made a permanent snapshot of your project.
Good commit messages: Write in English (that is the convention in almost all projects). Start with a verb: “Add”, “Fix”, “Update”. Keep it short: 72 characters max.
Open hallo.py in a text editor. Change the line to:
print("Hello Git! You remember my code.")Save the file. Now look at what Git sees:
git statusGit knows that hallo.py has changed. But what exactly has changed?
git diffgit diff shows exactly which lines you added (green with +) and removed (red with -). This is one of the most powerful things about Git: you literally see what changed, character by character, since your last commit.
Commit the change:
git add hallo.py
git commit -m "Update: greeting extended"This is where Git gets magical. Suppose: you regret your last change. You want to go back to the first version.
First show all commits:
git log --onelineYou see something like:
a1b2c3d Update: greeting extended
e4f5g6h My first commit: added hallo.pyEvery commit has a unique code, a hash. That long code (shortened to 7 characters) identifies exactly that one version of your project.
To see what hallo.py looked like in the first commit:
git show e4f5g6h:hallo.pyThere is your original print("Hello Git!") again.
Do you really want to go back in time? That is possible:
git checkout e4f5g6h -- hallo.pyNow hallo.py is back the way it was in your first commit. Check with cat hallo.py. You see the old version. But don’t worry: your second commit is not gone. With git checkout master -- hallo.py you go back to the newest version.
.gitignoreCreate a file that Git should ignore:
echo "GEHEIM_WACHTWOORD=12345" > geheimen.txtgit status now shows this file as “untracked”. Git has seen it, but doesn’t know if you want to track it. You can ignore it with a .gitignore file:
echo "geheimen.txt" > .gitignoreNow run git status again and geheimen.txt has disappeared from the list. Git ignores it. (Do add .gitignore itself to Git with git add .gitignore and git commit, so others know what has to be ignored too.)
A good .gitignore for a Python project:
# Python
__pycache__/
*.pyc
*.pyo
# Secrets
*.env
secrets.txt
# Your editor
.vscode/
.idea/Show a coach and a buddy:
git log --oneline: your commit history.git diff that shows what you changed..gitignore file.“Next time: two versions of your code at the same time. You work on a new feature while the old version keeps working. That’s called branches, and it’s what makes Git really powerful.”
git diff to look at the change, and commit it with a good message.git show to look at each version. Use git checkout to go back to the first commit, and then to the last one again..gitignore. Make sure __pycache__/ and .pyc files really are ignored (check with git status).Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing. Learning Git is doing.
Last session you learned to make commits: snapshots of your code on one timeline. Everything neatly one after the other.
But what if you want to try out a new idea? Adding a laser gun to your game, for example. You don’t know whether it will work. You don’t want to break your working code.
That is what branches are for.
A branch is a copy of your code where you can experiment freely. Your main code (the master branch) stays safe. If your experiment works, you paste it back into master. Doesn’t it work? You throw the branch away. Your master branch never noticed a thing.
Note: On GitHub and in many new projects the main branch is called
maininstead ofmaster. That is exactly the same, only the name is different. In this course we usemaster, but if you come acrossmainonline, you know it means the same thing.
Open your terminal in last session’s project. Check where you are:
git status
git log --onelineYou have a few commits on master. This is your stable base: you’ll branch off from here in a moment.
You’re going to add a new feature: a highscore.txt file that keeps the highest score. But first you make a branch:
git branch highscore-featureGit has now created a new branch. But you are still on master. Check which branches there are:
git branchYou see:
highscore-feature
* masterThe asterisk next to master means: “this is where you are now.” Switch to your new branch:
git switch highscore-feature(Older Git versions use git checkout highscore-feature. Both work.)
git branch one more time: the asterisk is now next to highscore-feature. Everything you commit now happens on this branch. master stays untouched.
Tip: You can create a branch and switch to it straight away in one command:
git switch -c mijn-nieuwe-branch-c stands for “create”. This is faster and you never forget to switch.
Create a new file:
echo "HIGHSCORE: 0" > highscore.txtCommit your work:
git add highscore.txt
git commit -m "Add highscore.txt with start value 0"Add another file that reads the highscore:
echo 'def lees_highscore():' > highscore_manager.py
echo ' with open("highscore.txt") as f:' >> highscore_manager.py
echo ' return int(f.read().split(": ")[1])' >> highscore_manager.pyCommit again:
git add highscore_manager.py
git commit -m "Add highscore_manager: function to read the highscore"Now look at your log:
git log --onelineYou see all your commits from master, plus the two new ones. Git remembers the whole chain.
Switch back to master:
git switch masterLook at what is here:
lshighscore.txt and highscore_manager.py are… gone. They only exist on the highscore-feature branch. On master everything is still exactly as you left it.
This is the magic of branches: two (or more) versions of your project, at the same time on your computer, without getting in each other’s way.
Switch back and forth once more to feel it:
git switch highscore-feature # the files are back
git switch master # the files are gone againgit mergeYour highscore feature works. Time to put it back into master. This is called merging.
Make sure you are on master:
git switch masterMerge the branch:
git merge highscore-featureGit shows something like Updating a1b2c3d..e4f5g6h and Fast-forward. Your highscore-feature commits are now part of master. Check:
ls
git log --onelinehighscore.txt and highscore_manager.py are now on master. All commits from the feature branch have been included in the history.
Suppose: you have changed something on master in highscore.txt, for example “HIGHSCORE: 100” instead of “HIGHSCORE: 0”. And at the same time you have changed highscore.txt on another branch too, to “HIGHSCORE: 500”.
If you now try to merge, Git doesn’t know which version is the right one. That is a merge conflict. Git asks you to decide.
Let’s provoke it on purpose.
On master:
echo "HIGHSCORE: 100" > highscore.txt
git add highscore.txt
git commit -m "Update highscore to 100 on master"Make a new branch and change the same file there:
git switch -c andere-highscore
echo "HIGHSCORE: 500" > highscore.txt
git add highscore.txt
git commit -m "Update highscore to 500"Switch back to master and try to merge:
git switch master
git merge andere-highscoreGit says: CONFLICT (content): Merge conflict in highscore.txt. Open highscore.txt in your editor. You see:
<<<<<<< HEAD
HIGHSCORE: 100
=======
HIGHSCORE: 500
>>>>>>> andere-highscoreThese are the two versions. HEAD is your master version. Below it is the version from andere-highscore. You choose which one you keep, or you combine them.
Remove the markers and choose one version:
HIGHSCORE: 500Save the file. Tell Git that the conflict is resolved:
git add highscore.txt
git commit -m "Merge: chose highscore from andere-highscore"Conflict solved. Git can carry on again.
Your feature is merged. You don’t need the branch anymore:
git branch -d highscore-feature
git branch -d andere-highscore-d stands for “delete”. Git refuses to delete a branch that hasn’t been merged yet: that is a safety valve. If you really want to throw it away without merging, you use -D (capital letter). But be careful with that.
| Do use a branch | No branch |
|---|---|
| New feature that takes time | Fixing a typo in a comment |
| Experiment you don’t know will work | Small bugfix of one line |
| Something that can break your working code | You work alone and your change is clear |
| You work together with others | (with solo work master is often enough) |
Rule of thumb: If you doubt, make a branch. A branch costs nothing. Losing code does.
master in the meantime, and the bigger the chance of conflicts.master straight away. Small, frequent merges are easier than one big monster merge.highscore-feature is good. test123 or branch2 are bad. In a week you won’t remember what was in test123.git branch or git status tells you which branch you are on. Make this a habit, so you avoid accidentally writing code on master that really belongs on a feature branch.Show a coach and a buddy:
git branch with at least two branches (including master).master.“Until now all your code only lives on your laptop. Next time we put it online. You make a GitHub account, push your repository to the internet, and your game can be seen all over the world. And: you can work together with others. GitHub: your code out into the world.”
master one by one. Check after every merge whether everything still works.master, make a change in the same file there too, and merge the branch. Solve the conflict carefully and check with git log --graph what the history looks like.Until now all your code lives on your laptop. Handy for you, but nobody else can get to it. And if your laptop crashes, you lose everything.
GitHub is a website where you put Git repositories online. It is a backup, a portfolio, and a collaboration platform in one. Today you put your code on GitHub. At the end of this session anyone with an internet connection can look at your projects, and you at theirs.
GitHub is not the same as Git. Git is the version control system that runs on your computer. GitHub is a website (github.com) that hosts Git repositories: a kind of Google Drive for code, but with Git underneath.
What GitHub adds to Git:
GitHub is not the only option; there are alternatives such as GitLab and Bitbucket. But GitHub is the biggest, with more than 100 million users. In these sessions we use GitHub.
git config: that saves hassle.Tip for coaches: check whether the ninjas have access to their email during the session. Some laptops are not logged in to webmail. Have a plan B ready: if email verification doesn’t work, ninjas can verify GitHub later at home. The rest of the session works without verification too.
You have a local project on your computer (from session 11). You are going to put it online now.
On GitHub, click the + icon at the top right → New repository.
Fill in:
| Field | What you fill in |
|---|---|
| Repository name | mijn-eerste-git-project (same as your local folder) |
| Description | “My first Git project (CoderDojo session 11)” |
| Public / Private | Choose Public: others may see your code |
| Initialize with README | Do NOT tick: you already have a local project |
Click Create repository.
GitHub now shows instructions. Choose the block under "…or push an existing repository from the command line". Copy those three commands: you’ll need them in a moment.
In your terminal (in your project folder):
git remote add origin https://github.com/JOUW_USERNAME/mijn-eerste-git-project.gitWhat happens here? remote is an external location where your repo also lives. origin is the name you give that location. “origin” is the default name for your most important remote. You can have several remotes (for example origin for GitHub, backup for GitLab), but one is usually enough.
Check whether it worked:
git remote -vYou see:
origin https://github.com/JOUW_USERNAME/mijn-eerste-git-project.git (fetch)
origin https://github.com/JOUW_USERNAME/mijn-eerste-git-project.git (push)fetch = this is where you get code from. push = this is where you send code to. Usually they are the same.
git pushNow comes the moment: sending your local commits to GitHub.
git push -u origin masterpush = send my commits to the remote. -u origin master = “remember that master on my computer belongs with master on origin.” Next time you can simply type git push, because Git remembers the link.
GitHub asks you to log in. There are two ways:
repo, copy the token, and paste it as the password in the terminal.For ninjas: method 1 (browser) is the easiest. If that doesn’t work, help with a token.
After a successful push, refresh your GitHub page. Your files are there! Your hallo.py, your commit history: everything.
Make a change locally:
echo 'print("GitHub is cool!")' >> hallo.py
git add hallo.py
git commit -m "Add GitHub enthusiasm"Push to GitHub:
git pushYou no longer need -u origin master, because Git knows the way. Refresh GitHub: your new commit is there.
This is the reverse route: you start on GitHub and bring it to your computer.
On GitHub: New repository. Call it python-spelletje. This time do tick Add a README file; GitHub then makes a repo with one file in it. Click Create repository.
Now you bring this repo to your computer. This is called cloning:
cd ~ # go to your home folder
git clone https://github.com/JOUW_USERNAME/python-spelletje.gitGitHub creates a folder python-spelletje with the README in it. And it is a full Git repository straight away: the .git folder is already in it. You don’t need to do git init.
cd python-spelletje
git log --oneline # you see the first commit GitHub made for youNow you can carry on working locally and push your commits to GitHub, exactly like in Steps 3 to 4.
Suppose: you worked on python-spelletje at home and pushed. Now you are on the dojo laptop and want to carry on. You don’t have the newest version yet.
git pullgit pull fetches the newest commits from GitHub to your computer. It is two actions in one: git fetch (fetching) + git merge (merging with your local code).
Always do git pull before you start working, otherwise you fall behind and you get merge conflicts.
Summary of the workflow:
| Command | What it does |
|---|---|
git pull | Fetch the newest version from GitHub |
Work on your code, make commits (git add, git commit) | |
git push | Send your commits to GitHub |
Remember: pull before you start, push when you’re done.
Show a coach and a buddy:
git push that has just arrived on GitHub.“Now your code is online. But what do you put in a commit message? How do you write a message that is still clear in three months? And how do you work with a team without breaking things? Next time: the professional side of Git: best practices every developer uses.”
git pull.You can type Git commands. Your code is on GitHub. But there’s a difference between “using Git” and “using Git well.”
This session is about the habits experienced developers apply every day. No new commands, just sharper rules. At the end you write commit messages you’ll still get something out of a year from now, and you work with a branching strategy that keeps your project tidy.
git stash is and when to use itA bad commit message looks like this:
git commit -m "fix"
git commit -m "update"
git commit -m "changed stuff"Three months from now you look back. What was “fix”? Which “things”? You don’t know any more. You have to dig through the code to work out what happened. That’s a waste of time.
A professional commit message has three parts:
1. A short summary (max 72 characters)
Write in English. Start with a verb in the present tense. This is called “imperative mood”: as if you’re giving an order to the codebase. Think: “Add”, “Fix”, “Remove”, “Update”, “Refactor”.
Add highscore tracking
Fix star spawning off-screen
Remove unused collision code2. A blank line
3. A longer explanation (optional, if it’s needed)
Add highscore tracking
Saves the highest score in highscore.txt. The score is
updated after every game-over. The file is created if it
doesn't exist yet.
This replaces the old method where the score only lived in
memory and got lost when the game closed.The longer explanation tells why you changed something, not just what. The code itself already shows the “what”. The “why” is what you need in three months.
| Do this | Not this |
|---|---|
| “Add player health bar” | “added health bar thing” |
| “Fix collision detection on left wall” | “fix” |
| “Remove debug print statements” | “cleanup” |
| Max 72 characters for the title | A whole story in the title |
| Present tense, English | Past tense (“added”, “fixed”) |
Why English? Because nearly every open source project uses English. Code is international. You write your Python code in English too (def calculate_score). Your commit messages follow that same language.
Why present tense? A commit describes what this change does, not what you did. “Add feature” (this commit adds a feature), not “Added feature” (I added a feature once). Git itself uses present tense too: Merge branch 'feature', not Merged.
You can write a commit message over several lines by not closing the quotes:
git commit -m "Add highscore tracking
Saves the highest score in highscore.txt. The file is
created automatically on the first game-over."Note: you only type the closing quote on the last line. Everything in between is part of your message.
Commit often. Commit small.
Not: working for three hours, one enormous commit with 47 changed files. Yes: a commit every 15 to 30 minutes, each time one logical piece is done.
A rule of thumb: if your commit message contains “and”, your commit is too big. “Add highscore tracking and fix menu bug and update sprites” → those should be three separate commits.
Small commits make it easier to:
You already know how to make branches (session 12). Now: when and why.
master always worksThe master branch is your shop window. Someone who clones your repo has to be able to run your code without error messages. master never contains half-finished work.
Not OK on master: code with # TODO: still needs finishing, commented-out functions, experiments you’ll “fix later.”
Fine on master: working code. Full stop.
A branch does exactly one thing. No more.
feature/player-health-barfeature/health-bar-and-menu-and-sound-effectsIf, while working on a feature, you think: “Oh, I can quickly fix that menu bug too”: stop. Make a new branch for the bugfix. Merge that one first. Then carry on with your feature.
A branch that exists for more than a few days becomes a problem. master changes in the meantime, and your branch falls behind. The longer you wait to merge, the bigger the chance of merge conflicts.
Merge your branch as soon as the feature is done. For small things (a 10-minute bugfix): you might not even need a separate branch.
Give branches names you’ll still understand a week from now:
feature/highscore-tracking
bugfix/star-spawn-offscreen
experiment/particle-effectsThe prefix (feature/, bugfix/, experiment/) immediately tells you what kind of work is in this branch. It’s a convention, not a technical requirement, but it helps.
git stash: putting work asideSay: you’re working on a feature branch. Your code is half done. You want to switch to master for a quick bugfix, but you don’t want to commit your half-finished work. That’s what git stash is for:
git stashGit stores all your changes temporarily and puts your working directory back to the last commit. Now you can switch, do the bugfix, commit, and come back:
git switch master
# ... do the bugfix, commit ...
git switch feature-branch
git stash popgit stash pop brings your stored changes back. Everything is as you left it: half done, but safely kept.
git pull --rebase: a cleaner historyNormally git pull does a merge. That creates an extra merge commit. Sometimes you don’t want that; you want a straight line of commits without branches.
git pull --rebaseThis fetches the newest commits and puts your local commits on top of them, as if you made them after the remote commits. The result is a linear history: no merge commits that only say “Merge branch ‘master’”.
Note: only use --rebase on branches that you alone use. On shared branches a plain git pull (merge) is safer.
Before you git push:
git pullAlways. Even if you think nobody else pushed. It takes one second and stops GitHub from refusing your push with “Updates were rejected because the remote contains work that you do not have locally.”
This rule matters most when you work together. If you’re the only one working on a repo, the risk is smaller, but the habit is worth gold.
.gitignore you always needEvery project has files that don’t belong in Git. Your .gitignore is your first line of defence against accidentally committing passwords.
Minimal .gitignore for a Python project:
# Python
__pycache__/
*.pyc
*.pyo
# Virtual environment
venv/
.venv/
# Environment variables (often contain passwords/keys)
.env
# Your editor
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.dbWhy ignore __pycache__/? These are compiled Python files that your program creates automatically. They differ per Python version and per computer. Putting them in Git only adds noise.
Why ignore .env? .env files often contain secret keys, API keys, database passwords. Those must never, ever go into Git.
Show a coach and a buddy:
.gitignore file.“Now you work with Git like a professional. But there’s one more concept that every team uses: the pull request. Your code is ready, but before it goes into master, someone else has to approve it. How does that work? Next time: pull requests, the review wall that every change has to pass through.”
feature/ prefix, work in it, merge it back, and delete it. Do the same for a bugfix/ branch.git stash to save half-finished work, switch to master, make a quick fix, push, and come back to your feature with git stash pop.git pull fails (clone your repo in two folders, make a different commit in both, push the first one, and try to push the second). Solve it: git pull, fix the merge conflict, and only then push.You built a feature on a branch. Good commit messages, tidy work. Now you want to get your code into master.
You could do git merge and be done. But in a team that doesn’t work. Someone else has to look at your code first: check for mistakes, for style, for things you missed. That is a pull request.
A pull request (PR) is a request: “Bring my changes in.” You open it on GitHub. Your teammates look at your code and give feedback. You change things. Only when everyone is happy does the code go to master.
In this session you make your first pull request. You review a buddy’s. And you find out what real teamwork with Git feels like.
Today you work together with a buddy. Agree who is the author (who builds the feature) and who is the reviewer (who looks at the PR). Halfway through you swap.
The author needs a GitHub repo that the reviewer has access to. The easiest way:
Now you can both push to the same repo.
Alternative (without collaborator access): the reviewer forks the author’s repo and opens a PR from the fork to the original. But today we keep it simple: direct collaborator access.
The author clones the repo (if that hasn’t happened yet) and creates a feature branch:
git clone https://github.com/AUTEUR_USERNAME/ons-samenwerk-project.git
cd ons-samenwerk-project
git switch -c feature/groet-functieCreate a new file:
echo 'def groet(naam):' > groet.py
echo ' return f"Hallo {naam}!"' >> groet.pyCommit and push:
git add groet.py
git commit -m "Add groet function
Returns a greeting for the given name."
git push -u origin feature/groet-functiebase: master ← compare: feature/groet-functie.)## What
A function `groet(naam)` that returns a greeting.
## Why
We need a reusable greeting for the user interface.
This stops us having to write the same string in
several places.
## How to test
```python
from groet import groet
print(groet("CoderDojo")) # "Hallo CoderDojo!"5. Click **Create pull request**.
Your PR is now open. Everyone with access to the repo can see it. GitHub shows the diff: exactly which lines you added (green) and removed (red).
## Step 3: Reviewing a PR (reviewer)
The reviewer opens the PR on GitHub. These are the steps of a review:
### 1. Read the description
Do you understand what this PR does? If not, ask for clarification.
### 2. Look at the diff
Click the **Files changed** tab. GitHub shows every changed line. Look at:
- **Does the code work?** Would you run it yourself if you cloned it?
- **Is the style consistent?** The same naming conventions as the rest of the project?
- **Are there mistakes?** Typos, missing imports, logic errors?
- **Are there no secrets in it?** Passwords, API keys, tokens?
- **Is it understandable?** Can someone seeing this code for the first time follow what happens?
### 3. Give feedback
Click a line to add a comment. Be specific:
**Good feedback:**
> In `groet.py` line 2: what happens if `naam` is empty? Maybe add a default value?
**Bad feedback:**
> This isn't good.
### 4. Submit your review
Click **Review changes**. You have three options:
- **Comment**: just feedback, no decision.
- **Approve**: the code is good, it can be merged.
- **Request changes**: something has to change before the PR can be merged.
On your first review: choose **Approve** if everything looks good, or **Request changes** if you want something changed.
## Step 4: Handling feedback (author)
The reviewer left a comment. The author changes the code:
```bash
# Make sure you're on your feature branch
git switch feature/groet-functieChange groet.py based on the feedback. For example:
def groet(naam="wereld"):
return f"Hallo {naam}!"Commit and push:
git add groet.py
git commit -m "Add default parameter to groet function"
git pushThe new commit automatically appears in the PR. The reviewer sees the update and can look again.
If the reviewer has Approved, it’s time to merge.
On GitHub, in the PR, you click Merge pull request. Choose Create a merge commit (the default option). Click Confirm merge.
The feature branch is now merged into master. On GitHub you can delete the branch with the Delete branch button that appears after merging.
Locally you have to clean up too:
git switch master
git pull # fetch the merge that just happened on GitHub
git branch -d feature/groet-functie # delete the local branchYour master is now up to date. Your feature branch is gone, locally and on GitHub. Ready for the next piece of work.
1. git switch -c feature/iets ← Create a feature branch
2. Work, commit, work, commit
3. git push -u origin feature/iets ← Push to GitHub
4. Open a PR on GitHub ← Ask for a review
5. Reviewer looks, gives feedback
6. Change code, commit, push ← Handle feedback
7. Reviewer approved
8. Merge the PR on GitHub ← Code to master
9. git switch master && git pull ← Bring master up to date locally
10. git branch -d feature/iets ← Clean up the branchA review is not: “Looks good.” A good review is specific, constructive and respectful.
Questions you ask yourself during a review:
How to give feedback:
Start with what’s good. Then be specific about what could be better. End with a clear conclusion.
Example:
The function works exactly as described. Two suggestions:
- On line 2: a default value for
naamwould be handy.- Consider adding a docstring that explains what the function returns.
Other than that it looks good. If you change these two things, I’ll gladly approve it.
Swap roles with your buddy and do the whole workflow once more: now you are the reviewer.
Show a coach:
master.This was the last Git session. What you can do now:
init, add, commit, log, diff)master (branch, switch, merge)push, pull, clone)This is not “extra”. This is how developers work every day. Whether you make games, build websites, or train AI: Git and GitHub are the tools that keep your project manageable. Use them.
good first issue label. Fork the repo, make a fix, and open a PR.You might know HTML from the internet: <h1>, <p>, <a>. But for notes and documentation HTML is far too much typing. That’s what Markdown is for.
Markdown is a mini-language you write in plain text. It looks readable without having to convert it first. And with one push of a button it becomes neat HTML: headings, lists, links, tables.
In this session you learn the whole basis of Markdown. At the end you write notes that look professional, on GitHub, in Obsidian, or in any other program that understands Markdown.
Markdown is everywhere:
README.md in Markdown. Issues, pull requests, wikis: all Markdown..md files.The big advantage: you write plain text. No complicated tags. And the result is always readable, even as plain text.
Markdown files end in .md. Make one:
mkdir mijn-notities
cd mijn-notities
echo "# My First Note" > test.mdYou can open .md files in any text editor: Notepad, VS Code, Thonny, Obsidian. VS Code has a built-in preview: click the icon with the magnifying glass and the book at the top right.
You make headings with #. The more #, the smaller the heading:
# This is a title (h1)
## This is a subtitle (h2)
### This is a smaller heading (h3)
#### Even smaller (h4)You make a paragraph by just typing text. A blank line between them starts a new paragraph:
This is the first paragraph. It just runs on.
This is the second paragraph. Because there's a blank line in between.Tip: you use one # only once per document: for the title. The rest starts at ##.
**This is bold**
*This is italic*
~~This is strikethrough~~
You can also **combine *bold and italic*** in the same sentence.The result:
**bold** becomes bold*italic* becomes italic~~strikethrough~~ becomes Note: no spaces between the asterisks and the text. ** bold ** doesn’t work. **bold** does.
Some editors also accept underscores: __bold__ and _italic_. But asterisks are the standard: use those.
You make a horizontal line with three (or more) dashes:
---Or with asterisks:
***The result is a line across the full width. Use it sparingly: one or two per document is enough. Too many lines make your notes messy.
Use -, * or +:
- Python
- Git
- Markdown
- GitHubYou can nest lists by indenting (2 or 4 spaces):
- Programming languages
- Python
- JavaScript
- Version control
- Git
- GitHub1. Open Thonny
2. Write your code
3. Click RunIt doesn’t matter which numbers you type: Markdown numbers automatically. This:
1. Step one
1. Step two
1. Step three…gives exactly the same result as 1, 2, 3. Handy if you want to add a step later without renumbering everything.
- [x] Install Git
- [x] Make your first commit
- [ ] Learn branches
- [ ] Open a pull request[x] is ticked, [ ] is open. Works in GitHub issues, pull requests, and many note-taking apps.
[click here for the CoderDojo site](https://python.coderdojohasselt.be)The word between [ ] is the clickable text. The URL between ( ) is the destination.
[View session 11](11-git-intro/)This links to the file 11-git-intro/_index.md in the same folder.
<https://github.com>This shows the URL as a clickable link, without separate text.
An image is almost the same as a link, but with a ! in front:
The text between [ ] is the alt text: it appears if the image doesn’t load, or is read out by screen readers.
Images and Git: Small images (SVG, small PNGs) you can put in Git. Big images (>1 MB) and videos better not, because they make your repo slow.
| Command | What it does |
|----------|-------------|
| `git init` | Create a new repository |
| `git add` | Get files ready for a commit |
| `git commit` | Record changes |
| `git push` | Send commits to GitHub |The | characters form the columns. The --- line separates the header from the data. The : in the separator line determines the alignment:
| Left aligned | Centred | Right aligned |
|:-----------------|:-----------:|------------------:|
| left | middle | right |Use backticks for short pieces of code in a sentence:
Use `git status` to see what has changed.For several lines of code you use three backticks:
```python
def groet(naam):
return f"Hallo {naam}!"
```The language (python) after the first backticks gives syntax highlighting. This works for dozens of languages: bash, html, css, javascript, json, yaml, markdown.
```
This is just text
without syntax highlighting
```Use > for quotes or important remarks:
> Git is not a backup. Git is a time machine.
> Someone on the internetSeveral > lines one after another form one block. An empty > line starts a new paragraph inside the quote.
A good README.md combines everything you know now. Here’s an example:
# My Project
A short description of what this project does.
## Installation
```bash
git clone https://github.com/jouw-naam/mijn-project.git
cd mijn-project
```
## Usage
1. Open `main.py` in Thonny.
2. Click **Run**.
3. Choose a level and start.
## What you'll learn
- [x] Pygame Zero basics
- [x] Collision detection
- [ ] Highscore system
- [ ] Sound effects
## Links
- [CoderDojo Hasselt](https://coderdojohasselt.be)
- [Source code on GitHub](https://github.com/jouw-naam/mijn-project)| What | Markdown |
|---|---|
| Heading level 1 | # Title |
| Heading level 2 | ## Subtitle |
| Heading level 3 | ### Smaller heading |
| Bold | **bold** |
| Italic | *italic* |
| Strikethrough | ~~strikethrough~~ |
| Horizontal line | --- or *** |
| List (unordered) | - item |
| List (numbered) | 1. item |
| Checklist | - [ ] open item |
| Link | [text](url) |
| Image |  |
| Inline code | `code` |
| Code block | ```language |
| Quote | > quote |
| Table | | column | column | |
Show a coach and a buddy:
Markdown is simple, but it’s everywhere. Your READMEs on GitHub, your notes in Obsidian, your documentation: everything becomes more readable with Markdown. Use it. Don’t overdo the formatting. Good notes are clear, not busy.
README.md for your favourite Python project. Use headings, a list and a code block.