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 (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-settilemap— TMX/TSX parsing (expat), tileset loadingworld— Scene struct, entity list ops, cameraphysics— Gravity, velocity, AABB tile + entity collisionsinput— SDL2 events → action mapping, configurable bindsanimation— Frame/tick tracking, sprite ID mappingprefabs— Mixin composition, entity instantiationrenderer—draw-sprite,draw-tilemap-layer,draw-textassets— Asset registry for thepreload:hookscene-loader—game-load-scene!,instantiate-prefabmixer— SDLmixer FFI (no Scheme deps)sound— Sound registry, music playbackengine—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 (Spring Lisp Game Jam 2025).
- Code: Gene Pasquet
- Demo Levels: Owen Pasquet
- Demo Art: Kenney (1-bit pack)
