# Downstroke A 2D tile-driven game engine for Chicken Scheme, built on SDL2. Targets old-school platformer and arcade games — NES-style and beyond. The API is inspired by Phaser 2: one call to start, lifecycle hooks to fill in. A minimal game is ~20 lines of Scheme. (define my-game (make-game title: "My Game" width: 320 height: 240 preload: (lambda (game) ...) ; load assets create: (lambda (game) ...) ; set up scene update: (lambda (game dt) ...))) ; per-frame logic (physics runs first) (game-run! my-game) ## Features - Tweens with easing helpers - Tile-based physics: gravity, velocity, AABB collision against TMX tilemaps - Built-in update pipeline (before your `update:` hook): tweens → acceleration → gravity → velocity → tile collisions → ground detection → entity collisions → group sync → animation; input is polled each frame and available to hooks and your own logic - Entities as association lists (alists) — purely functional, no classes; prefab data is often written as plists and converted when loaded - Data-driven prefab/mixin system for composing entity types - Configurable input: keyboard, joystick, and controller - Asset registry with `preload:` lifecycle hook - Scene and camera management - Sprite animation with frame/tick tracking - SDL2mixer audio via a thin FFI binding ## Status The engine lives in this repository as modular Chicken units, with demos, docs, and an egg spec (`downstroke.egg`). It still powers [macroknight](https://genepasquet.itch.io/macroknight) (Spring Lisp Game Jam 2025). The tree is at release candidate `1.0.0rc1` (`VERSION` in the `Makefile`, same string in the egg and the list below). Macroknight is not yet switched to depend on the published egg — that remains the final validation step. **Milestones:** - [DONE] **1–10** — Core refactoring and API stabilization - 1: Zero-risk module extraction - 2: Configurable input system - 3: Data-driven entity rendering - 4: Renderer abstraction - 5: Asset preloading - 6: Scene loading via `create:` hook - 7: `make-game` / `game-run!` API - 8: Camera follow - 9: Named scene states - 10: Tag-based entity lookup - [rc] **11** — Package as Chicken egg (v1.0.0rc1) - [pending] **12** — Macroknight ported to use the egg Milestones 1–6 were pure refactoring. Milestone 7 was the API pivot (`make-game`, `game-run!`, pipeline). Milestones 11–12 are packaging and proving the egg against macroknight. Finer-grained planning may live outside this repo (for example in `downstroke.org` or macroknight's historical `TODO-engine.org`). ## Dependencies System libraries: `SDL2`, `SDL2_image`, `SDL2_mixer`, `SDL2_ttf` Chicken eggs (mirrors `downstroke.egg`; install what you do not already have): chicken-install sdl2 sdl2-image sdl2-ttf expat matchable defstruct list-utils \ srfi-1 srfi-13 srfi-69 srfi-197 simple-logger From a git checkout, run tests with the `test` egg (`make test` runs `csi -s` on each suite). Games that ship their own FSM logic (for example with the `states` egg) declare that egg themselves; Downstroke does not depend on it. ## Building make # compile all modules to bin/ make test # run unit tests (=test= egg) make demos # build demo games (verifies they compile) make clean # remove bin/ Single test module (from repo root; space after each `-I` is required by `csi`): csi -I "$(pwd)" -I "$(pwd)/bin" -s tests/physics-test.scm The canonical version string is `VERSION` at the top of the `Makefile`; it must match `((version "…")` in `downstroke.egg` and the `v…` marker in the milestone table. Bump all three with `make bump-version NEW=1.2.3` (POSIX `sed`; `NEW` must not contain `&`, backslash, or `#`). ## Architecture Modules live at the project root. Each compiles as a Chicken unit (`csc -c -J -unit`). Compile order follows the dependency graph: entity → tween → tilemap → world → input → physics → renderer → assets → animation → engine → mixer → sound → prefabs → scene-loader **Modules:** - `entity` — Alist accessors: `entity-ref`, `entity-set` - `tilemap` — TMX/TSX parsing (expat), tileset loading - `world` — Scene struct, entity list ops, camera - `physics` — Gravity, velocity, AABB tile + entity collisions - `input` — SDL2 events → action mapping, configurable binds - `animation` — Frame/tick tracking, sprite ID mapping - `prefabs` — Mixin composition, entity instantiation - `renderer` — `draw-sprite`, `draw-tilemap-layer`, `draw-text` - `assets` — Asset registry for the `preload:` hook - `scene-loader` — `game-load-scene!`, `instantiate-prefab` - `mixer` — SDLmixer FFI (no Scheme deps) - `sound` — Sound registry, music playback - `engine` — `make-game`, `game-run!`, lifecycle orchestration Entities are alists at runtime — no classes, purely functional: '((#:type . player) (#:x . 100) (#:y . 200) (#:width . 16) (#:height . 16) (#:vx . 0) (#:vy . 0) (#:gravity? . #t) (#:on-ground? . #f) (#:tile-id . 29) (#:tags . (player)) (#:anim-name . walk) (#:animations . (((#:name . walk) (#:frames . (27 28)) (#:duration . 10)) ((#:name . idle) (#:frames . (28)) (#:duration . 10))))) Entity types are composed from mixins declared in a data file: (mixins (physics-body #:vx 0 #:vy 0 #:gravity? #t #:on-ground? #f) (has-facing #:facing 1)) (prefabs (player physics-body has-facing #:type player #:tile-id 29)) ## LLM (AI) disclosure Some engine code was written or refactored with an LLM (aka AI) prior to the initial release. I don't intend to keep using AI for engine code after that. The `docs/` tree is LLM-generated, and that is expected to continue. ## Credits Engine extracted from [macroknight](https://genepasquet.itch.io/macroknight) (Spring Lisp Game Jam 2025). - Code: Gene Pasquet - Demo Levels: Owen Pasquet - Demo Art: [Kenney](https://kenney.nl) (1-bit pack)