# CartScript Language Specification

CartScript is a small game language built so that AI agents (and people) can write a complete,
safe, playable game in a single file. It is **strict** (mistakes become clear errors instead of silent bugs),
**deterministic** (see [Determinism](#determinism)) and **sandboxed** (a game cannot touch files, the network or the page).

A game is one source file, `main.cs`, plus a `manifest.json`, packed into a `.cart` cartridge.

This document specifies **CartScript language versions 1 to 8** (see [Versions](#versions)). Everything here is version 1 unless it is marked **version 2**, **3**, **4**, **5**, **6**, **7** or **8**.

## Versions

The language has a whole-number **language version**. There are seven so far:

| Version | What it adds | A game that uses it says |
|---|---|---|
| 1 | The language itself: everything in this document that is not marked with a newer version. | `"cartscript": 1`, or nothing |
| 2 | [Leaderboard scores](#leaderboards-version-2): `submit_score` and the manifest's `leaderboards`. | `"cartscript": 2` |
| 3 | [8-bit color](#8-bit-color-version-3): `config { colors: 256 }`, 256-color sprites, `cube()` and `gray()`. | `"cartscript": 3` |
| 4 | [16-bit color](#16-bit-color-version-4): `config { colors: 65536 }`, so a pixel game can draw with `rgb()` and `hsl()` colors. | `"cartscript": 4` |
| 5 | [24-bit color](#24-bit-color-version-5): `config { colors: 16777216 }`, so the pixel screen keeps every `rgb()` color exactly. | `"cartscript": 5` |
| 6 | [Named palettes](#palettes-version-6): `config { palette: "gameboy" }` and seven more, so a pixel game can use the colors of a classic machine. | `"cartscript": 6` |
| 7 | [Custom palettes](#custom-palettes-version-7): `config { palette: ["#1a1c2c", ...] }`, and `set_palette()` and `get_palette()` to change colors while the game runs. | `"cartscript": 7` |
| 8 | [Fonts](#fonts-version-8): `font("classic")`, `font("bold")` and `font("italic")`, `font("bold-italic")` and six fonts from classic pixel typefaces (`"pixelify"`, `"silkscreen"`, `"arcade"`, `"tiny5"`, `"vt323"`, `"dotgothic"`) draw pixel-game text in bigger fonts with lowercase letters, `font("vector")`, `font("vector-bold")` and `font("vector-italic")` give hi-res games text that looks the same on every device, and `text_height()` measures it. | `"cartscript": 8` |

Each version includes everything in the versions before it and **changes nothing that an older game does**. A section whose heading ends in "(version N)", or a table row or list item marked with the number in bold, was added in that version. On the website, the **version picker** above this page hides everything newer than the version you choose, which is handy when you are writing for an older player such as the Mac app.

* A cartridge may say which version it was written for with `"cartscript": 1` in its `manifest.json` (see [Packaging a cartridge](#packaging-a-cartridge)). When the field is missing the version is 1. The starter template includes it.
* A player runs a cartridge only if it supports that version. Otherwise it refuses to start and says the game needs a newer Gameverse; it never runs the game "as best it can".
* **Within a version nothing that is valid changes meaning.** Version 1 gets no new syntax, reserved words, built-in functions or built-in constants, so a game written for it cannot start to clash with a name added later (a new built-in would make a game's own function of that name an error). It also keeps every limit in this document at least as generous. Anything that could break a version-1 game arrives as version 2, and players that support version 2 keep running version-1 cartridges as specified here.
* Bugs in a player that contradict this specification can still be fixed. That is the only reason the behavior of a valid version-1 game may change.
* The tools (`cart check`, `cart run`, `cart pack`) and the website apply the same rule to the manifest. Running a bare `.cs` file (no manifest) uses the latest version the tool supports.

## Quick start

```
config { width: 128, height: 128, bg: NAVY }

let x = 60
let score = 0

on update {
  if btn("left")  { x -= 2 }
  if btn("right") { x += 2 }
  x = clamp(x, 0, W - 5)
  if btnp("a") {
    score += 1
    sfx("coin")
  }
}

on draw {
  rectfill(x, 110, 5, 5, ORANGE)
  text("SCORE " + str(score), 2, 2, WHITE)
}
```

Workflow for agents:

1. Write `main.cs`.
2. `cart check` finds syntax errors, undefined names and wrong argument counts, with line numbers. (`cart` means `node tools/cart.mjs`: see [Command-line tool](#command-line-tool).)
3. `cart run --frames 120 --input "right@0-30,a@40" --vars score,lives` runs the game headlessly and prints those global variables, so you can check the logic. (See [Command-line tool](#command-line-tool) for all options.)
4. `cart run --frames 120 --dump --region 0,0,64,32` prints part of the screen as hex digits (one character per pixel, `.` is color 0).
5. Write `manifest.json` (or start from `cart new my-game`), then `cart pack` builds `my-game.cart`.
6. `cart dev` plays the game in your browser and reloads it each time you save.
7. Or paste the code into the **Playground** on the website to play it and download the cartridge.

## Program structure

A file contains only these top-level declarations, in any order:

| Declaration | Meaning |
|---|---|
| `config { ... }` | Optional. Screen size, frame rate, background color. |
| `const NAME = expr` | A constant (cannot be reassigned). |
| `let name = expr` | A global variable. Its initializer runs once at start-up, top to bottom, and may only use earlier globals. |
| `fn name(a, b) { ... }` | A function. |
| `sprite name [ "rows" ]` | A pixel-art image. |
| `on init { ... }` | Runs once, after all globals are set. Optional. |
| `on update { ... }` | Runs once per tick (30 or 60 times per second). Game logic goes here. |
| `on draw { ... }` | Runs once per tick after update. Drawing goes here. |

At least one of `on update` or `on draw` is required. Statements (`if`, `while`, assignments, calls) are only allowed inside blocks.

### Order, duplicates and forward references

* **Each name is defined once.** Defining a hook twice (`on update` ... `on update`), a function twice, a global twice, or one name as two different kinds of thing (a function and a variable) is a compile error. The checker reports it with the line of the second definition.
* **Functions can be used before they are defined.** All functions exist from the start, wherever they appear in the file: a hook, another function, or a global's initializer may call a function defined further down.
* **Globals are created top to bottom** when the game starts, and an initializer can only use globals declared **above** it. `let a = b` followed by `let b = 1` is an error at start-up ("undefined variable 'b'"). The same applies through a function: `let a = f()` where `f` reads a global declared below the `let a` stops with the same error, because that global does not exist yet.
* `const NAME = expr` follows the same order as `let`. Sprites have no initializer and can be declared anywhere.
* The order of the declarations in the file never changes when hooks run (see [Game loop](#game-loop)).

### config

```
config { width: 160, height: 120, fps: 60, bg: BLACK }
```

| Key | Default | Allowed |
|---|---|---|
| `mode` | `"pixel"` | `"pixel"` (retro pixel art) or `"hires"` (smooth vector graphics, see [Hi-res mode](#hi-res-mode)) |
| `width`, `height` | 128 | integers 16 to 512 in pixel mode, 64 to 4096 in hires mode |
| `fps` | 60 | 30 or 60 |
| `bg` | 0 | a color 0-15 (in hires mode also `rgb(...)` colors; 0-255 with `colors: 256`) |
| `palette` | `"default"` | **version 6**, pixel mode only: a named palette, such as `"gameboy"` or `"ega"` (see [Palettes](#palettes-version-6)), or from version 7 a list of your own colors (see [Custom palettes](#custom-palettes-version-7)) |
| `colors` | 16 | **version 3**, pixel mode only: `16`, `256` (see [8-bit color](#8-bit-color-version-3)) from **version 4**, `65536` (see [16-bit color](#16-bit-color-version-4)) or, from **version 5**, `16777216` (see [24-bit color](#24-bit-color-version-5)) |

The screen is cleared to `bg` before every `on draw`. The website scales the screen up to fit the window (pixels stay sharp in pixel mode).
Unknown keys are an error.

## Lexical rules

* Statements end at a newline or a `;`. Several statements on one line **must** be separated by `;`: `if hit { vx = -vx; sfx("blip") }`. Without the `;`, `{ a = 1 b = 2 }` is an error.
* A long expression may continue on the next line after an operator, `,`, `(`, `[` or `{`.
* Comments start with `//` and run to the end of the line.
* Strings use `"double"` or `'single'` quotes, on one line. Escapes: `\n \t \" \' \\`.
* Numbers are decimal: `3`, `0.5`. There is no `1e5` and no hex.
* Names: letters, digits, `_`, not starting with a digit. Case matters.
* Reserved words: `let const fn on sprite config if else while for in break continue return true false nil and or not`.
* Blocks use braces `{ }`. Indentation is not significant.

## Types and values

| Type | Examples | Notes |
|---|---|---|
| `number` | `0`, `-3`, `2.5` | Always finite. Overflow or NaN is an error. |
| `string` | `"hi"` | Immutable. `s[0]` gives a 1-character string. |
| `bool` | `true`, `false` | |
| `nil` | `nil` | "No value". |
| `list` | `[1, 2, 3]` | 0-indexed, mixed types allowed, at most 100000 items. |
| `map` | `{x: 1, "y": 2}` | String keys only. Keeps insertion order. |

There is no separate type for colors: a color is a `number` (see [Color values](#color-values)). `type(x)` only ever answers `"number"`, `"string"`, `"bool"`, `"nil"`, `"list"` or `"map"`.

Lists and maps are **references**: `let b = a` makes both names point to the same list. Use `copy(a)` for a copy.

### Lists and maps that contain themselves

A list or map may (directly or through other lists and maps) hold itself: `let a = []` then `push(a, a)`. This is allowed, and the built-ins that walk a value all deal with it the same way:

| Operation | A value that contains itself |
|---|---|
| `str(x)`, `print(x)`, `join` | Prints normally, but where a list or map would be printed inside itself it prints `[...]` or `{...}`: `str(a)` is `"[[...]]"`. Nesting deeper than 100 levels is cut off the same way. |
| `save(key, v)` | **Error**: `cannot save a list that contains itself`. Nothing is stored. (Nesting deeper than 20 levels is also an error.) |
| `==`, `!=`, `in`, `index_of` | Compare lists and maps by identity, so they never loop: `a == a` is `true`. |
| `copy(x)` | A shallow copy: the copy's items are the same items. A list that held itself now holds the original. |
| `for item in list` | Walks a snapshot of the list, so it finishes. |
| `cart run --state` | Shows `(unprintable: cannot save a list that contains itself)` in place of the variable. |
| Memory limit | Each list or map is counted once, so a cycle neither hides data nor counts twice (see [Limits](#limits)). |

Nothing else (`sort`, `reverse`, `slice`, ...) follows references into items, so nothing else can loop.

### Strictness rules (important)

* **Conditions must be bool.** `if x { }` is an error when `x` is a number. Write `if x != 0 { }` or `if len(items) > 0 { }`. The same applies to `while`, `and`, `or` and `not`. There is no "truthy".
* **`+` does not mix types.** `"score " + 5` is an error. Write `"score " + str(5)`.
* **Comparisons** `< <= > >=` need two numbers or two strings. `==` and `!=` work on anything, but lists and maps compare by identity.
* **Division by zero is an error.** So is `%` by zero.
* **Reading a missing list index is an error** (indexes run 0 to len-1, no negative indexes).
* **Declare before use.** Assigning to an undeclared name is an error; use `let` first.

## Expressions

Precedence from loosest to tightest:

1. `or`
2. `and`
3. `not`
4. `==  !=  <  <=  >  >=  in`
5. `+  -`
6. `*  /  %`
7. unary `-`
8. postfix: `f(args)`, `list[i]`, `map.key`, `map["key"]`

* `/` is real division (`7 / 2` is `3.5`). Use `idiv(a, b)` or `floor(a / b)` for whole-number division.
* `%` is a true modulo: the result has the sign of the right operand, so `-1 % 5` is `4` (good for wrapping).
* `x in list` tests membership, `"k" in map` tests for a key, `"ab" in "cabd"` tests for a substring.
* `and` / `or` short-circuit.

### Lists and maps

```
let items = [10, 20, 30]
items[0] = 99               // assignment to an existing index
push(items, 40)             // add at the end (items[4] = 1 would be an error)
let p = {x: 5, y: 7, name: "bob"}
p.x += 1                    // dot access: key must already exist
p["z"] = 3                  // bracket assignment can create a new key
let q = p["missing"]        // bracket read of a missing key gives nil
let r = p.missing           // ERROR: dot read of a missing key (catches typos)
```

* `m.key` reads or writes an **existing** key and is an error if the key is absent. This catches typos like `p.xx`.
* `m["key"]` reads a missing key as `nil`, and can add a key on assignment.
* Use `has(m, "key")` or `"key" in m` to test for a key.

## Statements

```
let name = expr             // declare a variable in the current block
const NAME = expr           // declare a constant (the variable cannot be reassigned)
name = expr                 // assign (also  +=  -=  *=  /=  %=  )
list[i] = expr
map.key = expr

if cond { ... } else if cond { ... } else { ... }
while cond { ... }
for item in list { ... }    // also: for key in map, for ch in "string"
for i in range(10) { ... }  // 0..9
break
continue
return                      // or  return expr  (inside fn); in a hook it ends that hook
f(args)                     // a function call used as a statement
```

* Variables are block scoped. A name can only be declared once per block, but an inner block may reuse an outer name.
* `for` iterates over a **snapshot** of the list: a shallow copy made when the loop starts. Adding or removing items of the list inside the loop does not skip or repeat elements. The items themselves are **not** copied, so when they are maps or lists, `for b in bricks { b.alive = false }` changes the real bricks. To remove while looping, build a new list (see the example game).
* `for i in 10` is an error; write `for i in range(10)`.
* `else` must be on the same line as the closing `}` or the start of the next line; both work. Use `else if`, not `elif`.
* There are no `++`, `&&`, `||`, `!`, ternary operators, classes, closures or lambdas. Use `+= 1`, `and`, `or`, `not` and `if`.

## Functions

```
fn dist2(x1, y1, x2, y2) {
  let dx = x2 - x1
  let dy = y2 - y1
  return dx * dx + dy * dy
}
```

* Called by name only: `dist2(0, 0, 3, 4)`. Functions are not values and there are no methods.
* A function without `return` gives `nil`. Arguments must match the parameter count exactly.
* Inside a function you can read and write **global** variables, parameters, and your own locals. Recursion is allowed up to 200 calls deep.
* A name cannot be both a function and a variable, and cannot reuse a built-in name (such as `px`, `line`, `text`, `rect`, `min`, `len`, `str`). Pick another name.

## Game loop

Each tick the runtime does:

1. read the buttons,
2. update the global `frame` (number of ticks done so far, starting at 0),
3. run `on update`,
4. if `on draw` exists: reset the camera, clear the screen to `bg`, run `on draw`.

`on init` runs once before the first tick. The tick rate is fixed (`fps`), so use per-tick speeds (pixels per tick) and no delta time. To count seconds use `frame / 60` (or your fps).

Button queries (`btn`, `btnp`) refer to the current tick.

### What each hook may do

* **`on update` runs exactly once per tick and `on draw` exactly once after it**, in every player (browser, `cart dev`, `cart run`). A player that falls behind runs several ticks in a row, each with its own update and draw: it never runs one without the other for a tick. A game with no `on draw` has no picture.
* **Do game logic in `update` and only drawing in `draw`.** The language does not forbid anything else, so know what happens if you do:
  * `draw` may read and assign globals, change lists and maps, and call `rnd()`, `save()`, `sfx()` and `print()`. It all takes effect and stays deterministic, in the fixed order of the loop. But `rnd()` in `draw` takes numbers from the **same random stream** as `update`, so adding or removing a `rnd()` in `draw` changes every later random number in `update`. Changing game state in `draw` is a bug magnet for the same reason. For decorative randomness (sparkle, flicker) prefer values derived from `frame`, such as `sin(frame * 7)`, or numbers made once in `on init`.
  * `draw` sees the state `update` left behind in the same tick, and `frame` has the same value in both.
  * Drawing calls made in `update` draw on the screen like any other, but the screen is cleared before each `draw` runs, so they only show in a game that has **no** `on draw` (and then they pile up, because nothing clears the screen). Put drawing in `draw`.
  * Input functions give the same answer in `update` and `draw` of one tick.
* An error in any hook stops the game (see [Errors](#errors)); no hook runs after it.
* Hooks cannot be called from your own code, and they take no parameters.

## Built-in constants

| Name | Value |
|---|---|
| `W`, `H` | screen width and height in pixels |
| `PI` | 3.14159... |
| `frame` | tick counter |
| `BLACK NAVY PURPLE GREEN BROWN DARK_GRAY LIGHT_GRAY WHITE RED ORANGE YELLOW LIME BLUE LAVENDER PINK PEACH` | colors 0 to 15 |

## Graphics

This section describes the default **pixel mode**. For smooth, full-resolution graphics see [Hi-res mode](#hi-res-mode); the drawing functions below work in both.

The screen is a grid of `W` x `H` pixels; `(0, 0)` is the top-left, `x` grows right, `y` grows down.
Every pixel is one of **16 colors**:

| # | Name | # | Name |
|---|---|---|---|
| 0 | BLACK | 8 | RED |
| 1 | NAVY | 9 | ORANGE |
| 2 | PURPLE | 10 (a) | YELLOW |
| 3 | GREEN | 11 (b) | LIME |
| 4 | BROWN | 12 (c) | BLUE |
| 5 | DARK_GRAY | 13 (d) | LAVENDER |
| 6 | LIGHT_GRAY | 14 (e) | PINK |
| 7 | WHITE | 15 (f) | PEACH |

Coordinates may be fractional (they are rounded down when drawing). Drawing outside the screen is safe and clipped.
A color argument must be an integer 0-15; anything else is an error (hi-res games can also use `rgb()` colors).

### Drawing functions

| Function | Description |
|---|---|
| `cls(c?)` | Fill the screen with color `c` (default: the config `bg`). |
| `px(x, y, c)` | Set one pixel. |
| `getpx(x, y)` | Read a pixel's color (pixel mode only). |
| `line(x1, y1, x2, y2, c)` | Line between two points. (Hi-res: optional `width`.) |
| `rect(x, y, w, h, c)` | Rectangle outline. (Hi-res: optional `width`.) |
| `rectfill(x, y, w, h, c)` | Filled rectangle. |
| `circ(x, y, r, c)` | Circle outline, center `(x, y)`, radius `r`. (Hi-res: optional `width`.) |
| `circfill(x, y, r, c)` | Filled circle. |
| `text(str, x, y, c?)` | Draw text (default color 7; hi-res games can add `size` and `align`). `(x, y)` is the top-left. Accepts any value (converted like `str`). |
| `text_width(str)` | Width of the text in pixels, in the current font. Use it to center: `text(s, (W - text_width(s)) / 2, 50)`. |
| `text_height(str)` | **version 8**: height of the text in pixels, in the current font: 5 in `"tiny"`, 9 in `"classic"`, `"bold"` and their italics, and the font's own height in the converted fonts (see the table in Fonts), plus the line spacing for each extra line. See [Fonts](#fonts-version-8). |
| `font(name)` | **version 8**: choose the font for the `text()`, `text_width()` and `text_height()` calls that follow. Pixel mode: `"tiny"`, `"classic"`, `"bold"`, `"italic"`, `"bold-italic"`, `"pixelify"`, `"silkscreen"`, `"arcade"`, `"tiny5"`, `"vt323"` or `"dotgothic"`. Hi-res mode: `"system"`, `"vector"`, `"vector-bold"` or `"vector-italic"`. See [Fonts](#fonts-version-8). |
| `spr(name, x, y, flip_x?, flip_y?, frame?)` | Draw a sprite. `name` is a string. For an animated sprite, `frame` picks the animation frame (see [Animated sprites](#animated-sprites)). |
| `spr_w(name)`, `spr_h(name)` | Sprite size in pixels. |
| `spr_frames(name)` | Number of animation frames (1 for a normal sprite). |
| `camera(x?, y?)` | Offset everything drawn after this call by `(-x, -y)`. `camera()` resets. It resets itself every tick. |

### Text

The built-in font is 3x5 pixels, each character advances 4 pixels and `\n` moves down 6 pixels.
It has **uppercase letters only** (lowercase is drawn as uppercase), digits, and these symbols: `. , ! ? : - + = / * ( ) [ ] ' " < > % _ #`.
Other characters draw as a box. A 128-pixel-wide screen fits 32 characters per line.

There is no alignment option; compute the position with `text_width`:

```
fn text_center(s, y, c) { text(s, (W - text_width(s)) / 2, y, c) }
fn text_right(s, y, c) { text(s, W - text_width(s) - 2, y, c) }
```

The font above is called `"tiny"`, and it is the font every game starts with. From version 8 a game can switch to a bigger one with lowercase letters: see [Fonts](#fonts-version-8).

Sprites cannot be rotated or scaled; draw several sprites (e.g. one per rotation) and pick one with `spr(...)`. `flip_x` / `flip_y` mirror a sprite.

### Fonts (version 8)

A pixel game has eleven fonts and a hi-res game has three of its own (see the end of this section). `font(name)` chooses the one that `text()`, `text_width()` and `text_height()` use from then on, and it stays chosen until the game calls `font()` again (a game starts every run in `"tiny"`). It works the same in `on draw` and anywhere else, and it only changes how text looks: it takes no time and changes nothing else.

| Font | Size | Characters | Line spacing |
|---|---|---|---|
| `"tiny"` | 3x5 pixels, 4 per character | Capital letters (lowercase is drawn as capitals), digits and `. , ! ? : - + = / * ( ) [ ] ' " < > % _ #`. Anything else is a box. | 6 |
| `"classic"` | 5x7 pixels (lowercase letters 5 tall, with `g j p q y` reaching 2 below), 6 per character | All 95 printable ASCII characters (codes 32 to 126): capitals, lowercase, digits and every symbol on a US keyboard. Anything else is a box. | 10 |
| `"bold"` | 7x7 pixels with two-pixel strokes (same rows as `"classic"`: lowercase 5 tall, tails reaching 2 below), 8 per character | The same 95 printable ASCII characters as `"classic"`, in a heavy arcade style for titles and scores. Anything else is a box. | 10 |
| `"italic"` | The `"classic"` letters leaning right: the top four rows of every character move one pixel along, so capitals and tops lean while the baseline and tails stay put. Same size and spacing as `"classic"` (6 per character). | The same 95 printable ASCII characters as `"classic"`. Anything else is a box. | 10 |
| `"bold-italic"` | The `"bold"` letters leaning right in the same way as `"italic"`. Same size and spacing as `"bold"` (8 per character). | The same 95 printable ASCII characters. Anything else is a box. | 10 |
| `"pixelify"` | Friendly, rounded pixel lettering, from Pixelify Sans: the default font for menus and dialogue. 15 rows tall (capitals 10), every character its own width (`i` moves on 4, `W` 12), lines 16 apart. | The same 95 printable ASCII characters, with real lowercase. Anything else is a box. | 16 |
| `"silkscreen"` | Compact, angular pixel lettering, from Silkscreen: for buttons, HUDs and inventory labels. 9 rows tall (capitals 5), every character its own width, lines 10 apart. Lowercase letters are drawn as small capitals. | The same 95 characters. | 10 |
| `"arcade"` | Classic chunky 1980s arcade lettering, from Press Start 2P (the original's license reserves its name, so it is called `"arcade"` here): for titles, scores and "GAME OVER". 9 rows tall (capitals 7), 8 pixels a character, lines 10 apart. | The same 95 characters. | 10 |
| `"tiny5"` | Minimal five-pixel letterforms, from Tiny5: for tiny labels and low-resolution games. 9 rows tall (capitals 6), every character its own width, lines 10 apart. | The same 95 characters. | 10 |
| `"vt323"` | Tall, narrow terminal lettering, from VT323: for computer screens and text adventures. 15 rows tall (capitals 10), 6 pixels a character, lines 16 apart. | The same 95 characters. | 16 |
| `"dotgothic"` | An old computer and Japanese RPG feel, from DotGothic16's 16x16 dot style: for retro dialogue and RPG interfaces. 16 rows tall (capitals 14), 8 pixels a character, lines 17 apart. Only its ASCII characters are included. | The same 95 characters. | 17 |

```
config { mode: "pixel", width: 160, height: 90, bg: 1 }
on draw {
  cls()
  text("Score 120", 4, 4, 7)              // tiny, as before
  font("classic")
  text("Hello, World!", 4, 20, 10)        // 5x7 with lowercase
  text(str(text_width("Hello, World!")), 4, 40, 7)   // 77: 13 characters x 6, less 1
  font("tiny")
}
```

* **Credits and licenses.** The six fonts from `"pixelify"` to `"dotgothic"` are bitmap conversions of open fonts (each character drawn at a whole pixel size, keeping the pixels more than half covered) and stay under the **SIL Open Font License 1.1**; their copyright notices and the license are in [licenses/fonts.txt](https://gameverse.lol/licenses/fonts.txt) and ship with the tools download and the npm package. A game that uses them needs to do nothing: the fonts are part of the runtime, not of the cartridge.
* **Some fonts are not fixed width.** In `"pixelify"`, `"silkscreen"` and `"tiny5"` characters move on by their own amounts, so `text_width()` adds them up (the last character counts only to its last pixel) instead of multiplying: always measure with `text_width()` when you center or right-align text.
* **Measure after choosing.** `text_width("Hello")` is 19 in `"tiny"`, 29 in `"classic"` and 39 in `"bold"`, so call `font()` before `text_width()` when you center or right-align text.
* **Centering both ways.** `text_height()` is the height of the whole block, from the top of the first line to the bottom of the last: `text(s, (W - text_width(s)) / 2, (H - text_height(s)) / 2)`. A single line is 5 tall in `"tiny"` and 9 in the bigger fonts (capitals fill the top 7, and the last 2 are for tails), and each extra line adds the line spacing: `text_height("a\nb")` is 11 in `"tiny"` and 19 in `"classic"` or `"bold"`.
* **The text position is the top-left corner of the character cell.** In `"classic"` and `"bold"` capitals and digits fill rows 0 to 6, so their baseline is at `y + 6`; lowercase letters start at row 2, and `g j p q y` reach rows 7 and 8. Lines are 10 apart, so descenders never touch the next line.
* **An unknown name** is an error that lists the fonts for the game's mode, and says so if the name is a font of the other mode.

**Hi-res fonts.** A hi-res game draws its text at the size you give `text()`, and from version 8 it can choose how:

| Font | What it is |
|---|---|
| `"system"` | The default: the player's own monospace font, so shapes and widths differ a little from device to device. This is what hi-res text has always been. |
| `"vector"` | Letters drawn as thin lines, in a font built into the runtime: **the same on every device**. |
| `"vector-bold"` | The same shapes with thick lines, for headings and scores. |
| `"vector-italic"` | The thin shapes leaning right: every point moves right by a fifth of its height above the baseline, so the tops lean and the tails lean the other way. Same widths and spacing as `"vector"`. |

All the vector fonts cover the 95 printable ASCII characters (anything else is a box) and are exactly as wide as the system font was estimated to be: each character is `0.6 x size` wide and lines are `1.2 x size` apart, so `text_width(str, size)` is **exact** with them and `align` works as before. The size is the height of the text cell: capitals are `0.6 x size` tall, lowercase letters `0.4 x size`, and tails reach `0.2 x size` below the baseline. Colors, alpha, glow and transforms apply to them like any other drawing.

```
config { mode: "hires", width: 480, height: 270, bg: rgb(16, 20, 40) }
on draw {
  font("vector-bold")
  text("SCORE 1200", 240, 20, YELLOW, 32, "center")
  font("vector")
  text("Press Z to start", 240, 100, WHITE, 20, "center")
}
```

`font("vector")` in a pixel game, or `font("classic")` in a hi-res game, is an error: each mode has its own fonts.

### Sprites

```
sprite ship [
  "...c...",
  "..ccc..",
  ".ccdcc.",
  "ccccccc",
]
```

* Each string is one row; all rows must have the same length (1 to 64).
* `.` is transparent. `0`-`9` and `a`-`f` are the color numbers (hex).
* Sprite names are strings in calls: `spr("ship", x, y)`. A wrong name is reported when the file is checked.
* Sprite names live in their own namespace, so `let ship = ...` next to `sprite ship` is fine.

### 8-bit color (version 3)

A pixel game normally has the 16 named colors. Add `colors: 256` to the config to get an 8-bit palette of 256 colors, and write `"cartscript": 3` in the manifest:

```
config { width: 128, height: 96, colors: 256, bg: gray(2) }
on draw {
  for i in range(0, 6) {
    rectfill(8 + i * 18, 20, 16, 40, cube(5, i, 0))
  }
  text("8-BIT COLOR", 30, 70, 255)
}
```

* **The palette.** Colors `0`-`15` are the 16 named colors, unchanged. Colors `16`-`231` are a 6 x 6 x 6 color cube: `16 + 36 * red + 6 * green + blue`, each of red, green and blue from 0 to 5 (the levels 0, 51, 102, 153, 204 and 255). Colors `232`-`255` are 24 grays from dark (232) to light (255).
* `cube(r, g, b)` returns the color number for a cube position (each of `r`, `g`, `b` a whole number from 0 to 5). `gray(n)` returns the gray for `n` from 0 (darkest) to 23 (lightest). Both are plain numbers, so `cube(0, 0, 0) == 16` and you can compute with them.
* With `colors: 256` every drawing function, `bg`, `cls` and `text` take a color from 0 to 255. Without it, a number above 15 is an error, so a 16-color game cannot slip into 256 colors by accident.
* `rgb()`, `rgba()` and `hsl()` still need hires mode, or [16-bit color](#16-bit-color-version-4). `colors: 256` is for pixel mode only.
* **Sprites** with more than 16 colors write two hex digits per pixel (`00` to `ff`) and put `256` after the sprite's name; `..` is transparent:

```
sprite gem 256 [
  "....e0e0....",
  "..e0f4f4e0..",
  "e0f4ffffe0e0",
]
```

  A sprite that uses a color above 15 can only be drawn by a game with `colors: 256`.
* A game on version 1 or 2 has no `colors` key, no `256` sprites and no `cube` or `gray`: the checker says they need version 3.

### 16-bit color (version 4)

Add `colors: 65536` to a pixel game's config, and write `"cartscript": 4` in the manifest, and the pixel screen takes `rgb()` and `hsl()` colors as well as the whole 256-color palette:

```
config { width: 128, height: 96, colors: 65536, bg: rgb(10, 14, 40) }
on draw {
  for y in range(0, 96) {
    rectfill(0, y, 128, 1, rgb(10 + y, 14 + y * 2, 40 + y * 2))
  }
  circfill(64, 48, 20, hsl(40, 100, 55))
}
```

* The screen keeps each pixel as **5 bits of red, 6 of green and 5 of blue** (the "RGB565" of 16-bit hardware): 32 levels of red, 64 of green and 32 of blue, 65,536 colors in all. Every color is snapped to the nearest of those when it is drawn, so `rgb(10, 20, 30)` comes out as `rgb(8, 20, 24)`. Pure black, white, red, green and blue are exact.
* **Palette colors** (`0` to `255`, `RED`, `cube(...)`, `gray(...)`) and 256-color sprites work as in an 8-bit game, and are snapped the same way.
* `getpx(x, y)` gives back an `rgb()` color: the snapped color that is on the screen.
* A color with opacity below 1 (`rgba(r, g, b, 0.5)`) is an error in a pixel game, because a pixel screen has no blending: use [hi-res mode](#hi-res-mode) for that.
* `colors: 65536` is for pixel games. Hi-res games already draw with full `rgb()` color.
* A game on version 3 or older cannot ask for it: `config { colors: 65536 }` says it needs version 4.

### 24-bit color (version 5)

Add `colors: 16777216` to a pixel game's config, and write `"cartscript": 5` in the manifest, and the pixel screen keeps **every color exactly**: 8 bits each of red, green and blue, 16,777,216 colors in all, with nothing snapped. It is [16-bit color](#16-bit-color-version-4) without the rounding:

```
config { width: 128, height: 96, colors: 16777216, bg: rgb(10, 14, 40) }
on draw {
  for y in range(0, 96) {
    rectfill(0, y, 128, 1, rgb(10 + y, 14 + y * 2, 40 + y * 2))
  }
  circfill(64, 48, 20, hsl(40, 100, 55))
}
```

* `rgb(10, 20, 30)` is drawn as exactly `rgb(10, 20, 30)`, and `getpx(x, y)` gives it back unchanged, so `getpx(x, y) == rgb(10, 20, 30)` is `true`. (In a 16-bit game it would come back as `rgb(8, 20, 24)`.)
* The **palette** (`0` to `255`, `RED`, `cube(...)`, `gray(...)`) and 256-color sprites work as in the other deep-color games, as their exact palette colors.
* A color with opacity below 1 (`rgba(r, g, b, 0.5)`) is an error in a pixel game, because a pixel screen has no blending: use [hi-res mode](#hi-res-mode) for that.
* `colors: 16777216` is for pixel games, which keep their blocky, pixel-exact look. Hi-res games already draw with full `rgb()` color.
* It costs memory (four bytes a pixel, so a 512 x 512 screen is 1 MB) but nothing else. A game on version 4 or older cannot ask for it: it says it needs version 5.
* Choose it when you want exact colors (gradients without banding, photographs turned into pixels). Choose 16-bit for the look of 16-bit hardware, and 8-bit for the 256-color look.

### Palettes (version 6)

A pixel game normally has the default palette. Add `palette: "name"` to its config, and write `"cartscript": 6` in the manifest, to give it the colors of a classic machine instead:

```
config { width: 64, height: 64, palette: "gameboy", bg: 0 }
on draw {
  rectfill(8, 8, 48, 48, 1)
  circfill(32, 32, 14, 2)
  text("HELLO", 22, 28)
}
```

The game's colors are then the palette's entries, numbered from 0, and nothing else. This game has the four greens of a Game Boy screen, so it draws with `0`, `1`, `2` and `3`.

| Name | Colors | What it is |
|---|---|---|
| `default` | 16, or 256 with `colors: 256` | The usual palette. Its first 16 colors are `BLACK` to `PEACH`. |
| `pico8` | 16 | The 16 colors of the PICO-8 fantasy console (the same as the default's first 16). |
| `grayscale` | 256 | Entry `i` is the gray `rgb(i, i, i)`: 0 is black and 255 is white. Needs `colors: 256`. |
| `ega` | 16 | The default 16 colors of IBM's EGA graphics adapter. |
| `cga` | 4 | CGA graphics mode, palette 1 at high intensity: black, cyan, magenta, white. |
| `cga-green` | 4 | CGA palette 0 at high intensity: black, green, red, yellow. |
| `gameboy` | 4 | Four greens, darkest to lightest: the Game Boy screen as the bgb emulator shows it. |
| `c64` | 16 | The Commodore 64's 16 colors, in the machine's own order, as the Colodore palette gives them. |
| `db32` | 32 | DawnBringer's 32-color palette, a favorite of pixel artists. Needs `colors: 256`. |

* **Numbers, not names.** The named colors (`BLACK`, `RED`, ...) are just the numbers 0 to 15, so they mean those colors only in the default and `pico8` palettes. In any other palette, use the palette's own numbers: using a number past the palette's last entry is an error that says which palette and how many colors it has.
* **Ink.** `text(...)` without a color draws in the palette's ink: 7 in the default and `pico8` palettes, 15 in `ega`, 1 in `c64`, 21 in `db32`, 255 in `grayscale` and the last (lightest) entry in the 4-color palettes. `bg` defaults to 0.
* **Sprites** use hex digits as always, so up to 16 colors. A palette with more than 16 colors (`db32`, `grayscale`) uses [256-color sprites](#8-bit-color-version-3), and its game says `colors: 256`.
* **Which games.** A palette is for pixel games with 16 or 256 colors: not hi-res games, and not `colors: 65536` or `colors: 16777216`, which draw with `rgb()` colors. `palette: "default"` is the same as leaving it out.
* **Only the look changes.** The numbers a game draws and `getpx` returns are the same whichever palette shows them, so the same game plays and scores exactly the same in every palette, and a replay never depends on the palette.
* **Credits.** The values come from: PICO-8's palette (Lexaloffle), the EGA and CGA tables (Wikipedia), the Game Boy greens (the bgb emulator, via Lospec), Colodore (Pepto, as shipped with the VICE emulator) and DB32 (DawnBringer, via Lospec). The names are only names of colors: Gameverse is not connected to any of those makers.

### Custom palettes (version 7)

Instead of a name, `palette` can be a list of your own colors, and the game's colors are those entries, numbered from 0:

```
config { width: 64, height: 64, palette: ["#1a1c2c", "#5d275d", "#b13e53", "#ef7d57", "#ffcd75"], bg: 0 }
on draw {
  for i in range(0, 5) {
    rectfill(4 + i * 11, 20, 10, 24, i)
  }
  text("SUNSET", 18, 6)
}
```

* **The entries.** Each one is `rgb(r, g, b)` or a string like `"#ff8800"`, and fully opaque. A palette has 1 to 256 entries; more than 16 needs `colors: 256`.
* **Everything about [named palettes](#palettes-version-6) applies.** The game draws with the entries' numbers, a number past the last entry is an error that says so, sprites use hex digits (up to 16 colors) or [256-color sprites](#8-bit-color-version-3), and only the look changes: the same game makes the same screen numbers in any palette. It is for pixel games with 16 or 256 colors.
* **Ink.** `text(...)` without a color draws in the palette's lightest entry (the first of them, if several are equally light).

#### Changing colors while the game runs

Any pixel game with 16 or 256 colors (the default palette, a named one or a custom one) can change its palette entries as it runs. Everything already drawn in that color changes with it, because the screen holds entry numbers, not colors. This is how palette cycling, fades and flashes are done.

| Function | Description |
|---|---|
| `set_palette(i, color)` | Make palette entry `i` the color `color`: `rgb(r, g, b)` or a string like `"#ff8800"`, fully opaque. An entry that does not exist is an error. |
| `get_palette(i)` | The color of entry `i` now, as an `rgb()` value (so it can be kept, compared or passed to `set_palette`). |

They are an error in a hi-res game and in a 16- or 24-bit game, which have no palette entries to change. The change shows from the next picture. It never changes the numbers the game draws or `getpx` returns, so a game plays the same whatever its colors do:

```
config { width: 64, height: 64, palette: ["#000000", "#003377", "#0077dd", "#99ddff"] }
on update {
  if frame % 8 == 0 {
    // cycle entries 1 to 3: the water seems to flow
    let first = get_palette(1)
    set_palette(1, get_palette(2))
    set_palette(2, get_palette(3))
    set_palette(3, first)
  }
}
on draw {
  for y in range(0, 8) {
    rectfill(0, y * 8, 64, 8, 1 + y % 3)
  }
}
```

### Animated sprites

A sprite can have several frames. Write a list of frames instead of a list of rows; every frame must have the same size:

```
sprite walker [
  [
    "..11..",
    ".1111.",
    ".1..1.",
  ],
  [
    "..11..",
    ".1111.",
    "..11..",
  ],
]

let step = 0

on update {
  if frame % 8 == 0 { step += 1 }
}

on draw {
  spr("walker", 20, 20, false, false, step)
}
```

* The optional 6th argument of `spr` is the frame number. It wraps around, so `step` can just keep counting up: on a 2-frame sprite, frames 0, 1, 2, 3 show frames 0, 1, 0, 1. Negative numbers wrap too.
* The frame must be an integer. To change frames every 8 ticks use `idiv(frame, 8)` (or `floor(frame / 8)`), not `frame / 8`.
* You choose the frame, so every character can animate at its own speed or offset, pause, or restart: `spr("walker", e.x, e.y, false, false, idiv(frame + e.id * 3, 6))`.
* `spr_frames("walker")` returns the number of frames (1 for a normal sprite), e.g. to play a one-shot animation and stop on the last frame: `min(idiv(age, 4), spr_frames("walker") - 1)`.
* A sprite written as a plain list of rows (as in the section above) is a one-frame sprite, so existing games work unchanged. At most 64 frames.

## Hi-res mode

By default a CartScript game is **pixel art**: a small grid (up to 512x512) of 16 colors with a built-in font, scaled up with sharp edges. Add `mode: "hires"` to `config` and the same language draws **smooth vector graphics at the player's full screen resolution**: anti-aliased lines, circles and polygons, any color, real text, transforms and glow. Nothing else changes: the same `on update` / `on draw` hooks, input, sound, saving, math and lists.

```
config { mode: "hires", width: 1280, height: 720, bg: rgb(8, 10, 24) }

let t = 0

on update { t += 1 }

on draw {
  xf_push()
  xf_translate(W / 2, H / 2)
  xf_rotate(t * 2)
  set_glow(18)
  poly([0, -60, 50, 40, 0, 20, -50, 40], hsl(t, 90, 60), 3)
  xf_pop()
  set_glow(0)
  text("HI-RES", W / 2, 40, WHITE, 48, "center")
}
```

### How it works

* `width` and `height` (64 to 4096) set the **logical** size of the screen: the coordinate system your code uses, with `(0, 0)` at the top-left. The player scales it to fit the window, keeping the shape, and draws it at the display's real pixel density, so a 1280x720 game is sharp on a 4K or retina screen. The bars left over at the sides are filled with `bg`.
* Coordinates are ordinary numbers and may be fractional: a ball at `x = 10.25` is drawn at exactly that place, not rounded down.
* Each tick, `on draw` records a list of shapes (at most **20,000** per frame; more is an error). The screen is cleared to `bg` first. The shapes are painted in the order you draw them: later ones cover earlier ones.
* There are no pixels to read back: `getpx()` is an error in hires mode.

### Colors

Palette colors (`RED`, `NAVY`, 0-15) still work. In hires mode you can also use:

| Call | Result |
|---|---|
| `rgb(r, g, b)` | A color from red, green and blue, each 0-255. |
| `rgba(r, g, b, a)` | The same with opacity `a` from 0 (invisible) to 1 (solid). |
| `hsl(hue, saturation, lightness)` | `hue` is in degrees (0-360, wraps around); `saturation` and `lightness` are 0-100. Handy for rainbows: `hsl(frame * 2, 90, 60)`. |

These return a color **value**: pass it to any drawing function or keep it in a variable (`let sky = rgb(20, 30, 60)`). In a pixel-mode game only the 16 palette colors can be drawn (256 with [8-bit color](#8-bit-color-version-3)), and using `rgb()` there is an error unless the game asks for [16-bit](#16-bit-color-version-4) or [24-bit color](#24-bit-color-version-5). A number that is not a palette color or a color made by these functions (such as `99`) is rejected when you draw with it.

### Color values

A color is an ordinary `number`. There is no color type, and nothing about colors needs special handling in your code:

| Question | Answer |
|---|---|
| `type(c)` | `"number"`, for palette colors and for `rgb()`, `rgba()` and `hsl()` results alike. |
| `==` and `!=` | Compare the numbers. Two `rgb()` calls with the same red, green and blue are equal, and so are two `rgba()` calls that also have the same opacity. `rgb(r, g, b)` equals `rgba(r, g, b, 1)`, and `hsl()` is converted to red, green and blue first, so it equals the `rgb()` of the same color. A made color is **never** equal to a palette color, even when it looks the same: `rgb(0, 0, 0) == BLACK` is `false`, while `RED == 8` is `true`. |
| `<`, `>`, `+`, `-` and the like | They run (colors are numbers) but the result means nothing: **do not do arithmetic on colors.** Change a color by changing its parts: `rgb(r + 10, g, b)` or `hsl(hue + 10, s, l)`. |
| `str(c)`, `print(c)` | Prints the number, which is not readable (`str(rgb(1, 2, 3))` is `"17909311"`). Print the parts instead. |
| `save(key, c)` | Allowed, like any number, and `load` gives back a value equal to `c` in the same language version. How a made color is packed into its number is **not** part of the language. For data that must stay valid for good, save the parts (`save("sky", [20, 30, 60])`) and rebuild the color with `rgb(...)` after `load`. |
| As a map key | Not possible: map keys are strings. |

### What changes in the drawing functions

All the functions in [Drawing functions](#drawing-functions) work in hires mode too. These also accept hires-only options:

| Function | Hires version |
|---|---|
| `line(x1, y1, x2, y2, c, width?)` | `width` is the thickness in screen units (default 1). |
| `rect(x, y, w, h, c, width?)`, `circ(x, y, r, c, width?)` | Outlines with a `width` (default 1). |
| `text(str, x, y, c?, size?, align?)` | `size` is the text height in units (default 16). `align` is `"left"` (default), `"center"` or `"right"`; `x` is the left edge, the middle or the right edge. Text is bold monospace, drawn with the player's system font; `\n` starts a new line 1.2 x `size` lower. |
| `text_width(str, size?)` | An **estimate** of the width: characters x `size` x 0.6. Exact with the vector fonts (version 8). Prefer `align` over measuring. |
| `spr(name, x, y, flip_x?, flip_y?, frame?, scale?)` | `scale` enlarges the pixel-art sprite (default 1: one sprite pixel is one unit). Scaled sprites keep hard pixel edges. |
| `camera(x?, y?)` | Accepts fractional values, for smooth scrolling. |

New shape functions (hires only):

| Function | Description |
|---|---|
| `rrect(x, y, w, h, radius, c, width?)` | Rounded rectangle outline. |
| `rrectfill(x, y, w, h, radius, c)` | Filled rounded rectangle: buttons, panels, bars. |
| `poly(points, c, width?)` | Closed polygon outline. `points` is a flat list of x, y pairs: `[0, 0, 10, 0, 5, 8]` is a triangle. At least 2 points, at most 2000. |
| `polyfill(points, c)` | Filled polygon (at least 3 points). Self-crossing shapes use the even-odd rule. |

### Transforms

Transforms move, turn and scale everything you draw after them: draw a ship at the origin, then place it. They start fresh every frame.

| Function | Description |
|---|---|
| `xf_translate(x, y)` | Shift the origin by `(x, y)`. |
| `xf_rotate(degrees)` | Turn clockwise around the current origin. |
| `xf_scale(sx, sy?)` | Scale (`sy` defaults to `sx`). |
| `xf_push()`, `xf_pop()` | Save and restore the current transform. Always pair them (at most 32 deep); `xf_pop()` with nothing saved is an error. |

Transforms combine in the order you call them, so translate first and then rotate to spin around a point. `camera()` is applied after the transforms.

```
fn draw_ship(x, y, angle) {
  xf_push()
  xf_translate(x, y)
  xf_rotate(angle)
  polyfill([20, 0, -14, 12, -8, 0, -14, -12], rgb(120, 240, 255))
  xf_pop()
}
```

### Transparency and glow

| Function | Description |
|---|---|
| `set_alpha(a)` | Opacity for everything drawn after this call: 0 (invisible) to 1 (solid, the default). Use it for fades and soft shadows. |
| `set_glow(radius, c?)` | A soft glow around everything drawn after this call. `radius` is 0-200 (0 turns it off); `c` is the glow color, by default the color of each shape. Glows are expensive: use them on a few shapes. |

Both reset to normal every frame.

### Testing a hi-res game without a browser

`cart run` works the same way, plus:

* `--commands [N]` prints the first N (default 60) drawing commands of the last frame as text, with colors, positions, transforms, alpha and glow.
* `--dump` prints a rough picture: one character per square of `--cell` units (default: the width divided by 96). Each character is the nearest palette color (hex digit, `.` is black). Thin lines are widened to about one character, text is a solid box and rounded corners are drawn square, so use it to check **layout and positions**, not fine detail.

### When to use which mode

| | Pixel mode | Hi-res mode |
|---|---|---|
| Look | Retro pixel art, 16 colors | Smooth shapes, any color, glow |
| Text | Built-in 3x5 font, uppercase | Any size, aligned, mixed case |
| Collision, positions | Whole pixels are natural | Fractional units |
| Sprites | The way to draw characters | Use for pixel-art accents; draw most things with shapes |
| Cost | Cheap | Up to 20,000 shapes per frame |

## Input

A game sees **seven virtual buttons** and nothing else: no mouse, no touch points, no analog sticks, no text entry and no way to ask what device is being used. Whatever the player uses is turned into these seven buttons before the game runs.

| Button | Keyboard |
|---|---|
| `"left"`, `"right"`, `"up"`, `"down"` | Arrow keys, or W A S D |
| `"a"` | Z, J or Space |
| `"b"` | X or K |
| `"start"` | Enter |

| Function | Description |
|---|---|
| `btn(name)` | `true` while the button is held. |
| `btnp(name)` | `true` only on the first tick after it was pressed. Use this for menus, jumping and shooting. |

Details of the keyboard mapping:

* Keys are matched by their **position** on the keyboard, not the letter printed on them, so W A S D are the same four keys on any layout (AZERTY, Dvorak).
* Two keys for one button (for example Z and Space) count as one held button: it is released when both are up. Opposite directions can be held at the same time, and both report `true`; a game decides what that means.
* Holding a key does not repeat `btnp`. When the page loses focus every button is released.
* A button pressed and released between two ticks is not seen. Pressing is sampled once per tick.

**Phones and tablets.** On a phone or tablet the website draws a **D-pad and the A, B and Start buttons** under the game. They are mapped onto the same seven buttons: the D-pad is `"left"`, `"right"`, `"up"` and `"down"` (a thumb in a corner holds two at once, as two keys would), and the two round buttons and the Start button are `"a"`, `"b"` and `"start"`. Several fingers work together, so a player can steer and press A at the same time. A game does nothing to get this: it needs no new function and `btn` and `btnp` behave exactly as before. The controls appear when a finger is the device's main pointer; adding `?touch=1` to the address shows them on a computer (to try them) and `?touch=0` hides them. Fullscreen takes them along.

**Gamepads are not supported yet**, so there is no mapping to document. The rule that makes adding them safe is the one above: any new device is mapped onto these same seven buttons (a controller's D-pad and face buttons), with no new functions and no change to how `btn` and `btnp` behave. A game that is fully playable with the seven buttons therefore works with new devices as they arrive. Do not write a game that needs a keyboard-only feature such as a particular key name; ask for `"a"`, not for "Space".

## Sound

| Function | Description |
|---|---|
| `sfx(name)` | Play a preset sound: `"jump"`, `"coin"`, `"hit"`, `"explode"`, `"blip"`, `"lose"`, `"win"`. |
| `beep(freq, ms)` | A square-wave tone. `freq` 20-12000 Hz, `ms` 1-2000. |

Sound is optional decoration; it never affects game logic.

## Saving

Requires `"storage"` in the `permissions` list of `manifest.json`.

| Function | Description |
|---|---|
| `save(key, value)` | Store a number, string, bool, nil, list or map (nested up to 20 levels is fine). Total saved data is limited to 64 KB. |
| `load(key, default?)` | Read it back, or `default` (nil if omitted) if nothing was saved. Works without the permission (always returns the default). |

Save data belongs to the cartridge's `id` and stays on the player's own device.

* What you save is a **copy** made at the moment of the call (as JSON), so changing the list afterwards does not change the saved data, and `load` gives you a fresh list or map each time.
* A list or map that contains itself cannot be saved: it is an error and nothing is stored (see [Lists and maps that contain themselves](#lists-and-maps-that-contain-themselves)). Saving a color stores its number (see [Color values](#color-values)).
* Numbers, strings, bools and `nil` come back exactly. Lists and maps come back as lists and maps with the same contents and key order.

## Color functions (version 3)

| Function | |
|---|---|
| `cube(r, g, b)` | The color number of a position in the 256-color palette's color cube: `16 + 36 * r + 6 * g + b`, with `r`, `g` and `b` whole numbers from 0 to 5. |
| `gray(n)` | The color number of one of the 24 grays, `n` from 0 (darkest) to 23 (lightest): `232 + n`. |

Both are version 3 functions. They only return numbers: drawing with a color above 15 needs `colors: 256` in the config (see [8-bit color](#8-bit-color-version-3)).

## Leaderboards (version 2)

A version-2 game can post a score to a shared leaderboard:

| Function | Description |
|---|---|
| `submit_score(board, value)` | Post `value` (a number) to the game's leaderboard called `board`. It returns nothing and has no effect on the game: like a sound, it is output. |

```
let score = 0
on update {
  if btnp("a") { score += 10 }
  if btnp("start") { submit_score("score", score) }
}
```

* **Post a score when a round ends**, not every frame. A game may call it at most 200 times per run; more is an error.
* **The board name** is 1 to 32 characters of `a-z`, `0-9` and `-`, and the game must declare it in its manifest (`leaderboards`, see [Packaging a cartridge](#packaging-a-cartridge)) and ask for the `scores` permission, so the player is told before the game runs. The board's `order` says whether a bigger number (`"high"`) or a smaller one (`"low"`, such as a time) is better, and optional `min` and `max` are the lowest and highest scores the game can really produce: a score outside them is refused.
* **Who sees it:** the score is only posted when the player is signed in, on a plan that allows it, and has chosen a public leaderboard name. The game cannot tell whether it was posted, and cannot read the leaderboard.
* **Scores are verified by replaying the game.** CartScript is deterministic, so the page records the run's seed, the save data it started with and the buttons held on every tick, and sends that with the score. The server plays the game again, from its own copy of the game's source, with those buttons, and only accepts the score if the game posts exactly that score on exactly that tick. A score cannot be typed in or changed after the fact; what a cheater can still do is play with a bot, which is playing. A run is recorded for up to 10 minutes (`36000` ticks at 60 per second): after that the game still plays, but later scores are not posted until the game is started again. Leaderboards are for the games Gameverse lists.
* In a game on version 1, `submit_score` is not defined: the checker says it needs version 2 (and a version-1 game may use that name for its own function).

## Randomness

Random numbers are deterministic per seed. On the website each run starts with a fresh random seed; the command-line tool starts with seed 1 so tests are repeatable.

| Function | Description |
|---|---|
| `rnd()` | Number from 0 up to (not including) 1. |
| `rnd_int(lo, hi)` | Integer from `lo` to `hi`, **inclusive**. |
| `rnd_pick(list)` | A random item. |
| `shuffle(list)` | A new shuffled copy. |
| `seed(n)` | Reset the generator to a fixed seed. |

All of these (and `rnd()` calls made in `on draw`) share **one stream**, so the n-th call always gives the same number for the same seed.

## Determinism

CartScript is deterministic in this sense:

> Given the same **language version**, the same **seed**, the same **button state on every tick** and the same **initial save data** (what `load` returns), a game produces the same results: the same global variables after every tick, the same `print` output, the same sounds, the same data written by `save`, and the same drawing, in pixel mode the same pixels and in hi-res mode the same list of drawing commands.

What the game can observe is only those four things. There is no clock, no real random source, no files, no network and no device information. Everything that looks random comes from the seed: the website picks a new random seed for each run, so by default no two plays are the same; the command-line tool uses seed 1 (`cart run --seed N` to change it). To replay a run, give the same seed and the same buttons on the same ticks. `seed(n)` in the game makes it deterministic regardless of how the player picked the seed.

What the guarantee does **not** cover:

* **How hi-res pictures are painted.** The drawing commands are identical, but the player paints them with the browser: text uses the player's system font (a different font gives different widths and shapes; `text_width` is only an estimate for this reason), and anti-aliasing and glow differ slightly between browsers and screens. Never let a game's logic depend on how text or shapes were painted.
* **Nothing about the numbers depends on the browser.** `sin`, `cos`, `atan2`, `pow` and `dist` are computed by CartScript itself, using only `+ - * /` and exact operations, so they give the same digits in every browser and on the server. (They agree with the usual math functions to about 1e-14, not to the last digit, so a game's numbers are the same everywhere but not always identical to what another language prints.) This is what lets a score be checked by replaying the game.
* **Time-dependent play.** A player may run ticks late or in a burst when the machine is slow; the game still sees one tick at a time, so the logic does not change, but when the buttons are pressed in real time depends on the person.
* **Sound output.** Which sounds are requested on which tick is deterministic; how they sound is up to the player.
* **A different language version.** The guarantee holds within a version (see [Versions](#versions)).

## Math

Angles are in **degrees**.

| Function | Description |
|---|---|
| `abs(x)`, `sign(x)` | |
| `min(a, b, ...)`, `max(a, b, ...)` | One or more numbers. |
| `floor(x)`, `ceil(x)`, `round(x)` | |
| `idiv(a, b)` | Floor division. |
| `sqrt(x)`, `pow(base, exp)` | |
| `sin(deg)`, `cos(deg)`, `atan2(y, x)` | `atan2` returns degrees. |
| `clamp(x, lo, hi)` | Limit `x` to the range. |
| `lerp(a, b, t)` | `a + (b - a) * t`. |
| `dist(x1, y1, x2, y2)` | Distance between points. |
| `collide(x1, y1, w1, h1, x2, y2, w2, h2)` | `true` if two rectangles `(x, y, width, height)` overlap. Rectangles that only touch along an edge do **not** collide (`collide(0,0,5,5, 5,0,5,5)` is `false`). |

## Lists

| Function | Description |
|---|---|
| `len(x)` | Length of a list, string or map. |
| `push(list, v)` | Append. |
| `pop(list)` | Remove and return the last item. |
| `insert(list, i, v)` | Insert before index `i` (`i` may equal `len`). |
| `remove(list, i)` | Remove and return the item at `i`. |
| `slice(list_or_string, start, end?)` | A new piece (end exclusive). |
| `concat(a, b)` | A new list with both. |
| `index_of(list, v)` | Index of the first equal item, or -1. |
| `reverse(list)` | A new reversed list. |
| `sort(list)` | A new sorted list (all numbers or all strings). |
| `sort_by(list, key)` | A new list of maps sorted by `m[key]`. |
| `range(n)` / `range(a, b)` | `[0 .. n-1]` / `[a .. b-1]`. |
| `copy(list_or_map)` | A shallow copy. |

## Maps

| Function | Description |
|---|---|
| `keys(m)`, `values(m)` | Lists of keys / values. |
| `has(m, key)` | `true` if the key exists. |
| `del(m, key)` | Remove a key (no error if absent). |

## Strings and conversion

| Function | Description |
|---|---|
| `str(x)` | Convert any value to text (`str(3.5)` is `"3.5"`; numbers print with up to 6 decimals). |
| `num(s)` | Parse a number, or `nil` if it is not one. |
| `upper(s)`, `lower(s)` | |
| `split(s, sep)`, `join(list, sep?)` | |
| `substr(s, start, length?)` | |
| `find(s, part)` | Index of `part` in `s`, or -1. |
| `pad_left(s, width, fill?)` | `pad_left(str(7), 3, "0")` is `"007"`. |
| `type(x)` | `"number"`, `"string"`, `"bool"`, `"nil"`, `"list"` or `"map"`. Colors are numbers, so `type(rgb(1, 2, 3))` is `"number"`. |

## Debugging

| Function | Description |
|---|---|
| `print(a, b, ...)` | Log values (shown by the command-line tool and the browser console). |
| `assert(cond, message?)` | Stop with an error if `cond` is false. |

## Errors

Mistakes are reported with a line and column. Before running, the checker finds: syntax errors, undefined variables and functions (with "did you mean" hints), wrong argument counts, unknown sprite/button/sound names in literal calls, assignment to constants, `break` outside a loop, and names that clash with built-ins.

At run time errors include: wrong types, out-of-range indexes, missing map keys (dot access), division by zero, and bad built-in arguments.

If an error happens while the game is running, the game stops and the screen shows `ERROR`, the line number and the message. In the command-line tool the message is printed as:

```
main.cs:12:5: error: cannot add a string and a number: convert numbers with str(x)
  12 |     text("hp " + hp, 2, 2)
     |                ^
```

## Limits

* Source file: 256 KB.
* Each of `on init`, `on update`, `on draw` may run at most 500,000 evaluation steps per tick; going over is an error (usually an infinite loop).
* Call depth 200. Lists 100,000 items. Strings 100,000 characters. Sprites up to 64 x 64.
* Hi-res games: at most 20,000 drawing commands per frame, 2000 points per polygon, transforms nested 32 deep.
* Saved data: 64 KB in total; nesting up to 20 levels.
* Scores (**version 2**): at most 200 `submit_score` calls per run.
* No network, files, clock, or access to the web page. The only outputs are pixels, sound and save data.

### Total memory

The limits above apply to one list or one string. A game that stacks many of them (a list of 100,000 lists of 100,000 numbers) could still use far more memory than any player can give it, so there are two limits on the whole game. Hitting either one stops the game with an error that names it, like any other error.

| Limit | Value | Meaning |
|---|---|---|
| **Memory held** | 1,000,000 values | Everything the game's globals can reach, counted after each tick: **1 per list item, 1 per map entry (plus 1 per 16 characters, rounded up, of its key), and 1 per 16 characters of every string held in a list or map or variable.** A list or map counts once however many names or lists point at it, so shared and self-containing structures are not counted twice. Numbers, bools and nil in a variable cost nothing. |
| **Created per tick** | 3,000,000 values | The most list items and map entries one hook (`on init`, `on update` or `on draw`) may create in one tick, **temporary ones included** (`range(100000)` creates 100,000 even if you throw it away). This bounds how much a single tick can allocate before the first limit is looked at. |

For scale: a 256 x 256 grid of numbers is 65,536 values, a 1,000-line text kept as strings is about 4,000, a 100,000-item list is 100,000. Games that stay within a few thousand values (most do) will never see these limits. The check runs after hooks that created a lot (not after every tick), so a game may briefly go over the memory limit by a small amount before it stops.

Drawing data is bounded separately and never accumulates: in hi-res mode the commands of the current frame (at most 20,000) are thrown away when the next `on draw` starts, and sprites are fixed by the source file.

## Command-line tool

The tools are one command, `cart`. It needs Node.js 20 or newer: run it with `npx gameverse <command>`, or install it with `npm install -g gameverse` and type `cart <command>`. Without npm, run `node tools/cart.mjs <command>` from the repository or from the tools download. This spec writes it as `cart <command>`.

| Command | What it does |
|---|---|
| `cart new <name>` | Make a folder from the CartScript starter template. |
| `cart dev [folder] [--port N] [--open]` | Play the game in the browser sandbox at `http://localhost:8080/` and reload when a file changes. |
| `cart check [folder \| file.cs \| file.cart] [--strict]` | Look for mistakes. Exit code 1 if there are errors. Also prints warnings: text wider than the screen, a sprite that is declared but never drawn, and `rnd()` called while drawing (see below). `--strict` makes warnings fail too. |
| `cart run [folder \| file.cs] [options]` | Play a CartScript game with no browser (below). |
| `cart pack [folder] [out.cart] [--obfuscate [--map file]]` | Build a cartridge. |
| `cart inspect <file.cart>` | List the manifest and files of a cartridge. |
| `cart unpack <file.cart> [folder] [--force]` | Extract a cartridge. |

**Warnings.** `cart check` also prints warnings for three things that run but are probably not what you meant. A warning never stops a game from running or packing.

* **Text that runs off the screen:** `text("...", x, y)` with a written-out string and position whose width (4 pixels a character in a pixel game, 0.6 of the size in a hi-res game) goes past the edge of the screen. Text and positions worked out while the game runs are never checked.
* **A sprite that is never drawn:** a `sprite` with no `spr("name", ...)`, `spr_w`, `spr_h` or `spr_frames` call for it. If any call chooses a sprite by a name worked out at run time, no sprite is reported.
* **`rnd()` while drawing:** `rnd()` or `rnd_int()` in `on draw`, or in a function it calls. See [What each hook may do](#what-each-hook-may-do): drawing shares the random stream with update.

With `--strict`, `cart check` exits with code 1 if there is any warning, which suits a script or a check before packing. The same warnings appear on the Pack a cart page and in the Playground.

`cart check <file.cs>` compiles only. It prints `OK: ...` or an error with line, column and a caret, and exits with code 1 on error.

`cart run [folder | file.cs] [options]` plays the game with no browser, with a fixed random seed, and prints a report. (`tools/gs.mjs` and `tools/cart-pack.mjs` still work and do the same.)

| Option | Meaning |
|---|---|
| `--frames N` | Number of ticks to run (default 60). |
| `--input SPEC` | Scripted buttons, see below. |
| `--seed N` | Random seed (default 1). |
| `--state` | After the last tick, print all global variables as one JSON line: `state: {...}`. |
| `--vars a,b,c` | Print only these globals (implies `--state`). Use it when you have big lists. |
| `--dump` | Print the final screen, one character per pixel: `.` is color 0, `1`-`9` and `a`-`f` are the other colors. |
| `--region x,y,w,h` | With `--dump`: only this part of the screen. A full 128x128 dump is long, so prefer a region. |
| `--cell N` | Hi-res games: units per character in `--dump` (default: the width divided by 96). |
| `--commands [N]` | Hi-res games: print the first N (default 60) drawing commands of the last frame. |
| `--storage` | Enables `save()`/`load()` without a file: the saved data lives in memory for this run. (`cart run` turns this on by itself when the manifest grants `storage`.) |
| `--save-file F.json` | Enables `save()`/`load()`: the file is read at start (if it exists) and written whenever `save` is called, so it will not exist until the game saves something. |

`--input` is a comma-separated list of `button@frames` items. Frames are tick numbers starting at 0.

* `a@2` holds `a` during tick 2 only (one tick = a single press, which is what `btnp` sees).
* `left@10-40` holds `left` from tick 10 to tick 40 inclusive.
* An item may be repeated for the same button: `a@2,a@50,a@90-95`.

The scripted buttons are applied from tick 0, so a press at `a@2` reaches the game on its third tick. A game that starts on a press needs a **separate** press to do the next thing (for example launching a ball), because holding does not count as a new `btnp`.

Everything the game prints is also reported:

* `print: ...` lines for each `print(...)` call.
* `sounds: coin@12 hit@40` lists every sound played as `name@tick`. This is the way to check that sound effects fire.
* `ran N frames OK`, or the runtime error (with line number) and exit code 1.

Paths: on Windows use paths the **tool** understands. In Git Bash, `/tmp/...` is not a Windows path, so pass `$(cygpath -w /tmp/save.json)` or a relative path such as `save.json`.

## Packaging a cartridge

`manifest.json`:

```json
{
  "cart": 1,
  "language": "cartscript",
  "cartscript": 1,
  "id": "com.example.mygame",
  "title": "My Game",
  "author": "Me",
  "version": "1.0.0",
  "description": "One sentence about the game.",
  "license": "MIT",
  "entry": "main.cs",
  "width": 512,
  "height": 512,
  "permissions": ["storage", "fullscreen"]
}
```

* `language` must be `"cartscript"` (it is the only language Gameverse runs; `"html"` is refused); `entry` defaults to `main.cs` and must end in `.cs`.
* A cartridge may hold only `manifest.json`, `.cs` source files and `.md`/`.txt` notes. Any other file (web pages, scripts, WebAssembly, images) makes the packer refuse to build it and the player refuse to run it.
* `cartscript` is the [language version](#versions) the game was written for: a whole number, `1` or `2`. It is optional (missing means 1) and only allowed for CartScript games. A cartridge that asks for a version the player does not have is refused with a message to use a newer Gameverse. (`"cart": 1` is a different number: the version of the cartridge *file format*.)
* `width` / `height` are the size of the window in pixels (64-4096). The game screen is scaled to fit inside it, keeping its shape, so use the same aspect ratio as `config` (a 128x128 game fits a 512x512 window).
* `license` is optional free text (up to 120 characters), for example `MIT`, `CC BY 4.0` or `All rights reserved`. It is shown to players on the game's info card before they run it. A cartridge is a plain zip file, so anyone can unzip and read its source: the license states what they may do with it.
* `permissions` may contain `storage` (needed for `save`), `fullscreen` and `scores` (needed for `submit_score`).
* `leaderboards` (**version 2** and newer, with the `scores` permission) lists up to 5 boards the game posts to, for example `"leaderboards": [{ "id": "score", "title": "High score", "order": "high", "max": 100000 }]`. Each board has an `id` (1-32 characters of `a-z`, `0-9`, `-`), a `title` (up to 40 characters), an `order` (`"high"` or `"low"`) and optional `min` and `max`. A game with `leaderboards` must say `"cartscript": 2` and ask for `scores`, and `scores` needs `leaderboards`.
* `buy` (an `https://` link), `price` (up to 30 characters of text) and `pledge` (0-100, percent of sales pledged to Gameverse) are optional and say where a game is sold; Gameverse shows a Buy button and does not process or check payments. See [Selling your game](make.html#selling-your-game).
* `id` is 3-64 characters of `a-z 0-9 . _ -` and names the save slot, so every game needs its own. Use the reverse of a domain you own plus the game name (for example `com.mystudio.mygame`). `com.example.mygame` above is only a placeholder: replace it.

Pack and test:

```
cart pack my-game-folder my-game.cart
```

`pack` refuses to build a cartridge whose `main.cs` does not compile.

Add `--obfuscate` to rename variables and functions and strip comments and layout in the cartridge's copy of `main.cs` (your files are not changed), and `--map names.json` to write the table of new names. The tool checks that the obfuscated game behaves identically before it packs. See [Making your source harder to read](make.html#making-your-source-harder-to-read) for what this does and does not hide.

## Common mistakes

| Wrong | Right |
|---|---|
| `if hp { }` | `if hp > 0 { }` |
| `"x" + 5` | `"x" + str(5)` |
| `x++` | `x += 1` |
| `if a && b` | `if a and b` |
| `elif` | `else if` |
| `for i in 5` | `for i in range(5)` |
| `list[len(list)] = v` | `push(list, v)` |
| `let px = 3` (built-in name) | `let player_x = 3` |
| `p.nokey = 1` (new key via dot) | `p["nokey"] = 1` |
| `spr(ship, 1, 2)` | `spr("ship", 1, 2)` |
| `text("hi", 1, 2, 20)` (color > 15) | `text("hi", 1, 2, WHITE)` |
| Reading input in `draw` to move things | Move things in `update` |

## Complete example

```
// Dodge the falling blocks. Left/right to move.
config { width: 128, height: 128, bg: NAVY }

const SPEED = 2
let player_x = 60
let blocks = []
let score = 0
let alive = true

fn reset() {
  player_x = 60
  blocks = []
  score = 0
  alive = true
}

on update {
  if not alive {
    if btnp("a") { reset() }
    return
  }
  if btn("left")  { player_x -= SPEED }
  if btn("right") { player_x += SPEED }
  player_x = clamp(player_x, 0, W - 8)

  if frame % 20 == 0 {
    push(blocks, {x: rnd_int(0, W - 8), y: -8})
  }

  let keep = []
  for b in blocks {
    b.y += 1.5
    if collide(player_x, 112, 8, 8, b.x, b.y, 8, 8) {
      alive = false
      sfx("explode")
    } else if b.y < H {
      push(keep, b)
    } else {
      score += 1
    }
  }
  blocks = keep
}

on draw {
  rectfill(player_x, 112, 8, 8, LIME)
  for b in blocks {
    rectfill(b.x, b.y, 8, 8, RED)
  }
  text("SCORE " + str(score), 2, 2, WHITE)
  if not alive {
    text("GAME OVER - PRESS Z", 12, 60, YELLOW)
  }
}
```
