aboutsummaryrefslogtreecommitdiff

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-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
  • rendererdraw-sprite, draw-tilemap-layer, draw-text
  • assets — Asset registry for the preload: hook
  • scene-loadergame-load-scene!, instantiate-prefab
  • mixer — SDLmixer FFI (no Scheme deps)
  • sound — Sound registry, music playback
  • enginemake-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)