An interactive, level-based Python game that teaches the four classic raster-graphics algorithms by visualising every single pixel decision in real time.
| Package | Purpose |
|---|---|
| Python 3.8+ | Language runtime |
| pygame | Window, event loop, 2-D drawing |
| numpy | (optional) numerical helpers |
pip install pygame numpynumpy is listed as a dependency but the core algorithms are pure-Python integer math; the app will run without it if you remove the import.
# 1. cd into the project folder
cd raster_master
# 2. launch
python main.pyA 1 280 × 720 window opens immediately. No configuration files needed.
raster_master/
├── main.py ← entry-point & event loop
├── algorithms.py ← DDA, Bresenham, Midpoint Circle & Ellipse
├── game_state.py ← shared mutable state (single source of truth)
├── levels.py ← level definitions, scoring, progression
├── ui.py ← all pygame drawing & click handling
├── save_load.py ← JSON quick-save / quick-load
├── test_algorithms.py ← 31 unit tests (no pygame needed)
└── README.md ← this file
| Key | Action |
|---|---|
| P | Play / Pause animation |
| N | Step forward one iteration |
| R | Reset visualisation to step 0 |
| C | Clear & restart current level |
| 1–5 | Jump directly to Level 1–5 |
| ← → | Previous / Next level |
| A | Toggle DDA ↔ Bresenham comparison overlay |
| S | Quick-save progress |
| L | Quick-load progress |
| ESC | Quit |
| Scroll (right panel) | Adjust animation speed (1–20) |
- Algorithm: Digital Differential Analyser
- What you do: Click a start point, then an end point on the left canvas.
- Watch: The floating-point increments
x_incandy_incin the variable watch panel; each step rounds to the nearest pixel.
- Algorithm: Bresenham (integer only)
- What you do: Same click workflow as Level 1.
- Watch: The
errdecision variable flip-flopping between positive and negative — that's how it avoids any floating-point math. - Tip: Press A to overlay the DDA result and compare pixel-by-pixel.
- Algorithm: Midpoint decision parameter, 8-fold symmetry
- What you do: Click the centre on the left canvas. Scroll on the right panel to change the radius, then press C to re-run.
- Watch: Only one octant is truly computed; the other 7 are mirrored.
- Algorithm: Two-region midpoint approach, 4-fold symmetry
- What you do: Click the centre. Scroll to change
a(semi-major); hold Shift while scrolling to changeb(semi-minor). - Watch: The
regionvariable switch from 1 → 2; that's where the slope crosses 45°.
- Timed (60 s): Draw a line AND a circle as close to the target as possible.
- Press Finish (or wait for the timer) to see your Jaccard-overlap score.
Score is computed as the Jaccard index between your drawn pixels and the ideal (pre-computed) target:
Score = |drawn ∩ target| / |drawn ∪ target| × 100
- 100 = perfect overlap
- 0 = no overlap at all
- A "ghost" of the target is shown in dim green on the left canvas so you can visually aim for it.
python test_algorithms.py # or: pytest test_algorithms.py -v31 tests covering:
| Suite | Cases |
|---|---|
TestDDA |
single point, H/V/diagonal lines, reversed direction |
TestBresenham |
single point, H/V, all octants, integer-only variables |
TestMidpointCircle |
r=0, r=1, 8-fold symmetry, pixel-count scaling |
TestMidpointEllipse |
circle case (a=b), wide, tall, two-region detection |
TestScoringLogic |
perfect/zero/partial overlap, empty sets |
| Colour | Meaning |
|---|---|
| Dim green | Target ghost (ideal solution) |
| Cyan / bright blue | Pixels plotted by the algorithm |
| Yellow glow | Currently active pixel (this step) |
| Orange dots | Comparison algorithm overlay |
GameStateis the single mutable object shared by every subsystem. No subsystem owns state; they all read/write through it.- Algorithm classes are stateless;
run()returns a flat list of step dicts. This makes pause/resume/seek trivial. UIRendererowns allpygame.drawcalls. It registers clickable button rects each frame and hit-tests them onMOUSEBUTTONDOWN.LevelManagerdecides which algorithm to instantiate, wires user parameters into it, and owns the scoring calculation.SaveManagerserialises only the three fields that need to persist (current_level,total_score,levels_completed) to a JSON file.
steps = max(|dx|, |dy|)
x_inc = dx / steps ← float
y_inc = dy / steps ← float
each iteration: plot(round(x), round(y)); x += x_inc; y += y_inc
err = dx - dy ← integer
each iteration:
e2 = 2·err
if e2 > -dy: err -= dy; x += sx
if e2 < dx: err += dx; y += sy
p = 1 - r
while x > y:
y += 1
if p < 0: p += 2y + 1
else: x -= 1; p += 2(y-x) + 1
plot 8 symmetric points
Region 1 (b²·x < a²·y): increment x, optionally decrement y
Region 2 (y ≥ 0): decrement y, optionally increment x
4-fold mirror after each step
Happy rasterising! 🖥️