1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
|
# Downstroke Demo Games Design
**Date:** 2026-04-05
**Status:** Approved
**Scope:** `demo/` folder, 5 demo games, Makefile `make demos` target, CLAUDE.md update
---
## Goal
Provide 5 small self-contained demo games in `demo/` that collectively exercise every engine system. Each demo compiles to its own executable (`bin/demo-*`). They replace the macroknight integration test for Milestone 8 and serve as living documentation of the engine API.
---
## File Layout
```
demo/
assets/ ← copied from macroknight/assets (not symlinked)
monochrome-transparent.png ← tileset spritesheet
monochrome_transparent.tsx ← tileset metadata (TSX)
level-0.tmx ← level used by platformer, topdown, sandbox
DejaVuSans.ttf ← font for audio demo text
jump.wav ← sound effect (platformer jump, shmup shoot)
theme.ogg ← music (audio demo)
platformer.scm
shmup.scm
topdown.scm
audio.scm
sandbox.scm
```
**Omitted from copy:** `prefabs.scm`, `macroknight.tiled-project`, `macroknight.tiled-session` — macroknight-specific files not needed by any demo.
**Audio modules:** `mixer.scm` and `sound.scm` are copied from `macroknight/` into the downstroke repo root and added to the build. They are engine-level modules and are required by any demo that uses audio.
When copying `sound.scm`, add one function that macroknight's version omits:
```scheme
(define (stop-music!) (mix-halt-music))
```
The audio demo uses `stop-music!` to toggle music off.
---
## Build
### Makefile additions
```makefile
DEMO_NAMES := platformer shmup topdown audio sandbox
DEMO_BINS := $(patsubst %,bin/demo-%,$(DEMO_NAMES))
demos: engine $(DEMO_BINS)
bin/demo-%: demo/%.scm $(OBJECT_FILES) | bin
csc demo/$*.scm $(OBJECT_FILES) -o bin/demo-$* -I bin
```
- `make` — builds engine modules only (unchanged)
- `make demos` — builds all 5 demo executables; depends on engine being built first
- Demos are compiled as programs (not units), linked against all engine `.o` files
- `$(OBJECT_FILES)` includes `mixer` and `sound` once those modules are added to `MODULE_NAMES`
### `render-scene!` nil guards
`render-scene!` in `renderer.scm` is updated so that entity drawing is **nested inside** the tilemap guard:
- Tilemap drawing only fires if `(scene-tilemap scene)` is not `#f`
- Entity drawing only fires if **both** `(scene-tilemap scene)` AND `(scene-tileset-texture scene)` are not `#f`
```scheme
(when tilemap
(draw-tilemap renderer camera tileset-texture tilemap)
(when tileset-texture
(let ((tileset (tilemap-tileset tilemap)))
(draw-entities renderer camera tileset tileset-texture entities))))
```
**Consequence for shmup:** since shmup has no tilemap, the engine will not draw its entities. Shmup draws its player, bullets, and enemies as **colored SDL rectangles** in its `render:` hook — matching the original "colored rects" intent. This is acceptable because shmup entities are simple geometric shapes, not sprites.
Audio (no scene at all) works because `engine.scm` guards `render-scene!` with `(when (game-scene game) ...)`.
### CLAUDE.md update
Add to the Build & Test section:
> `make demos` must always succeed. A demo that fails to compile is a build failure. Run `make && make demos` to verify both engine and demos build cleanly.
---
## Demo Code Pattern
Every demo follows this ~30-line structure:
```scheme
(import (prefix sdl2 "sdl2:")
(prefix sdl2-ttf "ttf:")
(prefix sdl2-image "img:")
downstroke/engine
downstroke/world
downstroke/tilemap
downstroke/renderer
downstroke/input
downstroke/physics
downstroke/assets)
(define *game*
(make-game
title: "Demo: <Name>" width: 600 height: 400
preload: (lambda (game) ...) ;; load tilemap, tileset texture, sounds
create: (lambda (game) ...) ;; build scene, place entities
update: (lambda (game dt) ...) ;; input dispatch, physics calls
render: (lambda (game) ...))) ;; HUD overlay (optional)
(game-run! *game*)
```
Tile IDs in entity plists are placeholder values — to be adjusted visually after first run.
---
## The 5 Demos
### 1. `demo/platformer.scm` — Platformer
**Systems exercised:** `input`, `physics` (gravity + tile collision), `renderer` (tilemap + entities), `world`/scene, camera follow, audio (sound effect)
**Mechanics:**
- Player entity with gravity, left/right movement, jump
- Tile collision via `apply-physics` from `physics.scm`
- Camera follows player horizontally
- Jump sound via `(play-sound 'jump)` (loaded in preload: as `'(jump . "demo/assets/jump.wav")`)
- Level: `demo/assets/level-0.tmx`
- Tile IDs: placeholder (user adjusts)
**Key entity plist:**
```scheme
(list #:type 'player
#:x 100 #:y 50
#:width 16 #:height 16
#:vx 0 #:vy 0
#:gravity? #t
#:on-ground? #f
#:tile-id 1)
```
**Update logic:** read input → set `#:vx` from left/right → jump sets `#:vy` → call physics step → update camera x to follow player x.
---
### 2. `demo/shmup.scm` — Shoot-em-up
**Systems exercised:** `entity` (spawning/removal), manual AABB entity-entity collision (removal-based, not `physics.scm`), `input`, `renderer` (SDL colored rects), `world`/scene (no tilemap)
**Mechanics:**
- Player ship at bottom, moves left/right
- Space bar fires bullet upward (new entity added to scene)
- Enemies spawn from top at random x positions every N frames, move downward
- Bullet-enemy collision: both entities removed from scene
- No tilemap — plain background (black/SDL clear)
- No gravity on any entity
- `jump.wav` plays on shoot
**Key entity plists:** (no `#:tile-id` — entities are drawn as colored rects)
```scheme
;; player
(list #:type 'player #:x 280 #:y 360 #:width 16 #:height 16 #:vx 0 #:vy 0)
;; bullet
(list #:type 'bullet #:x px #:y 340 #:width 4 #:height 8 #:vx 0 #:vy -5)
;; enemy
(list #:type 'enemy #:x rx #:y 0 #:width 16 #:height 16 #:vx 0 #:vy 2)
```
**Rendering:** shmup has no tilemap, so `render-scene!` draws nothing for this scene. Shmup implements its own `render:` hook using `sdl2:render-fill-rect!` with distinct colors (player = white, bullet = yellow, enemy = red). The camera is at (0,0) so screen coords == world coords.
**Update logic:** move entities by vx/vy each frame → manual AABB collision check between bullets and enemies (not `resolve-entity-collisions` — shmup uses removal, not push-apart) → filter removed entities from scene → spawn new enemy every 60 frames → read input for player movement and shoot.
---
### 3. `demo/topdown.scm` — Top-down explorer
**Systems exercised:** `input` (8-directional), `renderer` (tilemap + entity), `world`/scene, camera follow (both axes), `physics` (no gravity)
**Mechanics:**
- Player entity moves in 8 directions (WASD or arrows)
- No gravity (`#:gravity? #f`)
- Camera follows player on both x and y axes
- Level: `demo/assets/level-0.tmx` (same tilemap, different movement feel)
- Tileset texture loaded in preload: — required for entity sprite rendering (tilemap + tileset-texture both present so render-scene! draws entities via tileset)
- No audio
**Update logic:** read input → set `#:vx` and `#:vy` from direction keys → apply tile collision (no gravity component) → update camera to center on player.
---
### 4. `demo/audio.scm` — Audio showcase
**Systems exercised:** audio (sound effects + music), `renderer` (text via `draw-ui-text`), `input`, `assets`
**Mechanics:**
- Static screen with text instructions rendered via `draw-ui-text`
- Press **J** → play `jump.wav`
- Press **M** → toggle `theme.ogg` music on/off
- Press **Escape** → quit
- No tilemap, no physics, no moving entities
- Uses `DejaVuSans.ttf` for text
- Audio calls via `sound.scm` functions: `load-sounds!`, `play-sound`, `load-music!`, `play-music!`, `stop-music!` (added when copying sound.scm — see File Layout)
**Display:** colored rectangle background + text labels for each key binding.
---
### 5. `demo/sandbox.scm` — Physics sandbox
**Systems exercised:** `physics` (gravity + tile collision + entity-entity collision), `renderer`, `world`/scene, no player input
**Mechanics:**
- 10 entities spawned at random x positions near the top of the screen
- All have `#:gravity? #t` and `#:solid? #t` (required for `resolve-entity-collisions` to participate)
- Physics step runs each frame (gravity accelerates, tile collision stops them)
- Entities rest on floor tiles or bounce (depending on physics.scm behavior)
- No player — pure observation of physics pipeline
- Level: `demo/assets/level-0.tmx`
- After all entities settle (or after 10 seconds), loop: despawn all, respawn at new random positions
---
## Systems Coverage Matrix
| System / Module | platformer | shmup | topdown | audio | sandbox |
|---|---|---|---|---|---|
| `engine` (make-game, game-run!) | ✓ | ✓ | ✓ | ✓ | ✓ |
| `input` | ✓ | ✓ | ✓ | ✓ | — |
| `physics` (gravity) | ✓ | — | — | — | ✓ |
| `physics` (tile collision) | ✓ | — | ✓ | — | ✓ |
| `physics` (entity collision) | — | — | — | — | ✓ |
| manual AABB (removal) | — | ✓ | — | — | — |
| `renderer` (tilemap) | ✓ | — | ✓ | — | ✓ |
| `renderer` (entities) | ✓ | — | ✓ | — | ✓ |
| `renderer` (SDL colored rects) | — | ✓ | — | — | — |
| `renderer` (text) | — | — | — | ✓ | — |
| `world` / scene | ✓ | ✓ | ✓ | — | ✓ |
| `assets` registry | ✓ | ✓ | ✓ | ✓ | ✓ |
| audio (sound) | ✓ | ✓ | — | ✓ | — |
| audio (music) | — | — | — | ✓ | — |
| camera follow | ✓ (x) | — | ✓ (xy) | — | — |
---
## Out of Scope
- Animation state machine (`animation.scm`) — not yet extracted to downstroke
- AI (`ai.scm`) — not yet extracted
- Prefab system — not yet extracted
- Scene transitions — Milestone 9
- Asset-type-specific load helpers (`game-load-tilemap!` etc.) — Milestone 6
|