CoderDojo: Python & Pygame

Welcome! This is the curriculum for ninjas between 10 and 17 who want to learn Python and Pygame by building games.

How does it work?

  • 10 sessions of 3 hours, about once a month (July and August excluded).
  • Every session you build a complete little game in 3 hours.
  • Miss a session? No problem: you can always join in.
  • Enjoy it, experiment, and ask a coach when you get stuck.

Dojo Defender, for the regular ninjas

For anyone doing 4 sessions or more: together we build a house game, Dojo Defender, that grows every month. Only drop by now and then? Just do that day’s session game. It is always complete on its own.

October 30, 2026, 3:59 pm

Subsections of CoderDojo: Python & Pygame

Setup

To make a Python game you need two things:

  1. Python: the programming language that runs your code. The computer understands Python, not Dutch or English.
  2. An editor: the program where you type your code. Like Word, but for code.

Never programmed before? Start with Thonny, because it installs Python along with it and is made especially for beginners.

Choose your editor

  • Thonny: ★☆☆ Easy. For beginners. Installs Python automatically. This is the starting point for all CoderDojo sessions.
  • VS Code: ★★☆ Medium. The editor professional programmers use. Good when Thonny feels too small and you want to manage several files.
  • PyCharm: ★★★ Advanced. A full IDE with a built-in debugger and smart code help. Suitable if you’ve been programming for a while already.

Next: virtual environment

Picked an editor? Then also read how virtual environments work. That is the “backpack” of every Python project — a separate folder with exactly the right packages for your game.

Not working? Have a look at Troubleshooting. It lists the most common installation errors with solutions.

October 30, 2026, 3:59 pm

Subsections of Setup

Installing Thonny + Pgzero

Quick setup

Download the script for your operating system. One double-click and it installs Python, uv, Thonny, pgzero, pygame-ce and all the starter code for you. Done in 5-10 minutes.

Your operating system is being detected...

Show all downloads

Tip

The script is safe: it only installs the same programs as the manual steps below. You can look at the source code by opening the file in Notepad or a text editor.


Manual setup

Thonny is a free Python editor made especially for beginners. It installs Python along with it. After installing, check which Python version you have: your game works with Python 3.13, 3.14 or 3.15.

Installing Thonny

Go to thonny.org and download the version for your operating system.

Windows

Download the .exe file and double-click it. Follow the installation steps (just click “Next”). Afterwards you’ll find Thonny in the Start menu.

macOS

Download the .pkg file and open it. Drag Thonny to your Applications folder. The first time you open it, you right-click and choose “Open” (so that macOS trusts it).

Linux

Open a terminal and type:

bash <(wget -qO - https://thonny.org/installer-for-linux)

Or use your package manager if Thonny is available (e.g. sudo apt install thonny on Ubuntu).

Note

Use the version from thonny.org, not the version from the app store. That one is sometimes out of date.

Checking the Python version

Pygame-ce and pgzero have ready-made versions for Python 3.13, 3.14 and 3.15. Newer versions (3.16 or higher) don’t work yet.

  1. Open Thonny.
  2. Look at the bottom right of the window. There you see the active Python version, e.g. Python 3.14.8.
  3. Does it say 3.13, 3.14 or 3.15? Then everything is fine. Does it say something else? Then install Python 3.14 using the steps below.
Tip

Does it say Python 3.13, 3.14 or 3.15? Skip the rest of this part and continue with Installing Pygame-ce.

Installing Python 3.14 and pointing Thonny at it

  1. Go to python.org/downloads and click “All releases”. Find Python 3.14 in the list and download the installer for your operating system.
    • Windows: choose the Windows installer (64-bit) and leave the “Add Python to PATH” checkbox on.
    • macOS: choose the macOS 64-bit universal2 installer.
    • Linux: use your package manager, e.g. sudo apt install python3.14.
  2. Run through the installation and finish it.
  3. Open Thonny and click the Python version name at the bottom right (or go to Tools → Options → Interpreter).
  4. Choose “Alternative Python 3 interpreter or virtual environment”.
  5. Click the folder icon and browse to the Python 3.14 executable:
    • Windows: C:\Users\<jounaam>\AppData\Local\Programs\Python\Python314\python.exe
    • macOS: /Library/Frameworks/Python.framework/Versions/3.14/bin/python3
    • Linux: /usr/bin/python3.14
  6. Click OK and restart Thonny.

The bottom right now says Python 3.14. Continue with installing Pygame-ce.

Installing Pygame-ce

Pgzero needs Pygame to work. We use pygame-ce, the maintained community version that also works on Python 3.14 and 3.15. In your code it stays just import pygame.

  1. Open Thonny.
  2. At the top, click Tools → Manage packages…
  3. Type in the search box: pygame-ce
  4. Click the search result and then Install.
  5. Wait until the installation is done (you see “Done” at the bottom).

Installing Pgzero

Pgzero is the library you use to make games. This is how you install it:

  1. Go back to Tools → Manage packages…
  2. Type in the search box: pgzero
  3. Click the search result and then Install.

Afterwards, check that the version is 1.2 or higher. You see the version on the right next to the name in the package list.

Note

Does installing pgzero get stuck on Python 3.14 or 3.15? That happens because pgzero automatically pulls in the classic pygame as well, and that one has no ready-made version for those versions. In that case use the one-click installation script at the top of this page, or choose Python 3.13 in Thonny.

Testing whether everything works

Create a new file in Thonny and paste this code into it:

# test_setup.py
import pgzrun

WIDTH = 400
HEIGHT = 300

def draw():
    screen.fill((30, 30, 60))
    screen.draw.text("Pgzero works!", center=(200, 150), color="white")

pgzrun.go()

Save it as test_setup.py and click the green Run button in Thonny.

If everything is fine, a window opens with a dark blue background and the text “Pgzero works!”. Close the window by clicking the cross.

Not working?

Don’t panic! Have a look at the troubleshooting page for the most common errors and how to solve them.

October 30, 2026, 3:59 pm

VS Code (graduation track)

Have you already done a few Python sessions and do you want more control over your code? Then VS Code is a good next step. It is the editor real programmers use too.

When to switch?

Only when Thonny starts to feel too limited, for example when you want to manage several files at once, want to use Git, or are just curious how professional developers work.

Switching is not required. Thonny stays fine for beginners and short sessions.

Tip: give VS Code a try once you have done at least 3 sessions and you’re comfortable with the basics of Python.

Installing VS Code

Download VS Code via code.visualstudio.com. Choose the installer for your operating system (Windows, macOS or Linux) and follow the installation steps.

Installing the Python extension

VS Code works with extensions. For Python you need the official Microsoft extension:

  1. Open VS Code.
  2. Click the Extensions tab in the sidebar (or press Ctrl+Shift+X).
  3. Search for ms-python.python.
  4. Click Install.

After installing, VS Code automatically recognises Python files and you get autocomplete, error messages and more.

Installing uv

uv is a fast Python package manager that works much more easily than the standard pip. Install it via astral.sh/uv.

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

macOS / Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh

Restart your terminal after installing so that the uv command is available.

Setting up the project

Open a terminal in VS Code (`Ctrl+`` or via Terminal → New Terminal) and run these commands in your project folder:

# Use Python 3.13, 3.14 or 3.15
uv init --python 3.14
uv add pgzero pygame-ce pygbag
Note

Use Python 3.13, 3.14 or 3.15. We install pygame-ce instead of pygame: that is the community version that also works on the newest Python. Pgzero however still asks for the classic pygame.

So put this line in your pyproject.toml, so that uv skips that old pygame:

[tool.uv]
exclude-dependencies = ["pygame"]

After that you have to tell VS Code which Python version the project uses:

  1. Press Ctrl+Shift+P and type Python: Select Interpreter.
  2. Choose the interpreter that is in your .venv folder (it looks like .venv/bin/python or .venv\Scripts\python.exe).

VS Code now uses the right environment with all the packages you just installed.

Running Pgzero

Open a terminal in VS Code and use:

uv run pgzrun main.py

Or create a launch.json in the .vscode/ folder so you can start with F5:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Pgzero: run current file",
      "type": "debugpy",
      "request": "launch",
      "module": "pgzero",
      "args": ["${file}"],
      "console": "integratedTerminal"
    },
    {
      "name": "Pygame: run current file",
      "type": "debugpy",
      "request": "launch",
      "program": "${file}",
      "console": "integratedTerminal"
    }
  ]
}

Create the .vscode/ folder in your project folder if it isn’t there yet, and save the file as launch.json. You then see the configurations back in the Run and Debug panel (Ctrl+Shift+D).

Running Pygame

For normal Pygame projects (without pgzrun) you use:

uv run python main.py

Or choose the “Pygame: run current file” configuration in the Run and Debug panel if you have created the launch.json.

October 30, 2026, 3:59 pm

PyCharm (graduation track)

PyCharm is a fully equipped IDE from JetBrains. You don’t need it to get going with Pgzero or Pygame, but if you want to grow further as a programmer it’s a handy step. Think of a built-in debugger, smart code completion and powerful refactoring tools.

When to switch?

Just keep working with Thonny as long as that feels fine. Only consider PyCharm once you:

  • want to inspect variables while your code is paused (debugger),
  • want to quickly rename functions or variables across your whole project (refactoring), or
  • want autocomplete that also knows the types of your variables.

For beginners and most dojo sessions, Thonny is more than enough.

Installing PyCharm Community

Download the free Community edition at jetbrains.com/pycharm. Choose Community, not Professional. Professional is paid and you don’t need it.

  1. Go to the download page and click Community-Free.
  2. Run the installer and leave all the default settings as they are.
  3. Start PyCharm. You don’t have to create a JetBrains account.

Creating a new project and setting up the interpreter

When you start PyCharm for the first time, you choose New Project.

PyCharm 2024.1 or newer with uv:

If you already have uv on your system (see the Thonny guide), PyCharm recognises it automatically. Under Python version, choose 3.13, 3.14 or 3.15 and let PyCharm create a uv environment.

Older version, or without uv using venv:

  1. As Interpreter type, choose Virtualenv.
  2. Under Base interpreter, select the Python installation on your system (usually python3 or python3.x).
  3. Leave the Create a main.py welcome script checkbox on or turn it off. It doesn’t matter.

Then click Create. PyCharm creates the environment and opens the project.

Installing packages

You install pgzero, pygame-ce and pygbag through the settings:

  1. Open File → Settings (on macOS: PyCharm → Preferences).
  2. Navigate to Project: <naam> → Python Interpreter.
  3. Click the + icon at the bottom right.
  4. In the search box, search for pgzero and click Install Package. Do the same for pygame-ce and pygbag.
  5. Close the window when all three are done.

Check that it all worked: on the right in the list you now see pgzero, pygame-ce and pygbag with their version numbers.

Note

Doesn’t the Install Package button for pgzero work on Python 3.14 or 3.15? That’s because pgzero pulls in the classic pygame. In that case use the built-in terminal with uv add pgzero pygame-ce and put [tool.uv] exclude-dependencies = ["pygame"] in your pyproject.toml. See also Installing Thonny + Pgzero.

Run config for Pgzero

Pgzero works differently from a normal Python script: you start it as a module and pass your file name as an argument.

  1. At the top, click Add Configuration… (or open Run → Edit Configurations…).
  2. Click the + icon and choose Python.
  3. Fill in:
    • Name: Pgzero
    • Run: set the choice to Module name (not Script path)
    • Module name: pgzero
    • Parameters: main.py
    • Working directory: the folder of your project (if it is still empty, click the folder icon and choose your project folder)
  4. Click OK.

Then select the Pgzero configuration in the dropdown menu at the top and press the green play button to start your game.

Run config for Pygame

Pygame you do start as a normal script:

  1. Open Run → Edit Configurations… again and click + → Python.
  2. Fill in:
    • Name: Pygame
    • Run: leave the choice on Script path
    • Script path: browse to main.py in your project folder
    • Working directory: your project folder
  3. Click OK.

Choose the Pygame configuration in the dropdown menu and press the play button.


That’s all. You now have two ready-made run configurations: one for Pgzero games and one for Pygame games. Just switch configuration in the dropdown menu at the top, depending on what you’re building.

Handy tips

  • Using the debugger: Set a breakpoint by clicking to the left of a line number (red dot). Then start your run configuration with the bug button instead of the play button. PyCharm pauses on that line so you can look at your variables.
  • Automatic imports: If PyCharm puts a red underline under a name you just wrote, press Alt+Enter to automatically add the right import statement at the top.
  • Terminal: At the bottom you can open a built-in terminal (View → Tool Windows → Terminal). Handy for uv add or for running pygbag to publish.
October 30, 2026, 3:59 pm

Virtual environments (venv)

Imagine: you’re going on camp and you have to pack your backpack. For a sports camp you put different things in it than for a music camp. If you throw everything together, you never find anything back.

That’s how it works with Python projects too. Every project needs its own “backpack”: a virtual environment with exactly the right stuff in it.

What is a virtual environment?

A virtual environment (short: venv, pronounced “ven-vee”) is a separate folder on your computer that holds Python and all the extra packages for one project.

Say you have two projects:

  • Project A uses Pygame version 2.5
  • Project B uses Pygame version 2.6

Without virtual environments you could only install one version of Pygame on your computer. One of the projects would then stop working. With virtual environments every project has its own sealed box with the right version in it. They don’t get in each other’s way.

Metaphor: A virtual environment is like an own backpack per project. You put in exactly what that project needs, and the backpacks stand apart from each other in the cupboard.

Why do we use them?

  1. No fighting between projects. Version 2.5 and 2.6 can both exist, each in its own environment.
  2. Sharing is easy. If you share your project with a friend, you don’t have to send all the packages along. Your friend just creates their own virtual environment and installs the same packages.
  3. Your computer stays tidy. You don’t install anything “just like that” on your whole computer. Everything stays neatly in the backpack of the right project.

How do you make a virtual environment with uv

uv is the Python package manager we use at CoderDojo. It can create and manage virtual environments.

Step 1: Open a terminal

  • Windows: Press ⊞ Windows, type cmd and open “Command Prompt” (or use PowerShell).
  • macOS / Linux: Open “Terminal”.

Step 2: Go to your project folder

cd pad/naar/jouw/projectmap
Tip

Replace pad/naar/jouw/projectmap with the real path, for example cd Bureaublad/mijn-spel.

Step 3: Create the virtual environment

uv venv

uv now creates a .venv folder in your project folder. It holds a separate Python installation.

Want to use a specific Python version? That works too:

uv venv --python 3.14

Why would you do that? That way you’re sure you use a Python version that works with our game libraries. Choose 3.13, 3.14 or 3.15.

Step 4: Activate the virtual environment

The environment exists, but you still have to “switch it on”. We call that activating.

Windows (Command Prompt or PowerShell):

.venv\Scripts\activate

macOS / Linux:

source .venv/bin/activate

After activating you see (.venv) appear on the left in your terminal. That is the sign that you are now working inside your virtual environment.

(.venv) seppe@computer:~/project$

Step 5: Install packages

Now that your environment is active, you can install packages. They end up only in this one backpack:

uv add pgzero pygame-ce
Note

pygame-ce is the community version of Pygame and also works on Python 3.14 and 3.15. Pgzero however still asks for the classic pygame.

So also put this line in your pyproject.toml:

[tool.uv]
exclude-dependencies = ["pygame"]

Step 6: Turning it off (deactivating)

Done programming? Just type:

deactivate

The (.venv) sign disappears and you work with the normal Python of your computer again.

What do you need to watch out for?

⚠️ Don’t forget to activate the environment

The most common mistake: you open a new terminal and forget to activate the virtual environment. Python then complains that it can’t find the packages, even though you did install them.

Solution: always activate first with .venv\Scripts\activate (Windows) or source .venv/bin/activate (macOS/Linux) before you start your program.

⚠️ Put .venv in .gitignore

The .venv folder is big (sometimes hundreds of MBs). You should not share it via Git or GitHub. Put .venv in your .gitignore file, so that Git skips it:

.venv/

Your friend just creates their own .venv with uv venv and installs the same packages via the pyproject.toml file that is in Git.

⚠️ Use uv run as a shortcut

No mood to activate and deactivate every time? uv has a handy shortcut:

uv run python main.py

This runs python main.py inside the virtual environment, even when it isn’t activated. uv figures out by itself which environment belongs to the project.

⚠️ Different terminals, different environments

If you activate the environment in one terminal, it is not automatically active in another terminal. You have to activate each terminal separately where you want to use the environment.

Summary

ActionCommand (Windows)Command (macOS/Linux)
Createuv venvuv venv
Activate.venv\Scripts\activatesource .venv/bin/activate
Deactivatedeactivatedeactivate
Shortcutuv run python main.pyuv run python main.py

The golden rule: do you see (.venv) in your terminal? Then you’re good. Don’t see it? Activate first, program after.


What’s the difference with virtualenv and pip? Before uv there were already other tools, like virtualenv (for environments) and pip (for packages). With those you could do the same, but you had to use two separate commands and it worked a lot slower. uv does everything in one go, is much faster, and is a lot easier for beginners. That’s why at CoderDojo we prefer uv.

Having problems with your virtual environment? Have a look at Troubleshooting for common errors.

October 30, 2026, 3:59 pm

Troubleshooting

Your code doesn’t work and you don’t know why? Don’t panic: almost every problem has a simple solution. Scroll through the list below and see if your error message is in it.

Pgzero or pygame won’t install

You see an error like No matching distribution found or Package not found when installing pgzero or pygame.

Cause: you’re using a Python version for which the library has no ready-made version yet. That happens with Python 3.16 or higher, and with the classic pygame on Python 3.14 and 3.15. Use Python 3.13, 3.14 or 3.15 and the community version pygame-ce instead of pygame.

Ubuntu 26.04 has Python 3.14 as default. The one-click installation script solves this automatically:

  • An existing Python 3.13, 3.14 or 3.15 is used; if there isn’t one, Python 3.14 is installed (via the deadsnakes PPA, ppa:deadsnakes/ppa, if apt doesn’t have that version)
  • Thonny is detected via PATH, snap, pip or flatpak
  • If Thonny is installed as a snap, the configuration is written to the right folder (~/snap/thonny/current/.config/Thonny/)
  • If apt install thonny doesn’t work, the script tries pip install --user thonny

Solution: use Python 3.13, 3.14 or 3.15 as active interpreter:

  • Thonny: see Installing Thonny + Pgzero → Checking the Python version.
  • VS Code: create your environment again with uv venv --python 3.14.
  • PyCharm: create a new interpreter and select Python 3.14 at Base interpreter.

pgzrun: command not found

This means Pgzero isn’t installed, or that you’re using the wrong Python version.

Solution: Open Thonny, go to Tools → Manage Packages, search for pgzero and click Install. Then restart Thonny and try again.

ModuleNotFoundError: No module named 'pygame'

Python can’t find the pygame library. This almost always happens because Thonny is using the wrong Python interpreter.

Solution: Look at the bottom right in the status bar of Thonny to see which interpreter is active. Choose the same interpreter you used during the installation. Then install pygame-ce again via Tools → Manage Packages.

Window opens and closes straight away

The program window flashes up briefly and then disappears again.

  • Pgzero: You need WIDTH and HEIGHT as separate variables at the top of your file, outside every function. Without those variables Pgzero doesn’t know how big the window should be and it stops straight away.
  • Pygame: You need an event loop that calls pygame.event.get() every frame. Without that call the window freezes and the operating system closes it.

Sounds do nothing

You call sounds.jump.play() but you hear nothing.

Check the following:

  1. Is there a sounds/ folder next to your main.py? The folder has to be in exactly the same place.
  2. Are the file names in lowercase and do you use underscores instead of spaces? For example jump.wav, not Jump.wav or jump sound.wav.
  3. Pgzero only accepts .wav files in the sounds/ folder. Pgzero loads them automatically, so you don’t have to import anything yourself.

Sound doesn’t work in pygame (session 5 and later)

You use pygame.mixer.Sound(...) but you hear nothing, no error message; the game just starts without sound.

Cause: Python looks for the sound file relative to the folder you start the script from, not the folder main.py is in. If you open VS Code from the project root and your script is in a subfolder, Python looks for sounds/hit.wav in the project root and doesn’t find it. The try/except in the starter silently catches that error.

Solution: Use __file__ to make the path absolute:

import os
_DIR = os.path.dirname(os.path.abspath(__file__))
snd_hit = pygame.mixer.Sound(os.path.join(_DIR, 'sounds/hit.wav'))

__file__ is always the path to main.py itself, no matter where you start the script from. The starter files from session 5 on already do this. Add the two lines just above loading the sounds if your own game doesn’t have this yet.

Images are not displayed

Your actor or background isn’t visible.

Check the following:

  1. Is there an images/ folder next to your main.py?
  2. Are all file names in lowercase? player.png works; Player.png doesn’t.
  3. Prefer .png files, because they work most reliably with Pgzero.

KeyError on an image

You get a KeyError when you create an Actor or load an image.

Cause: The name you pass has to match the file name exactly, without the extension. Actor('basket') looks for images/basket.png. One typo or one capital letter too many is enough to cause the error.

Solution: Check the file name in the images/ folder and make sure it matches exactly what you wrote in the code.

Chromebook or no installation rights

You can’t install Thonny or Pgzero because you don’t have administrator rights on the computer.

Solution: Work together with someone who does have Thonny, because pair programming is a good idea anyway! At home you can continue with the pygbag version: it runs your Pygame project straight in the browser, without installation.

Antivirus blocks the installation

Your installation attempt is interrupted or blocked by antivirus software.

Solution: Temporarily turn off the antivirus software during the installation, or ask a coach to put a portable version of Thonny on a USB stick. That version needs no installation and works straight from the stick.


Still stuck?

No problem. Just ask a coach during the session, or post your question in the CoderDojo chat. Include the exact error message (copy and paste the text from Thonny), then we can help you faster.

October 30, 2026, 3:59 pm

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.

October 30, 2026, 3:59 pm

Subsections of Sessions

Python sessions

Ten sessions in which you build a complete Pygame game every month. Every session has a worksheet, a cheatsheet and starter code to download.

  • Session 1: Catch the Stars: A star falls, you catch it. You write your first draw()/update() loop and move a sprite with the arrow keys.
  • Session 2: Dodge Meteors: Meteors spawn and fall. You keep track of them in a list, remove them once they are off screen and take off a life on every collision.
  • Session 3: Space Blaster: Enemies drop down, you shoot them. You create bullets with SPACE and detect collisions with a loop over a list.
  • Session 4: Pong: Two paddles, one ball. You learn pygame.Rect, make the ball bounce by reversing its speed and keep the score for two players.
  • Session 5: Breakout: Smashing bricks with a bouncing ball. You add sound effects with pygame.mixer and build a win and lose screen.
  • Session 6: Platformer: Jumping and falling on platforms. You simulate gravity with a vel_y variable and detect platform collisions with pygame.Rect.
  • Session 7: Bug Zapper: Bugs crawl, you zap them. You write your first class with __init__ and make a subclass through inheritance.
  • Session 8: Sky Highway: A flying ship on a scrolling road. You spawn obstacles from the right and raise the speed as the score goes up.
  • Session 9: Boss Battle: A boss with an HP bar and attack phases. You build a state machine that changes the shooting and movement behaviour per phase.
  • Session 10: My Game: No fixed assignment. You pick the genre and combine everything you learned in nine sessions into your own game.
October 30, 2026, 3:59 pm

Subsections of Python sessions

Session 1: Catch the Stars

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.

What you’ll learn today

  • Using variables to position objects
  • The draw() function: what you draw every frame
  • The update() function: what changes every frame
  • Keyboard input: moving with the arrow keys

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py in Thonny.
  3. Click the green Run button. You see a dark blue window with a basket at the bottom and a star at the top, but nothing moves yet.
  4. Problems? Take a look at Installing Thonny.

Step 1: Let the star fall

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 + 3

Click 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).

Pygame Zero coordinate system: origin at the top left, y goes down

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:

Step 2: ✅ Basic: move the basket

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 + 5

Click 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.

Step 3: ⭐ Stretch: catch the star

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 = 0

The 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 = 0

Bonus: Make the star come back at a random spot every time. Add this at the top of the file:

import random

Then replace star.y = 0 (in both places) with:

        star.x = random.randint(20, WIDTH - 20)
        star.y = 0

Now the star falls at a different spot every time. Much harder to catch!

Step 4: 🔥 Expert: add a score

A real game keeps track of what you score. For that you need a variable that lives outside the functions: a global variable.

How a variable works: a box with a name and a value

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 = 0

In 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 = 0

global 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!

Showcase

Show your game to a coach and a buddy. Tell them what you made: what works, and what extras you added.

See you next time!

“Next time: things that FIGHT BACK. And they go BOOM, with real sound.”

Take it home

Try one of these extensions at home. Each one comes with a code hint to get you going.

  1. 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().

  2. 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.

  3. 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.

  4. Second star — Make sterren = [star, Actor("star")]. Give every star its own x. Update both of them in update() and draw them in draw().

  5. High score — Keep track of high_score. If score > high_score: high_score = score. Show f"Best: {high_score}" next to the score.

  6. Smaller basket — After every 5 points: basket.width = max(30, basket.width - 5). Reset basket.width on a restart.

  7. 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.

  8. Difficulty — After every 5 stars: raise the falling speed by 1. Build in a maximum: max(3, star_snelheid).

Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.

View the cheatsheet for this session

October 30, 2026, 3:59 pm

Session 2: Dodge Meteors

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.

What you’ll learn today

  • Using lists to keep track of several objects
  • Spawning (creating) and removing objects
  • Keeping track of lives and showing a game-over screen
  • Dojo Defender: adding your own spaceship to the bigger project

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py.
  3. Click Run. You see your spaceship at the bottom, but no meteors are falling yet.
  4. Problems? Take a look at Installing Thonny.

Step 1: Let meteors spawn

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.

Step 2: ✅ Basic: let meteors fall and disappear

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)
                pass

Step 3: ⭐ Stretch: game over and speeding up

Game 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 += snelheid

Step 4: 🔥 Expert: score and several meteor sizes

Score: 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.

Dojo Defender

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.

Showcase

Show your game to a coach and a buddy. How long do you survive? Who gets the highest score?

See you next time!

“Next time: your own spaceship SHOOTS BACK. Enemies in formation, and you blast them away.”

Take it home

  1. Easy: change the background colour to deep purple or dark green.
  2. Medium: add a sound when a meteor hits the ship (sounds/hit.wav).
  3. Hard: make meteors faster as there are more of them on the screen (not just the score).
  4. Very hard: add power-ups: a golden star that gives you an extra life when you touch it.

View the cheatsheet for this session

October 30, 2026, 3:59 pm

Session 3: Space Blaster

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.

What you’ll learn today

  • Keeping track of bullets in a list
  • Creating a bullet when you press SPACE
  • Collision between bullets and enemies
  • Game over when an enemy reaches the bottom

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py.
  3. Click Run. You see the ship at the bottom and 12 enemies in a grid, but nothing moves and you can’t shoot yet.

Step 1: Let the ship move

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 += 5

Step 2: ✅ Basic: shooting with SPACE

Fill 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)

Step 3: ⭐ Stretch: hitting enemies

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
            break

Win 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)

Step 4: 🔥 Expert: enemies move and game over

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 += 20

Game over when an enemy reaches the bottom:

        for vijand in enemies:
            if vijand.y > HEIGHT - 30:
                global game_over
                game_over = True

Faster 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 = 15

Showcase

Show your game to a coach. How fast do you clear all the enemies? Compare scores with a buddy.

See you next time!

“Next time: two players, one ball: PONG. And you learn real pygame without pgzrun.”

Take it home

  1. Easy: change the colour of the bullets or the enemies.
  2. Medium: make enemies move left and right (one direction per step, turn around at the edge).
  3. Hard: add a second wave: when all the enemies are gone, spawn a new row that comes down faster.
  4. Very hard: let enemies shoot back too: send a bullet down at random every N frames.

View the cheatsheet for this session

Dojo Defender

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.

October 30, 2026, 3:59 pm

Session 4: Pong

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.

What you’ll learn today

  • The pygame game loop: events, update, draw
  • pygame.Rect for rectangles and collisions
  • Making the ball bounce (reversing the speed)
  • Keeping score for two players

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py.
  3. Click Run. You see two paddles and a yellow ball in the middle, but the ball doesn’t move yet.

Difference with pgzrun: There are no draw() and update() functions any more. Everything sits in one while running: loop.

Step 1: Make the ball move

Find the lines:

# bal.x += ball_dx  # turn this on when you're done
# ball.y += ball_dy

Remove 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, 4

Click Run. The ball moves now, but it disappears off the edge. We’ll fix that in step 2.

Step 2: ✅ Basic: the ball bounces off the walls and the paddles

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_dx

The 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.

Step 3: ⭐ Stretch: keeping score

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 = -4

The score is already drawn in draw(). Check that the variables are right.

Step 4: 🔥 Expert: win condition and speeding up

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.05

Build in a maximum: ball_dx = max(-12, min(12, ball_dx)).

Showcase

Play a match against a buddy. First to 5 points wins. Show it to a coach.

See you next time!

“Next time: Breakout: the ball smashes bricks to pieces. And there’s SOUND.”

Take it home

  1. Easy: change the colour of the ball or the paddles.
  2. Medium: make one paddle bigger as that player falls behind (handicap).
  3. Hard: add an AI opponent: the right paddle follows the ball automatically.
  4. Very hard: add power-ups: an item that doubles the ball or makes the paddle bigger.

Dojo Defender

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.


View the cheatsheet for this session

October 30, 2026, 3:59 pm

Session 5: Breakout

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.

What you’ll learn today

  • Loading and playing sound with pygame.mixer
  • Collisions with several objects (a list of bricks)
  • Making the ball bounce in the right direction
  • Building a win and lose screen

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py.
  3. Click Run. You see the paddle, the ball and the bricks. The ball already moves horizontally but not vertically. You’re going to change that.

Step 1: Make the ball move vertically

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_dy

Click Run. The ball moves diagonally but disappears off the edges.

Step 2: ✅ Basic: ball bounces off walls and paddle

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 = -4

Step 3: ⭐ Stretch: breaking bricks

Check 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 frame

Win condition: If all the bricks are gone, the ninja has won. Show a message:

if not stenen:
    # show win screen

Step 4: 🔥 Expert: lives, explosion sound and speed

Explosion 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_factor

A new row of bricks when everything is gone: call stenen = maak_stenen() again and raise the difficulty.

Showcase

Who clears the most bricks? Compare scores and show your coach the longest streak.

See you next time!

“Next time: gravity. Your character falls, and you have to make it jump on platforms.”

Take it home

  1. Easy: change the colours of the bricks to your favourite colours.
  2. Medium: make some bricks twice as strong (they have to be hit twice).
  3. Hard: add a power-up that makes the paddle wider for a while.
  4. Very hard: add a second ball that spawns when score > 50.

View the cheatsheet for this session

Dojo Defender

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.

October 30, 2026, 3:59 pm

Session 6: Platformer

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.

What you’ll learn today

  • Simulating gravity with a vel_y variable
  • Jumping by making vel_y negative
  • Detecting platform collisions with pygame.Rect
  • Collecting items (collision with stars)
  • Playing sounds with pygame.mixer

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py in Thonny.
  3. Click the green Run button. You see a window with a player, platforms and stars, but the player doesn’t fall yet.
  4. Problems? Ask a coach.

Step 1: Gravity

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 = True

GRAVITY = 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).

Step 2: ✅ Basic: jumping

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 = -12

Click Run and test:

  • Arrow keys: move left/right
  • SPACE: jump up

Tip: If the jump feels too high or too low, change -12 to -10 (jump lower) or -15 (jump higher).

Step 3: ⭐ Stretch: collecting stars

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 += 1

And 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))

Step 4: 🔥 Expert: sound and win screen

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 = False

Showcase

Show your game to a coach and a buddy. Can you collect all the stars before the time runs out?

Dojo Defender

Today’s Dojo Defender snapshot builds on session 5:

  • Parallax star background — three layers of stars, each with its own speed
  • Particle effects — exhaust, explosion debris, sparks
  • Destructible asteroids — big → med → small → gone

Download the Dojo Defender starter or solution.

See you next time!

“Next time: click on bugs and ZAP them away, with your own classes.”

Take it home

Try one of these extensions at home:

  1. Easy: change the colour of the player when they’re in the air.
  2. Medium: add a timer. The player has 30 seconds to collect all the stars.
  3. Hard: add an enemy that walks back and forth on a platform; if the player touches it, the game restarts.
  4. Very hard: make the player double jump (press SPACE twice for a second jump in the air).

Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.

View the cheatsheet for this session

October 30, 2026, 3:59 pm

Session 7: Bug Zapper

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.

What you’ll learn today

  • Writing a class with __init__ and your own methods
  • Inheritance: a subclass that extends an existing class
  • Keeping track of bullets in a list
  • Collision detection between bullets and bugs

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py in Thonny.
  3. Click Run. You see a screen with a player at the bottom, but no bugs and no bullets yet.
  4. Problems? Ask a coach.

Step 1: Spawning bugs and shooting

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 there

Step 2: ✅ Basic: hitting bugs

Now 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!

Step 3: ⭐ Stretch: SlingerBug

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.

Step 4: 🔥 Expert: DuikBug

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.

Showcase

Show your game to a coach and a buddy. How many bugs can you zap before your lives run out?

See you next time!

“Next time: an endless highway in the sky, and everything SCROLLS past.”

Take it home

Try one of these extensions at home:

  1. Easy: make the bugs bigger or faster as the score goes up.
  2. Medium: add a lives system with 3 lives, and when a bug reaches the bottom you lose one.
  3. Hard: write a PantserbBug that needs 3 hits before it dies (use an hp attribute).
  4. Very hard: add mouse control: the player moves to the mouse position and shoots automatically every 0.5 seconds.

Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.

Dojo Defender: Boss

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.


View the cheatsheet for this session

October 30, 2026, 3:59 pm

Session 8: Sky Highway

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!

What you’ll learn today

  • Making a scrolling background with a scroll_x variable
  • Spawning objects that move from right to left
  • Collision detection with lives
  • Difficulty scaling: raising the speed as the score climbs

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py in Thonny.
  3. Click Run. You see a static screen with a ship and a sky background, but nothing moves yet.
  4. Problems? Ask a coach.

Step 1: Let the background scroll

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!

Step 2: ✅ Basic: obstacles and lives

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 = False

Drawing 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))

Step 3: ⭐ Stretch: collecting stars

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))

Step 4: 🔥 Expert: spawning and scaling the speed

Now you make the game harder and harder as the score climbs.

Scaling the speed (find # STEP 4):

speed = BASE_SPEED + score * 0.1

BASE_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)

Showcase

Show your game to a coach and a buddy. Who gets the highest score?

See you next time!

“Next time: a boss that fights back, in THREE phases.”

Take it home

Try one of these extensions at home:

  1. Easy: add a second kind of obstacle (for example a blue one that flies higher or lower).
  2. Medium: add power-ups. Catch a green capsule for a temporary shield (the ship can be hit once).
  3. Hard: let the ship move up and down with the arrow keys, and keep it inside the screen so it can’t fly off.
  4. Very hard: add parallax scrolling: a second background layer (clouds) that scrolls slower than the first layer.

Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.


Dojo Defender: Your own version

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.


View the cheatsheet for this session

October 30, 2026, 3:59 pm

Session 9: Boss Battle

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.

What you’ll learn today

  • Drawing an HP bar on the screen
  • A state machine: the boss behaves differently in each phase
  • Making the boss shoot with a bullet timer
  • Activating phases at HP thresholds

Step 0: Setup

  1. Open Thonny.
  2. Download the starter via this link, unpack the ZIP and open main.py in Thonny.
  3. Click Run. You see the boss at the top, your ship at the bottom. No movement, no bullets yet.
  4. Problems? Ask a coach.

Step 1: Making the boss move

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_speed

boss_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))

Step 2: ✅ Basic: hitting the boss and the HP bar

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!

Step 3: ⭐ Stretch: the boss shoots back

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 = False

Draw player_lives as hearts or red blocks at the bottom of the screen.

Step 4: 🔥 Expert: three phases

Now the boss reacts to its HP:

  • Phase 1 (HP 100 to 50): normal behaviour
  • Phase 2 (HP 50 to 25): move faster, shoot more often
  • Phase 3 (HP < 25): maximum speed, double bullets

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)

Showcase

Show your game to a coach and a buddy. Who reaches phase 3 and survives?

See you next time!

“Next time: your own game, from scratch, completely your own idea.”

Take it home

Try one of these extensions at home:

  1. Easy: change the colour of the boss per phase (green → orange → red).
  2. Medium: add a score: the faster you beat the boss, the more points.
  3. Hard: let the boss shoot diagonal bullets in phase 2 too (slanted left and right).
  4. Very hard: add a second player who can join in with WASD keys.

Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.

View the cheatsheet for this session

October 30, 2026, 3:59 pm

Session 10: My Game

This is your session. No fixed steps, no compulsory mechanics. You decide what you build, and today you make it.

Your game, your rules

In nine sessions you learned:

  • Variables, functions and game loops
  • Keyboard and mouse input
  • Sprites, collision detection and score
  • Sound and animation
  • Classes and inheritance
  • Scrolling backgrounds, spawning and difficulty scaling
  • HP bars, state machines and boss AI

All those building blocks are ready. Today you combine them in your own game.

Choose your concept

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.

Plan your game

Before you write code, answer these three questions:

  1. What does the player do? (move, shoot, jump, click, …)
  2. What is the goal? (get a score, beat the boss, reach the end, …)
  3. What makes it hard? (enemies, obstacles, a timer, …)

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.

Animation: switching sprite per frame

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)]

Getting started

Download the starter via this link. The starter has:

  • A working pygame game loop
  • An empty draw() and update() section
  • Colour constants and a font variable ready to go
  • Comments that show where you can put what

Open 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.

Showcase

At the end of the session everyone presents their game. You don’t have to finish it. Show what you made and tell:

  • What is the concept of your game?
  • Which technique from a previous session did you use?
  • What would you still like to add if you had more time?

What now?

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?

Keep building!

In ten sessions you built a platformer, a shooter, a boss battle and more. Take that code home, open it again next week, and just add one thing. That is how programmers work.

View the cheatsheet for this session

October 30, 2026, 3:59 pm

Git sessions

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.

October 30, 2026, 3:59 pm

Subsections of Git sessions

Session 11: Git: Your code's history book

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.

What is Git?

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:

  • Every version is kept. You can always go back to a previous commit if something breaks.
  • You work locally. Git runs on your computer. No internet needed.
  • Git is fast. Even projects with thousands of files are checked in milliseconds.

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 is not only for code

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.

Markdown and notes

Many note-taking apps these days use Markdown (.md files) to store your documents. Think of:

  • Obsidian: your notes are just .md files in a folder on your computer
  • Notion: exports to Markdown
  • Joplin: stores everything as Markdown
  • Typora, Zettlr, Logseq: all Markdown-based

Because 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.

When to use Git for notes and when not

Do use GitBetter 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, blogsReal-time collaboration like Google Docs (Git is too slow for that)
Your CoderDojo cheatsheetsPasswords or personal diaries (unless private repo)

In short: anything that is text you can put in Git. Anything that is binary, better not.

Why version control?

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.py

Recognise that? With Git you only need one file: game.py. Git remembers the rest.

What version control gives you:

  • Never lose work again. Everything you commit keeps existing. Always.
  • Understand what happened. You see exactly which lines you changed and when.
  • Experiment without risk. Make a separate copy of your code (a branch), try something weird, and throw it away if it doesn’t work. Your original code stays intact.
  • Work together. Several people can work on the same project at the same time without overwriting each other’s code.

What do you NOT put in Git?

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 GitDo NOT put in Git
Your Python files (.py)Passwords, API keys, tokens
Images your game usesFiles your program creates itself (.exe, .pyc)
Sound files (.wav, .mp3)The __pycache__ folder
Text files, configurationLarge files (>50 MB) like videos
Your README.mdSettings 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.

Step 0: Git setup

Windows

  1. Go to git-scm.com and download the Windows installer.
  2. Double-click the downloaded file.
  3. Click through the installation. The default options are fine.
  4. Important: when picking the text editor, choose Nano or Notepad++ (not Vim, unless you already know Vim).
  5. After the installation, open Git Bash (search for it in the start menu).

Mac

Open Terminal (search for “Terminal” in Spotlight) and type:

git --version

If Git is not installed, macOS asks you to install the developer tools. Click “Install”. Done.

Linux (Ubuntu/Debian)

sudo apt install git

Checking that it works

Open a terminal (Git Bash on Windows) and type:

git --version

You see something like git version 2.47.0. That means: Git is ready.

Step 1: Tell Git who you are

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.

Step 2: Making your first repository

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-project

Now you tell Git: “This folder is a repository.”

git init

You 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.

Step 3: Committing your first file

Create a file. For example a Python file with one line:

echo 'print("Hello Git!")' > hallo.py

Now you tell Git: “Keep an eye on this file.”

git add hallo.py

With 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 status

Git shows:

On branch master
Changes to be committed:
  new file:   hallo.py

Now 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.

Step 4: Tracking changes

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 status

Git knows that hallo.py has changed. But what exactly has changed?

git diff

git 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"

Step 5: Back in time

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 --oneline

You see something like:

a1b2c3d Update: greeting extended
e4f5g6h My first commit: added hallo.py

Every 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.py

There is your original print("Hello Git!") again.

Do you really want to go back in time? That is possible:

git checkout e4f5g6h -- hallo.py

Now 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.

Step 6: What does NOT go in Git: .gitignore

Create a file that Git should ignore:

echo "GEHEIM_WACHTWOORD=12345" > geheimen.txt

git 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" > .gitignore

Now 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/

Showcase

Show a coach and a buddy:

  1. Your repository with at least 3 commits.
  2. The result of git log --oneline: your commit history.
  3. A git diff that shows what you changed.
  4. Your .gitignore file.

See you next time!

“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.”

Take it home

  1. Easy: Make a repository for your notes. Put a Markdown file with what you learned today in Git, and commit it.
  2. Medium: Change something in your code or notes, use git diff to look at the change, and commit it with a good message.
  3. Hard: Make three commits with different changes. Use git show to look at each version. Use git checkout to go back to the first commit, and then to the last one again.
  4. Very hard: Make a Python project with a good .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.

October 30, 2026, 3:59 pm

Session 12: Git Branches: Parallel universes for your code

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 main instead of master. That is exactly the same, only the name is different. In this course we use master, but if you come across main online, you know it means the same thing.

What you’ll learn today

  • What a branch is and how Git stores branches internally
  • Creating, switching and merging branches
  • What a merge conflict is and how to solve it
  • When you should and shouldn’t use a branch

Step 0: Recap: where are you now?

Open your terminal in last session’s project. Check where you are:

git status
git log --oneline

You have a few commits on master. This is your stable base: you’ll branch off from here in a moment.

Step 1: Your first branch

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-feature

Git has now created a new branch. But you are still on master. Check which branches there are:

git branch

You see:

  highscore-feature
* master

The 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.

Step 2: Doing work on a branch

Create a new file:

echo "HIGHSCORE: 0" > highscore.txt

Commit 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.py

Commit again:

git add highscore_manager.py
git commit -m "Add highscore_manager: function to read the highscore"

Now look at your log:

git log --oneline

You see all your commits from master, plus the two new ones. Git remembers the whole chain.

Step 3: Two worlds at once

Switch back to master:

git switch master

Look at what is here:

ls

highscore.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 again

Step 4: Merging with git merge

Your highscore feature works. Time to put it back into master. This is called merging.

Make sure you are on master:

git switch master

Merge the branch:

git merge highscore-feature

Git shows something like Updating a1b2c3d..e4f5g6h and Fast-forward. Your highscore-feature commits are now part of master. Check:

ls
git log --oneline

highscore.txt and highscore_manager.py are now on master. All commits from the feature branch have been included in the history.

Step 5: What are merge conflicts?

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-highscore

Git says: CONFLICT (content): Merge conflict in highscore.txt. Open highscore.txt in your editor. You see:

<<<<<<< HEAD
HIGHSCORE: 100
=======
HIGHSCORE: 500
>>>>>>> andere-highscore

These 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: 500

Save 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.

Step 6: Cleaning up branches

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.

When a branch, when not?

Do use a branchNo branch
New feature that takes timeFixing a typo in a comment
Experiment you don’t know will workSmall bugfix of one line
Something that can break your working codeYou 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.

Tips for working smoothly with branches

  1. Keep branches short. A branch of 3 hours is fine. A branch of 3 weeks becomes a merge nightmare: the longer you wait to merge, the more has changed on master in the meantime, and the bigger the chance of conflicts.
  2. One thing per branch. “Add highscore” is a good branch. “Add highscore, menu, and new levels” is too much: split it up.
  3. Merge often. When you finish your feature, merge it back into master straight away. Small, frequent merges are easier than one big monster merge.
  4. Give branches clear names. highscore-feature is good. test123 or branch2 are bad. In a week you won’t remember what was in test123.
  5. Check where you are before you start. 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.

Showcase

Show a coach and a buddy:

  1. git branch with at least two branches (including master).
  2. A different file or different code on each branch.
  3. Switching back and forth: the files appear and disappear.
  4. A successful merge of a feature branch into master.

See you next time!

“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.”

Take it home

  1. Easy: Make a branch for a small feature in an existing project. Commit your work and merge it back.
  2. Medium: Deliberately create a merge conflict (two branches that change the same file) and solve it.
  3. Hard: Work with three branches at the same time on the same project. Merge them back into master one by one. Check after every merge whether everything still works.
  4. Very hard: Make a branch, commit three changes, switch back to 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.
October 30, 2026, 3:59 pm

Session 13: GitHub: Your code online, for everyone

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.

What is GitHub?

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:

  • Backup in the cloud. Your code is not only local. Laptop broken? Your code survives.
  • Working together. Several people can work on the same repo. GitHub manages who is allowed to do what.
  • Pull requests. You ask permission to put your changes into someone’s project, with discussion and review.
  • Issues. Reporting bugs, suggesting features, tracking tasks: all in your repo.
  • Portfolio. Your GitHub profile is your business card. Employers look at it.

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.

Step 0: Making a GitHub account

  1. Open github.com in your browser.
  2. Click on Sign up.
  3. Fill in your email address. Use the same address you set in session 11 with git config: that saves hassle.
  4. Choose a username. This becomes your identity on GitHub. Pick something you’ll still like in 5 years. You can change your username later, but old links to your profile won’t work anymore.
  5. Verify your account via the email GitHub sends.
  6. Choose the Free subscription. Everything you need is free: unlimited public and private repositories.

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.

Step 1: Your first repository on GitHub

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:

FieldWhat you fill in
Repository namemijn-eerste-git-project (same as your local folder)
Description“My first Git project (CoderDojo session 11)”
Public / PrivateChoose Public: others may see your code
Initialize with READMEDo 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.

Step 2: Linking your local repo to GitHub

In your terminal (in your project folder):

git remote add origin https://github.com/JOUW_USERNAME/mijn-eerste-git-project.git

What 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 -v

You 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.

Step 3: Putting your code online with git push

Now comes the moment: sending your local commits to GitHub.

git push -u origin master

push = 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:

  1. Via the browser: GitHub opens a browser window, you click “Authorize”. Done.
  2. With a Personal Access Token: create one via GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic). Give the token a name, tick 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.

Step 4: Pushing changes

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 push

You no longer need -u origin master, because Git knows the way. Refresh GitHub: your new commit is there.

Step 5: Starting a new project via GitHub

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.git

GitHub 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 you

Now you can carry on working locally and push your commits to GitHub, exactly like in Steps 3 to 4.

Step 6: What if someone else makes changes too?

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 pull

git 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:

CommandWhat it does
git pullFetch the newest version from GitHub
Work on your code, make commits (git add, git commit)
git pushSend your commits to GitHub

Remember: pull before you start, push when you’re done.

Showcase

Show a coach and a buddy:

  1. Your GitHub profile with at least one repository.
  2. The commit history of your repo on GitHub (click on “X commits” above the file list).
  3. A fresh git push that has just arrived on GitHub.

See you next time!

“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.”

Take it home

  1. Easy: Push an existing local project to a new GitHub repository.
  2. Medium: Make 3 commits locally, push them, and check on GitHub whether they are all visible.
  3. Hard: Clone a repo from GitHub, make a change, commit, and push back.
  4. Very hard: Simulate working on two computers: clone a repo, make a commit and push. Go to another folder, clone the same repo again (as if it were another computer), make a different commit, and try to push. What happens? Solve it with git pull.
October 30, 2026, 3:59 pm

Session 14: Git Best Practices: Write commits you'll still understand later

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.

What you’ll learn today

  • How to write a commit message that says something
  • Three simple rules for branching
  • What git stash is and when to use it
  • The pull-before-you-push rule

Part 1: Commit messages that say something

A 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.

The good commit message: 3 parts

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 code

2. 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.

Commit message rules at a glance

Do thisNot 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 titleA whole story in the title
Present tense, EnglishPast 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.

Multi-line commits in the terminal

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.

When do you commit?

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:

  • Understand what changed and when
  • Undo one specific change without breaking other things
  • Solve merge conflicts (small chunks = fewer conflicts)

Part 2: Branching: three simple rules

You already know how to make branches (session 12). Now: when and why.

Rule 1: master always works

The 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.

Rule 2: One feature, one branch

A branch does exactly one thing. No more.

  • Good: feature/player-health-bar
  • Bad: feature/health-bar-and-menu-and-sound-effects

If, 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.

Rule 3: Branches have a short life

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.

Branch naming

Give branches names you’ll still understand a week from now:

feature/highscore-tracking
bugfix/star-spawn-offscreen
experiment/particle-effects

The 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.

Part 3: Handy workflow commands

git stash: putting work aside

Say: 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 stash

Git 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 pop

git stash pop brings your stored changes back. Everything is as you left it: half done, but safely kept.

git pull --rebase: a cleaner history

Normally 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 --rebase

This 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.

The pull-before-you-push rule

Before you git push:

git pull

Always. 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.

Part 4: The .gitignore you always need

Every 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.db

Why 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.

Showcase

Show a coach and a buddy:

  1. Your last 5 commits, with good commit messages.
  2. Your .gitignore file.
  3. A branch name that follows the feature/ prefix convention.

See you next time!

“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.”

Take it home

  1. Easy: Rewrite the commit messages of your last 5 commits. Use the rules from this session.
  2. Medium: Create a branch with the feature/ prefix, work in it, merge it back, and delete it. Do the same for a bugfix/ branch.
  3. Hard: Use 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.
  4. Very hard: Create a situation where 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.
October 30, 2026, 3:59 pm

Session 15: Pull Requests: Your project's front door

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.

What you’ll learn today

  • What a pull request is and why teams use it
  • Opening a PR on GitHub
  • Updating your PR after feedback
  • Reviewing someone else’s PR
  • Merging a PR and cleaning up the branch

Step 0: Preparation: working in pairs

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:

  1. The author creates a repo on GitHub (public).
  2. The author goes to Settings → Collaborators → Add people.
  3. The author invites the reviewer with their GitHub username.
  4. The reviewer accepts the invitation (via email or GitHub notifications).

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.

Step 1: Creating and pushing a feature branch (author)

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-functie

Create a new file:

echo 'def groet(naam):' > groet.py
echo '    return f"Hallo {naam}!"' >> groet.py

Commit 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-functie

Step 2: Opening a pull request (author)

  1. Go to your repo on GitHub.
  2. GitHub now shows a yellow bar: “feature/groet-functie had recent pushes”: click Compare & pull request. (If you don’t see the bar: click the Pull requests tab, then New pull request, choose base: master ← compare: feature/groet-functie.)
  3. Write a clear title: “Add groet function”
  4. Write a description. A good PR description has three parts:
## 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-functie

Change 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 push

The new commit automatically appears in the PR. The reviewer sees the update and can look again.

Step 5: Merging and cleaning up

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 branch

Your master is now up to date. Your feature branch is gone, locally and on GitHub. Ready for the next piece of work.

The full PR workflow at a glance

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 branch

Reviewing someone else’s PR: how do you do that well?

A review is not: “Looks good.” A good review is specific, constructive and respectful.

Questions you ask yourself during a review:

  • Do I understand what this code does? (If not: ask for better comments or a clearer description.)
  • Does the code do what the PR description promises?
  • Can this be simpler?
  • Is anything missing? (Error handling, edge cases, documentation)
  • Are there secrets, passwords or personal data in it?
  • Does the code follow the style of the rest of the project?

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:

  1. On line 2: a default value for naam would be handy.
  2. 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.

Showcase

Swap roles with your buddy and do the whole workflow once more: now you are the reviewer.

Show a coach:

  1. An open pull request on GitHub with a description.
  2. At least one review comment on a buddy’s PR.
  3. A successfully merged PR: the feature code is now on master.

See you next time!

This was the last Git session. What you can do now:

  • Version-control your code with Git (init, add, commit, log, diff)
  • Experiment on branches without breaking master (branch, switch, merge)
  • Share your code online via GitHub (push, pull, clone)
  • Write professional commit messages
  • Work together via pull requests: opening and reviewing

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.

Take it home

  1. Easy: Open a pull request for a change in one of your own projects.
  2. Medium: Ask a friend or family member to make a GitHub account. Invite them as a collaborator and let them review a PR.
  3. Hard: Contribute to a real open source project. Search GitHub for issues with the good first issue label. Fork the repo, make a fix, and open a PR.
  4. Very hard: Build a small project with two others. Use branches, pull requests and reviews. Work as if you’re in a professional team.
October 30, 2026, 3:59 pm

Session 16: Markdown: Write notes that look like the web

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.

What you’ll learn today

  • Making headings and paragraphs
  • Formatting text: bold, italic, strikethrough
  • Lists and bullet points
  • Links to other pages and websites
  • Adding images
  • Making tables
  • Horizontal lines and code blocks

Why Markdown?

Markdown is everywhere:

  • GitHub: every repo has a README.md in Markdown. Issues, pull requests, wikis: all Markdown.
  • Obsidian, Joplin, Logseq: note-taking apps that save your documents as .md files.
  • Discord, Slack, Reddit: support Markdown for quick formatting in messages.
  • Hugo (this website!): all the session pages you see here are written in Markdown.
  • Jupyter Notebooks: the text cells are Markdown.

The big advantage: you write plain text. No complicated tags. And the result is always readable, even as plain text.

Step 0: Creating a Markdown file

Markdown files end in .md. Make one:

mkdir mijn-notities
cd mijn-notities
echo "# My First Note" > test.md

You 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.

Step 1: Headings and paragraphs

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 ##.

Step 2: Formatting text

**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 strikethrough

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.

Step 3: Horizontal lines

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.

Step 4: Lists

Unnumbered list

Use -, * or +:

- Python
- Git
- Markdown
- GitHub

You can nest lists by indenting (2 or 4 spaces):

- Programming languages
  - Python
  - JavaScript
- Version control
  - Git
  - GitHub

Numbered list

1. Open Thonny
2. Write your code
3. Click Run

It 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.

Checklist (GitHub style)

- [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.

Step 6: Images

An image is almost the same as a link, but with a ! in front:

![A star in Pygame Zero](/sessions/01-catch-the-stars/coordinaten.svg)

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.

Step 7: Tables

| 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             |

Step 8: Code and code blocks

Inline code

Use backticks for short pieces of code in a sentence:

Use `git status` to see what has changed.

Code blocks

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.

Code block without a language

```
This is just text
without syntax highlighting
```

Step 9: Blockquotes

Use > for quotes or important remarks:

> Git is not a backup. Git is a time machine.
> Someone on the internet

Several > lines one after another form one block. An empty > line starts a new paragraph inside the quote.

Step 10: Everything together: a real README

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)

Cheatsheet

WhatMarkdown
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![alt text](url)
Inline code`code`
Code block```language
Quote> quote
Table| column | column |

Showcase

Show a coach and a buddy:

  1. A Markdown document with at least one heading, a list, a link and bold text.
  2. A table with at least two columns and three rows.
  3. A checklist with ticked and open items.

See you next time!

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.

Take it home

  1. Easy: Write a README.md for your favourite Python project. Use headings, a list and a code block.
  2. Medium: Make a note in Obsidian (or another Markdown editor) about what you learned this month. Use all the formatting from the cheatsheet at least once.
  3. Hard: Build a “cheatsheet” page in Markdown for a topic of your choice (Git commands, Python syntax, maths formulas). Use tables and nested lists.
  4. Very hard: Make a Markdown document with a nested list that goes at least three levels deep, a table with alignment, and a code block with syntax highlighting.
October 30, 2026, 3:59 pm

Extra challenges

You’ve finished the ten sessions. Here you’ll find classic games to rebuild yourself. No step-by-step guide, just some grip and a starter file. Write the code yourself. How far do you get?

Challenges

  • Snake: Session 5+. Grow as long as you can. Eat fruit, avoid the walls and yourself.
  • Space Invaders: Session 7+. Shoot down the alien formation before they land.
October 30, 2026, 3:59 pm

Subsections of Extra challenges

Snake

Note

Requires: session 5 finished.

The snake moves by itself. You steer it with the arrow keys. Eat a piece of fruit: the snake grows one tile. Hit the wall or yourself: game over.

Getting started

Download the starter code and unpack the ZIP. Open main.py in Thonny and click Run. You see an empty grid. The controls and the timing are already there. You fill in the functions nieuw_fruit(), zet_stap() and teken().

How does it work?

Snake is played on a grid. The snake is a list of (column, row) coordinates. Every step you add a new head to the front of the list and remove the last element. When the snake eats fruit, you leave the last element in place, so the snake grows.

TILE = 20          # size of one tile in pixels
COLS = WIDTH // TILE
ROWS = HEIGHT // TILE

slang = [(10, 10), (9, 10), (8, 10)]  # list of (column, row)
dx, dy = 1, 0                          # direction: 1 step to the right

Loading and scaling images

The fruit pictures are 64×64 pixels. That is too big for one tile of 20×20. You load them with pygame.image.load() and then scale them with pygame.transform.scale(). This is already in the starter code.

surf = pygame.image.load('images/apple.png').convert_alpha()
fruit_images['apple'] = pygame.transform.scale(surf, (TILE, TILE))

convert_alpha() makes sure the transparency of the picture works properly. pygame.transform.scale() returns a new Surface at the size you ask for. The original stays unchanged.

To put a scaled picture on the screen, you use screen.blit():

screen.blit(fruit_surf, (fruit_pos[0] * TILE, fruit_pos[1] * TILE))

✅ Basic

  • Fill in nieuw_fruit(): pick a random fruit, find a free tile and store the position and the Surface.
  • Fill in zet_stap(): work out the new head, add it to the front of the list, remove the last element.
  • Fill in teken(): draw the grid (pygame.draw.line), the fruit (screen.blit), and every snake segment (pygame.draw.circle). Show the score with font.render() and screen.blit().
  • Spawn fruit when the snake eats it: leave out slang.pop() and call nieuw_fruit().

⭐ Stretch

  • Game over when the head leaves the grid.
  • Game over when the head hits one of its own segments (nieuw_hoofd in slang[1:]).
  • Show a game-over screen with the score. Press R to restart.

🔥 Expert

  • Raise the speed as the score goes up: lower STAP_MS based on the score (max(60, BASISINTERVAL - score * 10)).
  • Store the high score and show it next to the current score, also after a restart.
  • Make sure fruit never spawns on a tile the snake already fills.

Downloads

  • Starter code: main.py with the controls and timing ready; you fill in the functions.
  • Fruit images: if you start without the starter code (already included in the starter ZIP).

Fruit: Kenney Food Kit, CC0 (public domain). kenney.nl

October 30, 2026, 3:59 pm

Space Invaders

Note

Requires: session 7 finished.

Aliens are lined up in a 4×9 formation. They move back and forth, drop down at every edge and shoot back at random. You have one ship and three lives. Shoot them all down before they land.

Getting started

Download the starter code and unpack the ZIP. Open main.py in Thonny and click Run. You see the ship and the alien formation. The controls and the images are already there. You fill in the movement, the shooting and the collisions.

How does it work?

All the aliens are pygame.Rect objects in one list. Every frame you move them all by the same number of pixels. When the formation touches an edge, you reverse the direction and drop every alien by ALIEN_VAL pixels.

for alien in aliens:
    alien.x += snelheid * alien_richting

rechts = max(a.right for a in aliens)
links  = min(a.left  for a in aliens)
if rechts >= BREEDTE or links <= 0:
    alien_richting *= -1
    for alien in aliens:
        alien.y += ALIEN_VAL

Bullets are pygame.Rect objects too. Player bullets go up (k.y -= KOGEL_SNL), alien bullets go down (k.y += ALIEN_SNL). You check collisions with k.colliderect(alien).

✅ Basic

  • Draw the background (use random.seed(42) for fixed stars), the ship and all the aliens.
  • Move the ship left/right with the arrow keys.
  • Let the formation move back and forth and drop down at an edge.
  • Fire a bullet upwards when the player presses space. Use COOLDOWN_MS to stop insanely fast shooting.

⭐ Stretch

  • Player bullet hits an alien: remove both, score += 10.
  • Every alien shoots back at random: if random.random() < SCHIET_KANS.
  • Alien bullet hits the player: levens -= 1; game over at 0 lives.
  • Show the score and the lives in the top left.

🔥 Expert

  • The formation speeds up as fewer aliens are left: 1 + 3 * (1 - restant). From 1 pixel/frame at the start to 4 pixels/frame with the last alien.
  • Game over when an alien reaches the bottom (alien.bottom >= HOOGTE - 50).
  • Game over (won) when the list is empty.
  • Restart with R: set all the variables again through a reset() function.

Downloads

  • Starter code: main.py with the window, images and alien grid ready; you fill in the logic.
  • Images: ship.png, enemy.png and laser.png if you start without the starter code.

Images: Kenney Space Shooter Redux, CC0 (public domain). kenney.nl

October 30, 2026, 3:59 pm

Pac-Man

Note

Requires: session 9 finished.

A maze of blue walls. White dots everywhere. Four coloured ghosts coming at you. Eat all the dots to win. Touch a ghost and it’s game over.

Getting started

Download the starter code and unpack the ZIP. Open main.py in Thonny and click Run. The maze appears and Pac-Man is ready to go. You add the movement, the collisions and the ghosts.

How does it work?

Pac-Man moves in pixels, not in tiles. Every frame he moves snelheid × dt pixels in his direction. dt is the time (in seconds) the previous frame took. That way the game moves just as fast on slow and fast computers.

nieuw_x = self.rect.x + self.dx * PACMAN_SNL * dt

You check a wall collision by making a test rect at the new position and seeing whether it overlaps with a wall block:

test = self.rect.copy()
test.x = nieuw_x
test.y = nieuw_y
botsing = any(test.colliderect(m.rect) for m in muren)

✅ Basic

  • Let Pac-Man move in his current direction (the arrow keys are already there).
  • Add wall collision: stop when the new position touches a wall.
  • Make the direction change work: only switch to the new direction when it’s free.

⭐ Stretch

  • Eat white dots: when Pac-Man touches a dot, remove it and count 10 points.
  • Eat yellow power dots: remove, count 50 points and set self.powered_up = True.
  • Win when all the dots are gone: set self.staat = GEWONNEN.

🔥 Expert

  • Let the ghosts move: try directions and pick a free one (no wall collision).
  • Let the ghosts hunt Pac-Man: pick the direction that makes the distance to Pac-Man smaller.
  • If a ghost touches Pac-Man and powered_up is False: game over.
  • If a ghost touches Pac-Man and powered_up is True: eat the ghost (+200 points).

Downloads

  • Starter code: main.py + sound files; you fill in the movement and collisions.
October 30, 2026, 3:59 pm

Frogger

Note

Requires: session 5 (Breakout) finished.

A frog wants to reach the other side. For that he has to cross a busy road and a dangerous river. Cars drive in three lanes, logs float on the water. One wrong step and it’s over.

Getting started

Download the starter code and unpack the ZIP. Open main.py in Thonny and click Run. You see the playing field, but the frog doesn’t move yet. You fill in the functions.

How does it work?

The screen is split into horizontal lanes of 40 pixels:

  • Top: 5 ponds where the frog has to arrive safely
  • River (3 lanes): logs float past. On a log you’re safe, in the water you drown.
  • Grass verge: safe resting spot in the middle
  • Road (3 lanes): cars drive back and forth. Getting hit = dead.
  • Bottom: starting position of the frog
TEGEL = 40
KOLOMMEN, RIJEN = 13, 14

# Lanes: (row, direction, speed)
AUTO_BANEN = [
    (6,  1, 2),   # right, slow
    (7, -1, 4),   # left, fast
    (8,  1, 3),   # right, medium
]

Cars and logs are pygame.Rect objects in a list. Every frame you shift them all and remove the ones that fall off the screen.

The frog moves in fixed steps of TEGEL pixels. Press an arrow key and the frog jumps one tile in that direction.

You check collisions with rect.colliderect(andere_rect).

✅ Basic

  • Move the frog: catch the arrow keys in KEYDOWN and adjust kikker.x and kikker.y by TEGEL.
  • Spawn cars: fill in spawn_auto(). Pick a random lane, make a Rect just outside the screen, add it to auto_lijst.
  • Move cars: in update(): shift every car with rect.x += richting * snelheid. Remove cars outside the screen. Keep at least 6 cars.
  • Spawn logs: fill in spawn_hout(). The same principle, but with HOUT_BANEN and a bigger width.
  • Move logs: in update(): shift and remove like with the cars. Keep at least 4 logs.
  • Car collision: if kikker.colliderect(auto): call sterf().
  • Drowning: if the frog is in the river (row 2, 3 or 4) and not on a log: sterf().
  • Pond check: if the frog is close to an empty pond: set huizen_gevuld[i] = True, score += 10, reset the frog. 5 ponds full = won.

Done? You have a working Frogger!

⭐ Stretch

  • Lives counter: the levens system is already there. Make sure the game stops when levens <= 0 and show “GAME OVER”.
  • Timer: add a timer. Start with 30 seconds per level. When the timer runs out you lose a life. Show the timer in the HUD. Every pond you fill gives you 5 extra seconds.
  • Difficulty: raise the speed of the cars and logs as the score goes up (snelheid = basis + score // 50).

🔥 Expert

  • Bonus fly: a green insect appears at a random spot on the map and moves around. Catch it for 20 bonus points. Let it disappear after 5 seconds.
  • High score: store the highest score in a variable high_score. Show it next to the current score. Update it when the player sets a new record.
  • Different vehicles: add trucks (2 tiles wide, slower) and motorbikes (1 tile, faster). Use a separate list or a type field.
  • Animation: let the frog blink briefly when he dies (dood_timer is already there). Draw a small splash when the frog drowns.

Downloads

  • Starter code: main.py with the playing field, the frog and all the drawing code ready; you fill in the logic.

Inspired by the classic Frogger (1981, Konami). Drawings with pygame.draw.

October 30, 2026, 3:59 pm

Dojo Defender

Dojo Defender is our house game: a top-down survival shooter that we build together over several sessions.

Who is it for?

Dojo Defender is for ninjas who come along regularly. The game grows along with the normal lessons over 7 sessions. Only drop by now and then? No problem: do the mini-game of that session. It is always complete on its own.

Don’t count on a complete Dojo Defender if you miss a lot of sessions. But the template and all the snapshots are on this site, so building on at home is always possible.

How do I join in?

  • From lesson 2 you get a Dojo Defender snapshot at the end of every lesson: a ZIP file you can download from the lesson page.
  • Unpack it, open it in Thonny, and the next lesson you continue with it.
  • Missed a lesson? Download the snapshot of that lesson, or skip a lesson and pick it up further on.

Can I make it my own thing?

Yes! Change the name, the graphical style, the story. Dojo Defender is a template, not an obligation.

October 30, 2026, 3:59 pm

Subsections of Dojo Defender

Dojo Defender: Session 1

Your spaceship has landed in dark space. Time to bring it to life. In this session you add a ship you can steer and you make asteroids fall down. Dodge them or lose a life. How long do you survive?

What you’ll learn today

  • Using lists to keep track of multiple asteroids
  • Spawning and removing objects
  • Detecting collisions with colliderect
  • Keeping track of lives and showing an explosion effect
  • Forcing game over when your lives run out

Step 0: Setup

  1. Open Thonny.
  2. Download the Dojo Defender starter, unpack the ZIP and open main.py.
  3. Click Run. You see a dark blue screen with your spaceship at the bottom. You can already move it with the arrow keys.
  4. Problems? Take a look at Installing Thonny.

Step 1: Let asteroids spawn

A game without danger is boring. Add a list in which you keep track of all the asteroids, and a function that creates new asteroids:

# at the top, after WIDTH and HEIGHT:
asteroids = []

def spawn_asteroid():
    rock = Actor('asteroid_big')
    rock.x = random.randint(30, WIDTH - 30)
    rock.y = -30
    rock.speed = random.uniform(3, 5)
    asteroids.append(rock)

You also need import random at the top of the file. Add that line.

Call the function in update() with a counter so that an asteroid does not appear every single frame:

# at the top, outside the functions:
timer = 0

def update():
    global timer
    timer += 1
    if timer % 60 == 0:   # every second (60 frames)
        spawn_asteroid()

Put this before the movement code of the ship.

Step 2: ✅ Basic: dropping and drawing asteroids

Asteroids have to fall down and disappear when they are off the screen:

def update():
    global timer
    timer += 1
    if timer % 60 == 0:
        spawn_asteroid()

    for rock in asteroids[:]:      # [:] = copy so you can remove safely
        rock.y += rock.speed
        if rock.y > HEIGHT + 30:
            asteroids.remove(rock)

Don’t forget to draw the asteroids in draw():

def draw():
    screen.fill((10, 10, 30))
    for rock in asteroids:
        rock.draw()
    ship.draw()

Step 3: ⭐ Stretch: collision + lives

When an asteroid hits your ship, you lose a life. Add lives and game_over at the top:

lives = 3
game_over = False

Add collision detection in the for loop:

    for rock in asteroids[:]:
        rock.y += rock.speed

        if rock.y > HEIGHT + 30:
            asteroids.remove(rock)
        elif rock.colliderect(ship):
            asteroids.remove(rock)
            global lives, game_over
            lives -= 1
            if lives <= 0:
                game_over = True

Show the lives and the score in draw():

    screen.draw.text(f"Score: {score}", (10, 10), color="white", fontsize=24)
    screen.draw.text(f"Lives: {lives}", (10, 40), color="red", fontsize=24)

Stop the game when game_over is True:

def update():
    if game_over:
        return
    ...

Show a game-over screen in draw():

    if game_over:
        screen.draw.text(
            "GAME OVER",
            center=(WIDTH / 2, HEIGHT / 2),
            color="red",
            fontsize=60,
        )
        screen.draw.text(
            f"Final Score: {score}",
            center=(WIDTH / 2, HEIGHT / 2 + 60),
            color="white",
            fontsize=30,
        )

Step 4: 🔥 Expert: explosions + invisibility

When an asteroid hits you, you want to see an explosion instead of the ship just disappearing. Use the four explosion frames (explosion_1 to explosion_4):

explosion = Actor('explosion_1')
explosion_active = False
explosion_frame = 0
explosion_timer = 0

On a collision: hide the ship, start the explosion:

        elif rock.colliderect(ship):
            asteroids.remove(rock)
            global lives, game_over, explosion_active, explosion_frame, explosion_timer
            lives -= 1

            explosion.pos = (ship.x, ship.y)
            explosion.image = 'explosion_1'
            explosion_frame = 1
            explosion_timer = 5
            explosion_active = True
            ship.visible = False

            if lives <= 0:
                game_over = True

Play the explosion in update():

    if explosion_active:
        explosion_timer -= 1
        if explosion_timer <= 0:
            explosion_frame += 1
            if explosion_frame > 4:
                explosion_active = False
                ship.visible = True
                ship.pos = (WIDTH / 2, HEIGHT - 50)
            else:
                explosion.image = f'explosion_{explosion_frame}'
                explosion_timer = 5

Then add an invisibility period so that you don’t get hit again straight away:

invincible = False
invincible_timer = 0

In update():

    if invincible:
        invincible_timer -= 1
        if invincible_timer <= 0:
            invincible = False
            ship.visible = True

Adjust the collision check:

        elif rock.colliderect(ship) and not invincible and ship.visible:

Draw the explosion in draw():

    if explosion_active:
        explosion.draw()

Showcase

Show your Dojo Defender to a coach and a buddy. How long do you survive? Do you have explosions? Does it look cool?

See you next time!

“Next time: your spaceship SHOOTS BACK. Enemies in formation, and you blast them away with lasers. This is going to be epic.”

Take it home

Try one of these extensions at home:

  1. Sound — Add sounds/explosion.wav when an asteroid explodes (sounds.explosion.play()).
  2. Harder — Make asteroids fall faster as the score rises: rock.speed = random.uniform(3 + score // 10, 5 + score // 10).
  3. Power-up — Add a rare green power-up that gives an extra life.
  4. Smaller — Use asteroid_med or asteroid_small for variation in size and speed.

Download Dojo Defender

October 30, 2026, 3:59 pm

Dojo Defender: Session 2

Your ship has a weapon now. Enemies are coming. Shoot them down before they reach the bottom. Every 10 enemies means a new wave — and it gets harder and harder.

What you’ll learn today

  • Shooting with the space bar and a cooldown
  • Lists for bullets and enemies
  • Collision between bullets and enemies
  • Waves that get harder and harder
  • Enemies that make sine movements

Step 0: Open the starter

  1. Open Thonny.
  2. Download the Dojo Defender starter for today, unpack the ZIP and open main.py.
  3. Were you not there last time? Download the solution from session 1 and start with that.
  4. Click Run. You see the same game as last time: ship, asteroids, explosions.
  5. Problems? Take a look at Installing Thonny.

Step 1: Shooting with SPACE

Your ship has a laser cannon. Add a list for bullets at the top:

bullets = []

Use on_key_down — it is called only once when you press a key (unlike update, which keeps repeating):

def on_key_down(key):
    if key == keys.SPACE:
        bullet = Actor('bullet')
        bullet.x = ship.x
        bullet.y = ship.y - 20
        bullets.append(bullet)

Make the bullets move up in update() and remove them when they are off screen:

for bullet in bullets[:]:
    bullet.y -= 8
    if bullet.y < -10:
        bullets.remove(bullet)

Don’t forget to draw the bullets in draw():

for bullet in bullets:
    bullet.draw()

Step 2: ✅ Basic: shooting cooldown

Without a cooldown you fire dozens of bullets per second. Add a timer:

shoot_cooldown = 0

In update():

if shoot_cooldown > 0:
    shoot_cooldown -= 1

Adjust on_key_down so that you can only shoot when the cooldown is 0:

def on_key_down(key):
    global shoot_cooldown
    if key == keys.SPACE and shoot_cooldown == 0:
        bullet = Actor('bullet')
        bullet.x = ship.x
        bullet.y = ship.y - 20
        bullets.append(bullet)
        shoot_cooldown = 15

Step 3: ⭐ Stretch: adding enemies

Enemies spawn at the top and fall down. Add a list:

enemies = []

A spawn function:

def spawn_enemy():
    enemy = Actor('enemy_drone')
    enemy.x = random.randint(30, WIDTH - 30)
    enemy.y = -30
    enemy.speed = random.uniform(1, 3)
    enemies.append(enemy)

Call spawn_enemy() in update() with a spawn interval:

# at the top:
spawn_counter = 0
spawn_interval = 120

# in update():
spawn_counter += 1
if spawn_counter >= spawn_interval:
    spawn_counter = 0
    spawn_enemy()

Draw enemies in draw():

for enemy in enemies:
    enemy.draw()

Step 4: 🔥 Expert: collision + waves

Bullet hits enemy: remove both, +10 points:

for bullet in bullets[:]:
    bullet.y -= 8
    if bullet.y < -10:
        bullets.remove(bullet)
        continue
    for enemy in enemies[:]:
        if bullet.colliderect(enemy):
            bullets.remove(bullet)
            enemies.remove(enemy)
            global score
            score += 10
            break

Enemy hits the bottom or the ship: lose a life (same as an asteroid).

Waves: a new wave every 10 enemies. Enemies spawn faster:

# at the top:
wave = 1
enemies_destroyed = 0

# on destroy:
enemies_destroyed += 1
if enemies_destroyed % 10 == 0:
    wave += 1
    spawn_interval = max(60, spawn_interval - 10)

Show the current wave in draw():

screen.draw.text(f"Wave: {wave}", (10, 70), color="yellow", fontsize=24)

Show “WAVE N!” in big letters on a new wave (use a timer).

Sine movement: let enemies veer left and right:

import math  # at the top

enemy.x = enemy.start_x + math.sin(timer * 0.03 + enemy.strafe_offset) * 50

Showcase

Show your Dojo Defender to a coach. How many waves do you reach? Do you have the sine movement working? Compare scores with a buddy.

See you next time!

“Next time: POWER-UPS. Shields, speed boosts and spread shots. Your ship gets stronger the longer you survive.”

Take it home

Try one of these extensions at home:

  1. Enemy variation — Use enemy_charger.png for a faster enemy that doesn’t do a sine, but comes straight at you.
  2. Sound — Add sounds/laser.wav and sounds/explosion.wav.
  3. Hard mode — Enemies spawn faster and faster, up to every 30 frames at most.
  4. Score multiplier — The higher the wave, the more points per kill: score += 10 * wave.

Download Dojo Defender

October 30, 2026, 3:59 pm

Dojo Defender: Session 3

Today Dojo Defender moves from pgzero to real pygame. No more pgzrun.go() — you write the game loop yourself. The game stays the same, but the engine underneath is different. And at the end you add SOUND.

What you’ll learn today

  • The difference between pgzero and raw pygame
  • Writing the game loop yourself: while running: clock.tick(60)
  • Converting actors to Surface + Rect
  • Loading and playing sound with pygame.mixer
  • Looping background music

Step 0: Download the starter

Today’s starter is a full port of Dojo Defender Session 2 to raw pygame. No more pgzero, but it plays the same.

  1. Download the Dojo Defender starter for today, unpack it and open main.py.
  2. Were you not there last time? Download the solution from session 2 and try to port it.
  3. Click Run. The game works the way you’re used to: ship, asteroids, bullets, enemies, waves, explosions.

What is different?

In pgzero you wrote draw() and update() and pgzrun did the rest. In raw pygame you do everything yourself:

pgzero (before):

import pgzrun

def draw():
    screen.fill((10, 10, 30))
    ship.draw()

def update():
    if keyboard.left:
        ship.x -= 5

pgzrun.go()

raw pygame (now):

import pygame, sys
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()

running = True
while running:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit(); sys.exit()

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        ship.rect.centerx -= 5

    screen.fill((10, 10, 30))
    ship.draw(screen)
    pygame.display.flip()

The most important differences:

pgzeroraw pygame
Actor('plaatje')GameObject('plaatje.png', x, y)
keyboard.leftkeys[pygame.K_LEFT]
on_key_down(key)event.type == pygame.KEYDOWN
screen.draw.text(...)font.render() + screen.blit()
rock.colliderect(ship)rock.rect.colliderect(ship.rect)
pgzrun.go()while running: ... clock.tick(60)

Step 1: Explore the code

Open main.py and compare it with the old pgzero code of session 2. Look for these parts:

  1. Game loop — Look for the while running: loop. Do you see clock.tick(60), pygame.display.flip()?
  2. GameObject — Look for the class GameObject. How is an image loaded? What is rect?
  3. Input — Look for pygame.key.get_pressed() and pygame.KEYDOWN. How does the space bar work?

Change the WIDTH to 1024. What happens? (Everything gets wider — the game still works.)

Step 2: ✅ Basic — Add restart with the R key

In the pgzero version you could not restart after game over. Add that now:

# in the event loop, on KEYDOWN:
if event.key == pygame.K_r and game_over:
    # reset everything
    ship.rect.center = (WIDTH / 2, HEIGHT - 50)
    ship.visible = True
    asteroids.clear()
    bullets.clear()
    enemies.clear()
    lives = 3
    score = 0
    wave = 1
    enemies_destroyed = 0
    spawn_counter = 0
    spawn_interval = 120
    wave_display_timer = 0
    game_over = False
    invincible = False
    ship.visible = True

Also show a hint on the game-over screen: press R to play again.

Step 3: ⭐ Stretch — Add sound

The starter has no sound yet. Add sounds!

Make a sounds/ folder next to images/ and copy the sound files from dojo-defender-assets/sounds/ (ask the coach). Or download the solution, which already contains them.

Add at the top:

pygame.mixer.init()  # after pygame.init()

shoot_sound = pygame.mixer.Sound('sounds/shoot.wav')
explode_sound = pygame.mixer.Sound('sounds/explode.wav')
hit_sound = pygame.mixer.Sound('sounds/hit.wav')
pygame.mixer.music.load('sounds/bg_music.wav')
pygame.mixer.music.play(-1)  # -1 = keep repeating

Watch out: the path has to be right. Use os.path.join(_DIR, 'sounds/shoot.wav') like the starter also does with images.

Play the sounds at the right moments:

  • shoot_sound.play() — when you shoot
  • explode_sound.play() — when an enemy or asteroid explodes
  • hit_sound.play() — when your ship gets hit

Step 4: 🔥 Expert — Mute button and volume

Add an M key that turns the sound on and off:

muted = False

# in KEYDOWN:
if event.key == pygame.K_m:
    muted = not muted
    pygame.mixer.music.set_volume(0 if muted else 1)

Every sound then has to be checked:

if not muted:
    shoot_sound.play()

Volume per sound: Make the shooting sound quieter than the explosion:

shoot_sound.set_volume(0.3)
explode_sound.set_volume(0.7)

Showcase

Show a coach that:

  • Your game works in real pygame without pgzero
  • You added sound (shoot, explode, hit, bg music)
  • Bonus: restart with R or mute with M

See you next time!

“Next time: POWER-UPS. Shields, speed boosts and spread shots. And your ship gets stronger the longer you survive.”

Take it home

Try one of these extensions at home:

  1. Health bar — Replace the text “Lives: 3” with a visual bar: draw three small rectangles at the top.
  2. Pause — Add a P key that pauses the game. Setting running = False is not enough — use a separate paused variable.
  3. Particle effect — Let small white dots splatter around on an explosion (use a list of particles that get smaller and smaller).
  4. Your own skin — Replace player_ship.png with your own design.

Download Dojo Defender

October 30, 2026, 3:59 pm

Dojo Defender: Session 4

Today Dojo Defender becomes a real game with a menu. No more black start — you get a title screen, a game-over screen, and three different enemy types that each move in their own way.

What you’ll learn today

  • State machines: your game has states (MENU, PLAYING, GAME OVER)
  • Enemy AI: every enemy moves differently (straight, zigzag, charger)
  • Wave system: increasingly hard waves with a mix of enemies
  • A reset function: putting everything back neatly without restarting

Step 0: Download the starter

The starter is the solution of session 3 — Dojo Defender in raw pygame with sound.

  1. Download the starter for today, unpack it and open main.py.
  2. Weren’t you there? Download the solution of session 3.
  3. Click Run. The game works like before: ship, asteroids, bullets, drones, sound.

Step 1: Add a state machine

A state machine keeps track of which “state” the game is in:

state = "MENU"  # can be "MENU", "PLAYING", "GAME_OVER"

Put this variable at the top, with the other global variables.

Drawing the menu screen: Replace the screen.fill and all the drawing with a check on state:

if state == "MENU":
    # Draw title and instructions
    screen.fill((10, 10, 30))
    titel = title_font.render("DOJO DEFENDER", True, (0, 200, 255))
    screen.blit(titel, (WIDTH // 2 - titel.get_width() // 2, HEIGHT // 2 - 60))
    sub = big_font.render("Press SPACE to start", True, (200, 200, 200))
    screen.blit(sub, (WIDTH // 2 - sub.get_width() // 2, HEIGHT // 2))

elif state == "PLAYING":
    # = all your existing game code here

elif state == "GAME_OVER":
    # Draw "GAME OVER" + score

Make title_font and big_font at the top:

title_font = pygame.font.SysFont(None, 80)
big_font = pygame.font.SysFont(None, 60)

Now SPACE has to do the following:

  • If state == "MENU": reset the game and go to PLAYING
  • If state == "GAME_OVER": reset the game and go to PLAYING
  • If state == "PLAYING" and game_over == False: shoot like normal

Change the KEYDOWN logic:

if event.key == pygame.K_SPACE:
    if state == "MENU":
        reset_game()
        state = "PLAYING"
    elif state == "GAME_OVER":
        reset_game()
        state = "PLAYING"
    elif state == "PLAYING" and shoot_cooldown == 0 and not game_over:
        # shoot (existing code)

Step 2: ✅ Basic — Write the reset function

Make a function that puts everything back to the starting values:

def reset_game():
    global ship, asteroids, bullets, enemies
    global lives, score, game_over, timer
    global explosion_active, explosion_frame, explosion_timer
    global invincible, invincible_timer, shoot_cooldown
    global wave, wave_display_timer, enemies_destroyed
    global spawn_counter, spawn_interval

    ship.rect.center = (WIDTH / 2, HEIGHT - 50)
    ship.visible = True
    asteroids.clear()
    bullets.clear()
    enemies.clear()
    lives = 3
    score = 0
    game_over = False
    timer = 0
    explosion_active = False
    explosion_frame = 0
    explosion_timer = 0
    invincible = False
    invincible_timer = 0
    shoot_cooldown = 0
    wave = 1
    wave_display_timer = 0
    enemies_destroyed = 0
    spawn_counter = 0
    spawn_interval = 120

Set state = "GAME_OVER" when the player has 0 lives (at lives <= 0):

if lives <= 0:
    game_over = True
    state = "GAME_OVER"

Check: The game now starts with a blue title screen. SPACE starts the game. On game over you see GAME OVER and you can restart with SPACE.

Step 3: ⭐ Stretch — Three enemy types

Replace the single spawn_enemy() with three functions:

def spawn_enemy_drone():
    enemy = GameObject('enemy_drone.png', random.randint(30, WIDTH - 30), -30)
    enemy.type = 'drone'
    enemy.speed = random.uniform(1, 3)
    enemies.append(enemy)


def spawn_enemy_zigzag():
    enemy = GameObject('enemy_zigzag.png', random.randint(30, WIDTH - 30), -30)
    enemy.type = 'zigzag'
    enemy.speed = random.uniform(2, 4)
    enemy.dx = random.choice([-2, 2])
    enemies.append(enemy)


def spawn_enemy_charger():
    enemy = GameObject('enemy_charger.png', random.randint(30, WIDTH - 30), -30)
    enemy.type = 'charger'
    enemy.speed = random.uniform(3, 5)
    enemies.append(enemy)

Movement per type

In the game loop, where you did enemy.rect.y += enemy.speed, you now add different movement per type:

for enemy in enemies[:]:
    if enemy.type == 'drone':
        enemy.rect.y += enemy.speed

    elif enemy.type == 'zigzag':
        enemy.rect.x += enemy.dx
        enemy.rect.y += enemy.speed
        if enemy.rect.left <= 0 or enemy.rect.right >= WIDTH:
            enemy.dx = -enemy.dx

    elif enemy.type == 'charger':
        # Steer towards the player's x position
        dx = ship.rect.centerx - enemy.rect.centerx
        if dx > 0:
            enemy.rect.centerx += min(enemy.speed, abs(dx))
        elif dx < 0:
            enemy.rect.centerx -= min(enemy.speed, abs(dx))
        enemy.rect.y += enemy.speed * 0.8

    # After that: off-screen check, collision with the ship, etc.

Important: The charger has to descend more slowly (* 0.8) so the player has time to react.

Step 4: 🔥 Expert — Enhanced waves

Make a spawn_enemy() that decides which type spawns based on the current wave:

def spawn_enemy():
    if wave == 1:
        spawn_enemy_drone()
    elif wave <= 3:
        if random.random() < 0.5:
            spawn_enemy_drone()
        else:
            spawn_enemy_zigzag()
    else:
        r = random.random()
        if r < 0.3:
            spawn_enemy_drone()
        elif r < 0.65:
            spawn_enemy_zigzag()
        else:
            spawn_enemy_charger()

That way it gets harder and harder:

  • Wave 1: only drones (warming up)
  • Wave 2-3: drones + zigzag (50/50)
  • Wave 4+: all three, with more chargers (30% drone, 35% zigzag, 35% charger)

Showcase

Show a coach that:

  • You have a menu screen with a title and “Press SPACE to start”
  • You have three enemy types: drone (↓), zigzag (↘↗), charger (→ your ship)
  • The waves get harder and harder (more zigzags, then chargers)
  • Game over works with a score display and restart

See you next time!

“Next time: POWER-UPS. Shields, speed-boosts, spread-shots. And a real final boss.”

Take it home

Try one of these extensions at home:

  1. High score — Save the highest score in a file and show it on the menu.
  2. Difficulty — Let the player choose between “Easy” and “Hard” on the menu.
  3. Boss wave — Every 5 waves a big boss appears with lots of hits.
  4. Charger Warning — Draw a red line under a charger before it appears.

Download Dojo Defender

October 30, 2026, 3:59 pm

Dojo Defender: Session 5

Today you give Dojo Defender a depth effect with a parallax star background and explosion particles. Asteroids also become destructible: big asteroids split into smaller ones.

What you’ll learn today

  • Parallax maths: several layers of stars with their own speed
  • Particle lists: a list of dictionaries for particles
  • Position-based effects: spawning particles in the right spot
  • Recursive splitting: big asteroid → 2 medium → 2 small → gone

Step 0: Download the starter

The starter is the solution of session 4 — Dojo Defender with a menu, three enemy types and waves.

  1. Download the starter for today, unpack it and open main.py.
  2. Weren’t you there? Download the solution of session 4.
  3. Click Run. The game works like before: ship, asteroids, bullets, drones, zigzags, chargers.

Step 1: 🌟 Parallax star background

A parallax background has several layers that move at different speeds. Far away = slow, close by = fast. That gives depth.

Add a stars = [] list at the top of the file, with the other global variables:

stars = []

Make an init_stars() function that creates three layers of stars:

def init_stars():
    global stars
    stars = []
    layers = [
        (40, 0.3, 2, 120),   # far away: 40 stars, speed 0.3, size 2, darker
        (40, 0.7, 3, 180),   # middle:   40 stars, speed 0.7, size 3
        (30, 1.5, 4, 255),   # close by: 30 stars, speed 1.5, size 4, bright
    ]
    for count, speed, size, brightness in layers:
        for _ in range(count):
            stars.append({
                'x': random.randint(0, WIDTH),
                'y': random.randint(0, HEIGHT),
                'speed': speed,
                'size': size,
                'brightness': brightness,
            })

The maths behind it: every star gets a y speed. Far stars (0.3 px/frame) barely move, nearby stars (1.5 px/frame) move faster. Your brain reads that as depth, exactly like in a car: trees next to the road fly past, mountains far away stay still.

Also add the update function. It makes stars drop and puts them back at the top when they’re below the screen:

def update_stars():
    for star in stars:
        star['y'] += star['speed']
        if star['y'] > HEIGHT + star['size']:
            star['y'] = -star['size']
            star['x'] = random.randint(0, WIDTH)

And a drawing function. You draw small stars with pygame.draw.rect (2×2), bigger ones with pygame.draw.circle:

def draw_stars(surface):
    for star in stars:
        c = (star['brightness'], star['brightness'], star['brightness'])
        if star['size'] <= 2:
            pygame.draw.rect(surface, c, (star['x'], star['y'], star['size'], star['size']))
        else:
            pygame.draw.circle(surface, c, (int(star['x']), int(star['y'])), star['size'] // 2)

Calling them: Call init_stars() once (at start-up, after all the functions), update_stars() every frame (even in the menu!), and draw_stars(screen) after screen.fill() in ALL states.

Check

Do you see three layers of stars scrolling down, in the menu too? The far stars are smaller and move more slowly.

Step 2: ✅ Basic — Particle system

A particle is a small speck that moves, fades and disappears. We use a list of dictionaries:

particles = []  # {x, y, vx, vy, life, max_life, color, size}

Make a function to spawn particles:

def spawn_particles(x, y, count, colors, speed_range, size_range, life_range):
    for _ in range(count):
        angle = random.uniform(0, 2 * math.pi)
        speed = random.uniform(speed_range[0], speed_range[1])
        particles.append({
            'x': x,
            'y': y,
            'vx': math.cos(angle) * speed,
            'vy': math.sin(angle) * speed,
            'life': random.randint(life_range[0], life_range[1]),
            'max_life': life_range[1],
            'color': random.choice(colors),
            'size': random.uniform(size_range[0], size_range[1]),
        })

What happens here? Every particle gets:

  • A position (x, y)
  • A speed in x and y (vx, vy), calculated from an angle and a speed
  • A lifetime (life goes down by 1 every frame)
  • A colour and size

Update function (move every particle 1 step every frame and shorten its lifetime):

def update_particles():
    for p in particles[:]:
        p['x'] += p['vx']
        p['y'] += p['vy']
        p['life'] -= 1
        if p['life'] <= 0:
            particles.remove(p)

Why [:]? We change the list while looping over it (we remove particles). Without [:] you get a runtime error. The slice [:] makes a copy of the list so you can loop over it safely.

Drawing function with fading (alpha fade): the less lifetime left, the smaller and darker:

def draw_particles(surface):
    for p in particles:
        ratio = p['life'] / p['max_life']
        color = tuple(int(c * ratio) for c in p['color'])
        pygame.draw.circle(surface, color, (int(p['x']), int(p['y'])), int(p['size'] * ratio))

The ratio is a number between 1 (just born) and 0 (almost dead). We multiply the colour and size by it, so the particle slowly disappears.

Emptying the particle list in reset

Add particles.clear() to the reset_game() function, otherwise dead particles keep floating around after a restart.

Call update_particles() in the PLAYING state and draw_particles(screen) after draw_stars(screen) in all states.

Check

Start the game and press SPACE. Nothing visible happens yet — but the system is ready for the next steps.

Step 3: ⭐ Stretch — Position-based effects

Now we’re going to spawn particles at the right moments and positions.

Exhaust fumes

When the ship moves, spawn small red/orange particles under the ship. Add this in the if not game_over section, where you check the arrow keys:

keys = pygame.key.get_pressed()
moving = False
if keys[pygame.K_LEFT] and ship.rect.centerx > 30:
    ship.rect.centerx -= 5
    moving = True
if keys[pygame.K_RIGHT] and ship.rect.centerx < WIDTH - 30:
    ship.rect.centerx += 5
    moving = True

if moving and ship.visible:
    spawn_particles(
        ship.rect.centerx, ship.rect.bottom,
        1,  # or random.randint(1, 2)
        [(255, 100, 0), (255, 60, 0), (200, 50, 0)],
        (0.5, 1.5), (2, 4), (10, 20),
    )

The particles spawn under the ship (ship.rect.bottom) and get a small upward speed (negative vy because of the angle).

Explosion debris

When an enemy is hit by a bullet, spawn 8-12 particles in all directions. Add this where you remove an enemy from the list after a bullet collision:

spawn_particles(
    enemy.rect.centerx, enemy.rect.centery,
    random.randint(8, 12),
    [(255, 140, 0), (255, 0, 0), (255, 255, 0), (255, 255, 255)],
    (1, 4), (2, 5), (20, 40),
)

Enemy death sparks

When an enemy reaches the bottom of the screen, spawn 3-5 white sparks:

spawn_particles(
    enemy.rect.centerx, HEIGHT - 30,
    random.randint(3, 5),
    [(255, 255, 255), (200, 200, 255)],
    (1, 3), (1, 3), (15, 30),
)

Check

Move the ship — do you see orange exhaust fumes? Shoot an enemy — do you see 8-12 specks flying around? Let an enemy escape — do you see white sparks?

Step 4: 🔥 Expert — Destructible asteroids

Asteroids can now split recursively:

  • asteroid_big.png → gets hit → 2× asteroid_med.png
  • asteroid_med.png → gets hit → 2× asteroid_small.png
  • asteroid_small.png → gets hit → gone + particles

Change spawn_asteroid() so every asteroid gets an asteroid_size:

def spawn_asteroid(size='big'):
    rock = GameObject(f'asteroid_{size}.png', random.randint(30, WIDTH - 30), -30)
    rock.speed = random.uniform(3, 5)
    rock.asteroid_size = size
    asteroids.append(rock)
    return rock

Make a split_asteroid() function that splits an asteroid into two smaller ones:

def split_asteroid(rock):
    # First: exploding particles
    spawn_particles(
        rock.rect.centerx, rock.rect.centery,
        random.randint(8, 12),
        [(255, 140, 0), (255, 0, 0), (255, 255, 0), (255, 255, 255)],
        (1, 4), (2, 5), (20, 40),
    )
    if rock.asteroid_size == 'big':
        for _ in range(2):
            r = spawn_asteroid('med')
            r.rect.center = rock.rect.center
            r.rect.x += random.randint(-20, 20)
            r.speed = random.uniform(2, 4)
    elif rock.asteroid_size == 'med':
        for _ in range(2):
            r = spawn_asteroid('small')
            r.rect.center = rock.rect.center
            r.rect.x += random.randint(-15, 15)
            r.speed = random.uniform(1, 3)
    asteroids.remove(rock)

What happens here?

  1. We spawn explosion particles at the position of the asteroid
  2. We check the size: ‘big’ → 2 ‘med’, ‘med’ → 2 ‘small’, ‘small’ → no further splitting
  3. New asteroids spawn close to the old position with a small offset
  4. The smaller asteroids are slower than the bigger ones

Change the collision logic: where you did astersoids.remove(rock) on a bullet hit, now call split_asteroid(rock):

for rock in asteroids[:]:
    if bullet.rect.colliderect(rock.rect):
        bullets.remove(bullet)
        explode_sound.play()
        split_asteroid(rock)
        score += 5
        break

Important: You play the sound (explode_sound.play()) before split_asteroid() so it doesn’t accidentally play twice.

Check

Shoot a big asteroid. Do you see it split into two medium asteroids? Shoot those — they split into two small ones. Shoot the small ones — they disappear with a nice particle explosion.

Showcase

Show a coach that:

  • Parallax star background with three speeds
  • Particle system with fading (alpha fade)
  • Exhaust fumes under the moving ship
  • Explosion debris on destroyed enemies and asteroids
  • Asteroids splitting: big → med → small → gone

See you next time!

“Next time: POWER-UPS. Shields, speed-boosts, spread-shots. And a real final boss.”

Take it home

Try one of these extensions at home:

  1. Easy: change the colour of the stars per layer (e.g. far stars bluish, nearby ones yellow).
  2. Medium: add a fourth layer of stars, even faster (speed 3.0), with big bright stars.
  3. Tricky: let the particle colour change during its lifetime (from yellow to red to grey).
  4. Very tricky: add a “comet” particle type that has a small tail.

Download Dojo Defender

October 30, 2026, 3:59 pm

Dojo Defender: Session 6

Today you add a final boss to Dojo Defender. The boss appears every 5 waves and has 3 phases with their own behaviour. You learn to work with a state machine of its own, HP bars and screen effects.

What you’ll learn today

  • State machine for a boss: PHASE_1, PHASE_2, PHASE_3 and EXPLODING
  • Drawing an HP bar with a colour gradient (green → yellow → red)
  • Homing bullets: a bullet that moves towards the player
  • Screen shake: a random offset on the drawing coordinates
  • Phase transitions with visual effects

Step 0: Download the starter

The starter is the solution of session 5 — Dojo Defender with parallax stars, particles and splitting asteroids.

  1. Download the starter for today, unpack it and open main.py.
  2. Weren’t you there? Download the solution of session 5.
  3. Click Run. The game works like before. Kill 10 enemies to reach wave 5 — then the boss appears!

Step 1: ✅ Basic — Boss variables

A boss has variables of its own. Add these to the other global variables:

boss = None
boss_active = False
boss_hp = 0
boss_hp_max = 50
boss_phase = "PHASE_1"
boss_timer = 0
boss_phase_transition_timer = 0
boss_defeated_timer = 0
boss_shoot_timer = 0
boss_minion_spawn_timer = 0
boss_bullets = []
boss_minions = []
screen_shake_x = 0
screen_shake_y = 0
boss_direction = 1

Also add emptying boss_bullets and boss_minions in reset_game().

Why so many variables? A boss is more complex than ordinary enemies. It has to move, shoot, spawn minions, have phases and a death animation. Every variable is one aspect of that behaviour.

Check

Start the game. Nothing changes yet — but the variables are ready.

Step 2: ⭐ Stretch — Spawning and drawing the boss

Spawn function

Write a function that activates the boss:

def start_boss():
    global boss_active, boss, boss_hp, boss_phase, boss_timer
    global boss_bullets, boss_minions, boss_shoot_timer
    global boss_minion_spawn_timer, boss_direction, screen_shake_x, screen_shake_y
    boss_active = True
    boss = GameObject('boss.png', WIDTH / 2, -80)
    boss_hp = boss_hp_max
    boss_phase = "PHASE_1"
    boss_timer = 0
    boss_shoot_timer = 0
    boss_minion_spawn_timer = 0
    boss_direction = 1
    screen_shake_x = 0
    screen_shake_y = 0
    boss_bullets.clear()
    boss_minions.clear()
    roar_path = os.path.join(_DIR, 'sounds/boss_roar.wav')
    if os.path.exists(roar_path):
        pygame.mixer.Sound(roar_path).play()

Boss wave detection

Every 5th wave (from wave 5 on) a boss appears. Add this where the wave goes up:

def check_boss_wave():
    if wave >= 5 and wave % 5 == 0 and not boss_active:
        start_boss()

Call check_boss_wave() after wave += 1.

Stopping enemies during the boss

Make sure ordinary enemies don’t spawn during a boss fight. Put the spawn logic in an if:

if not boss_active:
    spawn_counter += 1
    if spawn_counter >= spawn_interval:
        spawn_counter = 0
        spawn_enemy()

Drawing the boss

Draw the boss when it’s active, and also draw the HP bar at the top of the screen:

if boss_active and boss is not None:
    boss.draw(screen)
    # HP bar
    bar_width = 300
    bar_height = 20
    bar_x = (WIDTH - bar_width) // 2
    bar_y = 10
    bar_bg = pygame.Rect(bar_x, bar_y, bar_width, bar_height)
    pygame.draw.rect(screen, (100, 20, 20), bar_bg)
    hp_ratio = max(0, boss_hp / boss_hp_max)
    fill_width = int(bar_width * hp_ratio)
    if hp_ratio > 0.5:
        color = (0, 255, 0)
    elif hp_ratio > 0.25:
        color = (255, 255, 0)
    else:
        color = (255, 0, 0)
    fill_rect = pygame.Rect(bar_x, bar_y, fill_width, bar_height)
    pygame.draw.rect(screen, color, fill_rect)
    pygame.draw.rect(screen, (255, 255, 255), bar_bg, 2)
    label = font.render(f"BOSS  {boss_hp}/{boss_hp_max}", True, (255, 255, 255))
    screen.blit(label, (bar_x + bar_width // 2 - label.get_width() // 2, bar_y - 5))

Check

Play until wave 5. Do you see the boss come in with an HP bar? Ordinary enemies no longer spawn.

Step 3: 🔥 Expert — Boss AI (3 phases)

The boss has a state machine with 3 phases. Every phase is different behaviour.

Phase 1 (HP 34-50, 100-66%)

The boss drifts slowly to the left and right and now and then fires a homing bullet.

def fire_boss_bullet():
    dx = ship.rect.centerx - boss.rect.centerx
    dy = ship.rect.centery - boss.rect.centery
    dist = math.sqrt(dx * dx + dy * dy)
    if dist == 0:
        dist = 1
    speed = 3
    bullet = {
        'x': boss.rect.centerx,
        'y': boss.rect.centery + 30,
        'vx': dx / dist * speed,
        'vy': dy / dist * speed,
    }
    boss_bullets.append(bullet)

Maths: By dividing dx and dy by the distance (dist) you get a unit vector — a direction with length 1. Multiply by speed for the speed you want. This works in every direction, not just horizontal/vertical.

Add the AI update in the game loop (if boss_active and boss is not None):

if boss.rect.centery < 80:
    boss.rect.centery += 2  # coming in
else:
    if boss_phase == "PHASE_1":
        boss.rect.centerx += boss_direction * 1.5
        if boss.rect.centerx > WIDTH - 60:
            boss_direction = -1
        if boss.rect.centerx < 60:
            boss_direction = 1
        boss_shoot_timer += 1
        if boss_shoot_timer >= 60:
            boss_shoot_timer = 0
            fire_boss_bullet()

Phase transition (HP ≤ 66%)

Check whether the boss has to change phase. Call this after the boss update:

hp_ratio = boss_hp / boss_hp_max
if hp_ratio <= 0.66 and boss_phase == "PHASE_1":
    boss_phase = "PHASE_2"
    boss_phase_transition_timer = 20

During a phase transition you make the boss flash:

if boss_phase_transition_timer > 0 and boss_phase_transition_timer % 4 < 2:
    flash_surf = pygame.Surface(boss.image.get_size(), pygame.SRCALPHA)
    flash_surf.fill((255, 255, 255, 180))
    screen.blit(flash_surf, (boss.rect.x, boss.rect.y))

Phase 2 (HP 17-33, 66-33%)

The boss moves faster, fires more often and spawns minions:

elif boss_phase == "PHASE_2":
    boss.rect.centerx += boss_direction * 3
    ...
    boss_shoot_timer += 1
    if boss_shoot_timer >= 40:
        boss_shoot_timer = 0
        fire_boss_bullet()
    boss_minion_spawn_timer += 1
    if boss_minion_spawn_timer >= 180:
        boss_minion_spawn_timer = 0
        spawn_boss_minion()

Spawning a minion:

def spawn_boss_minion():
    minion = GameObject('boss_minion.png', random.randint(30, WIDTH - 30), -30)
    minion.speed = random.uniform(1, 2)
    boss_minions.append(minion)

Phase 3 (HP 0-16, 33-0%)

The boss is enraged: fires two bullets at once, spawns minions faster, and the screen shakes:

elif boss_phase == "PHASE_3":
    boss.rect.centerx += boss_direction * 4
    ...
    boss_shoot_timer += 1
    if boss_shoot_timer >= 20:
        boss_shoot_timer = 0
        fire_boss_bullet()
        if boss_hp < boss_hp_max * 0.15:
            fire_boss_bullet()
    ...

Screen shake: add a random offset to ALL the drawing coordinates:

if boss_phase == "PHASE_3":
    screen_shake_x = random.randint(-3, 3)
    screen_shake_y = random.randint(-3, 3)
else:
    screen_shake_x = 0
    screen_shake_y = 0

Change all the draw functions so they accept offset_x and offset_y parameters. For example draw_stars(screen, screen_shake_x, screen_shake_y).

Check

Play until wave 5. Do you see the boss go through 3 phases? Phase 1: slow, now and then a bullet. Phase 2: faster + minions. Phase 3: screen shakes, rain of bullets.

Step 4: 💀 Expert — Defeating the boss

When the boss reaches 0 HP, the exploding sequence starts:

if boss_hp <= 0:
    boss_phase = "EXPLODING"
    boss_defeated_timer = 90
    boss_phase_transition_timer = 60
    score += 500
    explode_sound.play()

During EXPLODING you spawn a particle burst every 10 frames:

if boss_phase == "EXPLODING":
    if boss_phase_transition_timer > 0 and boss_phase_transition_timer % 10 == 0:
        spawn_particles(
            boss.rect.centerx + random.randint(-40, 40),
            boss.rect.centery + random.randint(-40, 40),
            random.randint(10, 20),
            [(255, 255, 0), (255, 100, 0), (255, 0, 0), (255, 255, 255)],
            (2, 6), (3, 7), (20, 40),
        )

Show “BOSS DEFEATED!” at the top of the screen:

if boss_defeated_timer > 0:
    defeat_msg = big_font.render("BOSS DEFEATED!", True, (255, 255, 0))
    screen.blit(defeat_msg, (WIDTH // 2 - defeat_msg.get_width() // 2, HEIGHT // 2 - 100))

Watch out: after the boss, switch the ordinary enemy spawning back on. The wave continues to the next one.

Check

Defeat the boss. Do you see the explosion animation, the “BOSS DEFEATED!” text and +500 score? Do the ordinary waves continue after that?

Showcase

Show a coach that:

  • Boss appears every 5 waves with an HP bar
  • 3 phases with their own behaviour (slow → minions → enraged)
  • Homing bullets that fly towards the player
  • Screen shake in phase 3
  • Minions spawning in phase 2 and 3
  • Boss defeated: explosion sequence, “BOSS DEFEATED!”, +500 score

See you next time!

“Next time: the polished version. Sound, menu screen, high scores — and your own twist on the game.”

Take it home

Try one of these extensions at home:

  1. Easy: change the boss colour or size via the image.
  2. Medium: add a fourth phase (HP < 10%) in which the boss teleports.
  3. Tricky: let the boss fire delay-bullets that leave a trail.
  4. Very tricky: add a second boss type that appears in a different wave.

Download Dojo Defender

October 30, 2026, 3:59 pm

Dojo Defender: Session 7

Today you decide what happens in Dojo Defender. No step-by-step instructions — pick a feature, plan it, and build it.

You start from the complete Dojo Defender with everything on it: ship, asteroids, 3 enemy types, waves, particles, parallax stars, and a final boss with 3 phases.

This is your game. Make something of it.

Choose what you want to build

Below are some ideas. Pick one — or come up with your own feature.

Easy

  • Power-ups: enemies sometimes (20%) drop a power-up. Red = spread shot (3 bullets), green = shield, yellow = speed boost. Power-ups work for 5 seconds.
  • Difficulty: make the game easier (more lives, slower enemies) or harder (fewer lives, faster spawns).
  • Sound: add a power-up sound (powerup.wav), a game-over sound, or background music that speeds up at higher waves.

Medium

  • High score: save the highest score in a file (highscore.txt). Show it on the menu and game-over screen. Only overwrite it if the new score is higher.
  • Difficulty scaling: make the game a bit harder every wave — faster enemies, smaller asteroids, more frequent spawning.
  • Poison enemies: a new enemy type that leaves a trail of poisonous particles.
  • Ship animation: the ship can only fire 3 bullets at a time. Load a “heat” indicator or add a cooldown.

Hard

  • Second boss: make a new boss that appears in a different wave (e.g. wave 7). Different looks, different behaviour.
  • Extending the power-up system: add a 4th power-up (e.g. “time freeze” — enemies freeze for 3 seconds).
  • Combo system: the faster you kill enemies, the more points. A “combo multiplier” that goes up.
  • Boss has its own power-up drops: the boss drops a rare power-up when it’s defeated.

Own idea

Got an idea of your own? Great. Discuss it briefly with a coach and start building.

Tips for planning your feature

Before you start writing code, answer these questions:

  1. What should the player see? (a new power-up, a text, an icon?)
  2. When does it happen? (when you press SPACE, when an enemy dies, when wave 10 starts?)
  3. How long does it last? (5 seconds? Permanent? Until the next wave?)
  4. What has to change in the code? (new variables? a new list? a timer?)
  5. How do I know it works? (what is the “check” — what do I have to see to know it’s finished?)

Write your answers on paper or in comments at the top of the file.

Cheatsheets from previous sessions

Can’t remember something anymore? Use the cheatsheets from previous sessions:

Coach-guided planning

You fill this part in together with a coach before you start coding.

My feature: _________________________________________________

I choose:  Easy  /  Medium  /  Hard  /  Own idea

New variables I need:
- __________________________________
- __________________________________
- __________________________________

New images or sounds:
- __________________________________
- __________________________________

Check: how do I know it works?
- __________________________________

Showcase

At the end of the session you show a coach and your buddy what you built. Tell them in 30 seconds:

  • What you wanted to make
  • Whether it worked
  • What was the hardest part

Step-by-step plan (for when you can’t figure it out yourself)

  1. Open main.py in Thonny and click Run — does it still work?
  2. Make a copy of the game folder in case you get stuck.
  3. Start with the smallest possible change (e.g. adding a variable).
  4. Test. Does it work? → Next step. Doesn’t it work? → Ask a coach.

See you next time!

“Next time: a completely new game. What will it be?”

October 30, 2026, 3:59 pm

Progress per session

DD SessionAfter curriculumWhat is new?Snapshot
1S2Spaceship + asteroids + lives + explosions(follows)
2S3Shooting + enemies + score + waves(follows)
3S4 (Pygame)Rebuilt in Pygame with sprites + sound(follows)
4S5Menu + 3 enemy types(follows)
5S6Parallax stars + particles + splitting asteroids(follows)
6S7Boss fight with 3 phases + HP bar(follows)
7S8Your own version (power-ups, high score, …)(follows)
October 30, 2026, 3:59 pm

Resources

(Content to follow; this section will be filled in later.)

October 30, 2026, 3:59 pm

Subsections of Resources

Free assets

Your game looks a lot more professional straight away with good sprites, sound effects and music, and it doesn’t have to cost you anything. Below you’ll find the best places to look for free assets, neatly sorted by category.

Sprites & tilesets

  • Kenney.nl: The absolute favourite. Kenney makes hundreds of sprite packs, tilesets and UI elements, all released as CC0 (public domain). You don’t need to credit anyone and you may use everything in commercial projects too. Start here.
  • OpenGameArt.org: Big community with sprites, tilesets and animations in all kinds of styles. Do check the licence per asset: some are CC0, others CC-BY or CC-BY-SA. Filter by licence so you don’t get surprises.
  • itch.io free game assets: Lots of indie artists put free packs on itch.io. Quality varies, but there are real gems in there. Always read the licence text on the maker’s page.

Sound effects

  • Freesound.org: Huge library of sound effects from all over the world. Filter the search results on “Creative Commons 0” to see only CC0 sounds, then you don’t have to credit anyone.
  • Kenney audio packs: Kenney also delivers ready-made CC0 packs for sound: retro sounds, UI clicks, explosions, everything in one download.
  • sfxr.me: Generate your own 8-bit sound effects in the browser. Click a category (laser, explosion, powerup…), adjust the sliders to your taste and download the .wav. No licence worries: you generate it yourself.

Music

  • Incompetech: Kevin MacLeod composed hundreds of tracks in all genres, from chiptune to orchestra. The music is free to use under CC-BY: you simply mention “Music by Kevin MacLeod (incompetech.com)” somewhere in your credits or README.
  • Pixabay music: Tracks released as CC0 or under their own free licence. You don’t have to credit anyone and you may use it in any project.

Fonts

  • Google Fonts: Thousands of open-source fonts you may download and use for free, in games too. Want a retro look? Search for “pixel” or “monospace” for fonts that work well in Pygame (pygame.font.Font).

Licences explained

Not all “free” assets are the same. These are the three licences you run into most often:

LicenceMay you use it?Attribution needed?Commercial use?
CC0YesNoYes
CC-BYYesYes, mention the authorYes
CC-BY-NCYesYesNo

Practical advice: If you’re in doubt, use CC0. You don’t need permission, you don’t have to credit anyone and you never risk breaking the rules by accident. Kenney.nl is your best friend.


See how we document assets in this project on the Asset licences page.

October 30, 2026, 3:59 pm

Asset licences (this project)

Most assets are CC0 (public domain). Exceptions are listed in the table with licence and required credit text.

Images

FileSourceLicenceAuthorDate downloaded
basket.pngKenney Space Shooter ReduxCC0Kenney (kenney.nl)2026-05-11
star.pngKenney Space Shooter ReduxCC0Kenney (kenney.nl)2026-05-11
ship.pngKenney Space Shooter ReduxCC0Kenney (kenney.nl)2026-05-11
challenges/01-snake/fruits/*.png (9 items)Kenney Food KitCC0Kenney (kenney.nl)2026-05-24

Files live in static/_shared/images/ and are available on the published URL path /_shared/images/<filename>.

Sounds

FileSourceLicenceAuthorDate downloaded
shoot.wavCC02026-05-11
hit.wavCC02026-05-11
explosion.wavCC02026-05-11
pickup.wavCC02026-05-11
jump.wavCC02026-05-11
music.oggincompetech.comCC-BY 4.0Kevin MacLeod2026-05-17

Required credit for music.ogg

“Ethereal Relaxation” Kevin MacLeod (incompetech.com)
Licensed under Creative Commons: By Attribution 4.0
creativecommons.org/licenses/by/4.0

How to add assets

  1. Download from a CC0 source (kenney.nl, freesound.org filter Creative Commons 0).
  2. Put it in static/_shared/images/ or static/_shared/sounds/.
  3. Update a row in the table above with source, licence, author, date.
  4. For non-CC0 (e.g. CC-BY): put the credit text under the table.
October 30, 2026, 3:59 pm

Publishing with pygbag

You’ve built a cool game and now you want to share it with the rest of the world. With pygbag you turn your Pygame game into a web page that anyone can play in the browser. No setup, no fuss. Just send a link and you’re done!

What is pygbag?

Pygbag compiles your Python code to WebAssembly and packs it with a small Pygame runtime that runs in the browser. The player doesn’t have to install anything: just open the page and play. That makes it perfect for sharing your game with friends, family or on itch.io.

Limitations

Pygbag does not work with Pgzero (the simplified library from the first sessions). You need real Pygame, so this is available from session 4 onwards.

On top of that, a few things don’t work in the browser:

  • No disk access: you can’t read or write files from inside the game.
  • No threads: don’t use threading or multiprocessing.
  • No blocking calls: everything has to run asynchronously (see below).

Installing

In a uv project:

uv add pygbag

Or in Thonny through the package manager:

pip install pygbag

Getting the project ready

Pygbag requires your main loop to be asynchronous. That means: your main function becomes async def main(), and in every frame iteration you add await asyncio.sleep(0). That one line gives the browser the chance to breathe between frames.

Below you’ll find a ready-made basic structure: Pygbag-ready structure for main.py:

import asyncio
import pygame

pygame.init()
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()

async def main():
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
        screen.fill((30, 30, 60))
        pygame.display.flip()
        clock.tick(60)
        await asyncio.sleep(0)  # required for pygbag

asyncio.run(main())

Adapt your own game code so everything sits inside async def main(), and don’t forget await asyncio.sleep(0) at the bottom of the while loop.

Building

In your terminal, go to the folder with main.py and run:

uv run python -m pygbag --build main.py

The output appears in the build/web/ folder. That folder contains everything you need to host your game.

Testing locally

Want to see whether it works before you publish? Use the same command without --build:

uv run python -m pygbag main.py

Pygbag starts a local web server on http://localhost:8000. Open that link in your browser and you play your own game, exactly the way others will see it.

Publishing

Once everything works, there are two ways to share your game:

  1. Through your coach: zip the build/web/ folder and give the zip file to your coach. They take care of the hosting.
  2. Through itch.io: make a free account on itch.io, create a new project as an HTML game, and upload the zip of build/web/. Within a few minutes your game has its own page that anyone can visit!

Congratulations! You’ve just published a real web game. 🎮

October 30, 2026, 3:59 pm

Translation guide

This site exists in Dutch, French and English. This page explains how the translations fit together, so you can maintain them without breaking the structure.

How the languages are stored

Every page has one file per language, in the same folder:

content/sessions/python/01-catch-the-stars/_index.md      ← Dutch (default language)
content/sessions/python/01-catch-the-stars/_index.en.md   ← English
content/sessions/python/01-catch-the-stars/_index.fr.md   ← French
  • Dutch lives on the root (/sessions/...), French and English under /fr/ and /en/ with the same path.
  • If a translation is missing, the language switcher still shows that language, but takes you to the home page of that language. So there is never a dead link.
  • Adding a new page? Create the three files straight away. title, weight, hidden and description belong in every language; weight and hidden stay identical.

What you don’t translate

  • Code identifiers (basket, star, ship, score) stay English, because they live in the Python file.
  • File names (star.png, boom.wav, starter.zip) stay language-neutral.
  • Product names: Pygame, Pgzéro/Pgzero, Thonny, PyCharm, VS Code, uv, Python, Git, GitHub, pygbag.
  • Dojo terms we use as loanwords: ninja, coach, dojo, cheatsheet, cliffhanger, starter, checkpoint, Dojo Defender.

What you do translate

  • All prose text, headings, titles and description fields.
  • Comments and docstrings in code blocks; identifiers and strings the program really needs stay as they are.
  • Labels of the interactive demos (sliders, canvas texts).
  • Coach notes and the “take it home” suggestions.

Fixed terms

DutchEnglishFrench
sessiesessionsession
uitdagingchallengedéfi
werkbladworksheetfiche de travail
oplossingsolutionsolution
bronnenresourcesressources
installerensetupinstallation
coach-notitiescoach notesnotes du coach
Wat je vandaag leertWhat you’ll learn todayCe que tu vas apprendre aujourd’hui
Neem mee naar huisTake it homeÀ emporter chez toi
Vastgelopen?Stuck?Bloqué ?
Tot de volgende keer!See you next time!À la prochaine !

Tone: Dutch uses “je”, English uses “you”, French uses “tu” (tutoiement) — the texts are for ninjas between 10 and 17 years old.

  • Links to other pages use {{% relref "/sessions/python/01-catch-the-stars" %}}, so they automatically follow the right language.
  • Links to files in static/ (images, demo JS, ZIPs) are absolute and language-neutral.
  • ZIPs with code exist per language: starter.zip (Dutch), starter.en.zip and starter.fr.zip. Every language page links to its own ZIP. Asset packs without code (images.zip, fruits.zip) have no language variant.

Code per language

The source code lives in code/. Comments are Dutch; the translations sit as main.en.py and main.fr.py next to the original. When the ZIPs are built, <naam>.<taal>.py is renamed to <naam>.py and the variants of the other languages are dropped. make zips takes care of that.

Interactive demos

Every demo JS in static/sessions/ has a STRINGS table at the top with Dutch, English and French. The language comes from the lang attribute of the page; without a table or with an unknown language the demo falls back to Dutch.

Check before you push

make build

The build fails on a broken relref. Then go through the three languages of the changed page: title, headings, links and downloads have to work the same in all three.