From 027053b11a3a5d861ed2fa2db245388bd95ac246 Mon Sep 17 00:00:00 2001 From: Gene Pasquet Date: Sun, 5 Apr 2026 19:47:05 +0100 Subject: Progress --- ...2026-04-05-milestone-8-game-object-lifecycle.md | 826 --------------------- 1 file changed, 826 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-05-milestone-8-game-object-lifecycle.md (limited to 'docs/superpowers/plans/2026-04-05-milestone-8-game-object-lifecycle.md') 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 -- cgit v1.2.3