---
title: The cartridge format
format: cart
version: 1
canonical: https://cart.slopped.site/spec.md
html: https://cart.slopped.site/docs/
---

# The cartridge format, version 1

A cartridge (a Slopbox cartridge) is one UTF-8 text file that is a whole game: its rules, pictures, maps, sounds, music, an optional shader
and its words. This is the complete specification of version 1. The tables of keywords, built-ins and limits
are generated from the validator and the engine that run every cartridge, so they agree with them; the prose is
written by hand. Where this file and the validator disagree, the validator is right and this file is a bug.

- Play and build cartridges on the Slopbox: https://cart.slopped.site/ (the builder: https://cart.slopped.site/build/)
- The same rules for people, with pictures: https://cart.slopped.site/docs/
- How to write a good one (for people and AI agents): https://cart.slopped.site/skills/writing-cartridges/SKILL.md
- Validate one without a browser: the `validate_cartridge` tool on the slopped.site MCP server (https://mcp.slopped.site/)

## 1. The file

- UTF-8 text, at most 65,536 bytes and 4,000 lines. No control characters except tab and new line.
  `\r\n` line endings are read as `\n`.
- The first line is always `cart 1`: the format, then its version. A cartridge of another version is refused
  by name rather than half-read.
- Then meta lines (`key: value`), then sections. A section starts with a line that is only its header in square
  brackets, and runs to the next header. Sections can come in any order.
- Four kinds of section take a name (`[sprite NAME]`, `[map NAME]`, `[sound NAME]`, `[music NAME]`); the others don't.
  Defining the same section twice is an error.
- A name is lowercase letters, digits and `_`, 1 to 24 characters, starting with a letter: `/^[a-z][a-z0-9_]{0,23}$/`.
- A line starting `//` is a comment in every section except `[shader]` (GLSL keeps its own comments). In `[game]`,
  `//` also starts a comment at the end of a line.

The sections:

| Section | What |
| --- | --- |
| `[palette]` | the colours (optional: the house palette otherwise) |
| `[sprite NAME]` | a picture, as rows of palette letters (named) |
| `[map NAME]` | a grid of letters that become things or tiles (named) |
| `[sound NAME]` | one synthesizer recipe (named) |
| `[music NAME]` | a piece in Patter (named) |
| `[shader]` | one GLSL function over the finished picture (optional) |
| `[text]` | the game’s words |
| `[game]` | the rules (required) |

## 2. 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` | `WxH` in pixels, each 64 to 256. | `160x144` |
| `background` | One palette letter: the colour behind everything. | the palette’s second colour |
| `touch` | How a phone plays it (section 12). One of `tap`, `hold`, `sides`, `swipe`, `drag`, `pad`, `buttons`. | `pad` |
| `hud` | Up to three variables shown top left, by name. | `score` |
| `about` | One line, 140 characters at most. | empty |

## 3. [palette]

Up to 16 lines of `<one character> <#rrggbb> [name]`. `.` is always see-through and can't be a
colour; `/` can't be either. Leave the section out and the house palette is used:

| 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 |

## 4. [sprite NAME]

Rows of palette letters, `.` for see-through. Every row of a frame is the same width. A blank line starts the next
frame; every frame is the same size as the first. At most 64 sprites, each at most
32 × 32 pixels and 8 frames. A thing shows frame `self.frame`,
wrapped round its number of frames.

```text
[sprite pearl]
..ww..
.wwww.
wwwwlw
wwwwww
.wwww.
..ww..
```

## 5. [map NAME]

An optional `tile N` line (the cell size in pixels, 2 to 32; 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 may differ in
length. At most 8 maps, each at most 64 × 64 cells. A letter in the rows that
isn't in the legend is an error at that row and column.

```text
[map tin]
# = wall
################
#..............#
################
```

`place tin` lays it at 0, 0; `place tin at x, y` puts its top-left corner there.

## 6. [sound NAME]

One synthesizer recipe, one setting a line (the recipe format of learn.slopped.site's tinysound). At most
32 sounds. A sound lasts 20 ms to 2 s.

| Line | Meaning |
| --- | --- |
| `wave` | `wave W`: `sine`, `triangle`, `square`, `sawtooth`, `noise`. |
| `pitch` | `pitch HZ`: 30 to 4,000 Hz at the start (not needed for noise). In music, the note replaces it. |
| `slide` | `slide HZ`: the pitch at the end, 30 to 4,000 Hz. Moves with the note in music. |
| `arp` | `arp N N … step S`: 2 to 8 whole semitones (−24 to 24), stepped every 0.02 to 0.2 s. |
| `noise` | `noise X`: 0 to 1, how much noise is mixed in. |
| `filter` | `filter TYPE HZ [Q]`: `lowpass`, `highpass` or `bandpass`; 40 to 8,000 Hz; Q 0.3 to 12. |
| `attack` | `attack S`: 0 to 1 s. |
| `decay` | `decay S`: 0 to 2 s. |
| `sustain` | `sustain X`: a level, 0 to 1. |
| `hold` | `hold S`: 0 to 2 s. |
| `release` | `release S`: 0 to 2 s. |
| `volume` | `volume DB`: the peak, −48 to 0 dB. |

Required: `wave`, `pitch` (except for noise), `attack`, `decay` and `volume`; the rest are optional (`sustain`,
`hold` and `release` are 0 if left out). A sound's length is attack + decay + hold + release. An attack under 3 ms or a
fade-out under 10 ms clicks: the validator allows it, and it sounds like a click.

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.

```text
[sound gulp]
wave triangle
pitch 420
slide 180
attack 0.004
decay 0.12
release 0.04
volume -14
```

## 7. [music NAME] and Patter

Music is written in Patter, a small pattern language made for cartridges (in the spirit of the TidalCycles
mini-notation; none of its code). At most 8 pieces, each at most 6 voices.

```text
[music tide]
bpm 90
loop 4
bell bell | e5 ~ [g5 e5] ~ <c5 d5> ~ ~ ~
bass thump -6 | a2 . e2 .
```

- `bpm N`: 40 to 240 (110 if left out).
- `loop N`: 1 to 64 cycles before the piece repeats. Without it, the piece repeats when every voice's turns line up
  again (64 cycles at most).
- A voice line: `<voice> <sound> [dB] | <pattern>`: a name, one of the cartridge's sounds, an optional level from −40
  to +6 dB, and a pattern. One pattern is one cycle, and one cycle is one bar of four beats.

| Write | Plays |
| --- | --- |
| `c4 e4 g4 c5` | Steps sharing the cycle equally. |
| `c4 d#4 eb4` | Notes: a letter a–g, 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 that many times within its own space. |

Patter limits: 400 characters a pattern, 6 bracket nesting, 64 steps a group, 16 largest `*N`, 256 notes a cycle of one voice.

Patter grammar:

```ebnf
pattern  = sequence ;
sequence = step , { " " , step } ;
step     = atom , [ "*" , count ] ;                (* count: 1 to 16 *)
atom     = note | "x" | "~" | "." | "_" | group | layers | turns ;
note     = letter , [ "#" | "b" ] , octave ;       (* letter a-g, octave 0-8 *)
group    = "[" , sequence , "]" ;                  (* squeezed into one step *)
layers   = "[" , sequence , { "," , sequence } , "]" ;   (* played together: a chord *)
turns    = "<" , sequence , ">" ;                  (* one step per cycle, in turn *)
```

## 8. [shader]

One GLSL ES 3.00 function over the finished picture. Write only `effect`; the console writes the header and `main()`.

```text
[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 uniforms are fixed: `sampler2D screen` (the frame the game drew), `vec2 resolution` (the screen in game pixels),
`float time` (seconds of game time, ticks ÷ 60, so replays see the same values) and `vec4 game` (up to four game
variables, named on an optional first line `uses a b c d`).

The text is checked before WebGL sees it: 4,000 characters at most, 64 runs a loop at most, 2 loops nested at most, 4 game variables in `uses` at most, 200 lines at most; plain ASCII; loops only as
`for (int i = 0; i < N; i++)` with N a literal number; the loop variable never changed in its body; no `while`, `do`,
`discard`, preprocessor lines, `uniform`, `layout` or `precision` declarations, `main()` or `gl_` names. Compile
errors from the GPU come back with the shader's own line numbers. Without WebGL2 the game plays without its shader.

## 9. [text]

Lines of `key: words`, at most 160 characters each and 64 lines. `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 capitals: `A`–`Z`, `0`–`9` and
`. , : ' ! ? - + / ( ) % | > < _ = @ ~ * # $ ; & " [ ]`. Lower case is drawn as capitals; anything else shows as a
space. One panel holds 4 lines of (screen width − 16) ÷ 4 characters.

## 10. [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`.

### 10.1 Grammar

```ebnf
game       = { line } ;
line       = var | kind | scene | rule ;
var        = "var" , name , "=" , expr ;
kind       = "kind" , name , [ "sprite" , name ] , [ "size" , int , int ] , NEWLINE ,
             { INDENT , name , "=" , expr } ;            (* a sprite, a size, or both *)
scene      = "scene" , name ;
rule       = "on" , event , NEWLINE , block ;
event      = "start" | "enter" | "tick" | "every" , int
           | ( "press" | "release" | "hold" ) , button
           | "hit" , name , name | "each" , name | "out" , name ;
button     = "left" | "right" | "up" | "down" | "a" | "b" | "any" ;
block      = INDENT , statement , { statement } , DEDENT ;
statement  = target , ( "=" | "+=" | "-=" | "*=" | "/=" ) , expr
           | "if" , expr , block , { "else" , "if" , expr , block } , [ "else" , block ]
           | "spawn" , name , "at" , expr , "," , expr , [ block ]
           | "destroy" , [ "other" | "all" , name ]
           | "bounce" | "block"
           | "scatter" , [ "other" ] , int                     (* a grid of 2 to 64 pixels *)
           | "sound" , name | "music" , ( name | "off" )
           | "go" , name | "win" | "lose" | "say" , name
           | "shake" , expr | "burst" , expr , [ colour ]
           | "place" , name , [ "at" , expr , "," , expr ] ;
target     = name | ( "self" | "other" | name ) , "." , name ;
expr       = or ;
or         = and , { "or" , and } ;
and        = compare , { "and" , compare } ;
compare    = sum , { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) , sum } ;
sum        = product , { ( "+" | "-" ) , product } ;
product    = unary , { ( "*" | "/" | "%" ) , unary } ;
unary      = ( "-" | "not" ) , unary | atom ;
atom       = number | name | ( "self" | "other" | name ) , "." , name
           | function , "(" , [ expr , { "," , expr } ] , ")" | "(" , expr , ")" ;
```

`held`, `pressed` take a button name and `count` a kind name, not an expression. A name used as a variable, kind,
field or scene can't be a word of the language: `var`, `kind`, `scene`, `on`, `if`, `else`, `spawn`, `at`, `destroy`, `all`, `other`, `self`, `bounce`, `block`, `scatter`, `sound`, `music`, `off`, `go`, `win`, `lose`, `say`, `shake`, `burst`, `place`, `and`, `or`, `not`, `sprite`, `size`, `any`; nor the name of a function (10.6), such as `floor`
or `rnd`.

### 10.2 Variables

`var lives = 3`. Every number is a double. Built in, writable: `score`, `camx`, `camy`. Built in, read-only: `tick`, `W`, `H`, `px`, `py`, `pointing`.
At most 64 variables. Variables keep their values across scenes.

### 10.3 Kinds and fields

```text
kind ball sprite ball
  vx = 1
  vy = 0 - speed
kind gate size 4 64
```

`int` is a whole number written out. A kind needs a sprite, a size (1 to 256 each way), or both; a kind with only a size is invisible (useful for
triggers). Each indented 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 these fields: `x` (left edge), `y` (top edge), `vx` (pixels a tick, across), `vy` (pixels a tick, down), `w` (width), `h` (height), `frame` (which frame of its sprite), `life` (ticks left (0: forever)), `hidden` (not drawn while not 0), `flip` (drawn mirrored while not 0). 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.

Fields are always written with whose they are. Under `spawn`, write `self.life = 300`, not `life = 300`: a bare
name is a variable.

### 10.4 Scenes and rules

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, puts the camera back at 0, 0, then runs its `on enter`. At most 200 rules; blocks
nest at most 8 deep. Kinds must be declared before the rules that name them.

| 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` | The tick a button goes down. |
| `on release B` | The tick a button goes up. |
| `on hold B` | Every tick a button is down. |
| `on hit K1 K2` | Each overlapping pair: `self` is the K1, `other` the K2. |
| `on each K` | Every tick, once for each thing of kind K, as `self`. |
| `on out K` | Each tick a K is wholly off the camera’s view. |

### 10.5 Statements

| Statement | Does |
| --- | --- |
| `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` | Plays a sound. |
| `music M`, `music off` | Starts or stops a piece. |
| `go SCENE` | Changes scene at the end of the tick. |
| `win` | Ends the run, won. Nothing after it in that tick runs. |
| `lose` | Ends the run, lost. Nothing after it in that tick runs. |
| `say T` | Shows a `[text]` line for two and a half seconds. |
| `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. |
| `if e` | With an indented block. Zero is false; anything else is true. |
| `else if e`, `else` | With an indented block each, after an `if`. |
| `X = e`, `+=`, `-=`, `*=`, `/=` | Sets a variable, or a field: `self.x`, `other.vy`, `ball.x` (the first ball made that still exists). |

### 10.6 Expressions and functions

Numbers, variables, fields, `+ - * / %`, `== != < <= > >=`, `and or not` and brackets. Comparisons give 1 or 0.
Zero is false; anything else is true. Dividing by zero gives 0, `%` never gives a negative answer, and anything that
would be NaN or infinite becomes 0. `ball.x` reads the first ball made that still exists.

| Function | Gives |
| --- | --- |
| `abs(x)` | absolute value |
| `min(a, b)` | the smaller |
| `max(a, b)` | the larger |
| `floor(x)` | round down |
| `ceil(x)` | round up |
| `round(x)` | round to nearest |
| `sign(x)` | −1, 0 or 1 |
| `sqrt(x)` | square root (0 for a negative) |
| `clamp(x, lo, hi)` | x held between lo and hi |
| `rnd(n)` | a whole number from 0 to n − 1, from the run’s seed |
| `held(B)` | 1 while button B is down |
| `pressed(B)` | 1 on the tick button B went down |
| `count(K)` | how many things of kind K exist |

There is no sine or cosine: their last digits can differ between browsers, and the same game has to give the same
numbers everywhere.

## 11. One tick

60 ticks a second, always in this order:

1. `press`, `release` and `hold` rules, from this tick's buttons against the last tick's.
2. `tick` rules, then `every N` rules.
3. `each K` rules, for every K in the order they were made.
4. Movement: `x += vx` and `y += vy` for everything (speeds are pixels a tick), and `life` counts down.
5. `hit` rules for every overlapping pair, in the order the rules are written, then the order things were made.
6. `out` rules.
7. Removals, then a scene change if one was asked for.

At most 256 things exist at once (a spawn past that is skipped), and one tick may take
50,000 steps; past that the run stops with a message.

## 12. Buttons, keys and touch

Six buttons and a pointer. Buttons as bits: `left` 1, `right` 2, `up` 4, `down` 8, `a` 16, `b` 32.

| Button | Keys |
| --- | --- |
| `left` | ←, A |
| `right` | →, D |
| `up` | ↑, W |
| `down` | ↓, S |
| `a` | Z, Space, Enter, J, Y |
| `b` | X, K, N, Shift |

P pauses, M mutes. A gamepad's d-pad or left stick moves; its first button (or Start) is `a`, its second `b`.
On a touchscreen the screen is the controller, and the cartridge's `touch:` line says how:

| `touch:` | A finger on the screen |
| --- | --- |
| `tap` | Holds `a` while a finger is down. |
| `hold` | Holds `a` while a finger is down. |
| `sides` | Holds `left` or `right`, by which half the finger 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`. |
| `buttons` | Draws a controller on touch screens: a d-pad bottom left and `a` and `b` bottom right (only the buttons the rules read), each finger on its own, so a thumb can steer while the other holds `a`. A finger on the game itself is as in `drag`. For anything that needs two buttons at once: racers, platformers, shooters. |

## 13. Determinism, seeds and replays

- The simulation never reads a clock or `Math.random`. `rnd(n)` comes from the run's seed (mulberry32). Particles and
  screen shake use a separate generator and never touch the game's numbers.
- Only `+ - * /`, comparisons and the functions above; no trigonometry.
- The first run uses the cartridge's `seed:`; each new run after it adds one.
- A run is (cartridge, seed, input). The console records input as runs of identical ticks:
  `[ticks, buttons, px, py, pointing]`; as text, `ticks.buttons` (with `.px.py` while pointing) joined by `_`.
  Replaying them gives the same game, number for number, in every browser and in Node.
- At the end of a run the console reports the score, won or lost, the tick count and a hash of the game's state.

## 14. Share links

A cartridge can travel inside a link: its text, compressed with raw DEFLATE and written in URL-safe base64 (no
padding) after `#c=1.`, as in `https://cart.slopped.site/play/#c=1.…`. The prefix
`1.` is the link format's version. The part after `#` never leaves the browser, so nothing is uploaded.
Links stop at 16,000 characters (a bigger cartridge travels as a file). Opening one inflates at most
65,536 bytes and validates the result like any other cartridge. `/build/#c=…` opens the same link in the
builder.

## 15. Where it runs

The console is a sandboxed frame with an opaque origin (`sandbox="allow-scripts"`) and a content security policy
that allows exactly one script, the console itself, by its hash, 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.

## 16. Every limit

| What | At most |
| --- | --- |
| File size, bytes | 65,536 |
| File lines | 4,000 |
| Palette colours | 16 |
| Sprites | 64 |
| Sprite width and height, pixels | 32 |
| Frames a sprite | 8 |
| Maps | 8 |
| Map width and height, cells | 64 |
| Sounds | 32 |
| Pieces of music | 8 |
| Voices a piece | 6 |
| Text lines | 64 |
| Characters a text line | 160 |
| Screen side, smallest | 64 |
| Screen side, largest | 256 |
| Things at once | 256 |
| Steps a tick | 50,000 |
| Share link characters | 16,000 |
| Game lines | 1,500 |
| Kinds | 32 |
| Variables | 64 |
| Fields a kind (besides the built-in ones) | 16 |
| Rules | 200 |
| Block nesting | 8 |
| Largest N in `on every N` | 3,600 |
| Patter: characters a pattern | 400 |
| Patter: bracket nesting | 6 |
| Patter: steps a group | 64 |
| Patter: largest `*N` | 16 |
| Patter: notes a cycle of one voice | 256 |
| Shader: characters | 4,000 |
| Shader: runs a loop | 64 |
| Shader: loops nested | 2 |
| Shader: game variables in `uses` | 4 |
| Shader: lines | 200 |

## 17. Errors

The validator returns `{ ok, errors: [{ line, col, section, msg }], warnings, stats }`. Every message says what would
have been right, and a misspelt name gets a "Did you mean" when something is within two letters of it. Warnings
(a missing `help:` or `over:` line) don't stop a cartridge from playing.

## 18. A whole example

```cart
cart 1
title: Crumb
screen: 64x64
touch: pad

[sprite crumb]
.yy.
yyyy
.yy.

[sprite you]
kk
kk

[text]
help: Arrow keys. Eat the crumb.
over: Crumbs everywhere.

[game]
kind you sprite you
kind crumb sprite crumb

scene table
on enter
  spawn you at 30, 30
  spawn crumb at 0, 0
    scatter 8

on hold left
  you.x -= 1
on hold right
  you.x += 1
on hold up
  you.y -= 1
on hold down
  you.y += 1

on hit you crumb
  score += 1
  scatter other 8
```
