# Quick reference

Everything needed to write a model, on one page. Details: [reference](https://tabletwin.com/docs/reference/),
[expressions](https://tabletwin.com/docs/expressions/), [engine](https://tabletwin.com/docs/engine/); workflow and more patterns: [guide](https://tabletwin.com/docs/guide/).
Every snippet below validates and plays: paste it into the
[validator](https://tabletwin.com/validate/) to try it.

**Basics.** One YAML file (YAML 1.1: quote `yes`/`no`/`on`/`off`, `010`, `1:30`). A field `f` is text
for people; `f_x` is an expression the engine runs (`_x` wins). Text without `_x` does nothing or
stops the engine: the [validator](https://tabletwin.com/validate/) lists these gaps under "What the
engine still misses". Players are `0 … n−1`: rulebook "player 1" is `0`.
Index 0 of a zone is the top. Quote expressions containing `: # [ {` or starting with a quote.

## Top-level keys

| Key | Meaning |
|---|---|
| `game`, `label` | Id (lower-case, hyphens) and display name. Required by the schema |
| `num_players` | Default 2 |
| `mode` | `competitive` (default) or `cooperative` |
| `players`, `summary`, `demonstrates`, `scope`, `description`, `utility` | For people only |
| `constants` | `{NAME: value}`; read-only |
| `boards`, `functions` | Board geometry; own pure functions `{name: {params: [a, b?], let: {...}, x: expr}}` |
| `state` | `zones`, `vars`, `derived` (below); `entity_types` is descriptive |
| `flow`, `flows` | Main flow (one node, required); named subflows `{name: node}` |
| `actions` | `{name: {pre_x, cost, effect, speed}}` |
| `triggers`, `modifiers`, `priority`, `outcomes`, `agents`, `content`, `code` | Reactions, value changes, windows, skill, policies, cards as data, escape hatch |
| `open_questions` | `[{id, question, about, status, answer, source}]` ([§22](https://tabletwin.com/docs/reference/#22-open_questions)) |
| `end` | List of end conditions (below) |

## State

| Zone field (`state.zones.<name>`) | Meaning |
|---|---|
| `owner` | `global` (default, one list) or `player` (list per player; path `hand[p]`, unindexed is an error) |
| `visibility` | `public` (default), `owner`, `hidden`: who sees what is drawn into it |
| `shuffled` | `true`: drawing is a chance node (order unknown except known top/bottom) |
| `init_x` | Initial list (`owner: player`: list of lists). Dicts become instances with a `uid` |
| `persist` | `campaign`: kept by `reset` |
| `drawn_from` | Hidden zone dealt from that shuffled zone (so look-ahead players do not peek) |
| `kind`, `holds`, `size`, `label`, `note` | Descriptive |

| Var field (`state.vars.<name>`) | Meaning |
|---|---|
| `scope` | `global` (default, one value) or `player` (list, path `score[p]`; a non-list `init` is copied per player) |
| `init` / `init_x` | Literal (`null` = `None`) / expression (sees constants, `players`, `n_players`, `content`, vars declared above; no zones) |
| `persist` | `campaign`: kept by `reset` |
| `type`, `range`, `label`, `note` | Descriptive |

`state.derived`: `{name: "expr"}` or `{name: {text, x}}`, computed on use; may use derived values
declared above.

## Flow nodes

| Kind | Fields | Does |
|---|---|---|
| all | `id` (unique), `kind`, `label`, `note`, `window`, `end_event` | After the node's own work: its `window` opens, then each `end_event` is emitted with an end check after each. On a loop: once, after the whole loop |
| `sequence` | `children` | Children in order. `children: []` + `window` = a node that only opens a window |
| `loop` | `children`, `over_x`, `as`, `until_x`, `once`, `order` (V) | Before each round: stop if `until_x`. Per value of `over_x` (evaluated each round) set var `as`, run children. With `over_x` and no `until_x` it repeats round after round until an end condition fires. `once: true`: one round. No `over_x`: children once per round. 10,000 rounds: error |
| `branch` | `cases: [{when_x, children}]`, `switch` (V) | First case whose `when_x` is true; `when_x: "True"` as default |
| `step` | `do` (action or list), `actor_x`, `secret`, `visible_to_x` | Legal = actions whose `pre_x` holds; none: skipped (no end check); one: runs without asking; several: decision for `actor_x`. Then cost, effect, end check |
| `subflow` | `ref` (name in `flows`) | Runs that flow; clears `results[ref]`; scoped end conditions apply |

Declare the `as` variable in `state.vars` (a validator error otherwise: it would only exist once the
loop has started, and `reset` would skip it).
Flow expressions see no local names: use the `as` variable. A step without `actor_x` is a system
step: fine if it never needs a decision.

## Actions and ops

Action: `pre_x` (legal when; default always), `cost` (ops run when played), `effect` (ops),
`speed` (playable in windows that `allow` it). In `pre_x`, `cost` and `effect`, `actor` is the
acting player. Names bound in `cost` stay for `effect`. Fields accepting literals:
`value amount what at n die options min max items until target of`; any other text needs `_x`.
`value` text in `set`/`let` is a string literal.

| Op | Fields (`f`/`f_x` = literal or expression) |
|---|---|
| `set` | `var` path, or `of_x` + `attr`; `value`/`value_x` |
| `add` | `var` path, or `of_x` + `attr`; `amount`/`amount_x` (`+`: lists concatenate) |
| `let` | `bind`, `value`/`value_x` |
| `place` | `what`/`what_x`, `to`, `at`/`at_x` (replace index), `position` (`top`/`bottom`/`random`) |
| `create` | `what`/`what_x` (dict → instance), `to`, `n`/`n_x`, `with_x` (extra state), `position`, `bind` (last) |
| `move` | `what`/`what_x` (item or list; first equal element), `from`, `to`, `position` |
| `destroy` | `what`/`what_x`, `from` (else first zone holding it) |
| `draw` | `from`, `to`, `n`/`n_x`, `visible_to_x` (default by destination visibility), `position` |
| `shuffle` | `zone`: forget known top/bottom of a shuffled zone |
| `look` | `from`, `n`/`n_x`, `bind` (list), `actor_x`, `visible_to_x` |
| `search` | `from`, `bind`, `filter_x` (item `it` or `as`), `as`, `actor_x`, `min`/`max`, `visible_to_x` |
| `reveal` | `what`/`what_x`, `visible_to_x` (default everyone) |
| `roll` | `die`/`die_x` (faces), `bind`, `visible_to_x` |
| `choose` | `options`/`options_x`, `bind`, `actor_x` (default `actor`), `secret`, `visible_to_x`, `min`/`max` (subset), `agent` |
| `if` | `cond_x`, `then`, `else` |
| `for_each` | `items`/`items_x`, `as` (default `it`), `body` (copy of names) |
| `repeat` | `until`/`until_x` (tested before each pass), `body` |
| `emit` | `event` (literal name), `impact` (ops; triggers may replace it) |
| `sample` | `model` (name in `outcomes`) |
| `exec` | `of_x` (content item with `effect`; `self` = item), `content` (catalogue path, V) |
| `run` | `flow`: run a named flow inside the effect |
| `window` | `name` (none: every speed) |
| `cancel` | `target`/`target_x`: index in `stack` (0 bottom, −1 top); sets `cancelled` now |
| `modify` | `stat`, `change_x` (`value` = so far), `layer`, `if_x`, `until` (event) |
| `reset` | `scope` (label): every var/zone without `persist` back to its initial value |
| `call` | `code`: escape hatch to code outside the model; always `NotExecutable` (a gap) |
| `say` | `actor`, `channel`, `constraint`, `bind`, `agent`: always `NotExecutable` |

## Windows, triggers, content, outcomes, agents

| Key | Fields |
|---|---|
| `priority.protocol` | `first_x` (default local `actor`, else 0; evaluated once), `closes` (`all_pass_in_succession`/`single_pass_each`), `resolution` (`immediate`/`nested`/`lifo_stack`), `after_action` (`next`/`same`) |
| `priority.windows.<name>` | `allows: [speed, ...]`, `label`, `note` |
| `triggers[]` | `id`, `event`, `timing` (`before`/`when`/`instead`/`after`), `optional` + `controller_x`, `if_x`, `do` |
| `modifiers[]` | `stat`, `change_x`, `layer`, `if_x`; read with `modified('stat', base, k=v)` |
| content item | `id`, `label`, `status` (`verified`/`uncertain`/`missing`), `text`, `effect`, `triggers`, `modifiers`, `abilities` (actions with `speed`), `keywords`, `active_x` |
| `outcomes.<m>` | `derived_x`, `outcomes: [{id, p, p_x, effect}]` (sum 1), `skill` |
| `agents.<a>` | `policy_x` returns an option label (`options`, `decider` in scope) |

Window run: priority from `first_x`, then index + 1 (eliminated skipped). A player with legal
candidates picks one or `pass`; otherwise passes silently. `immediate`: cost + effect + end check
at once. `lifo_stack`: cost at once, item `{action, actor, cancelled}` on `stack`; when all pass,
the top resolves (cancelled: dropped, no check; else effect with its `actor` and cost names, then
end check), and priority goes back to the player `first_x` gave when the window opened (not
re-evaluated). All pass with an empty stack: the window closes, no end check. `cancel` in an
`effect` marks its target only when that effect resolves; cancelling twice is harmless. Give each
fast action a `pre_x` or `cost` that runs out: an always-legal one can keep the window open
forever (validator warning `always playable in window`).

## End conditions

| Field | Meaning |
|---|---|
| `id` | Reason reported |
| `if_x` | Condition (without it: never fires) |
| `result` | `win`, `loss`, `draw`, `score`, `eliminate`, `continue` |
| `who_x` | Competitive `win`/`loss`: one index (a list is an error; several winners: `score`). `eliminate`: index or list |
| `returns_x` | `score`: one number per player |
| `scope` | Name in `flows`: only while that subflow runs |

| Result | Competitive | Cooperative |
|---|---|---|
| `win` | +1 for `who_x`, −1 all others | +1 all |
| `loss` | −1 for `who_x`, +1 all others | −1 all |
| `draw` | 0 all | 0 all |
| `score` | `returns_x` | `returns_x` |
| `eliminate` | add `who_x` to `eliminated`; play and checking go on | same |
| scoped `win`/`loss`/`draw` | `results[scope] = 'win'`/`'loss'`/`'draw'`, the subflow ends, its caller continues | same |

Checked in list order; the first that fires ends the game (`eliminate` and `continue` never stop
the scan). Other results with a `scope` act unscoped while the subflow runs (a scoped `score` ends
the game). **When**: after each step that ran an action; in a window after each played action
(`immediate`) or each resolved, uncancelled item (`lifo_stack`); after each `end_event`. Never
mid-effect, after a cost, when a window closes, or at loop boundaries. Flow ran out: 0 for all.

## Names in scope

| Name | Where |
|---|---|
| constants, vars (incl. `as` loop vars), zones, derived, `players`, `n_players`, `eliminated`, `results` (`{flow: 'win'/'loss'/'draw'}`), `content`, `modified`, functions | Every expression (var `init_x`: no zones, derived or `modified`) |
| `actor` | Action `pre_x`/`cost`/`effect` (step actor; in a window who has priority; on resolution who played it); triggers fired from there. Not in flow or `end` |
| bound names (`bind`, `let`, `for_each` `as`) | Later ops of the same action run; `cost` → `effect`. `for_each`, `emit`, `exec`, `sample` get a copy |
| `it` / `as` | `search` `filter_x`; `for_each` body |
| `self` | Abilities, `exec` effects, content triggers, modifiers, `active_x` |
| `stack`, `window` | Window actions' `pre_x`, `cost` (stack as played on) and `effect` (stack without the item) |
| `event` | Trigger `if_x`, `controller_x`, `do` (plus the emitter's names) |
| `value` | Modifier `change_x` (plus `modified()` keyword args) |
| `options`, `decider` | Agent `policy_x` |

Built-ins: `len any all min max sum range abs round sorted list tuple set dict enumerate zip int
float str bool sigmoid exp sqrt log`; methods `.get .items .keys .values` (dict), `.count .index`
(list, tuple, text). No assignment, lambda, `_names`, `|`, f-strings, comments.

## Places and option labels

| Place (`where`; tactics `at:`) | From |
|---|---|
| `step <id>` | Choice between a step's actions (options: action names) |
| `action <name>` / `action <name> (cost)` | Ops in an action's effect / cost |
| `window <name>` (`window None` for `window: true`) | Priority: legal fast actions + `pass` |
| `trigger <id>` | Optional trigger (`use`/`skip`) and its ops |
| `content <id>` | Ops of an `exec`'d item |
| `<place> · k`, `<place> · <model>`, `<place> · <event>` | k-th pick of a subset (`done` ends it), a `sample`, an `emit` impact |

Other options are labelled by value: text as is, a dict by its `label`, else `id`, else `uid`.

## Checking your model

Everything runs in the browser; nothing is uploaded.

| Tool | What you get |
|---|---|
| [Workshop](https://tabletwin.com/workshop/) | Everything below in one page, as you type: the checks of the validator with the lines they are about, the rules as a track, the table with your view and images, the player with your game and your tactic, and a simulator (many games, wins with 95% intervals, two setups compared on the same games). Games stay in your browser; share by link or package. See [the workshop](https://tabletwin.com/docs/workshop/) |
| [Validator](https://tabletwin.com/validate/) | Paste the model (and optionally a [view](https://tabletwin.com/docs/views/)), press Check (Ctrl+Enter): errors; warnings, including `unknown key`; `% executable` with rules, content, steps, ops, code, open questions; "What the engine still misses" (the gaps, below 100%); "Test games: X of 20 finished" with the end reasons, "Where games stopped" and games that did not end within 5000 requests; "The flow as a tree" |
| [Player](https://tabletwin.com/play/) | "Open in the player" (after a test game finished) adds "Your model". One game question by question: place, options, choice, automatic choices, what changed, the table. Players: a seat by hand, everyone by hand, random, a `policy_x` heuristic, always the first option. "Game (seed)" replays the same game; "view as" shows one player's view |
| [Diagram](https://tabletwin.com/diagram/) | "Draw your own model": the flow as a tree and as a track diagram, with the markers per step |

Done means: no errors, every warning understood, rules 100% executable, 20 of 20 test games
finish with plausible reasons, and one game in the player reads like the rulebook. To measure
balance or compare tactics, use the workshop's Simulate pane.

## Patterns

**Turn loop.** The loop repeats the round until an end condition fires; the end check runs right
after the step, so `current` is still the player who just moved.

```yaml
game: race
label: Race
num_players: 2
state:
  vars:
    pos: {scope: player, init: 0}
    current: {init: 0}                 # the `as` variable
flow:
  id: game
  kind: loop
  over_x: players
  as: current
  children: [{id: turn, kind: step, actor_x: current, do: [walk, run]}]
actions:
  walk: {effect: [{op: add, var: "pos[actor]", amount: 1}]}       # actor = the player who acts
  run: {effect: [{op: roll, die: [0, 3], bind: d}, {op: add, var: "pos[actor]", amount_x: d}]}
end:
  - {id: finish, if_x: "pos[current] >= 6", result: win, who_x: current}
```

**Cards: setup step without actor, deck, hidden hands, check/fold–bet/call, rank function.**

```yaml
game: mini-poker
label: Mini poker
num_players: 2
constants: {CARDS: [J, Q, K]}
functions:
  rank: {params: [c], x: "'JQK'.index(c)"}          # constant order: J < Q < K
state:
  zones:
    deck: {kind: stack, visibility: hidden, shuffled: true, init_x: "list(CARDS)"}
    hand: {kind: set, owner: player, visibility: owner}
  vars:
    current: {init: 0}
    stake: {scope: player, init: 1}                  # ante
    bet_out: {init: false}
    folded: {init: null}
    called: {init: false}
    checks: {init: 0}
flow:
  id: game
  kind: sequence
  children:
    - {id: deal, kind: step, do: deal}               # no actor_x: a system step
    - id: betting
      kind: loop
      over_x: players
      as: current
      children: [{id: act, kind: step, actor_x: current, do: [check_fold, bet_call]}]
actions:
  deal:
    effect: [{op: for_each, items_x: players, as: p, body: [{op: draw, from: deck, to: "hand[p]"}]}]
  check_fold:
    effect:
      - {op: if, cond_x: bet_out, then: [{op: set, var: folded, value_x: actor}], else: [{op: add, var: checks, amount: 1}]}
  bet_call:
    effect:
      - {op: add, var: "stake[actor]", amount: 1}
      - {op: if, cond_x: bet_out, then: [{op: set, var: called, value: true}], else: [{op: set, var: bet_out, value: true}]}
end:
  - {id: fold, if_x: "folded is not None", result: score, returns_x: "[-stake[p] if p == folded else stake[1 - p] for p in players]"}
  - {id: showdown, if_x: "called or checks == n_players", result: score,
     returns_x: "[stake[1 - p] if rank(hand[p][0]) > rank(hand[1 - p][0]) else -stake[p] for p in players]"}
```

**Campaign: scoped subflow, reset, reward after a won scenario.** The `turns` loop has no `until_x`:
the scoped end conditions stop the scenario; `reward` then reads `results.get('scenario')`.

```yaml
game: trek
label: Trek
num_players: 1
constants: {SCENARIOS: 3}
state:
  vars:
    played: {init: 0, persist: campaign}
    gold: {init: 0, persist: campaign}
    hp: {init: 2}                                    # back to 2 at each reset
    dist: {init: 0}
flows:
  scenario:
    id: scenario
    kind: sequence
    children:
      - {id: setup, kind: step, do: start_scenario}
      - {id: turns, kind: loop, children: [{id: turn, kind: step, actor_x: "0", do: [advance, rest]}]}
flow:
  id: campaign
  kind: loop
  until_x: "played >= SCENARIOS"
  children: [{id: play, kind: subflow, ref: scenario}, {id: reward, kind: step, do: reward}]
actions:
  start_scenario: {effect: [{op: reset}]}
  advance:
    effect:
      - {op: roll, die: [0, 1, 2], bind: d}
      - {op: add, var: dist, amount_x: d}
      - {op: add, var: hp, amount: -1}
  rest: {pre_x: "hp < 2", effect: [{op: add, var: hp, amount: 1}]}
  reward:
    effect:
      - {op: if, cond_x: "results.get('scenario') == 'win'", then: [{op: add, var: gold, amount: 1}]}
      - {op: add, var: played, amount: 1}
end:
  - {id: won, scope: scenario, if_x: "dist >= 3", result: win}
  - {id: lost, scope: scenario, if_x: "hp <= 0", result: loss}
  - {id: campaign_over, if_x: "played >= SCENARIOS", result: score, returns_x: "[gold]"}
```

**Window: node that only opens it, counterspell, end after it closes.** The counter binds its
target in `cost` (cost ops see `stack`): the topmost opponent item not yet cancelled. A target is
marked `cancelled` only when the counter resolves. `end_event` + trigger sets a flag that the end
check right after it reads (a forced step after the window works too).

```yaml
game: duel
label: Duel
num_players: 2
state:
  vars:
    hp: {scope: player, init: 2}
    bolts: {scope: player, init: 1}
    counters: {scope: player, init: 1}
    closed: {init: false}
priority:
  protocol: {first_x: "0", resolution: lifo_stack}
  windows: {fight: {allows: [fast]}}
flow:
  id: game
  kind: sequence
  children:
    - {id: fight, kind: sequence, children: [], window: fight, end_event: fight_over}
actions:
  bolt:
    speed: fast
    pre_x: "bolts[actor] > 0"
    cost: [{op: add, var: "bolts[actor]", amount: -1}]
    effect: [{op: add, var: "hp[1 - actor]", amount: -1}]
  counter:
    speed: fast
    pre_x: "counters[actor] > 0 and any(s['actor'] != actor and not s['cancelled'] for s in stack)"
    cost:
      - {op: add, var: "counters[actor]", amount: -1}
      - {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}]
triggers:
  - {id: close, event: fight_over, do: [{op: set, var: closed, value: true}]}
end:
  - {id: closed, if_x: closed, result: score, returns_x: "[hp[p] - hp[1 - p] for p in players]"}
```
