# 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)
**[š Full Documentation](https://docs.etenil.net/downstroke/guide.html)**
## 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)