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
|
* 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
- Tile-based physics: gravity, velocity, AABB collision against TMX tilemaps
- Built-in update pipeline: input → acceleration → gravity → x-collision → y-collision → ground detection → entity collisions
- Entities as plists — purely functional, no classes
- 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
Early extraction phase. The engine logic is fully working — it powers [[https://genepasquet.itch.io/macroknight][macroknight]], built for Spring Lisp Game Jam 2025. Work is underway to extract it into a standalone, installable Chicken egg.
| Milestone | Description | Status |
|-----------+------------------------------------+----------|
| 1 | Zero-risk module extraction | DONE |
| 2 | Configurable input system | pending |
| 3 | Data-driven entity rendering | pending |
| 4 | Renderer abstraction | pending |
| 5 | Asset preloading | pending |
| 6 | Scene loading via =create:= hook | pending |
| 7 | =make-game= / =game-run!= API | pending |
| 8 | Camera follow | pending |
| 9 | Named scene states | pending |
| 10 | AI tag-based lookup | pending |
| 11 | Package as Chicken egg (v1.0.0rc1) | pending |
| 12 | Macroknight ported to use the egg | pending |
Milestones 1–6 are pure refactoring. Milestone 7 is the design pivot where the public API stabilises. Milestones 11–12 produce the first installable egg and validate it against macroknight.
** Dependencies
System libraries: =SDL2=, =SDL2_image=, =SDL2_mixer=, =SDL2_ttf=
Chicken eggs:
#+begin_example
chicken-install sdl2 sdl2-image expat matchable defstruct \
srfi-197 simple-logger srfi-64
#+end_example
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 SRFI-64 test suites
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, tilemap → world → animation, physics → input → prefabs → mixer → sound → engine
| 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 plists — no classes, purely functional:
#+begin_src scheme
(list #: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 '((idle #:frames (28) #:duration 10)
(walk #:frames (27 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 — Levels: Owen Pasquet — Art: [[https://kenney.nl][Kenney]] (1-bit pack)
|