A pattern is a pure function of position
Strudel looks like it is about time, but the idea underneath is more general. A pattern is a pure function: give it a position and it tells you what happens there.
events = pattern(clock, context)
- Clock is where you are.
- Context is what's going on.
- Pattern is the rules, which never change.
In this repository a position is measured in cycles, and one cycle is one bar. mini("x ~ x x") doesn't know about seconds, tempo, audio or games. Ask it what starts between bar 3 and bar 3.5 and it answers.
Because patterns are pure you get a lot for free:
- Scrub, rewind and preview. Any moment can be computed without playing up to it.
- Lookahead. A scheduler can ask what happens in the next 120 ms and book exact audio times.
- Determinism. The same code and the same clock give the same music and the same game. Replays, ghost runs and multiplayer sync come cheaply.
The clock decides what position means
The pattern core (src/pattern in Jump) has one Clock class. It converts between seconds on some timebase (usually an AudioContext's currentTime) and beats and bars. Any clock that can do that can drive any pattern.
| Clock | Context | |
|---|---|---|
| Strudel | tempo time | almost none: sliders, MIDI |
| Chop | the track's playhead and beat grid | the sound itself: stems, energy, key, drops |
| Jump | musical time, shared by game and music | game state: danger, lives, thrust |
Predictability decides the engineering
The real difference between these clocks is how predictable they are.
- Time is fully predictable, so you can schedule perfectly ahead.
- A track is predictable until someone scratches or jumps, so you follow it and resync.
- State is unpredictable, so you quantise: a change waits for the next grid line.
That is why state drives context, not the clock. If game state were a clock it would lurch around. Instead state selects and bends patterns, and the clock stays steady.
Jump makes quantisation free. Music layers are functions of state, and the scheduler evaluates them only when it books their next steps. A state change therefore lands on the next step that hasn't been booked yet. Nothing has to be cut.
One rule for games
The player's actions are instant. The world's actions land on the beat.
A bullet leaves the ship the moment you tap. Its sound lands on the next sixteenth. Rocks spawning, the saucer arriving, waves starting and respawns all happen on the same clock as the music.
Planning backwards from a predictable clock
A track is a predictable clock, so a game can work backwards from the music. Chop knows when the drop is. Jump can launch rocks off-screen early so that they arrive on the drop. Every spawn is worked out from the moment it should be seen:
launch = arrive − time to travel from off-screen to the screen edge
The travel time is integrated backwards through any slow-motion sections (breakdowns run at 45% speed), so a rock launched before a breakdown and arriving after it still lands on its beat. In the headless playtests, rocks cross onto the screen within one frame of their beat.
Separate libraries, shared core
Chop and Jump are separate projects with no dependency on each other. The pattern core lives in Jump; Chop keeps its own small copy of the Clock and key helpers so it stands alone. They meet at the clock: trackClock(map, ...) from Chop is a Clock, and Jump accepts any Clock. The Asteroids demo is the place where they are combined.