aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 7980f7164950ba8ca8d1b227d0f363e9eb018e7a (plain)
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.

    (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
-   SDL2<sub>mixer</sub> 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` — SDL<sub>mixer</sub> 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)