# Model reference

Every key of a Tabletwin game model, layer by layer. A model is one YAML file. The engine plays it
(in the [player](https://tabletwin.com/play/) and in the validator's test games); the
[validator](https://tabletwin.com/validate/) checks it. This page describes what both actually do.

Related pages:
- [expressions.md](https://tabletwin.com/docs/expressions/): the expression language used in every `_x` field, and which
  names are in scope where.
- [board-geometry.md](https://tabletwin.com/docs/board-geometry/): `boards` and `functions` in depth.
- [engine.md](https://tabletwin.com/docs/engine/): how a model is run, step by step.
- [guide.md](https://tabletwin.com/docs/guide/): how to turn a rulebook into a model.
- [cheatsheet.md](https://tabletwin.com/docs/cheatsheet/): the essentials of this page on one page, with complete patterns.
- [tactics.md](https://tabletwin.com/docs/tactics/): tactic files (players as a separate layer).
- [schema.json](https://tabletwin.com/models/docs/schema.json): JSON Schema for editors; the
  validator also uses it to report unknown keys. Where it disagrees with this page, this page wins.

In the tables, **E** means the engine uses the field, **V** means it is for people and the
viewer only. A V field never changes how the game plays.

## 1. Conventions

### Text and expression (`_x`)

Most fields that carry rules come in two forms:

| Form | Example | Meaning |
|---|---|---|
| `field` with text | `pre: "has enough gold"` | For people and the viewer. The engine cannot run it. |
| `field_x` with an expression | `pre_x: "gold[actor] >= 3"` | What the engine runs. |
| `field` with a number, list, bool or dict | `amount: -1`, `die: [1, 2, 3]` | A literal value, used as is (only for the fields listed below). |

If both are present, `_x` wins. An `_x` field may also hold a YAML number or boolean
(`if_x: true`); it is turned into text and evaluated.

**Fields that accept a literal** (a non-text value without `_x`): `value`, `amount`, `what`,
`at`, `n`, `die`, `options`, `min`, `max`, `items`, `until` (of `repeat`), `target`, `of`.
For these, a **text** value without `_x` is an error at run time:
`field 'what' is text without what_x`. The exception is `value` in `set` and `let`: there a
text is a literal string (`value: open` stores `'open'`; `value: "x + 1"` stores the text
`'x + 1'`).

So to place the string `'gold'` in a zone, write `what_x: "'gold'"`, not `what: gold`.

**Fields that only work as `_x`.** What the engine does when only the text form is present:

| Text field without `_x` | Engine |
|---|---|
| `pre` (action), `if` (trigger), `cond` (if op), `when` (branch case), `first` (priority protocol), `of` (exec), `change` (modifier, modify), `p` (outcome) | Error `NotExecutable` when reached |
| `over`, `until` (loop) | Ignored: the loop runs its children once per round, and never stops by itself |
| `actor` (step), `controller` (trigger) | Ignored: no actor / the event's actor decides |
| `if` (end condition) | The condition is skipped |
| `if` (modifier) | Ignored: the modifier always applies |
| `filter` (search) | Ignored: every item is a candidate |
| `who` (end condition) | Ignored, so `who_x` is missing: error when a competitive `win`/`loss` fires, and at the first end check for `eliminate` |

The validator counts most of these places as gaps (its list "What the engine still misses", see
[§20](#20-validator)).

### Paths

| Kind | Syntax | Example |
|---|---|---|
| Variable path (`var` in `set`, `add`) | `name` or `name[expr]` | `score[current]`, `pieces['e4']` |
| Zone path (`from`, `to`, `zone`) | `name` or `name[expr]` | `deck`, `hand[actor]` |

The part in `[ ]` is an expression, evaluated with the local names of the op. A zone with
`owner: player` must be indexed (`hand[1]`); without an index the engine raises
`zone 'hand' belongs to a player; write hand[player]`.

### Players

Players are the integers `0 … num_players − 1`. In expressions, `players` is that list and
`n_players` its length.

### YAML 1.1 pitfalls

Models are read as YAML 1.1:
- `yes`, `no`, `on`, `off` (lower, title or upper case) are booleans, like `true` and `false`. A key `on:` becomes `true` and is rejected
  (`key true is not text; put it in quotes`). That is why triggers use `event:`, not `on:`, and
  branches use `switch:`. `options: [yes, no]` gives `[True, False]`.
- `010` is octal (8), `1:30` is sexagesimal (90). Quote such values.
- Dates (`2024-01-01`) stay text.
- Keys must be text; `<<` merge keys work.

## 2. Top-level keys

| Key | Type | | Meaning |
|---|---|---|---|
| `game` | text | V | Short id (lower case, hyphens) |
| `label` | text | V | Display name |
| `num_players` | integer | E | Number of players (default 2) |
| `mode` | `competitive` / `cooperative` | E V | How end conditions pay out (default `competitive`; anything else is an error) |
| `players` | text | V | E.g. `"2–4 players"` |
| `summary` | text | V | Two or three sentences about the game |
| `demonstrates`, `scope`, `description` | text | V | What the model shows and what it does (not) cover. This `scope` is unrelated to `scope` in `end` |
| `utility` | `{min, max}` | V | Range of a player's return. The engine does not use it |
| `constants` | dict | E | [§3](#3-constants) |
| `boards` | dict | E | [§4](#4-boards) |
| `functions` | dict | E | [§5](#5-functions) |
| `state` | dict | E | [§6](#6-state) |
| `flow` | node | E | The main flow. **Required** by the engine ([§9](#9-flow-and-flows)) |
| `flows` | dict | E | Named subflows |
| `actions` | dict | E | [§10](#10-actions) |
| `triggers` | list | E | [§14](#14-triggers) |
| `modifiers` | list | E | [§15](#15-modifiers) |
| `priority` | dict | E | [§13](#13-priority-windows) |
| `outcomes` | dict | E | [§17](#17-outcomes) |
| `agents` | dict | E | [§18](#18-agents) |
| `content` | dict | E | [§16](#16-content) |
| `code` | dict | E | [§21](#21-code) |
| `open_questions` | list | V | [§22](#22-open_questions) |
| `end` | list | E | [§19](#19-end) |

The JSON Schema also requires `game`, `label`, `actions` and `end`, and allows no other top-level
keys.

## 3. `constants`

`{name: value}`. Fixed numbers, lists and tables. Every expression sees them under their name.
Use upper case. Numbers you may want to change later (a target score, a hand size) belong here:
the site always runs a model with the values written in it, so you change them in one place.

```yaml
constants:
  GOAL: 20
  CARDS: [{id: ace, value: 1}, {id: king, value: 13}]
```

Constants are model data: they are never changed during play. A dict from `constants` placed in
a zone with `place` stays read-only; use `create` to make an instance ([§7](#7-instances)).

## 4. `boards`

`{name: board}`. Fixed boards with geometry: square or hex cells, neighbours, holes, walls.
Expressions call board functions with the board name as first argument
(`neighbours('map', cell)`). Full description in [board-geometry.md](https://tabletwin.com/docs/board-geometry/).

```yaml
boards:
  board: {kind: square, width: 8, height: 8, names: letters, neighbours: 8}
```

## 5. `functions`

`{name: function}`. Your own pure functions in the expression language.

| Field | | Meaning |
|---|---|---|
| `params` | E | List of parameter names (`^[A-Za-z][A-Za-z0-9_]*$`). `name?` is optional (default `None`). Call by position or by name |
| `let` | E | `{name: expression}`, evaluated in order; each sees the parameters and earlier `let`s |
| `x` | E | The result (required) |
| `text`, `note` | V | Explanation |

```yaml
functions:
  other:
    params: [p]
    x: "1 - p"
```

A function sees its parameters, its `let`s and the global part of the scope (constants, state,
derived values, board functions, other functions; recursion up to 100 calls deep). It does not
see the caller's local names (`actor`, `self`, bound names): pass them as arguments. Its name
must not clash with a constant, variable, zone, derived value, builtin, board function or
reserved name (`modified content players n_players eliminated results None`). See
[board-geometry.md](https://tabletwin.com/docs/board-geometry/#3-own-functions).

## 6. `state`

### `state.entity_types`

`{name: {attrs: {name: type}, extends?: name, note?}}`. **V** only: describes the kinds of things,
and `holds` of a zone may name one. Things in zones are plain values (text, number or dict).

### `state.zones`

`{name: zone}`. Places where things lie. In expressions a zone is a list; a zone with
`owner: player` is a list of lists (`hand[1]`). **Index 0 is the top.**

| Field | | Default | Meaning |
|---|---|---|---|
| `owner` | E V | `global` | `global`: one list. `player`: one list per player, path `name[i]`. `team`, `entity`: descriptive (treated as global) |
| `visibility` | E V | `public` | Who sees what is drawn into it: `public` everyone, `owner` only the owning player, `hidden` nobody |
| `shuffled` | E V | `false` | `true`: drawing from it is random (a chance node); order is unknown except a known top and bottom ([§8](#8-stack-order)) |
| `persist` | E V | – | `campaign`: kept by `reset`. Any other value is a validator error |
| `drawn_from` | E | – | For a hidden zone: the shuffled zone its cards come from. A player that looks ahead redistributes its cards together with the unknown part of that zone, so it does not peek. See [tactics.md](https://tabletwin.com/docs/tactics/#look-ahead-search) |
| `init_x` | E | empty | Initial content (a list; for `owner: player` a list of lists). Dicts become instances ([§7](#7-instances)) |
| `kind` | V | – | `grid`, `hexgrid`, `graph`, `plane`, `stack`, `set`, `bag`, `slot`, `track` |
| `holds`, `size`, `label`, `note` | V | – | Descriptive |

```yaml
state:
  zones:
    deck: {kind: stack, visibility: hidden, shuffled: true, init_x: "list(CARDS)"}
    hand: {kind: set, owner: player, visibility: owner}
    discard: {kind: stack}
```

`init_x` of a zone sees the constants, `content`, `players`, `n_players`, board and own functions,
and all variables. It does not see other zones.

### `state.vars`

`{name: var}`.

| Field | | Default | Meaning |
|---|---|---|---|
| `scope` | E V | `global` | `player`: one value per player (a list; path `name[i]`). `team`, `entity`: descriptive |
| `init` | E | `None` | Literal initial value (`null` is `None`). With `scope: player` and a non-list value, every player gets a copy |
| `init_x` | E | – | Initial value as an expression; sees constants, `content`, `players`, `n_players`, board and own functions, and the variables declared **above** it |
| `persist` | E V | – | `campaign`: kept by `reset` |
| `type` | V | – | `int`, `real`, `bool`, `enum`, `ref`, `set`, `list`, `dict`, `str` (descriptive) |
| `range`, `label`, `note` | V | – | Descriptive |

```yaml
state:
  vars:
    score: {scope: player, type: int, init: 0}
    current: {type: ref, init: 0}
    pool_size: {init_x: "2 * n_players"}
```

### `state.derived`

`{name: "expression"}` or `{name: {text, x}}`. Named values, computed each time an expression
uses them (directly or through another derived value). A derived value may use derived values
declared **above** it. It is computed in the scope of the expression that uses it; write it so
that it needs only the global state.

```yaml
state:
  derived:
    total: "sum(score)"
    best: {text: "highest score so far", x: "max(score)"}
    leaders: "[p for p in players if score[p] == best]"
```

## 7. Instances

A dict in a zone is an **instance**: a thing with its own state that can change during play
(damage, exhausted, tokens). Instances come from `create` with a dict, and from a zone's
`init_x` that yields dicts.

- Each instance gets a unique `uid` (1, 2, 3, … in order of creation).
- If the source dict is model data (from `constants` or `content`), the instance copies its
  simple fields (numbers, text, booleans, `None`) and refers to the source under the key
  **`origin`** (`inst['origin']['effect']`). Nested fields such as `effect` or `triggers` are not
  copied. A dict built by an expression is copied whole.
- `with_x` on `create` adds initial state: `with_x: "{'damage': 0}"`.
- Read: `c['damage']`, `c.get('exhausted')`, `c['origin']['effect']`.
- Write: `{op: add, of_x: c, attr: damage, amount: 2}`,
  `{op: set, of_x: c, attr: exhausted, value: true}`. Changing model data itself is an error:
  `'X' is model data; make an instance with create first`.
- In options, chance outcomes and observations an instance is named by its `label`, else `id`,
  else `uid`. Two instances with the same label are indistinguishable to a player.
- `exec` needs a dict with `effect`; for an instance use `of_x: "c['origin']"`.

```yaml
- {op: create, what_x: "content['units']['archer']", to: "army[actor]", with_x: "{'damage': 0}", bind: u}
- {op: add, of_x: u, attr: damage, amount: 1}
```

## 8. Stack order

Index 0 of a zone is the **top**: `draw` and `look` take from there. Inserting without `position`
appends at the end (the bottom).

For a zone with `shuffled: true` the order is unknown, except for a known top and a known
bottom:
- known on top: after `look`, or after `place`/`create`/`move`/`draw` with `position: top`;
- known at the bottom: after `place`/`create`/`move`/`draw` with `position: bottom`;
- the rest is unordered; a chance node decides which card it is when it is drawn or looked at;
- something inserted without `position` (or `position: random`) goes among the unknown cards;
- `shuffle` makes all of it unknown again.

Drawing takes the known top first, then (chance node, uniform) one of the unknown cards, and
only when those are gone the known bottom. For a zone without `shuffled`, `shuffle` does nothing
and `draw` always takes index 0.

## 9. `flow` and `flows`

`flow` is the main flow: one node, usually a `sequence` or `loop`. `flows` is `{name: node}`
with named subflows (used by `subflow` nodes and the `run` op).

| Field | On | | Meaning |
|---|---|---|---|
| `id` | all | E V | Unique in the whole model (validator error on duplicates) |
| `kind` | all | E V | `sequence`, `loop`, `branch`, `step`, `subflow` |
| `label` | all | V | Display name (else `id`) |
| `children` | sequence, loop | E V | List of nodes, run in order |
| `over_x` | loop | E | List (or other iterable) of values to run the children for, evaluated at the start of each round. Usually `players` |
| `as` | loop | E | Variable set to each value of `over_x` before the children run. Declare it in `state.vars` (a validator error otherwise): the engine creates an undeclared one only when the loop first sets it, so it does not exist before that, and `reset` leaves it alone |
| `until_x` | loop | E | Stop condition, tested before each round. Without it the loop repeats round after round (with `over_x`: all values again, from the first) and never stops by itself; the game ends through `end` |
| `once` | loop | E V | `true`: exactly one round (each value of `over_x` once) |
| `over`, `until` | loop | V | Text for people |
| `order` | loop | V | `fixed`, `clockwise_from_first`, `initiative`, `simultaneous`, `deck_order`, `players_choose` (descriptive; other values are a validator error) |
| `cases` | branch | E V | `[{when, when_x, label, note, children}]`. The first case whose `when_x` is true runs; none true: nothing. A case without `when_x` reached before a match is an error |
| `switch` | branch | V | What the branch looks at (text) |
| `ref` | subflow | E V | Name in `flows` |
| `do` | step | E V | Action name, or a list of action names among which the actor chooses |
| `actor_x` | step | E | Who acts (player index). Needed if more than one action is legal, and for any `choose` in the action without its own `actor_x` |
| `actor` | step | V | Text for people |
| `secret` | step | E | Only the actor sees which action was chosen |
| `visible_to_x` | step | E | Who sees the choice (list of player indexes) |
| `window` | all | E V | A priority window opened after the node: a name from `priority.windows`, or `true` (all speeds) |
| `end_event` | all | E V | Event name (or list) emitted when the node is done, after its window |
| `note` | all | V | Explanation |

What each kind does:
- **sequence**: children in order.
- **loop**: before each round, if `until_x` is true the loop ends. With `over_x`, the children
  run once per value (with `as` set); without it, once per round. More than 10,000 rounds is an
  error (`loop does not end`). Eliminated players are **not** skipped: filter them yourself
  (`over_x: "[p for p in players if p not in eliminated]"`).
- **branch**: the first case with a true `when_x`. Use `when_x: "True"` for a default case.
- **subflow**: runs `flows[ref]`. `results[ref]` is cleared when it starts; end conditions with
  `scope: ref` apply while it runs ([§19](#19-end)).
- **step**: (1) evaluate `actor_x` (else the actor is `None`); (2) legal actions are those in
  `do` whose `pre_x` is true, with `actor` in scope; (3) none legal: the step is skipped;
  (4) one legal: it runs without a decision; (5) several: a decision for the actor (an error
  without `actor_x`); (6) the action's `cost`, then its `effect`, with the same local names;
  (7) end conditions are checked.

After a node's own work comes its `window`, then its `end_event`s (each followed by an end
check). On a loop both happen once, after the whole loop.

```yaml
flow:
  id: game
  kind: loop
  over_x: players
  as: current
  children:
    - {id: turn, kind: step, actor_x: current, do: [roll, stop]}
```

Flow expressions (`over_x`, `until_x`, `when_x`, `actor_x`, `visible_to_x`) see only the global
part of the scope: no `actor` and no local names. Use the loop variable from `as`.

## 10. `actions`

`{name: action}`.

| Field | | Meaning |
|---|---|---|
| `pre_x` | E | When the action is legal (default: always). Sees `actor` (and `self` for abilities; `window` and `stack` in a window) |
| `cost` | E | Ops run first when the action is played. With `lifo_stack` they run immediately, while `effect` waits on the stack. Same local names as `pre_x`; in a window that includes `stack` as it is before this action is put on it |
| `effect` | E | List of ops |
| `speed` | E V | Makes the action playable in priority windows whose `allows` contains this speed ([§13](#13-priority-windows)). Actions with a speed are usually not in any step |
| `pre`, `actor`, `label`, `note` | V | Descriptive |

```yaml
actions:
  stop:
    pre: the pot is not empty
    pre_x: "pot > 0"
    effect:
      - {op: add, var: "score[current]", amount_x: pot}
      - {op: set, var: pot, value: 0}
```

In `pre_x`, `cost` and `effect`, `actor` is the player who acts: the step's `actor_x`, in a
window the player who has priority, and when an effect resolves from the stack the player who
played it. Names bound in `cost` (by `choose`, `let`, …) are still there in `effect`, also when the
effect resolves later from the stack.

## 11. Ops

An effect is a list of ops, run top to bottom. Each op is a dict with `op` and its fields.
Local names: `bind` in `choose`, `roll`, `let`, `look`, `search`, `create` adds a local name for
the rest of the effect. `if` and `repeat` share the local names with the surrounding effect;
`for_each`, `emit` (impact and triggers), `exec` and `sample` work on a copy, so names bound
inside do not leak out.

| Op | Fields | Summary |
|---|---|---|
| [`set`](#set) | `var` or `of_x`+`attr`; `value`/`value_x` | Set a variable or an instance attribute |
| [`add`](#add) | `var` or `of_x`+`attr`; `amount`/`amount_x` | Add to a variable or attribute |
| [`let`](#let) | `bind`, `value`/`value_x` | Bind a local name |
| [`place`](#place) | `what`/`what_x`, `to`, `at`/`at_x`, `position` | Put a value in a zone |
| [`create`](#create) | `what`/`what_x`, `to`, `n`/`n_x`, `with_x`, `position`, `bind` | Add new instances |
| [`move`](#move) | `what`/`what_x`, `from`, `to`, `position` | Move items between zones |
| [`destroy`](#destroy) | `what`/`what_x`, `from` | Remove an item from play |
| [`draw`](#draw) | `from`, `to`, `n`/`n_x`, `visible_to_x`, `position` | Take the top card(s) |
| [`shuffle`](#shuffle) | `zone` | Make a shuffled zone's order unknown again |
| [`look`](#look) | `from`, `n`/`n_x`, `bind`, `actor_x`, `visible_to_x` | Look at the top card(s) |
| [`search`](#search) | `from`, `bind`, `filter_x`, `as`, `actor_x`, `min`/`max`, `visible_to_x` | Look through a zone and pick |
| [`reveal`](#reveal) | `what`/`what_x`, `visible_to_x` | Show a value |
| [`roll`](#roll) | `die`/`die_x`, `bind`, `visible_to_x` | Uniform chance node |
| [`choose`](#choose) | `options`/`options_x`, `bind`, `actor_x`, `secret`, `visible_to_x`, `min`/`max` | A decision |
| [`if`](#if) | `cond_x`, `then`, `else` | Conditional |
| [`for_each`](#for_each) | `items`/`items_x`, `as`, `body` | Loop over values |
| [`repeat`](#repeat) | `until`/`until_x`, `body` | Loop until a condition |
| [`emit`](#emit) | `event`, `impact` | Fire an event (triggers) |
| [`sample`](#sample) | `model` | Draw from an outcome model |
| [`exec`](#exec) | `of_x`, `content` | Run a content item's effect |
| [`run`](#run) | `flow` | Run a named flow inside the effect |
| [`window`](#window) | `name` | Open a priority window |
| [`cancel`](#cancel) | `target`/`target_x` | Cancel a stack item |
| [`modify`](#modify) | `stat`, `change_x`, `layer`, `if_x`, `until` | Add a temporary modifier |
| [`reset`](#reset) | `scope` | Campaign: start a new scenario |
| [`call`](#call) | `code` | Escape hatch: code outside the model (not executable) |
| [`say`](#say) | `actor`, `channel`, `constraint`, `bind`, `agent` | Free speech (not executable yet) |

Any other `op` is a validator error and `NotExecutable` at run time. Every op may carry a `note`
(V).

### set

| Field | Default | Meaning |
|---|---|---|
| `var` | – | Variable path. Required unless `attr` is given |
| `of` / `of_x` | – | With `attr`: the instance to change |
| `attr` | – | Attribute name (literal) |
| `value` / `value_x` | – | New value. A text `value` is a literal string |

```yaml
- {op: set, var: phase, value: combat}          # the string 'combat'
- {op: set, var: "score[actor]", value_x: "score[actor] * 2"}
- {op: set, of_x: target, attr: exhausted, value: true}
```

### add

| Field | Default | Meaning |
|---|---|---|
| `var` | – | Variable path. Required unless `attr` is given |
| `of` / `of_x`, `attr` | – | Add to an instance attribute; a missing (or falsy) attribute counts as 0 |
| `amount` / `amount_x` | – | Added with `+` (negative subtracts; on a list variable `+` concatenates) |

```yaml
- {op: add, var: "gold[actor]", amount: -3}
- {op: add, of_x: unit, attr: damage, amount_x: "hit['power']"}
```

### let

| Field | Meaning |
|---|---|
| `bind` | Local name (required) |
| `value` / `value_x` | Value. A text `value` is a literal string |

```yaml
- {op: let, bind: target, value_x: "hand[actor][0]"}
```

### place

| Field | Default | Meaning |
|---|---|---|
| `what` / `what_x` | – | The value |
| `to` | – | Zone path |
| `at` / `at_x` | – | Index: replaces the element at that index (negative counts from the end) |
| `position` | – | Without `at`: `top`, `bottom`, `random` or none ([§8](#8-stack-order)). Any other value is an error |

`place` does not make an instance; use `create` for things with their own state.

```yaml
- {op: place, what_x: "'X'", to: grid, at_x: square}
```

### create

| Field | Default | Meaning |
|---|---|---|
| `what` / `what_x` | – | The thing; a dict becomes a new instance ([§7](#7-instances)) |
| `to` | – | Zone path |
| `n` / `n_x` | 1 | Number of copies |
| `with_x` | – | Dict of extra initial state, merged into each instance |
| `position` | – | `top`, `bottom`, `random` or none |
| `bind` | – | Local name for the (last) instance created |

```yaml
- {op: create, what_x: "content['tokens']['wound']", to: "wounds[actor]", n: 2}
```

### move

| Field | Default | Meaning |
|---|---|---|
| `what` / `what_x` | – | An item or a list of items |
| `from`, `to` | – | Zone paths |
| `position` | – | `top`, `bottom`, `random` or none |

For each item the first equal element (deep equality) in `from` is removed and inserted into
`to`. An item that is not in `from` is an error: `move: 'X' is not in hand[actor]` (the path as
written).

```yaml
- {op: move, what_x: card, from: "hand[actor]", to: discard}
```

### destroy

| Field | Default | Meaning |
|---|---|---|
| `what` / `what_x` | – | The item |
| `from` | – | Zone path. Without it: the first zone (in declaration order) that holds the item |

Not found is an error. Adds the event `destroy:<label>`.

### draw

| Field | Default | Meaning |
|---|---|---|
| `from`, `to` | – | Zone paths |
| `n` / `n_x` | 1 | Number of cards; stops early when `from` is empty |
| `visible_to_x` | destination | Who sees the drawn card. Default: from the destination's `visibility` (`public` everyone, `owner` its owner, `hidden` nobody) |
| `position` | – | Where it goes in `to` |

From a `shuffled` zone without a known top and with more than one unknown card, each draw is a
chance node (uniform over the unknown cards; [§8](#8-stack-order)). Otherwise the top card is
taken.

```yaml
- {op: draw, from: deck, to: "hand[actor]", n: 2}
```

### shuffle

| Field | Meaning |
|---|---|
| `zone` | Zone path. Without it the op does nothing |

Forgets the known top and bottom of a `shuffled` zone. On a zone without `shuffled` it does
nothing.

### look

| Field | Default | Meaning |
|---|---|---|
| `from` | – | Zone path |
| `n` / `n_x` | 1 | How many cards from the top |
| `bind` | – | Local name for the list of cards seen |
| `actor_x` | the local `actor` | Who looks |
| `visible_to_x` | `[actor]` | Who sees them (everyone if there is no actor) |

On a `shuffled` zone, chance nodes fix the unknown cards among the top `n`; afterwards they are
the known top.

### search

| Field | Default | Meaning |
|---|---|---|
| `from` | – | Zone path |
| `bind` | – | Local name for the chosen item (required) |
| `filter_x` | all | Which items may be chosen; the item is named `it` (or `as`) |
| `as` | `it` | Name of the item in `filter_x` |
| `actor_x` | the local `actor` | Who searches |
| `min`, `max` (and `_x`) | – | Choose a subset, as in `choose`; `bind` gets a list |
| `visible_to_x` | `[actor]` | Who sees the choice |

The searcher sees the whole zone (an observation for the actor only). Then a `choose` among the
matching items follows (one match: no decision). No match: `bind` is `None`, or `[]` with
`min`/`max`. `search` does not move or shuffle anything; add `move` and `shuffle` yourself.

```yaml
- {op: search, from: deck, filter_x: "it['type'] == 'ally'", bind: found}
- {op: if, cond_x: "found is not None", then: [{op: move, what_x: found, from: deck, to: "hand[actor]"}]}
- {op: shuffle, zone: deck}
```

### reveal

| Field | Default | Meaning |
|---|---|---|
| `what` / `what_x` | – | The value shown |
| `visible_to_x` | everyone | Who sees it |

Adds an observation. The zone's visibility does not change.

### roll

| Field | Default | Meaning |
|---|---|---|
| `die` / `die_x` | – | List of faces (numbers, text or dicts) |
| `bind` | – | Local name for the face rolled |
| `visible_to_x` | everyone | Who sees the result |

A chance node; each face is equally likely. A random card from a hand: `die_x: "hand[p]"`.

```yaml
- {op: roll, die: [1, 2, 3, 4, 5, 6], bind: pips}
```

### choose

| Field | Default | Meaning |
|---|---|---|
| `options` / `options_x` | – | The options (a list or other iterable) |
| `bind` | – | Local name for the chosen option (required) |
| `actor_x` | the local `actor` | Who chooses |
| `secret` | `false` | Only the chooser sees the choice |
| `visible_to_x` | everyone | Who sees the choice |
| `min` / `min_x`, `max` / `max_x` | – | Subset choice (see below) |
| `agent` | – | Name in `agents` (validator checks it exists; the engine ignores it) |

One option: no decision (the player lists it under "automatic"). No options: an error. More than
one option and no actor: `NotExecutable`.

**Subset.** With any of `min`, `max`, `min_x`, `max_x`, the chooser picks elements one at a time,
in the order of `options`: each time from the elements after the last one picked (only those
that still leave enough elements to reach `min`), plus the option `done` once at least `min` are
chosen. Defaults: `min` 0, `max` the number of options. It stops at `max`, at `done`, or when no
candidates are left. `bind` gets the list. No options and `min` 0 gives `[]`; fewer than `min`
available is an error. Each element is its own decision, at place `<place> · 1`, `· 2`, …

```yaml
- {op: choose, options_x: "hand[actor]", bind: discards, min: 0, max: 2}
```

### if

| Field | Meaning |
|---|---|
| `cond_x` | Condition (required; `cond` alone is an error) |
| `then`, `else` | Lists of ops; `else` optional |

### for_each

| Field | Default | Meaning |
|---|---|---|
| `items` / `items_x` | – | Values to loop over, evaluated once at the start |
| `as` | `it` | Local name of the current value |
| `body` | – | Ops, run once per value with a copy of the local names |

```yaml
- op: for_each
  items_x: "[p for p in players if p != actor]"
  as: p
  body: [{op: add, var: "gold[p]", amount: 1}]
```

If `items_x` is a zone itself (`hand[actor]`) and the body moves things out of that zone, the
loop skips items. Loop over a copy: `items_x: "list(hand[actor])"`.

### repeat

| Field | Meaning |
|---|---|
| `until` / `until_x` | Tested before each pass; the body runs while it is false |
| `body` | Ops (same local names as the surrounding effect) |

More than 10,000 passes is an error (`repeat does not end`).

### emit

| Field | Meaning |
|---|---|
| `event` | Event name (literal text; there is no `event_x`) |
| `impact` | Optional ops: what the event itself does |

Order: triggers with `timing` `when`/`before`; then `instead` triggers (the first one that fires
replaces the impact); then the `impact` (if not replaced); then `after` triggers. Afterwards,
temporary modifiers with `until` equal to this event expire. Triggers see the local names at the
`emit` plus `event`. A chain of more than 50 nested emits is an error. See
[§14](#14-triggers).

```yaml
- op: emit
  event: attack
  impact: [{op: add, of_x: target, attr: damage, amount_x: power}]
```

### sample

| Field | Meaning |
|---|---|
| `model` | Name in `outcomes` |
| `inputs` | Text for people (the engine passes the local names instead) |

Evaluates the model's `derived_x` and `p_x`, makes a chance node over its outcomes, adds the
event `<model>:<outcome id>` and runs that outcome's `effect`. See [§17](#17-outcomes).

### exec

| Field | Meaning |
|---|---|
| `of_x` | The content item (a dict with `effect`). Required |
| `content` | Catalogue path for the validator (e.g. `cards.*`); the engine ignores it |

Runs the item's `effect` with `self` set to the item, in place `content <id>`. Adds the event
`exec:<id>`. An item with `status: missing` or without `effect` is `NotExecutable`.

### run

| Field | Meaning |
|---|---|
| `flow` | Name in `flows` |

Runs the named flow inside this effect, with its own cursor. `results[flow]` is cleared first;
end conditions with `scope: flow` apply while it runs.

### window

| Field | Meaning |
|---|---|
| `name` | Name in `priority.windows`; without it all speeds are allowed |

Opens a priority window from inside an effect. The window sees the op's local names (and
`first_x` is evaluated with them). See [§13](#13-priority-windows).

### cancel

| Field | Meaning |
|---|---|
| `target` / `target_x` | Index in `stack` of the innermost open window (0 is the bottom; −1 the top). `None` does nothing; an index out of range is an `IndexError` |

Sets `cancelled: true` on that stack item **when the op runs**: in a `cost` at once, in an `effect`
(the usual place) only when that effect resolves. Until then the target still shows
`cancelled: false`, so two players can both target the same item. Cancelling an item that is
already cancelled is allowed and changes nothing. A cancelled item is removed without effect
when it would resolve (event `cancelled:<action>`; its cost was already paid). Outside an open
window: an error. Only useful with `resolution: lifo_stack`.

Fix the target when the counter is **played**: bind it in `cost` with `let` (cost ops see
`stack`), and cancel it in `effect`. Items below the counter do not move before it resolves, so
the index stays valid. Here the target is the topmost item of another player that is not yet
cancelled:

```yaml
counter:
  speed: fast
  pre_x: "any(s['actor'] != actor and not s['cancelled'] for s in stack)"
  cost:
    - {op: let, bind: target, value_x: "max(i for i, s in enumerate(stack) if s['actor'] != actor and not s['cancelled'])"}
  effect: [{op: cancel, target_x: target}]
```

A bare `target: -1` in `effect` means "the item directly below this counter when it resolves"
(the counter itself is already off the stack), which may be your own item or one that is already
cancelled.

### modify

| Field | Default | Meaning |
|---|---|---|
| `stat` | – | Which quantity (required) |
| `change_x` | – | New value; `value` is the value so far (required) |
| `layer` | 0 | Order among modifiers |
| `if_x` | – | Only active while true |
| `until` | – | Event at which the modifier expires; without it, it lasts until `reset` |

Adds a temporary modifier. The local names at the time of `modify` (except `stack`) are kept and
visible in `if_x` and `change_x`; `self` (if any) is kept as its content item. See
[§15](#15-modifiers).

```yaml
- {op: modify, stat: attack, change_x: "value + 2 if who == actor else value", until: round_end}
```

### reset

| Field | Default | Meaning |
|---|---|---|
| `scope` | `scenario` | Label only (event `reset:<scope>`) |

Campaign: every variable and zone without `persist: campaign` gets its initial value again
(`init_x` is recomputed); `eliminated` and temporary modifiers are cleared. `results` is kept.

### call

| Field | Meaning |
|---|---|
| `code` | Name in `code` |

Escape hatch: marks a place where the work would be done by code outside the model
(`code.<name>`). A model cannot bring its own code, so a `call` always stops the engine with
`NotExecutable` (`code 'x' → m:f is not implemented`) and counts as a gap. Use it only to record
honestly what the ops cannot express yet; avoid it where the model can express the rule.

### say

Fields `actor`, `channel`, `constraint`, `bind`, `agent`. Free speech for language games. Always
`NotExecutable` for now (`say needs a language agent`); the validator warns without `agent`.

## 12. Places, options and events

The engine names every decision and chance node by its **place** (`where`). Tactics match on it
(`at:`), and the player shows it for every question.

| Place | Where it comes from |
|---|---|
| `step <id>` | Choice between the actions of a step |
| `action <name>` | A `choose`, `roll`, `draw`, `look` or `search` in the action's `effect` (also for abilities) |
| `action <name> (cost)` | The same in its `cost` |
| `<place> · <k>` | The k-th element of a subset choice (`action play · 1`) |
| `window <name>` | A priority decision; `window None` when opened with `window: true` or a `window` op without a name |
| `trigger <id>` | The use/skip decision of an optional trigger, and ops in its `do` (`id` of the trigger, else of its content item) |
| `content <id>` | Ops in an effect run by `exec` |
| `<place> · <model>` | The chance node of a `sample` |
| `<place> · <event>` | Ops in the `impact` of an `emit` |
| `end <node id>` | The `emit` of an `end_event` (only seen in error messages) |

Option labels made by the engine: `pass` (priority window), `done` (end of a subset choice),
`use` / `skip` (optional trigger). Other options are labelled by their value: text as is, an
instance by `label`/`id`/`uid`, anything else in Python notation.

Events recorded by the engine (the player shows them after each question; the ones you do not
emit yourself appear under "messages"):

| Event | When |
|---|---|
| `<event>` | Every `emit` (including `end_event`s) |
| `instead:<trigger id>` | An `instead` trigger fired |
| `stack:<action>`, `resolves:<action>`, `cancelled:<action>` | `lifo_stack`: put on the stack, resolved, removed as cancelled |
| `<model>:<outcome id>` | A `sample` |
| `exec:<id>` | An `exec` |
| `destroy:<label>` | A `destroy` |
| `reset:<scope>` | A `reset` |
| `eliminated:<player>` | An `eliminate` end condition |
| `<scope>:<result>` | A scoped end condition ended a subflow |

## 13. Priority windows

```yaml
priority:
  protocol:
    first: the active player            # V
    first_x: "current"                  # E: who gets priority first
    closes: all_pass_in_succession      # E: or single_pass_each
    resolution: immediate               # E: immediate, nested or lifo_stack
    after_action: next                  # E: next or same
    order: clockwise_from_first         # V
  windows:
    combat: {label: Combat, allows: [fast], note: ...}
```

| Field | | Default | Meaning |
|---|---|---|---|
| `protocol.first_x` | E | the local `actor`, else player 0 | Who gets priority first, and again after each stack item resolves; evaluated once when the window opens. `first` without `first_x` is an error |
| `protocol.closes` | E | `all_pass_in_succession` | `all_pass_in_succession`: closes when all non-eliminated players pass one after the other. `single_pass_each`: a player who passes is out; closes when all are out |
| `protocol.resolution` | E | `immediate` | `immediate`: cost and effect at once. `nested`: the engine treats it like `immediate` (use trigger timing for reactions). `lifo_stack`: cost at once, effect on a stack |
| `protocol.after_action` | E | `next` | Who has priority after a played action: the next player, or the `same` |
| `protocol.order`, `protocol.note` | V | | `clockwise_from_first`, `any_order`, `active_then_clockwise` |
| `windows.<name>.allows` | E | – | List of speeds playable in this window |
| `windows.<name>.label`, `note` | V | | |

How a window runs:
- Candidates are all actions (including content abilities) with a `speed` that the window allows
  (`window: true` or a `window` op without a name: any speed). No candidates: the window closes
  at once.
- Priority passes from player to player (index + 1, wrapping; eliminated players are skipped).
  A player with legal candidates (`pre_x` true) chooses one of them or `pass`; without legal
  candidates the player passes automatically.
- In scope (in `pre_x`, `cost` and `effect` of the candidates): `actor` (who has priority),
  `window` (the window name, `None` for `true`), `stack` (list of `{action, actor, cancelled}`,
  bottom first; empty unless `lifo_stack`), plus the local names of a `window` op. `cost` sees the
  stack before the action is put on it.
- `immediate`/`nested`: cost, effect, then an end check.
- `lifo_stack`: cost runs, the action goes on the stack (event `stack:X`). When all players pass
  with a non-empty stack, the top item resolves: if cancelled it is removed (event `cancelled:X`,
  no effect, no end check); otherwise its effect runs (event `resolves:X`) with `actor` = who
  played it, the names bound in its cost, and `stack` without the item; then an end check. Then
  priority returns to the player `first_x` gave when the window opened (`first_x` is **not**
  evaluated again, even if the effect changed what it reads), and passing starts over
  (`single_pass_each`: everyone is back in). That player passes automatically if they have no
  legal candidate.
- All pass with an empty stack: the window closes (no end check).
- `pass` is reserved: an action named `pass` with a `speed` is a validator error. `speed` and
  `allows` are free names; they only have to match.

## 14. `triggers`

A list of rules that react to `emit` events.

| Field | | Default | Meaning |
|---|---|---|---|
| `id` | E V | – | Unique name; used in the place `trigger <id>` |
| `event` | E V | – | Event name it reacts to (exact match) |
| `timing` | E V | `after` | `when` or `before` (before the impact), `instead` (replaces the impact; only the first that fires), `after` |
| `optional` | E V | `false` | The controller decides `use` or `skip` |
| `controller_x` | E | the event's `actor` | Who decides on an optional trigger. Without it and without an `actor`: an error |
| `if_x` | E | – | Condition, tested right before the trigger would fire (an earlier trigger can disable a later one) |
| `do` | E V | – | Ops |
| `if`, `controller`, `source`, `label`, `note` | V | | `source`: where the rule comes from (card, rule, keyword) |

Within one timing phase: mandatory triggers first, then optional ones; each group in model
order, model triggers before content triggers. In scope: the local names at the `emit`, `event`,
and `self` for a content trigger. A trigger on an `end_event` has only `event`.

```yaml
triggers:
  - id: thorns
    event: attack
    timing: after
    if_x: "target['id'] == 'bramble'"
    do: [{op: add, of_x: attacker, attr: damage, amount: 1}]
```

## 15. Modifiers

A modifier changes a quantity (`stat`) wherever the rules ask for it with
`modified('stat', base, name=value, ...)`.

Static modifiers live in the top-level `modifiers` list and on content items. Temporary ones are
added by the `modify` op.

| Field | | Default | Meaning |
|---|---|---|---|
| `stat` | E V | – | Which quantity. Modifiers without `stat` are ignored |
| `change_x` | E | – | New value; `value` is the value so far. Missing: an error when applied |
| `layer` | E V | 0 | Lower layers first; within a layer the static ones (top-level list first, then content items), then temporary ones in order of creation |
| `if_x` | E | – | Only active while true |
| `until` | E | – | (temporary) expires at this event |
| `id`, `source`, `target`, `change`, `if`, `duration`, `note` | V | | `duration`: `permanent`, `round`, `turn`, `while` |

`modified('stat', base, k=v, ...)`: the extra keyword arguments are local names in `if_x` and
`change_x` (e.g. who attacks). A content modifier also sees `self`, and applies only while its
item's `active_x` is true. Modifiers that refer to each other more than 20 deep are an error.

```yaml
modifiers:
  - {id: veteran, stat: attack, layer: 1, if_x: "who == 0", change_x: "value + 1"}
actions:
  hit:
    effect:
      - {op: add, var: "hp[1]", amount_x: "-modified('attack', 2, who=actor)"}
```

The validator warns when a `stat` is never read with `modified('stat', ...)` (a literal string).

## 16. `content`

Content is the game's material as data: cards, dice, faces, skills. The structure is free:
`{catalogue: {key: item}}`, nested as deep as you like, lists allowed. Expressions see it as
`content`.

```yaml
content:
  cards:
    bolt:
      id: bolt
      label: Lightning Bolt
      status: verified
      text: "Deal 3 damage."
      effect: [{op: add, var: "hp[1 - actor]", amount: -3}]
```

An **item** is a dict with an `id`:

| Field | | Meaning |
|---|---|---|
| `id`, `label` | E V | Identity and name |
| `status` | V | `verified`, `uncertain` or `missing`. The validator warns for every item without one of these |
| `text` | V | The card text in your own words. An item with `text` but no behaviour counts as a gap |
| `effect` | E | Ops, run with `exec` (`self` is the item) |
| `active_x` | E | When the item's `triggers` and `modifiers` apply; `self` is the item. Without it they **always** apply |
| `triggers`, `modifiers` | E V | As in [§14](#14-triggers) and [§15](#15-modifiers), bound to the item |
| `abilities` | E V | Actions on the card itself (`id` unique across all actions and abilities, `speed`, `pre_x`, `cost`, `effect`). Playable in priority windows like any action with `speed`; `self` is the item. Without `speed` never playable (validator warning). The validator rejects them in a step's `do` |
| `keywords` | E V | `"Sentinel, Ranged"` or a list. Their behaviour lives in the rules, which test them, e.g. `'Ranged' in c.get('keywords', '')`. The validator warns for a keyword that no rule expression mentions in quotes |
| `faces` | V | Die faces as `"type number"`; the validator checks that `type` is a key of some `face_types` dict in `content` |
| `source`, `cost`, `requires`, `note` | V | |

Triggers, modifiers and abilities belong to the **item** (the card kind), not to one instance.
With two copies of a card in play, its trigger still fires once per event; `if_x` picks the
right instance through the event's local names (e.g. `card['origin']['id'] == self['id']`, where
`card` is a name bound before the `emit`), and
`active_x` limits when the item counts at all (e.g. only while a copy is in play).

## 17. `outcomes`

`{name: model}`. An outcome model replaces physics or skill by a probability distribution.

| Field | | Meaning |
|---|---|---|
| `outcomes` | E V | `[{id, p, p_x, effect, note}]`. The `p_x` must be ≥ 0 and add up to 1 (± 1e-6), else an error. The validator requires `p` (text) on each outcome as well |
| `derived_x` | E | `{name: expression}`, evaluated in order before `p_x`; visible in `p_x` and `effect` |
| `skill` | V | Depends on skill (`skill` marker instead of `chance`) |
| `inputs`, `params`, `derived`, `doc` | V | Descriptive |

```yaml
outcomes:
  shot:
    skill: true
    derived_x: {q: "sigmoid(skill[actor] - DIFFICULTY)"}
    outcomes:
      - {id: hit, p: q, p_x: q, effect: [{op: add, var: "score[actor]", amount: 1}]}
      - {id: miss, p: 1 - q, p_x: "1 - q"}
```

Used by `{op: sample, model: shot}`; the chance node's labels are the outcome ids.

## 18. `agents`

`{name: agent}`.

| Field | | Meaning |
|---|---|---|
| `policy_x` | E | Returns the label of the option to choose. In scope: `options` (list of labels), `decider` (the player), and the global state. A result that is not one of the labels: a random choice |
| `kind` | V | `search`, `heuristic`, `llm`, `human` |
| `roles`, `sees`, `prompt`, `note` | V | Descriptive |

In the player, an agent with `policy_x` appears under Players as "heuristic <name>", next to
random players, "always the first option" and playing seats yourself. Tactic files (see
[tactics.md](https://tabletwin.com/docs/tactics/)) are the other way to describe how someone plays.

```yaml
agents:
  greedy: {kind: heuristic, policy_x: "'stop' if 'stop' in options and pot >= 10 else options[0]"}
```

## 19. `end`

A list of end conditions. At every check they are tested in **list order**, and the first one
that fires ends the game (or, with a `scope`, the subflow); later ones are not tested.
`eliminate` and `continue` never stop the scan.

| Field | | Meaning |
|---|---|---|
| `id` | E V | Name; the reason reported when the game ends |
| `if_x` | E | When the condition holds. Without `if_x` the condition is skipped |
| `result` | E V | `win`, `loss`, `draw`, `score`, `eliminate`, `continue` |
| `who_x` | E | `win`/`loss` in a competitive game: the winner resp. loser (one index; a list, tuple or set is an `ExprError` when the condition fires, and a validator error when `who_x` is obviously one, such as a list literal or comprehension). `eliminate`: an index or a list |
| `returns_x` | E | `score`: list with one number per player |
| `scope` | E V | A name in `flows`: applies only while that subflow runs ([§9](#9-flow-and-flows)) |
| `if`, `who`, `note` | V | Text |
| `check` | V | `immediate`, `end_of_turn`, `end_of_round`, `end_of_scenario` (descriptive) |

**When**: after each step's action (a skipped step has no check), after each action played (or
resolved) in a window, and after each `end_event`. Not halfway through an effect, not after a cost, not after a cancelled
item, not when a window closes, not at the start or end of a loop round.

**What**:
- A condition with `scope` is skipped unless that subflow is running. If it holds and the result
  is `win`, `loss` or `draw`, `results[scope]` is set to the result (event
  `<scope>:<result>`), the subflow ends and the calling flow continues. A `scope` equal to the
  main flow's `id` means the whole game.
- `eliminate`: the players from `who_x` are added to `eliminated` (event `eliminated:<p>`); play
  goes on.
- `continue`: nothing.
- The first other condition that holds ends the game. A `score` (or `eliminate`, `continue`)
  condition with a `scope` is only tested while that subflow runs and then acts as without scope:
  a scoped `score` ends the whole game.

Returns per player:

| Result | Competitive | Cooperative |
|---|---|---|
| `win` | +1 for `who_x`, −1 for everyone else | +1 for everyone |
| `loss` | −1 for `who_x`, +1 for everyone else | −1 for everyone |
| `draw` | 0 for every player | 0 for every player |
| `score` | `returns_x` | `returns_x` |

Several winners (or losers) in a competitive game cannot be expressed with `win`/`loss`: `who_x`
must be one player index. Use `score`, e.g.
`returns_x: "[1 if p in winners else -1 for p in players]"`.

If the main flow finishes without an end condition, the game ends with 0 for everyone and the
reason `flow finished without an end condition`.

```yaml
end:
  - {id: won, if_x: "score[current] >= GOAL", result: win, who_x: current}
  - {id: out, if_x: "any(hp[p] <= 0 for p in players)", result: eliminate, who_x: "[p for p in players if hp[p] <= 0]"}
  - {id: tally, if_x: "rounds_played >= ROUNDS", result: score, returns_x: "[float(s) for s in score]"}
```

## 20. Validator

The [validator page](https://tabletwin.com/validate/) shows the errors and warnings below, the
executability with a few figures, the gaps ("What the engine still misses", per layer), the flow as
a text tree, and the result of 20 random test games. See [engine.md](https://tabletwin.com/docs/engine/#the-validator) for
what each part means.

**Errors** (must fix): unknown `mode`; `persist` other than `campaign`; duplicate flow `id`;
unknown `kind` or `order`; branch without `cases`; `subflow` to a missing flow; loop `as`
variable not declared in `state.vars`; `window` not in
`priority.windows`; a step's action missing from `actions`; unknown op; undeclared zone in
`from`/`to`/`zone`; undeclared `code`, `flow` (run), outcome `model` (sample) or `agent`
(choose/say); `exec` content path that does not exist; `end_event` that is not a name or list of
names; expression syntax errors; invalid board; function name clash, missing `x`, bad `params`
or `let`; unknown trigger `timing`; unknown `resolution` or `closes`; a `speed` action named
`pass`; end `scope` that is not a named flow; competitive `win`/`loss` whose `who_x` obviously
gives several players (a list, tuple or set literal, a comprehension, or `list()`, `sorted()`,
`set()`, `tuple()`); outcome model without outcomes or an outcome
without `p`; ability without `id` or with a name that already exists; undeclared face type;
open question without `question`.

**Warnings**: `drawn_from` to a zone that is not shuffled, or on a public zone; content item
without a valid `status`; ability without `speed`; keyword not used by any rule; board or
function never used; `say` without `agent`; `exec` without `content` or `of_x`; optional
trigger without `controller_x`; speed not allowed by any window; a fast action that is always
playable in a window that is opened (below); modifier `stat` (static or via
`modify`) never read with `modified()`; trigger on an event that is never emitted; action not
used by any step (unless it has `note` or `speed`); flow never called; open question with a
status other than `open`, `assumed`, `answered`; a key the
[JSON Schema](https://tabletwin.com/models/docs/schema.json) does not allow
(`unknown key 'x' at actions.y`: a typo, or a field the engine ignores).

**Always playable** (`action X: always playable in window Y; random players may keep the window
open forever`): the action has a `speed` that window `Y` allows, `Y` is opened somewhere (a
node's `window` or a `window` op; `window: true` allows every speed), and the action has no
`pre`/`pre_x`, no `cost`, and nothing in its `effect` that could end the game: no `set`/`add` of
a variable, and no zone op on a zone, that an end condition's `if_x` reads (also through derived
values and functions), no `emit` that a trigger listens to, no attribute write, and no `run`,
`exec`, `sample`, `call`, `reset` or `say`. Such an action can be played again and again: random
players rarely all pass in succession, and an agent that never passes hits `window does not
close`. Give it a `pre_x` (a resource, once per window) or a `cost`.

**Unknown keys**: the validator walks the model alongside the JSON schema and reports every
key where the schema allows no other keys, with its path. `content` has a free structure and is
not checked; `constants` and other free dicts neither.

The validator does **not** check that a `var` path names a declared variable, nor that
expressions only use known names; those fail at run time, which the test games usually show.

**Figures**:

| Figure | Meaning |
|---|---|
| `executable` | Share of engine-needed fields that have an `_x` or a literal (rules and content together) |
| rules | The same, for flow, actions, triggers, modifiers, priority, outcomes and end |
| content | Share of content items with behaviour (or `text`, or `status: missing`) that are fully executable, with counts; `-` without content |
| steps | Number of step nodes |
| ops | Number of ops in actions, triggers, outcomes and content |
| code | Share of `call` ops |
| open questions | Number of `open_questions` |

**Markers** of a step (shown in the [diagram](https://tabletwin.com/docs/diagram/#markers); from the ops of its actions,
including flows they `run`):
- `choice`: several actions in `do`, or a `choose`, `say` or `search`;
- `chance`: `shuffle`, `roll`, `draw` from a shuffled zone, `look` at a shuffled zone, `sample`
  from a model without `skill`;
- `skill`: `sample` from a model with `skill: true`;
- `hidden`: `choose` with `secret`, `move`/`draw` to an unshuffled zone with visibility `hidden`
  or `owner`, `search`, `look`;
- `window`: a `window` on the step, or a `window` op in its actions (a `window` on any other
  kind of node is not counted);
- `trigger`: an `emit` of an event some trigger listens to;
- `language`: `say`, or `choose` with an `llm` agent;
- `persistent`: `set`/`add` on a persistent variable, `move`/`draw`/`place`/`create` into a
  persistent zone, or `reset`;
- `code`: `call`.

## 21. `code`

`{name: {lang, ref, doc, kind}}`. Describes code the rules would need but the ops cannot express:
`doc` says what it does, `ref` names it (`"module:function"`), `lang` its language. `kind`:
`content` (should really be in `content`) or `algorithm`. A [`call`](#call) to it is never
executable; the share of `call` ops measures what the metamodel does not (yet) express.

## 22. `open_questions`

A list of gaps or ambiguities in the source: what the rulebook does not say, and what the model
assumes. The engine ignores them; the diagram shows them as a gap report, and the profile counts
them.

| Field | | Meaning |
|---|---|---|
| `id` | V | Short unique name (required by the JSON Schema) |
| `question` | V | The question, in one or two sentences. **Required**: missing is a validator error |
| `about` | V | What it concerns: a path in the model (`actions.attack`, `flow.round`, `end.won`) or a short topic (`setup`) |
| `status` | V | `open` (undecided; the default when missing), `assumed` (the model follows an assumption, given in `answer`), `answered` (the source or the designer settled it). Anything else is a validator warning |
| `answer` | V | The assumption or the answer |
| `source` | V | Where the question comes from (rulebook page, FAQ, forum thread) |

The JSON Schema allows no other fields. Write one entry per question; every `assumed` entry should
say in `answer` what the model does.

```yaml
open_questions:
  - {id: q1, about: actions.attack, question: "Can a unit attack twice?", status: assumed, answer: "No", source: "rulebook p. 6"}
  - {id: q2, about: setup, question: "Who starts?", status: open}
```

## 23. Limits

| Limit | Value | Error |
|---|---|---|
| Rounds of one loop | 10,000 | `loop does not end` |
| Passes of `repeat` | 10,000 | `repeat does not end` |
| Iterations of one window | 10,000 | `window does not close` |
| Nested `emit`s | 50 | `trigger chain too deep` |
| Own function call depth | 100 | `more than 100 calls deep` |
| Modifier nesting | 20 | `modifiers for 'x' refer to each other in a cycle` |
| `range()` length | 10,000,000 | `range too large` |

## 24. Tools on the site

| Tool | Use it to |
|---|---|
| [Validator](https://tabletwin.com/validate/) | Check a pasted model (and optionally its [view](https://tabletwin.com/docs/views/)): errors, warnings, executability, gaps, the flow as a tree, 20 random test games. Then "Open in the player" |
| [Player](https://tabletwin.com/play/) | Follow one game question by question, with random players, a heuristic from `agents`, or seats played by hand. "Game (seed)" replays the same game |
| [Diagram](https://tabletwin.com/diagram/) | See the rules as a text tree and a track diagram ("Draw your own model") |

Everything runs in the browser; nothing is uploaded. Playing many games to measure win rates and
comparing variants or tactics are not yet available on the site. See
[engine.md](https://tabletwin.com/docs/engine/#checking-and-running-a-model-on-the-site).
