1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
* 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.
#+begin_src 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)
#+end_src
** 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
- SDL2_mixer 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 [[https://genepasquet.itch.io/macroknight][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 table below). Macroknight is not yet switched to depend on the published egg — that remains the final validation step.
| Milestone | Description | Status |
|-----------+------------------------------------+----------|
| 1 | Zero-risk module extraction | DONE |
| 2 | Configurable input system | DONE |
| 3 | Data-driven entity rendering | DONE |
| 4 | Renderer abstraction | DONE |
| 5 | Asset preloading | DONE |
| 6 | Scene loading via =create:= hook | DONE |
| 7 | =make-game= / =game-run!= API | DONE |
| 8 | Camera follow | DONE |
| 9 | Named scene states | DONE |
| 10 | Tag-based entity lookup | DONE |
| 11 | Package as Chicken egg (v1.0.0rc1) | rc |
| 12 | Macroknight ported to use the egg | pending |
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 (=CLAUDE.md= points to the extraction notes).
** Dependencies
System libraries: =SDL2=, =SDL2_image=, =SDL2_mixer=, =SDL2_ttf=
Chicken eggs (mirrors =downstroke.egg=; install what you do not already have):
#+begin_example
chicken-install sdl2 sdl2-image sdl2-ttf expat matchable defstruct list-utils \
srfi-1 srfi-13 srfi-69 srfi-197 simple-logger
#+end_example
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
#+begin_src sh
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/
#+end_src
Single test module:
#+begin_src sh
csi -s tests/physics-test.scm
#+end_src
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
| Module | Responsibility |
|----------------+---------------------------------------------------|
| =entity= | Plist 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= | SDL_mixer 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:
#+begin_src scheme
'((#: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)))))
#+end_src
Entity types are composed from mixins declared in a data file:
#+begin_src scheme
(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))
#+end_src
** 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 [[https://genepasquet.itch.io/macroknight][macroknight]] (Spring Lisp Game Jam 2025).
- Code: Gene Pasquet
- Demo Levels: Owen Pasquet
- Demo Art: [[https://kenney.nl][Kenney]] (1-bit pack)
|