# Tabletwin: full documentation All documentation pages in one file (quick reference and reference first), followed by two example models. Links are absolute. # 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.`) | 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.`) | 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.` | `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.` | `derived_x`, `outcomes: [{id, p, p_x, effect}]` (sum 1), `skill` | | `agents.` | `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 ` | Choice between a step's actions (options: action names) | | `action ` / `action (cost)` | Ops in an action's effect / cost | | `window ` (`window None` for `window: true`) | Priority: legal fast actions + `pass` | | `trigger ` | Optional trigger (`use`/`skip`) and its ops | | `content ` | Ops of an `exec`'d item | | ` · k`, ` · `, ` · ` | 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 | |---|---| | [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. Simulating many games, comparing variants and loading your own tactics are not yet available on the site. ## 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]"} ``` # 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: