aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-04-05-milestone-8-game-object-lifecycle.md826
-rw-r--r--docs/superpowers/specs/2026-04-05-demos-design.md223
-rw-r--r--docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md234
3 files changed, 0 insertions, 1283 deletions
diff --git a/docs/superpowers/plans/2026-04-05-milestone-8-game-object-lifecycle.md b/docs/superpowers/plans/2026-04-05-milestone-8-game-object-lifecycle.md
deleted file mode 100644
index 0d12308..0000000
--- a/docs/superpowers/plans/2026-04-05-milestone-8-game-object-lifecycle.md
+++ /dev/null
@@ -1,826 +0,0 @@
-# Milestone 8 — Game Object and Lifecycle API
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Introduce `make-game` + `game-run!` as the public entry point for downstroke, backed by a minimal asset registry, and port macroknight to use them.
-
-**Architecture:** Two new modules (`assets.scm` — key/value registry; `engine.scm` — game struct, lifecycle, frame loop), plus `render-scene!` added to `renderer.scm`. `game-run!` owns SDL2 init, window/renderer creation, and the frame loop; lifecycle hooks (`preload:`, `create:`, `update:`, `render:`) are user-supplied lambdas.
-
-**Tech Stack:** Chicken Scheme, SDL2 (sdl2 egg), sdl2-ttf, sdl2-image, SRFI-64 (tests), defstruct egg
-
-**Spec:** `docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md`
-
----
-
-## File Map
-
-| Action | File | Purpose |
-|---|---|---|
-| Create | `assets.scm` | Key→value asset registry |
-| Create | `tests/assets-test.scm` | SRFI-64 unit tests for assets.scm |
-| Modify | `renderer.scm` | Add `render-scene!` |
-| Modify | `tests/renderer-test.scm` | Add tests for `render-scene!` |
-| Create | `engine.scm` | `make-game`, `game-run!`, accessors |
-| Create | `tests/engine-test.scm` | Unit tests for make-game, accessors (SDL2 mocked) |
-| Modify | `Makefile` | Add assets + engine to build, add test targets |
-| Modify | `/home/gene/src/macroknight/game.scm` | Port to `make-game` + `game-run!` |
-
----
-
-## Task 1: `assets.scm` — minimal key/value registry
-
-**Files:**
-- Create: `assets.scm`
-- Create: `tests/assets-test.scm`
-
-### What this module does
-
-A thin wrapper around a hash table. No asset-type logic — that's Milestone 6. Three public functions:
-
-```scheme
-(make-asset-registry) ;; → hash-table
-(asset-set! registry key val) ;; → unspecified (mutates)
-(asset-ref registry key) ;; → value or #f if missing
-```
-
-- [ ] **Step 1: Write the failing tests**
-
-Create `tests/assets-test.scm`:
-
-```scheme
-(import scheme (chicken base) srfi-64)
-
-(include "assets.scm")
-(import downstroke/assets)
-
-(test-begin "assets")
-
-(test-group "make-asset-registry"
- (test-assert "returns a value"
- (make-asset-registry)))
-
-(test-group "asset-set! and asset-ref"
- (let ((reg (make-asset-registry)))
- (test-equal "missing key returns #f"
- #f
- (asset-ref reg 'missing))
-
- (asset-set! reg 'my-tilemap "data")
- (test-equal "stored value is retrievable"
- "data"
- (asset-ref reg 'my-tilemap))
-
- (asset-set! reg 'my-tilemap "updated")
- (test-equal "overwrite replaces value"
- "updated"
- (asset-ref reg 'my-tilemap))
-
- (asset-set! reg 'other 42)
- (test-equal "multiple keys coexist"
- "updated"
- (asset-ref reg 'my-tilemap))
- (test-equal "second key retrievable"
- 42
- (asset-ref reg 'other))))
-
-(test-end "assets")
-```
-
-- [ ] **Step 2: Run test to confirm it fails**
-
-```bash
-cd /home/gene/src/downstroke
-csi -s tests/assets-test.scm
-```
-
-Expected: error — `assets.scm` not found / `downstroke/assets` not defined.
-
-- [ ] **Step 3: Implement `assets.scm`**
-
-Create `assets.scm`:
-
-```scheme
-(module downstroke/assets *
-
-(import scheme
- (chicken base)
- (srfi 69))
-
-(define (make-asset-registry)
- (make-hash-table))
-
-(define (asset-set! registry key value)
- (hash-table-set! registry key value))
-
-(define (asset-ref registry key)
- (hash-table-ref/default registry key #f))
-
-) ;; end module
-```
-
-- [ ] **Step 4: Run test to confirm it passes**
-
-```bash
-cd /home/gene/src/downstroke
-csi -s tests/assets-test.scm
-```
-
-Expected: all tests pass, no failures reported.
-
-- [ ] **Step 5: Commit**
-
-```bash
-cd /home/gene/src/downstroke
-git add assets.scm tests/assets-test.scm
-git commit -m "feat: add assets.scm — minimal key/value asset registry"
-```
-
----
-
-## Task 2: Add `render-scene!` to `renderer.scm`
-
-**Files:**
-- Modify: `renderer.scm` (add one function after `draw-entities`)
-- Modify: `tests/renderer-test.scm` (add one test group)
-
-### What this adds
-
-`render-scene!` draws a complete scene (all tilemap layers + all entities) given a renderer and a scene struct. It delegates to the already-tested `draw-tilemap` and `draw-entities`.
-
-Existing signatures in `renderer.scm`:
-- `(draw-tilemap renderer camera tileset-texture tilemap)` — 4 args
-- `(draw-entities renderer camera tileset tileset-texture entities)` — 5 args
-
-The `tileset` is extracted from `tilemap` via the `tilemap-tileset` accessor (from `tilemap.scm`).
-
-- [ ] **Step 1: Write the failing test**
-
-Add to `tests/renderer-test.scm`, before `(test-end "renderer")`:
-
-```scheme
-(test-group "render-scene!"
- ;; render-scene! calls draw-tilemap and draw-entities — both are mocked to return #f.
- ;; We verify it doesn't crash on a valid scene and returns unspecified.
- (let* ((cam (make-camera x: 0 y: 0))
- (tileset (make-tileset tilewidth: 16 tileheight: 16
- spacing: 0 tilecount: 100 columns: 10
- image-source: "" image: #f))
- (layer (make-layer name: "ground" width: 2 height: 2
- map: '((1 2) (3 4))))
- (tilemap (make-tilemap width: 2 height: 2
- tilewidth: 16 tileheight: 16
- tileset-source: ""
- tileset: tileset
- layers: (list layer)
- objects: '()))
- (scene (make-scene entities: '()
- tilemap: tilemap
- camera: cam
- tileset-texture: #f)))
- (test-assert "does not crash on valid scene"
- (begin (render-scene! #f scene) #t))))
-```
-
-- [ ] **Step 2: Run test to confirm it fails**
-
-```bash
-cd /home/gene/src/downstroke
-csi -s tests/renderer-test.scm
-```
-
-Expected: error — `render-scene!` not defined.
-
-- [ ] **Step 3: Add `render-scene!` to `renderer.scm`**
-
-Add after the `draw-entities` definition (before the closing `)`):
-
-```scheme
- ;; --- Scene drawing ---
-
- (define (render-scene! renderer scene)
- (let ((camera (scene-camera scene))
- (tilemap (scene-tilemap scene))
- (tileset-texture (scene-tileset-texture scene))
- (tileset (tilemap-tileset (scene-tilemap scene)))
- (entities (scene-entities scene)))
- (draw-tilemap renderer camera tileset-texture tilemap)
- (draw-entities renderer camera tileset tileset-texture entities)))
-```
-
-- [ ] **Step 4: Run tests to confirm they pass**
-
-```bash
-cd /home/gene/src/downstroke
-csi -s tests/renderer-test.scm
-```
-
-Expected: all tests pass.
-
-- [ ] **Step 5: Commit**
-
-```bash
-cd /home/gene/src/downstroke
-git add renderer.scm tests/renderer-test.scm
-git commit -m "feat: add render-scene! to renderer — draw full scene in one call"
-```
-
----
-
-## Task 3: `engine.scm` — game struct, constructor, accessors
-
-**Files:**
-- Create: `engine.scm`
-- Create: `tests/engine-test.scm`
-
-This task covers only the data structure and constructor — no `game-run!` yet. TDD applies to the parts we can unit-test (struct creation, accessors).
-
-`game-run!` requires a live SDL2 window and cannot be unit-tested; it is tested in Task 6 via macroknight integration.
-
-### Mock strategy for tests
-
-`engine.scm` imports `renderer.scm`, `input.scm`, `world.scm`, `assets.scm`, and the SDL2 sub-libraries. The test file follows the same mock-module pattern as `tests/renderer-test.scm`: define mock modules that satisfy the imports, then `(include "engine.scm")`.
-
-- [ ] **Step 1: Write the failing tests**
-
-Create `tests/engine-test.scm`:
-
-```scheme
-(import scheme (chicken base) (chicken keyword) srfi-64 defstruct)
-
-;; --- Mocks ---
-
-(module sdl2 *
- (import scheme (chicken base))
- (define (set-main-ready!) #f)
- (define (init! . args) #f)
- (define (quit! . args) #f)
- (define (get-ticks) 0)
- (define (delay! ms) #f)
- (define (pump-events!) #f)
- (define (has-events?) #f)
- (define (make-event) #f)
- (define (poll-event! e) #f)
- (define (num-joysticks) 0)
- (define (is-game-controller? i) #f)
- (define (game-controller-open! i) #f)
- (define (create-window! . args) 'mock-window)
- (define (create-renderer! . args) 'mock-renderer)
- (define (destroy-window! . args) #f)
- (define (render-clear! . args) #f)
- (define (render-present! . args) #f))
-(import (prefix sdl2 "sdl2:"))
-
-(module sdl2-ttf *
- (import scheme (chicken base))
- (define (init!) #f))
-(import (prefix sdl2-ttf "ttf:"))
-
-(module sdl2-image *
- (import scheme (chicken base))
- (define (init! . args) #f))
-(import (prefix sdl2-image "img:"))
-
-;; --- Real deps (include order follows dependency order) ---
-(import simple-logger) ;; required by input.scm
-(include "entity.scm") (import downstroke/entity)
-(include "tilemap.scm") (import downstroke/tilemap)
-(include "world.scm") (import downstroke/world)
-(include "input.scm") (import downstroke/input)
-(include "assets.scm") (import downstroke/assets)
-
-;; Mock renderer (render-scene! can't run without real SDL2)
-(module downstroke/renderer *
- (import scheme (chicken base))
- (define (render-scene! . args) #f))
-(import downstroke/renderer)
-
-(include "engine.scm")
-(import downstroke/engine)
-
-;; --- Tests ---
-
-(test-begin "engine")
-
-(test-group "make-game defaults"
- (let ((g (make-game)))
- (test-equal "default title"
- "Downstroke Game"
- (game-title g))
- (test-equal "default width"
- 640
- (game-width g))
- (test-equal "default height"
- 480
- (game-height g))
- (test-equal "default frame-delay"
- 16
- (game-frame-delay g))
- (test-equal "scene starts as #f"
- #f
- (game-scene g))
- (test-equal "window starts as #f"
- #f
- (game-window g))
- (test-equal "renderer starts as #f"
- #f
- (game-renderer g))
- (test-assert "assets registry is created"
- (game-assets g))
- (test-assert "input state is created"
- (game-input g))))
-
-(test-group "make-game with keyword args"
- (let ((g (make-game title: "My Game" width: 320 height: 240 frame-delay: 33)))
- (test-equal "custom title" "My Game" (game-title g))
- (test-equal "custom width" 320 (game-width g))
- (test-equal "custom height" 240 (game-height g))
- (test-equal "custom frame-delay" 33 (game-frame-delay g))))
-
-(test-group "game-asset and game-asset-set!"
- (let ((g (make-game)))
- (test-equal "missing key returns #f"
- #f
- (game-asset g 'no-such-asset))
- (game-asset-set! g 'my-font 'font-object)
- (test-equal "stored asset is retrievable"
- 'font-object
- (game-asset g 'my-font))
- (game-asset-set! g 'my-font 'updated-font)
- (test-equal "overwrite replaces asset"
- 'updated-font
- (game-asset g 'my-font))))
-
-(test-group "make-game hooks default to #f"
- (let ((g (make-game)))
- (test-equal "preload-hook is #f" #f (game-preload-hook g))
- (test-equal "create-hook is #f" #f (game-create-hook g))
- (test-equal "update-hook is #f" #f (game-update-hook g))
- (test-equal "render-hook is #f" #f (game-render-hook g))))
-
-(test-group "make-game accepts hook lambdas"
- (let* ((called #f)
- (g (make-game update: (lambda (game dt) (set! called #t)))))
- (test-assert "update hook is stored"
- (procedure? (game-update-hook g)))))
-
-(test-end "engine")
-```
-
-- [ ] **Step 2: Run test to confirm it fails**
-
-```bash
-cd /home/gene/src/downstroke
-csi -s tests/engine-test.scm
-```
-
-Expected: error — `engine.scm` not found.
-
-- [ ] **Step 3: Implement `engine.scm` struct + constructor + accessors (no `game-run!`)**
-
-Create `engine.scm`. `game-run!` is a stub for now — it will be filled in Task 4.
-
-```scheme
-(module downstroke/engine *
-
-(import scheme
- (chicken base)
- (chicken keyword)
- (prefix sdl2 "sdl2:")
- (prefix sdl2-ttf "ttf:")
- (prefix sdl2-image "img:")
- defstruct
- downstroke/world
- downstroke/input
- downstroke/assets
- downstroke/renderer)
-
-;; ── Game struct ────────────────────────────────────────────────────────────
-;; constructor: make-game* (raw) so we can define make-game as keyword wrapper
-
-(defstruct (game constructor: make-game*)
- title width height
- window renderer
- input ;; input-state record
- input-config ;; input-config record
- assets ;; asset registry (hash-table from assets.scm)
- frame-delay
- preload-hook ;; (lambda (game) ...)
- create-hook ;; (lambda (game) ...)
- update-hook ;; (lambda (game dt) ...)
- render-hook ;; (lambda (game) ...) — post-render overlay
- scene) ;; current scene struct; #f until create: runs
-
-;; ── Public constructor ─────────────────────────────────────────────────────
-
-(define (make-game #!key
- (title "Downstroke Game")
- (width 640) (height 480)
- (frame-delay 16)
- (input-config *default-input-config*)
- (preload #f) (create #f) (update #f) (render #f))
- (make-game*
- title: title
- width: width
- height: height
- window: #f
- renderer: #f
- scene: #f
- input: (create-input-state input-config)
- input-config: input-config
- assets: (make-asset-registry)
- frame-delay: frame-delay
- preload-hook: preload
- create-hook: create
- update-hook: update
- render-hook: render))
-
-;; ── Convenience accessors ──────────────────────────────────────────────────
-
-;; game-camera: derived from the current scene (only valid after create: runs)
-(define (game-camera game)
- (scene-camera (game-scene game)))
-
-;; game-asset: retrieve an asset by key
-(define (game-asset game key)
- (asset-ref (game-assets game) key))
-
-;; game-asset-set!: store an asset by key
-(define (game-asset-set! game key value)
- (asset-set! (game-assets game) key value))
-
-;; ── game-run! ──────────────────────────────────────────────────────────────
-;; Stub — implemented in Task 4
-
-(define (game-run! game)
- (error "game-run! not yet implemented"))
-
-) ;; end module
-```
-
-- [ ] **Step 4: Run tests to confirm they pass**
-
-```bash
-cd /home/gene/src/downstroke
-csi -s tests/engine-test.scm
-```
-
-Expected: all tests pass.
-
-- [ ] **Step 5: Commit**
-
-```bash
-cd /home/gene/src/downstroke
-git add engine.scm tests/engine-test.scm
-git commit -m "feat: add engine.scm — game struct, make-game constructor, accessors"
-```
-
----
-
-## Task 4: Implement `game-run!`
-
-**Files:**
-- Modify: `engine.scm` (replace stub with real implementation)
-
-`game-run!` is tested via macroknight integration in Task 6, not via unit tests (it requires a real SDL2 display).
-
-- [ ] **Step 1: Replace the `game-run!` stub in `engine.scm`**
-
-Replace the stub `(define (game-run! game) (error ...))` with:
-
-```scheme
-(define (game-run! game)
- ;; 1. SDL2 init (audio excluded — mixer.scm not yet extracted;
- ;; user calls init-audio! in their preload: hook)
- (sdl2:set-main-ready!)
- (sdl2:init! '(video joystick game-controller))
- (ttf:init!)
- (img:init! '(png))
-
- ;; Open any already-connected game controllers
- (let init-controllers ((i 0))
- (when (< i (sdl2:num-joysticks))
- (when (sdl2:is-game-controller? i)
- (sdl2:game-controller-open! i))
- (init-controllers (+ i 1))))
-
- ;; 2. Create window + renderer
- (game-window-set! game
- (sdl2:create-window! (game-title game) 'centered 'centered
- (game-width game) (game-height game) '()))
- (game-renderer-set! game
- (sdl2:create-renderer! (game-window game) -1 '(accelerated vsync)))
-
- ;; 3. preload: hook — user loads assets here
- (when (game-preload-hook game)
- ((game-preload-hook game) game))
-
- ;; 4. create: hook — user builds initial scene here
- (when (game-create-hook game)
- ((game-create-hook game) game))
-
- ;; 5. Frame loop
- (let loop ((last-ticks (sdl2:get-ticks)))
- (let* ((now (sdl2:get-ticks))
- (dt (- now last-ticks)))
- ;; Collect all pending SDL2 events
- (sdl2:pump-events!)
- (let* ((events (let collect ((lst '()))
- (if (not (sdl2:has-events?))
- (reverse lst)
- (let ((e (sdl2:make-event)))
- (sdl2:poll-event! e)
- (collect (cons e lst))))))
- (input (input-state-update (game-input game) events
- (game-input-config game))))
- (game-input-set! game input)
- (unless (input-held? input 'quit)
- ;; update: hook — user game logic
- (when (game-update-hook game)
- ((game-update-hook game) game dt))
- ;; render: engine draws world, then user overlay
- (sdl2:render-clear! (game-renderer game))
- (when (game-scene game)
- (render-scene! (game-renderer game) (game-scene game)))
- (when (game-render-hook game)
- ((game-render-hook game) game))
- (sdl2:render-present! (game-renderer game))
- (sdl2:delay! (game-frame-delay game))
- (loop now)))))
-
- ;; 6. Cleanup
- (sdl2:destroy-window! (game-window game))
- (sdl2:quit!))
-```
-
-Note: `sdl2:pump-events!` processes the OS event queue; `sdl2:has-events?`/`sdl2:make-event`/`sdl2:poll-event!` drain it into a list. This matches the pattern used in macroknight's existing game loop (lines 661–667).
-
-**Spec discrepancy:** The spec document shows `(sdl2:collect-events!)` in its pseudocode — that function does not exist in the sdl2 egg. The plan's pump+collect loop above is the correct implementation. Ignore the spec's `sdl2:collect-events!` reference.
-
-- [ ] **Step 2: Verify engine-test.scm still passes**
-
-```bash
-cd /home/gene/src/downstroke
-csi -s tests/engine-test.scm
-```
-
-Expected: all tests pass (game-run! itself is not called in unit tests).
-
-- [ ] **Step 3: Commit**
-
-```bash
-cd /home/gene/src/downstroke
-git add engine.scm tests/engine-test.scm
-git commit -m "feat: implement game-run! — SDL2 init, lifecycle hooks, frame loop"
-```
-
----
-
-## Task 5: Update the Makefile
-
-**Files:**
-- Modify: `Makefile`
-
-- [ ] **Step 1: Add `assets` and `engine` to `MODULE_NAMES`**
-
-In `Makefile`, change:
-```makefile
-MODULE_NAMES := entity tilemap world input physics renderer
-```
-to:
-```makefile
-MODULE_NAMES := entity tilemap world input physics renderer assets engine
-```
-
-- [ ] **Step 2: Add dependency declarations**
-
-Add after `bin/renderer.o: bin/entity.o bin/tilemap.o bin/world.o`:
-
-```makefile
-bin/assets.o:
-bin/engine.o: bin/renderer.o bin/world.o bin/input.o bin/assets.o
-```
-
-`assets.scm` has no inter-module dependencies. `engine.scm` depends on renderer, world, input, and assets.
-
-- [ ] **Step 3: Add test targets**
-
-In the `test:` rule, add:
-```makefile
- @csi -s tests/assets-test.scm
- @csi -s tests/engine-test.scm
-```
-
-- [ ] **Step 4: Verify the build**
-
-```bash
-cd /home/gene/src/downstroke
-make clean && make
-```
-
-Expected: all `.o` files created in `bin/`, including `bin/assets.o` and `bin/engine.o`. No errors.
-
-- [ ] **Step 5: Run all tests**
-
-```bash
-cd /home/gene/src/downstroke
-make test
-```
-
-Expected: all test suites pass.
-
-- [ ] **Step 6: Commit**
-
-```bash
-cd /home/gene/src/downstroke
-git add Makefile
-git commit -m "build: add assets and engine modules to Makefile"
-```
-
----
-
-## Task 6: Port macroknight to `make-game` + `game-run!`
-
-**Files:**
-- Modify: `/home/gene/src/macroknight/game.scm`
-
-This is the integration test for the whole milestone. macroknight's current `game.scm` is ~678 lines with SDL2 init, the frame loop, and all game state mixed together. After this task it should be ≤50 lines: only `make-game`, the three lifecycle hooks, and `game-run!`.
-
-### What moves vs. what stays
-
-**Moves to engine (delete from game.scm):**
-- Lines 87–137: SDL2 init block — from `(sdl2:set-main-ready!)` through `(define *title-font* ...)` inclusive. This covers: `sdl2:set-main-ready!`, `sdl2:init!`, `init-game-controllers!` call, `ttf:init!`, `img:init!`, audio init calls, `on-exit` handlers, exception handler wrapper, `sdl2:set-hint!`, `sdl2:create-window!`, `sdl2:create-renderer!`, `ttf:open-font` calls.
-- Lines 659–673: The main `let/cc` game loop
-
-**Stays in macroknight (wrapped in lifecycle hooks):**
-- `init-audio!` + `load-sounds!` + `load-music!` → `preload:` hook
-- `ttf:open-font` calls (fonts) → `preload:` hook
-- `make-initial-game-state` → `create:` hook
-- `update-game-state` dispatch → `update:` hook
-- `render-frame` mode dispatch (minus clear/present) → `render:` hook
-
-**macroknight-specific state** (`game-state` struct: mode, menu-cursor, scene, input, etc.) stays as the module-level `*gs*` variable — set in `create:`, mutated in `update:`.
-
-**`render-frame` split:** The existing `render-frame` calls `sdl2:render-clear!` and `sdl2:render-present!` — those are now owned by the engine. Modify `render-frame` to remove those two calls, keeping only the draw-color set and the mode dispatch. The modified `render-frame` becomes the body of the `render:` hook. Since the background color is black (`0 0 0`) and the engine's clear already clears to black, there is no visual difference.
-
-### Implementation steps
-
-- [ ] **Step 1: Add `downstroke/engine` to macroknight's imports**
-
-In `/home/gene/src/macroknight/game.scm`, add to the import list:
-```scheme
-downstroke/engine
-downstroke/assets
-```
-
-- [ ] **Step 2: Restructure as `make-game` call**
-
-Replace the top-level SDL2 init block and the game loop at the bottom of `game.scm` with a `make-game` call. The three lifecycle hooks capture the existing functions from the rest of the file.
-
-**First, modify `render-frame`** to remove the SDL2 clear/present calls (the engine now owns those). Change lines 378–385 of `game.scm` from:
-
-```scheme
-(define (render-frame gs)
- (set! (sdl2:render-draw-color *renderer*) +background-color+)
- (sdl2:render-clear! *renderer*)
- (case (game-state-mode gs)
- ((main-menu) (draw-main-menu gs))
- ((stage-select) (draw-stage-select gs))
- ((playing) (draw-playing gs)))
- (sdl2:render-present! *renderer*))
-```
-
-to:
-
-```scheme
-(define (render-frame gs)
- (set! (sdl2:render-draw-color *renderer*) +background-color+)
- (case (game-state-mode gs)
- ((main-menu) (draw-main-menu gs))
- ((stage-select) (draw-stage-select gs))
- ((playing) (draw-playing gs))))
-```
-
-**Then add module-level state and the engine entry point** at the bottom of `game.scm` (replacing the old `let/cc` loop):
-
-```scheme
-;; ── Module-level game state ────────────────────────────────────────────────
-
-(define *gs* #f) ;; macroknight game state, set in create:
-
-;; ── Engine entry point ─────────────────────────────────────────────────────
-
-(define *the-game*
- (make-game
- title: "MacroKnight"
- width: +screen-width+
- height: +screen-height+
- frame-delay: 10
-
- preload: (lambda (game)
- ;; Audio (mixer not yet extracted — call directly)
- (init-audio!)
- (load-sounds! '((jump . "assets/jump.wav")))
- (load-music! "assets/theme.ogg")
- (play-music! 0.6)
- ;; Fonts
- (set! *font* (ttf:open-font "assets/DejaVuSans.ttf" 12))
- (set! *title-font* (ttf:open-font "assets/DejaVuSans.ttf" 32))
- ;; Make the SDL2 renderer available to macroknight functions
- (set! *renderer* (game-renderer game)))
-
- create: (lambda (game)
- (set! *gs* (make-initial-game-state)))
-
- update: (lambda (game dt)
- ;; update-game-state uses a continuation for the "quit" menu item;
- ;; pass a no-op since the engine handles Escape→quit automatically.
- (update-game-state *gs* (lambda () #f))
- (maybe-advance-level *gs*))
-
- render: (lambda (game)
- ;; engine has already called render-clear! and render-scene!
- ;; (game-scene is #f so render-scene! is a no-op for now);
- ;; render-frame draws the mode-specific content.
- (render-frame *gs*))))
-
-(game-run! *the-game*)
-```
-
-**Notes:**
-- `*renderer*`, `*font*`, `*title-font*` remain module-level variables (set in `preload:`). Keep their `(define ...)` declarations at the top of `game.scm` as `(define *font* #f)` etc. (uninitialized — they'll be set in `preload:`).
-- The engine's input handles Escape → quit via `input-held? input 'quit`; the old `let/cc exit-main-loop!` mechanism and `exit-main-loop!` call in `update-game-state` are no longer needed. **However, do not change `update-game-state` in this milestone** — the `(lambda () #f)` passed as `exit!` means the "Quit" menu item is a no-op for now. Fix it in a later milestone.
-- `(game-scene game)` is never set from macroknight, so the engine's `render-scene!` is always a no-op. All rendering happens in the `render:` hook via `render-frame`. This is intentional for this port milestone.
-
-- [ ] **Step 3: Remove the old SDL2 init block from `game.scm`**
-
-Delete lines 87–137 — everything from `(sdl2:set-main-ready!)` through `(define *title-font* (ttf:open-font ...))` inclusive.
-
-This also removes the `on-exit` cleanup handlers (lines 107–108) and the custom exception handler (lines 113–117). **This is intentional for this milestone.** The engine's `game-run!` calls `sdl2:destroy-window!` and `sdl2:quit!` at cleanup, but does not install an exception handler. If macroknight crashes mid-run, SDL2 will not be shut down cleanly. This will be addressed in a later milestone (either by adding error handling to `game-run!` or by macroknight wrapping `game-run!` in a guard).
-
-After deletion, add these uninitialized stubs near the top of `game.scm` (after the imports) so the rest of the file can still reference the variables. Note: `*window*` only appears inside the deleted range, so its stub is precautionary only.
-
-```scheme
-(define *window* #f) ;; owned by engine; not used after this port
-(define *renderer* #f) ;; set in preload:
-(define *font* #f) ;; set in preload:
-(define *title-font* #f) ;; set in preload:
-(define *text-color* (sdl2:make-color 255 255 255))
-```
-
-- [ ] **Step 4: Remove the old main loop from `game.scm`**
-
-Delete lines 659–678 (the `let/cc` game loop through the end of the file including the `"Bye!\n"` print). The engine's `game-run!` replaces it. The new `(define *the-game* ...)` and `(game-run! *the-game*)` from Step 2 are the new end of the file.
-
-- [ ] **Step 5: Build macroknight to verify it compiles**
-
-```bash
-cd /home/gene/src/macroknight
-make clean && make
-```
-
-Expected: successful compilation. Fix any symbol-not-found or import errors before continuing.
-
-- [ ] **Step 6: Run macroknight to verify it plays**
-
-```bash
-cd /home/gene/src/macroknight
-./bin/game
-```
-
-Expected: game launches, title screen appears, gameplay works correctly (title → stage select → play a level). Press Escape to quit.
-
-**Known regression (intentional):** Selecting "Quit" from the main menu is a no-op in this milestone. The old `exit-main-loop!` continuation no longer exists; the `update:` hook passes `(lambda () #f)` in its place. This will be fixed in a later milestone.
-
-- [ ] **Step 7: Verify game.scm is ≤50 lines**
-
-```bash
-wc -l /home/gene/src/macroknight/game.scm
-```
-
-If still over 50 lines, extract helper functions that belong in other modules. Game-specific logic (menu rendering, level loading) can stay; SDL2 boilerplate must be gone.
-
-- [ ] **Step 8: Commit**
-
-```bash
-cd /home/gene/src/macroknight
-git add game.scm
-git commit -m "feat: port macroknight to use make-game + game-run! (Milestone 8)"
-```
-
-Then commit the downstroke side:
-
-```bash
-cd /home/gene/src/downstroke
-git add -A
-git commit -m "feat: Milestone 8 complete — make-game + game-run! engine entry point"
-```
-
----
-
-## Acceptance Criteria
-
-- [ ] `make test` in downstroke passes all suites including assets and engine
-- [ ] `make` in downstroke builds all modules including `bin/assets.o` and `bin/engine.o`
-- [ ] `make && ./bin/game` in macroknight launches and plays correctly
-- [ ] macroknight `game.scm` is ≤50 lines with no SDL2 init or frame-loop boilerplate
-- [ ] `make-game` + `game-run!` are the sole entry point — no top-level SDL2 calls outside them
diff --git a/docs/superpowers/specs/2026-04-05-demos-design.md b/docs/superpowers/specs/2026-04-05-demos-design.md
deleted file mode 100644
index f51f260..0000000
--- a/docs/superpowers/specs/2026-04-05-demos-design.md
+++ /dev/null
@@ -1,223 +0,0 @@
-# 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.
-
----
-
-## 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
-
-### 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 macroknight's `sound.scm` (or direct SDL_mixer call)
-- 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), entity-entity collision via `physics.scm`, `input`, `renderer`, `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:**
-```scheme
-;; player
-(list #:type 'player #:x 280 #:y 360 #:width 16 #:height 16 #:vx 0 #:vy 0 #:tile-id 2)
-;; bullet
-(list #:type 'bullet #:x px #:y 340 #:width 4 #:height 8 #:vx 0 #:vy -5 #:tile-id 3)
-;; enemy
-(list #:type 'enemy #:x rx #:y 0 #:width 16 #:height 16 #:vx 0 #:vy 2 #:tile-id 4)
-```
-
-**Update logic:** move entities by vx/vy each frame → AABB collision check between bullets and enemies → filter dead 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)
-- 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 macroknight's `sound.scm` functions (`load-sounds!`, `play-sound!`, `load-music!`, `play-music!`, `stop-music!`) — or direct SDL_mixer FFI if sound.scm is not available
-
-**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`
-- 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) | — | ✓ | — | — | ✓ |
-| `renderer` (tilemap) | ✓ | — | ✓ | — | ✓ |
-| `renderer` (entities) | ✓ | ✓ | ✓ | — | ✓ |
-| `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
diff --git a/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md b/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md
deleted file mode 100644
index d6e71be..0000000
--- a/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md
+++ /dev/null
@@ -1,234 +0,0 @@
-# Milestone 8 — The Game Object and Lifecycle API
-
-**Date:** 2026-04-05
-**Status:** Approved
-**Scope:** `engine.scm`, `assets.scm`, macroknight port
-**Milestone numbering:** Follows `downstroke.org` / `TODO-engine.org` (Milestone 8). CLAUDE.md refers to the same work as "Milestone 7".
-
----
-
-## Goal
-
-Introduce `make-game` and `game-run!` as the public entry point for downstroke. A minimal game becomes ~20 lines of Scheme. This is the "Phaser moment" — the API stabilises around a game struct with lifecycle hooks.
-
----
-
-## Architecture
-
-Two new files:
-
-- **`engine.scm`** — `make-game` struct, `game-run!`, the frame loop, and all public accessors.
-- **`assets.scm`** — minimal key→value asset registry. No asset-type-specific logic; that is Milestone 6's domain.
-
-`engine.scm` imports: `renderer.scm`, `input.scm`, `world.scm`, `assets.scm`, the sdl2 egg (prefix `sdl2:`), `sdl2-ttf` (prefix `ttf:`), and `sdl2-image` (prefix `img:`).
-
-**Audio / mixer is not initialised by the engine in this milestone.** `mixer.scm` and `sound.scm` remain in macroknight and are not yet extracted. macroknight's `preload:` hook calls `init-audio!` directly from `sound.scm` until those modules are extracted.
-
-`physics.scm` is **not** imported by the engine. Physics is a library; the user requires it if needed and calls physics functions from their `update:` hook.
-
-### New function in `renderer.scm`
-
-`render-scene!` does not yet exist. It must be added to `renderer.scm` as part of this milestone. The module uses `export *` so no explicit export update is needed.
-
-Existing signatures in `renderer.scm` that `render-scene!` calls:
-- `(draw-tilemap renderer camera tileset-texture tilemap)` — 4 args
-- `(draw-entities renderer camera tileset tileset-texture entities)` — 5 args
-- `tilemap-tileset` is a field accessor on the `tilemap` struct (defined in `tilemap.scm`)
-
-```scheme
-(define (render-scene! renderer scene)
- (let ((camera (scene-camera scene))
- (tilemap (scene-tilemap scene))
- (tileset-texture (scene-tileset-texture scene))
- (tileset (tilemap-tileset (scene-tilemap scene)))
- (entities (scene-entities scene)))
- (draw-tilemap renderer camera tileset-texture tilemap)
- (draw-entities renderer camera tileset tileset-texture entities)))
-```
-
-### Build order (Makefile)
-
-Only modules that currently exist in downstroke are listed. Modules still in macroknight (`animation`, `ai`, `prefabs`, `mixer`, `sound`) are not prerequisites for this milestone.
-
-```
-entity → tilemap → world → input → renderer → assets → engine
-```
-
-`physics` is not a link dependency of `engine` and is omitted from this chain. It is a separate build target.
-
----
-
-## Data Structures
-
-### `game` struct (`engine.scm`)
-
-`defstruct` is called with `constructor: make-game*` so the generated raw constructor does not collide with the public keyword constructor `make-game`.
-
-```scheme
-(defstruct (game constructor: make-game*)
- title width height
- window renderer
- input ;; input-state record (accessor: game-input)
- input-config ;; input-config record (accessor: game-input-config)
- assets ;; asset registry hash table (from assets.scm)
- frame-delay
- preload-hook ;; (lambda (game) ...)
- create-hook ;; (lambda (game) ...)
- update-hook ;; (lambda (game dt) ...)
- render-hook ;; (lambda (game) ...) — post-render overlay
- scene) ;; current scene struct (accessor: game-scene)
-```
-
-**Note:** There is no separate `camera` field. The camera lives inside the scene (`scene-camera`). `game-camera` is a convenience accessor (see Accessors section).
-
-### `make-game` keyword constructor
-
-```scheme
-(define (make-game #!key
- (title "Downstroke Game")
- (width 640) (height 480)
- (frame-delay 16)
- (input-config *default-input-config*)
- (preload #f) (create #f) (update #f) (render #f))
- (make-game*
- title: title width: width height: height
- window: #f renderer: #f scene: #f
- input: (create-input-state input-config) ;; create-input-state takes one arg: config
- input-config: input-config
- assets: (make-asset-registry)
- frame-delay: frame-delay
- preload-hook: preload
- create-hook: create
- update-hook: update
- render-hook: render))
-```
-
-### Asset registry (`assets.scm`)
-
-```scheme
-(define (make-asset-registry) (make-hash-table))
-(define (asset-set! reg key val) (hash-table-set! reg key val))
-(define (asset-ref reg key) (hash-table-ref reg key (lambda () #f)))
-```
-
----
-
-## Lifecycle: `game-run!`
-
-```scheme
-(define (game-run! game)
- ;; 1. SDL2 init (no audio — mixer not yet extracted, user calls init-audio! in preload:)
- (sdl2:init! '(video joystick game-controller))
- (ttf:init!)
- (img:init! '(png))
-
- ;; 2. Create window + renderer
- (game-window-set! game
- (sdl2:create-window! (game-title game) 'centered 'centered
- (game-width game) (game-height game) '()))
- (game-renderer-set! game
- (sdl2:create-renderer! (game-window game) -1 '(accelerated vsync)))
-
- ;; 3. preload hook
- (when (game-preload-hook game) ((game-preload-hook game) game))
-
- ;; 4. create hook
- (when (game-create-hook game) ((game-create-hook game) game))
-
- ;; 5. Frame loop
- (let loop ((last-ticks (sdl2:get-ticks)))
- (let* ((now (sdl2:get-ticks))
- (dt (- now last-ticks))
- (events (sdl2:collect-events!))
- (input (input-state-update (game-input game) events (game-input-config game))))
- (game-input-set! game input)
- (unless (input-held? input 'quit)
- (when (game-update-hook game) ((game-update-hook game) game dt))
- (sdl2:render-clear! (game-renderer game))
- (when (game-scene game)
- (render-scene! (game-renderer game) (game-scene game)))
- (when (game-render-hook game) ((game-render-hook game) game))
- (sdl2:render-present! (game-renderer game))
- (sdl2:delay! (game-frame-delay game))
- (loop now))))
-
- ;; 6. Cleanup
- (sdl2:destroy-window! (game-window game))
- (sdl2:quit!))
-```
-
-**Notes:**
-- `sdl2:collect-events!` pumps the SDL2 event queue and returns a list of events (from the sdl2 egg).
-- `input-state-update` is the existing function in `input.scm`: `(input-state-update state events config)`. It is purely functional — it returns a new `input-state` record and does not mutate the existing one.
-- `create-input-state` in `input.scm` takes one argument: `(create-input-state config)` — it initialises all actions to `#f`.
-- Quit is detected via `(input-held? input 'quit)` — the `'quit` action is in `*default-input-config*` mapped to `escape` key: `(escape . quit)` in `keyboard-map`.
-- `sdl2:render-clear!` and `sdl2:render-present!` are called directly from the sdl2 egg (no wrapper needed).
-- The loop exits when `input-held?` returns true for `'quit`. No user-settable `game-running?` flag in this milestone.
-
----
-
-## `render:` Hook Semantics
-
-Following Phaser 2.x: the engine draws the full scene first via `render-scene!` (tilemap layers + entities), then calls the user's `render:` hook. The hook is for post-render overlays only — debug info, HUD elements, custom visuals. It does not replace or suppress the engine's render pass.
-
----
-
-## Physics
-
-Physics (`physics.scm`) is a library, not a pipeline. `game-run!` does not call any physics function automatically. Users who want physics `(require-extension downstroke/physics)` and call functions from their `update:` hook.
-
----
-
-## Public Accessors
-
-All `game-*` readers and `game-*-set!` mutators are generated by `defstruct` and exported. Additional convenience accessors:
-
-| Accessor | Description |
-|---|---|
-| `game-renderer` | SDL2 renderer — available from `preload:` onward |
-| `game-window` | SDL2 window |
-| `game-scene` / `game-scene-set!` | Current scene struct; user sets this in `create:` |
-| `game-camera` | Convenience: `(scene-camera (game-scene game))` — derived, not a struct field. Only valid after `create:` sets a scene; calling before that is an error. |
-| `game-assets` | Full asset registry hash table |
-| `game-input` | Current `input-state` record |
-| `game-input-config` | Current `input-config` record |
-| `(game-asset game key)` | Convenience: `(asset-ref (game-assets game) key)` |
-| `(game-asset-set! game key val)` | Convenience: `(asset-set! (game-assets game) key val)` |
-
-`game-camera` is not a struct field — it is a derived accessor:
-```scheme
-(define (game-camera game)
- (scene-camera (game-scene game)))
-```
-
----
-
-## Scene-Level Preloading
-
-Out of scope for this milestone. A `preload:` slot will be reserved in the scene struct when `scene-loader.scm` is implemented (Milestone 7), and the two-level asset registry (global + per-scene) will be designed then.
-
----
-
-## macroknight Port
-
-macroknight's `game.scm` must be ported to use `make-game` + `game-run!` as part of this milestone. It is the primary integration test.
-
-**What moves to the engine:** SDL2 init block (lines 87–137), the main loop (line 659+), and all frame-level rendering.
-
-**What stays in macroknight `game.scm`:** Only the three lifecycle hooks and `game-run!`:
-- `preload:` — load fonts, sounds, tilemaps (currently scattered across lines 87–137)
-- `create:` — build initial scene (currently `make-initial-game-state`, line 196)
-- `update:` — game logic, input dispatch, physics calls (currently the per-frame update functions)
-
-**Acceptance criterion:** macroknight compiles (`make` succeeds), the game runs and plays correctly (title screen → stage select → gameplay), and `game.scm` is ≤50 lines with no SDL2 init or frame-loop boilerplate.
-
----
-
-## Out of Scope
-
-- Scene-level preloading (Milestone 7)
-- Asset-type-specific load functions (`game-load-tilemap!`, `game-load-font!`, etc.) — Milestone 6
-- `game-running?` quit flag
-- Scene state machine (Milestone 9)
-- AI tag lookup (Milestone 10)
-- Extraction of `animation`, `ai`, `prefabs`, `mixer`, `sound` modules into downstroke