The shape of a file
A cartridge is UTF-8 text, 65,536 bytes and 4,000 lines at most, with no control characters except tabs and new lines. The first line is always cart 1: the format, then its version. A version 2 cartridge is refused by name rather than half-read.
After that come meta lines (key: value), then sections. A section starts with a line that is only its header in square brackets. Four kinds take a name, four don’t:
[palette] the colours (optional: the house palette otherwise)
[sprite NAME] a picture, as rows of palette letters
[map NAME] a grid of letters that become things or tiles
[sound NAME] one synthesizer recipe
[music NAME] a piece in Patter
[shader] one GLSL function over the finished picture (optional)
[text] the game’s words
[game] the rules (required)
Names are lowercase letters, digits and _, up to 24 characters, starting with a letter. A section defined twice is an error that names the first one’s line. A line starting // is a comment in every section except [shader], where GLSL keeps its own comments. Sections can come in any order.
The validator (parseCart in engine/format.js) is one function that runs in Node, in the builder and inside the console, so all three agree on what a cartridge is. Each error names its line, often its column, its section, and what would have been right; a misspelt name gets a “Did you mean” when something is within two letters of it.
Meta lines
Before the first section, each line is key: value. Unknown keys and repeated keys are errors.
| Key | What | Default |
|---|---|---|
title | Required. 1 to 40 characters. | none |
by | A name or handle: letters, digits, spaces, . _ -, up to 32, optionally starting with @. | none |
version | The cartridge’s own version: 1, 1.2 or 1.2.3. | 1 |
seed | A whole number, 0 to 2,147,483,647. The first run’s seed. | 1 |
screen | Width x height in pixels, each 64 to 256. | 160x144 |
background | One palette letter. | the palette’s second colour |
touch | How a phone plays it: tap, hold, sides, swipe, drag or pad (below). | pad |
hud | Up to three variables shown top left, by name. | score |
about | One line, 140 characters at most. | empty |
[palette]
Up to 16 lines of <one character> <#rrggbb> [name]. . is always see-through and can’t be a colour; / starts a comment and can’t either. Leave the section out and you get the house palette:
| Letter | Colour | Name |
|---|---|---|
k | #171815 | ink |
w | #f4f1e8 | paper |
a | #e3ff59 | acid |
m | #6b6b62 | muted |
l | #d4d2c9 | line |
p | #ff8cc6 | pink |
o | #ffa53d | orange |
g | #728600 | moss |
s | #9fb3c4 | tin |
b | #3f6fd8 | blue |
r | #d8483f | red |
y | #d9a35b | toast |
[sprite NAME]
Rows of palette letters, . for see-through. Every row is the same width; a sprite is 32 × 32 at most. A blank line starts the next frame (8 at most), and every frame is the same size as the first. A thing shows frame self.frame, wrapped round the number of frames.
[sprite pearl]
..ww..
.wwww.
wwwwlw
wwwwww
.wwww.
..ww..
[map NAME]
An optional tile N line (the cell size, 2 to 32 pixels; 8 if left out), then legend lines X = name, then the rows. Each legend name is either a kind from [game] (each cell spawns one of those things there) or a sprite (each cell is drawn as a background tile and is nothing to collide with). . and spaces are empty. Rows can differ in length; the map is 64 × 64 cells at most, and a cartridge has 8 maps at most. A letter in the rows that isn’t in the legend is an error at that row and column.
[map tin]
# = wall
################
#..............#
################
place tin puts it at 0, 0; place tin at x, y puts its top-left corner there.
[sound NAME]
One recipe for learn.slopped.site’s workshop synthesizer, tinysound, written one setting a line. The cartridge doesn’t have a synth of its own: the same renderer that learn’s sound pathway measures plays these, and the same checks refuse a bad recipe.
| Line | Meaning |
|---|---|
wave W | sine, triangle, square, sawtooth or noise. |
pitch HZ | 30 to 4,000 Hz at the start (not needed for noise). In music, the note replaces it. |
slide HZ | The pitch at the end, 30 to 4,000 Hz. Moves with the note in music. |
arp N N … step S | 2 to 8 whole semitones (−24 to 24), stepped every 0.02 to 0.2 s. |
noise X | 0 to 1: how much noise is mixed in. |
filter TYPE HZ [Q] | lowpass, highpass or bandpass; 40 to 8,000 Hz; Q 0.3 to 12. |
attack decay sustain hold release | The envelope: attack 0 to 1 s, decay 0 to 2 s, sustain 0 to 1 (a level), hold and release 0 to 2 s. |
volume DB | The peak, −48 to 0 dB. |
A sound is 20 ms to 2 s long. A note in music holds for its length when the sound has a sustain above 0; a sound without one plays its own length, like a pluck. 32 sounds at most.
[music NAME] and Patter
Music is written in Patter, a small pattern language made for these cartridges. Each line is a voice:
[music tide]
bpm 90
loop 4
bell bell | e5 ~ [g5 e5] ~ <c5 d5> ~ ~ ~
bass thump -6 | a2 . e2 .
bpm is 40 to 240 (110 if left out). A voice line is <voice> <sound> [dB] | <pattern>: a name, one of the cartridge’s sounds, an optional level from −40 to +6 dB, and the pattern. One pattern is one cycle, and one cycle is one bar of four beats. loop N (1 to 64) says how many cycles before the piece repeats; without it, it repeats when every voice’s turns line up again (64 at most). Six voices a piece, eight pieces a cartridge.
| Write | Plays |
|---|---|
c4 e4 g4 c5 | Four steps sharing the cycle equally. |
c4 d#4 eb4 f#3 | Notes: a letter, an optional # or b, an octave 0 to 8. |
x | The sound at its own pitch (drums, effects). |
~ or . | A rest. |
_ | Holds the note before it for another step. A pattern can’t start with one. |
[e4 g4] | Squeezes its steps into one step. |
[c4,e4,g4] | Commas inside square brackets play layers together: a chord. |
<c4 a3> | Plays one of its steps per cycle, in turn. |
c4*4 | Plays the step 1 to 16 times within its own space. |
A pattern is 400 characters at most, brackets nest 6 deep, a group has 64 steps at most, and one cycle of one voice makes 256 notes at most.
Where this comes from. Patter is written in the spirit of the mini-notation of TidalCycles, which Strudel ports to the browser: a line is a cycle, steps share it, brackets subdivide, angle brackets take turns. That is the idea, borrowed with thanks. None of their code is here. Strudel is licensed AGPL-3.0, so it isn’t embedded in this site, and Patter isn’t compatible with either: it is much smaller, and some of the same symbols mean slightly different things. The notes are played by learn’s tinysound recipes, rendered in plain JavaScript and handed to Web Audio.
[shader]
One GLSL ES 3.00 function that sees the finished picture and returns a colour per pixel. Write only this:
[shader]
uses score
vec4 effect(vec2 uv) {
vec4 c = texture(screen, uv);
return vec4(c.rgb * (1.0 - game.x * 0.01), 1.0);
}
The console writes the header and main(). The uniforms are fixed:
| Uniform | What |
|---|---|
sampler2D screen | The frame the game drew, before the shader. |
vec2 resolution | The screen in game pixels. |
float time | Seconds of game time (ticks ÷ 60), so a replay sees the same values. |
vec4 game | Up to four game variables, named on an optional first line uses a b c d. |
A shader that hangs takes the browser down with it, so the text is checked before WebGL sees it: 4,000 characters and 200 lines at most, plain ASCII; loops only as for (int i = 0; i < N; i++) with N a number and 64 runs at most, nested two deep, with the loop variable never changed inside its body; no while, do, discard, preprocessor lines, uniform, layout or precision declarations, main() or gl_ names. Compile errors from the browser come back with your line numbers. Without WebGL2 the game plays without its shader, and says so.
[text]
Lines of key: words, 160 characters each and 64 lines at most. say key shows one for two and a half seconds. Three keys the console uses itself: help on the title screen, over when a run is lost (“Game over.” without one), won when it is won.
The console draws text in its own 3 × 5 pixel letters: capitals, digits and . , : ' ! ? - + / ( ) % | > < _ = @ ~ * # $ ; & " [ ]. Anything else shows as a space.
[game]: the language
Small on purpose: no loops, no functions you can write, no strings, no arrays. A game is variables, kinds of thing, scenes and rules. Blocks are made by indenting with spaces (tabs are refused). A line at the left edge starts with var, kind, scene or on.
Variables
var lives = 3
Every number is a double. Every game already has score, camx and camy (the camera’s top-left corner), and can read tick, W, H, px, py and pointing. 64 variables at most. Variables keep their values across scenes.
Kinds and fields
kind ball sprite ball
vx = 1
vy = 0 - speed
kind gate size 4 64
A kind needs a sprite, a size (1 to 256 each way), or both; a kind with only a size is invisible, which is useful for triggers. Indented under it, each line is a field and its starting value, worked out when each thing is made (so rnd gives each one its own). Every thing has x y vx vy w h frame life hidden flip; up to 16 more fields a kind, 32 kinds a cartridge. life counts down one a tick when above 0 and removes the thing when it reaches 0; 0 means forever. Kinds are drawn in the order they are declared, later ones on top.
Scenes and rules
scene play
on enter
place tin
spawn eel at 64, 64
on hit eel sardine
score += 1
scatter other 8
A scene line starts a scene; the rules after it belong to it until the next. Rules before the first scene are global. The cartridge starts in the first scene unless on start says go somewhere else. Entering a scene removes every thing and tile and puts the camera back at 0, 0, then runs its on enter. 200 rules at most; blocks nest 8 deep.
| Event | When |
|---|---|
on start | Once, before the first scene. Only before any scene line. |
on enter | When its scene starts. Only inside a scene. |
on tick | Every tick. |
on every N | Every N ticks (1 to 3,600), including tick 0. |
on press B, on release B | The tick a button goes down or up. |
on hold B | Every tick a button is down. |
on each K | Every tick, once for each thing of kind K, as self. |
on hit K1 K2 | Each overlapping pair: self is the K1, other the K2. |
on out K | Each tick a K is wholly off the camera’s view. |
Buttons are left right up down a b, or any. Kinds must be declared before the rules that name them.
Statements
| Statement | Does |
|---|---|
X = e, +=, -=, *=, /= | Sets a variable or a field: score, self.x, other.vy, or ball.x (the first ball made that still exists). |
if e / else if e / else | With an indented block each. Zero is false; anything else is true. |
spawn K at e, e | Makes a thing. An indented block under it runs as the new thing (self), with the maker as other. |
destroy, destroy other, destroy all K | Removes things at the end of the tick; they stop taking part at once. |
bounce | In on hit: pushes self out of other along the shallower overlap and turns that speed away. |
block | The same push, but stops that speed instead. |
scatter [other] N | Moves it to a random empty cell of an N-pixel grid (2 to 64) on the camera’s view. |
sound S, music M, music off | Plays a sound; starts or stops a piece. |
go SCENE | Changes scene at the end of the tick. |
win, lose | Ends the run. Nothing after it in that tick runs. |
say T | Shows a [text] line. |
shake e | Shakes the screen for that many ticks (0 to 60). |
burst e [colour] | 0 to 40 particles from self’s middle, in a palette letter or self’s main colour. |
place MAP [at e, e] | Spawns a map’s things and lays its tiles. |
Fields are always written with whose they are. Under spawn, it is self.life = 300, not life = 300: a bare name is a variable, and the validator says so.
Expressions
Numbers, variables, fields, + - * / %, == != < <= > >=, and or not and brackets. Comparisons give 1 or 0. Dividing by zero gives 0, % never gives a negative answer, and anything that would be NaN or infinite becomes 0. Functions:
abs(x) min(a, b) max(a, b) floor(x) ceil(x) round(x) sign(x) sqrt(x) clamp(x, lo, hi)
rnd(n) a whole number from 0 to n − 1, from the run’s seed
held(B) 1 while a button is down pressed(B) 1 on the tick it went down
count(K) how many K there are
There is no sine or cosine: those can differ in the last digit between browsers, and the same game has to give the same numbers everywhere.
One tick
Sixty ticks a second, always in this order:
press,releaseandholdrules, from this tick’s buttons against the last tick’s.tickrules, thenevery Nrules.each Krules, for every K in the order they were made.- Movement:
x += vxandy += vyfor everything (speeds are pixels a tick), andlifecounts down. hitrules for every overlapping pair, in the order the rules are written, then the order things were made.outrules.- Removals, then a scene change if one was asked for.
At most 256 things exist at once (a spawn past that is skipped, and the console says so), and one tick may take 50,000 steps. Past that, the run stops with a message rather than hanging the page.
Buttons and touch
Six buttons and a pointer. Keyboard: arrow keys or WASD; Z, Space, Enter, J or Y for a; X, K, Shift or N for b. P pauses, M mutes. A gamepad’s d-pad or left stick, its first button (or Start) for a, its second for b.
On a touchscreen the screen is the controller, and the cartridge’s touch: line says how:
touch: | A finger on the screen |
|---|---|
tap, hold | Holds a while it is down. |
sides | Holds left or right, by which half it is on. |
swipe | A swipe presses that direction; keep the finger down to swipe again. A quick tap presses a. |
drag | Sets px, py and pointing, and holds a. A mouse sets the pointer just by moving. |
pad | Holds the direction from the middle of the screen to the finger; near the middle, it holds a. |
Seeds, runs and replays
The simulation never reads a clock or Math.random: rnd comes from the run’s seed. The first run uses the cartridge’s seed:; each “another” after it adds one. So a run is its cartridge, its seed and what was pressed on each tick, and replaying those gives the same game, number for number, in Chromium, Firefox and Node.
The console keeps each run’s input as runs of identical ticks, [ticks, buttons, px, py, pointing], buttons as bits (left 1, right 2, up 4, down 8, a 16, b 32). In text, runs are ticks.buttons (with .px.py while pointing), joined by _. At the end of a run it reports the score, won or lost, the tick count and a hash of everything the game remembers. A board could check a claimed score by replaying it; none does yet.
Where it runs
A cartridge is somebody else’s idea of a game, so it plays in a box. The console is a frame with an opaque origin (sandbox="allow-scripts", never allow-same-origin) and a policy that allows exactly one script, the console itself, by its fingerprint, and nothing from the network: no storage, no cookies, no other frames, no workers, no eval. The cartridge reaches it as text in a message and is parsed there; it never becomes a web page. The frame talks to the page around it over one private message port, and the page makes the sound, because your key presses land on the page, not in the frame.
Every limit
| What | At most |
|---|---|
| File | 65,536 bytes, 4,000 lines |
| Screen | 64 to 256 pixels each way |
| Palette | 16 colours |
| Sprites | 64, each 32 × 32, 8 frames |
| Maps | 8, each 64 × 64 cells, cells 2 to 32 pixels |
| Sounds | 32, each 20 ms to 2 s |
| Music | 8 pieces, 6 voices each, 64 cycles a loop |
| A pattern | 400 characters, nesting 6, 64 steps a group, ×16, 256 notes a cycle |
| Text | 64 lines of 160 characters |
| Game | 1,500 lines, 32 kinds, 16 fields a kind, 64 variables, 200 rules, nesting 8, every up to 3,600 |
| Running | 256 things at once, 50,000 steps a tick |
| Shader | 4,000 characters, 200 lines, loops of 64 nested 2 deep, 4 game variables |
| Share link | 16,000 characters |
An example
A cut-down First tin, the Slopwright’s tutorial cartridge: a whole cartridge that validates and plays, in 60-odd lines. Open it in the builder to change it, or play it.
cart 1
title: First tin
by: the Slopwright
seed: 1
screen: 128x128
background: w
touch: pad
hud: score time
[sprite tin]
.ssssss.
ssssssss
slllllls
skssssks
ssssssss
saaaaaas
ssssssss
.ssssss.
[sprite pearl]
..ww..
.wwww.
wwwwlw
wwwwww
.wwww.
..ww..
[sound bell]
wave triangle
pitch 523
attack 0.004
decay 0.3
release 0.1
volume -20
[music tide]
bpm 90
bell bell | e5 ~ [g5 e5] ~ <c5 d5> ~ ~ ~
[text]
help: Arrow keys, WASD or the pad to move. Five pearls before the tide.
won: Five pearls. The tin is full.
[game]
var time = 30
kind pearl sprite pearl
kind tin sprite tin
scene beach
on enter
spawn tin at 60, 60
spawn pearl at 0, 0
scatter 16
music tide
on hold left
tin.x -= 1
on hold right
tin.x += 1
on hold up
tin.y -= 1
on hold down
tin.y += 1
on every 60
time -= 1
if time <= 0
lose
on hit tin pearl
score += 1
sound bell
scatter other 16
if score >= 5
win
The format grew out of learn’s tiny-game spec, the capstone of the Slop Curriculum’s games pathway, where a game is a small JSON object the course can check exactly. A cartridge is the same idea with more room: still data, still checked line by line, still nobody’s code but the console’s.
