# 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