# 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** (same inputs give the same game) and **sandboxed** (a game cannot touch files, the network or the page).

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

## 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.gs`.
2. `node tools/gs.mjs check main.gs` finds syntax errors, undefined names and wrong argument counts, with line numbers.
3. `node tools/gs.mjs run main.gs --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. `node tools/gs.mjs run main.gs --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`, then `node tools/cart-pack.mjs pack <folder> game.cart`.
6. 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.

### config

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

| Key | Default | Allowed |
|---|---|---|
| `width`, `height` | 128 | integers 16 to 512 |
| `fps` | 60 | 30 or 60 |
| `bg` | 0 | a color 0-15 |

The screen is cleared to `bg` before every `on draw`. The website scales the screen up to fit the window (pixels stay sharp).
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. |

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

### 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).

Do game logic in `update` and only drawing in `draw`. Button queries (`btn`, `btnp`) refer to the current tick.

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

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.

### 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. |
| `line(x1, y1, x2, y2, c)` | Line between two points. |
| `rect(x, y, w, h, c)` | Rectangle outline. |
| `rectfill(x, y, w, h, c)` | Filled rectangle. |
| `circ(x, y, r, c)` | Circle outline, center `(x, y)`, radius `r`. |
| `circfill(x, y, r, c)` | Filled circle. |
| `text(str, x, y, c?)` | Draw text (default color 7). `(x, y)` is the top-left. Accepts any value (converted like `str`). |
| `text_width(str)` | Width of the text in pixels. Use it to center: `text(s, (W - text_width(s)) / 2, 50)`. |
| `spr(name, x, y, flip_x?, flip_y?)` | Draw a sprite. `name` is a string. |
| `spr_w(name)`, `spr_h(name)` | Sprite size in pixels. |
| `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) }
```

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.

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

## Input

Seven virtual buttons:

| Button | Keyboard |
|---|---|
| `"left"`, `"right"`, `"up"`, `"down"` | Arrow keys or WASD |
| `"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. |

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

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

## 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"`. |

## 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.gs: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.
* No network, files, clock, or access to the web page. The only outputs are pixels, sound and save data.

## Command-line tool

`node tools/gs.mjs check <file.gs>` compiles only. It prints `OK: ...` or an error with line, column and a caret, and exits with code 1 on error.

`node tools/gs.mjs run <file.gs> [options]` plays the game with no browser, with a fixed random seed, and prints a report.

| 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. |
| `--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",
  "id": "com.example.mygame",
  "title": "My Game",
  "author": "Me",
  "version": "1.0.0",
  "description": "One sentence about the game.",
  "entry": "main.gs",
  "width": 512,
  "height": 512,
  "permissions": ["storage", "fullscreen"]
}
```

* `language` must be `"cartscript"`; `entry` defaults to `main.gs`.
* `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).
* `permissions` may contain `storage` (needed for `save`) and `fullscreen`.
* `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:

```
node tools/cart-pack.mjs pack my-game-folder my-game.cart
```

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

## 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)
  }
}
```
