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)
Installation
Clone and install from source:
git clone https://git.etenil.net/downstroke
cd downstroke
chicken-install -s
This will install the downstroke egg and all its dependencies. Ensure you have the required system libraries installed first (see Dependencies below).
Note: On modern systems, installing expat can result in incompatible pointer errors. A way around this is to invoke chicken-install like so:
CFLAGS="-Wno-incompatible-pointer-types" chicken-install -s
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
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)
