4.0 KiB
Symph
Rhythm-driven grid traversal. Real-time spatial movement over a quantized musical beat, on a 3×3 grid.
Concept
A song advances one chord at a time on the beat (the Z-axis). Each chord is a 3×3 matrix of notes. The player holds a cell on the grid and moves across lanes (X) and rows (Y) to collect items and avoid walls. Lane shifts persist; row shifts are transient and collapse back to the base row after a fixed number of chords — a jump you must ride out or duck out of. Returning into a wall on landing is fatal.
The engine is a deterministic, side-effect-free state machine. Rendering and input polling run at host framerate (~60Hz), decoupled from beat progression. Frontends translate native input to platform-neutral actions and drain queued gameplay events into audio.
Mechanics
| Item | Glyph | Effect |
|---|---|---|
| Energy | * |
+1 energy, consumed on contact |
| Magnet | M |
sweeps every Energy note in the lookahead window, all grid positions |
| Shield | S |
arms one absorb charge; no expiry |
| Boost | B |
doubles tempo for a fixed duration; refreshes, does not stack |
| Wall | # |
fatal, unless a charge absorbs it or a Boost is active |
Two persistent effects (Shield charges, Boost deadline) cross level boundaries and clear on death. A Boost grants wall immunity but the field keeps rendering walls lethal — Boost expiry inside a wall band is the primary death mode.
Build
make build-term # terminal frontend (symph-term)
make build-raylib # raylib frontend, linux only (symph-raylib)
make test
Requires Go 1.26+. The raylib frontend builds CGO-free (embedded .so, linux amd64/arm64).
Run
bin/symph-term
Controls
Vim-style bindings:
hjkl— left, down, up, rightEnter— restart after game overCtrl+S— toggle muteEsc/Ctrl+C/q— quit
Host controls (quit, mute) bypass the engine and work in every phase, including paused.
Frontends
- Terminal (
symph-term): full Unicode glyph set, density-shaded walls, wall-band colored borders. Pauses and shows a notice below minimum viewport size. - Raylib (
symph-raylib, linux): same presentation contract, drawn with primitives. Substitutes ASCII for the Unicode atlas (default font covers codepoints 32..126); draws walls as rectangles.
Both share the grid-scan and burn-out projection pipeline in render; presentation policy lives there, not in the engine.
Audio
Fire-and-forget effects, PCM synthesized at startup. PipeWire backend (pw-play) on Linux; silent nullEngine elsewhere or when pw-play is absent — the game stays playable without audio. FreeBSD (sndio/OSS) is unimplemented and falls through to silence.
Layout
Unidirectional dependency flow (supports a future CGO/gomobile mobile target):
types/— dependency-free structures and the value registry (identity, glyph, polarity)parameter/— compile-time constants: grid bounds, timings, glyphs, geometrygame/— pure state engine; contact behavior and persistent effectsinput/— native key → neutralKey→game.Actionrender/— appearance tables and the shared draw pipeline;render/raylib/is the graphical frontendaudio/— synthesized effects and platform backendscmd/— hosts: wiring, event poller, ticker
Adding an item kind is three rows — identity (types.valueSpecs), behavior (game.resolvers), appearance (render.valueVisuals) — with no switch edits. init asserts registry completeness at process start.
Status
Early development. Terminal and raylib frontends are functional.
Planned:
- Android frontend via
gomobile(mobile/,platform/android/). Theinputpackage is bypassed on that path — touch/gesture translates directly togame.Action. - Symphony (multi-song) and Symphace (bounded environment) domain layers.
- Persistent audio stream to replace per-effect
pw-playspawns before latency matters. - FreeBSD audio backend (sndio/OSS).
License
See LICENSE.