# Guide: from rulebook to model

This guide takes you from a rulebook to a model that validates and plays. It is written for people
and for AI agents: with the rulebook, this guide and the [reference](https://tabletwin.com/docs/reference/) you should be
able to write a working model without further help. All you need besides a text editor is the
site: the [validator](https://tabletwin.com/validate/) checks a model, the
[player](https://tabletwin.com/play/) plays it and the [diagram](https://tabletwin.com/diagram/)
draws it.
The [expression language](https://tabletwin.com/docs/expressions/) and the [engine](https://tabletwin.com/docs/engine/) pages give the exact
behaviour; this page gives the workflow, the decisions and the patterns. The
[quick reference](https://tabletwin.com/docs/cheatsheet/) has the essentials of all of them on one page.

The guide has four parts:

1. [What you build](#what-you-build): one YAML file, two levels.
2. [Tutorial](#tutorial-pig-from-rulebook-to-executable-model): a complete game, step by step,
   with what the validator and the player show.
3. [The workflow for any game](#the-workflow-for-any-game), [decision rules](#decision-rules) and
   [patterns](#patterns) for everything Pig does not need.
4. [Pitfalls](#pitfalls) and the [checklist](#checklist-before-you-call-a-model-done).

## What you build

A model is one YAML file that describes the rules of one game: what lies on the table (`state`),
the course of play (`flow`), what players can do (`actions`), reactions (`triggers`, windows,
`modifiers`), cards and dice as data (`content`) and how the game ends (`end`).

Every rule can be written at two levels:

| Level | You write | You get |
|---|---|---|
| **Diagram** | Structure and text: `pre: "has enough gold"` | The flow diagram and the rules overview on the site |
| **Executable** | Next to each text field an expression `<field>_x`: `pre_x: "gold[actor] >= 3"` | Test games in the validator, games in the player, tactics |

The text stays when you add the expression: people and the diagram read `pre`, the engine reads
`pre_x`. Always write the diagram level first and add the `_x` fields afterwards. A model may be
partly executable; the validator reports the percentage.

Two other kinds of files sit next to a model, and neither changes the rules:

| File | What | Page |
|---|---|---|
| A tactic | How someone plays: rules per decision place | [tactics](https://tabletwin.com/docs/tactics/) |
| A view | How the table looks in the player | [views](https://tabletwin.com/docs/views/) |

### The smallest model

```yaml
game: my-game              # short id: lower case, hyphens
label: My game             # display name
num_players: 2
mode: competitive          # or cooperative
state:
  vars:
    done: {scope: global, type: bool, init: false}
flow:
  id: game
  kind: sequence
  children:
    - {id: move, kind: step, do: do_something}
actions:
  do_something:
    effect: [{op: set, var: done, value: true}]
end:
  - {id: over, if: the move is done, if_x: done, result: draw}
```

The flow has one step; the step runs the action; the action sets a flag; after the step the engine
checks `end`, finds `done` true and ends the game in a draw. Paste it into the
[validator](https://tabletwin.com/validate/): no errors, 100% executable, and 20 of 20 test games
end with `over`.

To get completion and checking in VS Code (YAML extension by Red Hat), put this on the first line
of a model file:

```yaml
# yaml-language-server: $schema=https://tabletwin.com/models/docs/schema.json
```

## Tutorial: Pig from rulebook to executable model

We model Pig, a dice game, from its rules to a tested, executable model. Write the model in any
text editor and check it by pasting it into the [validator](https://tabletwin.com/validate/). The
finished model of the same game, with the rulebook as comments, is
[pig.yaml](https://tabletwin.com/models/docs/examples/pig.yaml); it differs from this one in small
details, and it is in the player as "Pig".

The rulebook:

> **Pig** (2 players). Players take turns. On your turn, roll a die as often as you like and add
> each result to a running total for this turn, the *pot*. If you roll a 1, the pot is lost and your
> turn ends. You may stop after any roll; the pot is then added to your score and your turn ends.
> The first player to reach 20 points wins.

### Step 1: write down the facts

Before writing YAML, answer five questions from the rulebook:

| Question | Pig |
|---|---|
| How many players? | 2 |
| Competitive or cooperative? | Competitive |
| How does the game end, and who wins? | As soon as a player has 20 points or more; that player wins |
| What is the course of play? | Turns in order; within a turn, roll or stop until the turn ends |
| What does the rulebook not say? | Who starts. Whether you may stop before your first roll |

The last row becomes `open_questions` (step 9). Do not guess silently: a recorded question is
worth more than an invented rule that looks convincing.

### Step 2: header

```yaml
game: pig
label: Pig
players: "2 players"
num_players: 2
mode: competitive
summary: >
  A push-your-luck dice game. On your turn you roll as often as you like and add the pips to a
  pot. A 1 empties the pot and ends your turn; stopping banks the pot. First to 20 wins.
```

Players are numbered from 0: "player 1" in a rulebook is index 0.

### Step 3: state

Everything that lies on the table or must be remembered. Pig has no cards, so there are no zones,
only variables. Fixed numbers from the rulebook become constants, so they are easy to find and
change.

```yaml
constants:
  GOAL: 20

state:
  vars:
    score: {scope: player, type: int, init: 0}       # one value per player: score[0], score[1]
    pot: {scope: global, type: int, init: 0}
    current: {scope: global, type: ref, init: 0, note: "set by the turn loop"}
    turn_over: {scope: global, type: bool, init: false}
```

`current` is the loop variable of the turn loop. `turn_over` is a flag the inner loop tests.

### Step 4: flow, at diagram level

The flow is the skeleton of rounds, turns and phases. Pig is a loop over the players; inside each
turn there is a start and then a loop of "roll or stop".

```yaml
flow:
  id: game
  label: Game
  kind: loop
  over: all players in turn
  as: current
  order: fixed
  children:
    - id: turn
      label: Turn
      kind: sequence
      children:
        - {id: start, label: Start of turn, kind: step, do: start_turn}
        - id: rolling
          label: Roll until you stop or roll a 1
          kind: loop
          until: the turn is over
          children:
            - {id: choice, label: Roll or stop, kind: step, actor: the current player, do: [roll, stop]}
```

The outer loop has no `until`: after the last player it starts a new round with the first player,
and it keeps doing so until an end condition fires. A step with a list in `do` is a choice between
actions.

### Step 5: actions, at diagram level

```yaml
actions:
  start_turn:
    effect:
      - {op: set, var: pot, value: 0}
      - {op: set, var: turn_over, value: false}
  roll:
    effect:
      - {op: roll, die: [1, 2, 3, 4, 5, 6], bind: pips}
      - op: if
        cond: a 1
        then:
          - {op: set, var: pot, value: 0}
          - {op: set, var: turn_over, value: true}
        else:
          - {op: add, var: pot, amount: the pips}
  stop:
    pre: the pot is not empty
    effect:
      - {op: add, var: "score[current]", amount: the pot}
      - {op: set, var: turn_over, value: true}
```

`roll` binds the result to the local name `pips`. `stop` has a condition (`pre`): stopping with an
empty pot is pointless, which also answers the second open question. Inside an action's `pre`,
`cost` and `effect`, the local name `actor` is the player who acts (the step's `actor_x`), so
`score[actor]` would work here as well as `score[current]`.

### Step 6: end

```yaml
end:
  - {id: reached_goal, if: the current player has reached the goal, result: win}
```

### Step 7: validate

Paste steps 2 to 6 into the [validator](https://tabletwin.com/validate/) and press Check
(or Ctrl+Enter). It shows:

- **No errors**, and no warnings. The model is a valid diagram.
- **43% executable**, with rules 43%, content `-`, steps 2, ops 9, code 0%, open questions 0.
- **What the engine still misses**, the gap list:

```text
== Pig: executable 43% (rules 6/14), 8 gaps
  text: 8
      action roll: if.cond
      action roll: add.amount
      action stop: pre
      action stop: add.amount
      flow game: loop.over
      flow rolling: loop.until
      … and 2 more
```

  The two not shown are `step.actor` (who chooses in step `choice`) and `end.if/who`.
- **Test games: 0 of 20 finished**, with under "Where games stopped":
  `20× Not executable: action stop: pre is text without pre_x`. The engine cannot decide whether
  `stop` is legal, because `pre` is text for people.
- **The flow as a tree** (click to open), to compare with the rulebook:

```text
loop Game for each current in all players in turn (no until)
  sequence Turn
    - Start of turn: start_turn
    loop Roll until you stop or roll a 1 until the turn is over
      - Roll or stop: roll | stop · by the current player
          stop allowed if: the pot is not empty
```

"Open in the player" stays disabled until at least one test game finishes.

### Step 8: add the expressions

Add an `_x` field next to every text field the gap report lists. Keep the text.

```yaml
  over: all players in turn
  over_x: players                    # the list [0, 1]
  ...
          until: the turn is over
          until_x: turn_over
  ...
            - {id: choice, ..., actor: the current player, actor_x: current, do: [roll, stop]}
  ...
        cond: a 1
        cond_x: "pips == 1"
  ...
          - {op: add, var: pot, amount: the pips, amount_x: pips}
  ...
    pre: the pot is not empty
    pre_x: "pot > 0"
  ...
      - {op: add, var: "score[current]", amount: the pot, amount_x: pot}
  ...
  - {id: reached_goal, if: the current player has reached the goal, if_x: "score[current] >= GOAL", result: win, who_x: current}
```

Quote expressions that contain `:`, `#`, `[`, `{`, or start with a number or a quote.
Check again. Now the validator shows **100% executable** (the gap list is gone), and:

```text
Test games: 20 of 20 finished
On average 26 requests per game. How they ended:
  reached_goal: 20×
```

Every game ends the way the rulebook says, and "Open in the player" is enabled.

Why `who_x: current` works: the engine checks the end conditions right after each step, before
the turn loop moves on, so `current` still holds the player who just stopped.

### Step 9: play one game and read it

Press "Open in the player". The [player](https://tabletwin.com/play/) opens with "Your model: Pig",
random players and game (seed) 1. On the left is the track of the rules with the train at the
current step; on the right, what is asked now, what was chosen and what happened until the next
question; below, the table and a timeline. Step through the questions with the playback buttons.

Game 1 goes like this:

- The first question is already a die roll. Player 0's turn has started, and with an empty pot
  only `roll` is legal, so the engine took it without asking: one legal option is not a decision.
- Question 1 is a chance at `action roll`, out of 6 equally likely outcomes: a 1. Under "Effect
  until the next question" you see the `if` reset the pot and set `turn_over`; the inner loop
  ends and the train moves on to player 1's turn. "automatic" lists
  `step choice: player 1 → roll`: player 1's first roll is not a decision either.
- Player 1 rolls 6; at `step choice` (options `roll`, `stop`) chooses `roll` and rolls 5, chooses
  `roll` again and rolls 2, then chooses `stop`: `score` becomes `[0, 13]`.
- After a few more turns player 1 stops with 25 points. The game ends after 15 questions with
  "End: reached_goal" and the payoff player 0 −1 · player 1 1.

"Zones and values" in the table shows `score`, `pot`, `current` and `turn_over` at every question.
"Game (seed)" picks the game: the same seed and the same players give the same game, and "Random
game" tries another one.

To try a specific line of play, play a seat yourself: under Players choose "You play player 0
(others: random)". The player stops at each of your decisions ("Your turn") and offers the options
as buttons. "Everyone by hand (one screen)" lets you make every choice. To try another goal,
change `GOAL` in the model and check it again.

### Step 10: record what the rulebook does not say

```yaml
open_questions:
  - id: first_player
    about: setup
    question: Who starts? The rulebook does not say.
    status: assumed
    answer: Player 0 starts; the turn loop goes over players in index order.
```

`status` is `open` (undecided), `assumed` (the model follows the `answer`) or `answered` (the
source or the designer settled it).

### Step 11: a player who is better than random

How a good player chooses is not part of the rules. A [tactic](https://tabletwin.com/docs/tactics/) is a separate file
with rules per decision place:

```yaml
tactic: pig-hold-at-8
game: pig
rules:
  - at: step choice
    when_x: "pot >= 8 or score[me] + pot >= GOAL"
    pick: stop
  - at: step choice
    pick: roll
```

The built-in Pig in the player has this tactic. Choose "Pig (roll until you stop)" as the game and
"tactic stop at 8 vs random" under Players, and follow its choices; or play a seat yourself
against it. Loading your own tactic files is not yet available on the site.

Playing thousands of games to measure balance is not yet available on the site either. For the
record: over 2000 games with random players the first player wins about 55% (a goal of 50 shrinks
that advantage), and the tactic above wins about 75% against a random player.

### The finished model

```yaml
game: pig
label: Pig
players: "2 players"
num_players: 2
mode: competitive
summary: >
  A push-your-luck dice game. On your turn you roll as often as you like and add the pips to a
  pot. A 1 empties the pot and ends your turn; stopping banks the pot. First to 20 wins.

constants:
  GOAL: 20

state:
  vars:
    score: {scope: player, type: int, init: 0}
    pot: {scope: global, type: int, init: 0}
    current: {scope: global, type: ref, init: 0, note: "set by the turn loop"}
    turn_over: {scope: global, type: bool, init: false}

flow:
  id: game
  label: Game
  kind: loop
  over: all players in turn
  over_x: players
  as: current
  order: fixed
  children:
    - id: turn
      label: Turn
      kind: sequence
      children:
        - {id: start, label: Start of turn, kind: step, do: start_turn}
        - id: rolling
          label: Roll until you stop or roll a 1
          kind: loop
          until: the turn is over
          until_x: turn_over
          children:
            - {id: choice, label: Roll or stop, kind: step, actor: the current player, actor_x: current, do: [roll, stop]}

actions:
  start_turn:
    effect:
      - {op: set, var: pot, value: 0}
      - {op: set, var: turn_over, value: false}
  roll:
    effect:
      - {op: roll, die: [1, 2, 3, 4, 5, 6], bind: pips}
      - op: if
        cond: a 1
        cond_x: "pips == 1"
        then:
          - {op: set, var: pot, value: 0}
          - {op: set, var: turn_over, value: true}
        else:
          - {op: add, var: pot, amount: the pips, amount_x: pips}
  stop:
    pre: the pot is not empty
    pre_x: "pot > 0"
    effect:
      - {op: add, var: "score[current]", amount: the pot, amount_x: pot}
      - {op: set, var: turn_over, value: true}

end:
  - {id: reached_goal, if: the current player has reached the goal, if_x: "score[current] >= GOAL", result: win, who_x: current}

open_questions:
  - id: first_player
    about: setup
    question: Who starts? The rulebook does not say.
    status: assumed
    answer: Player 0 starts; the turn loop goes over players in index order.
```

## The workflow for any game

Work in this order; each step builds on the previous one. Validate after every step, not only at
the end.

1. **Read the rulebook and write down the facts**: number of players, competitive or cooperative,
   how the game ends and who wins, the course of a round (usually a section like "Playing the
   game"), and everything the rulebook leaves open.
2. **Header**: `game`, `label`, `num_players`, `mode`, `summary` (two or three sentences in your own
   words), optionally `players` ("2–4 players").
3. **State**:
   - things that *lie* somewhere (cards, tokens, pieces, cells) → `zones`;
   - numbers, flags, whose turn it is → `vars` (one per player: `scope: player`);
   - fixed numbers and tables from the rulebook → `constants`;
   - values you can compute from the state → `state.derived`;
   - a board with distances, lines of sight or paths → `boards` ([board geometry](https://tabletwin.com/docs/board-geometry/)).
4. **Flow**: rounds, turns and phases as a tree of `sequence`, `loop`, `branch`, `subflow` and
   `step`. Each step names one action, or a list of actions the actor chooses from. Follow the
   rulebook's headings: a phase in the rulebook is a node in the flow.
5. **Actions**: per action when it is allowed (`pre`) and what it does (`effect`, a list of ops).
6. **End**: when the game stops and what every player gets.
7. **Reactions and exceptions**: triggers (something happens automatically after an event),
   priority windows (players may play something in between), modifiers (a value is adjusted).
8. **Content and open questions**: cards and dice faces as data (`content`); every gap or
   ambiguity as `open_questions`.
9. **Expressions**: add the `_x` fields until the validator's list "What the engine still misses"
   is gone (or only lists what you deliberately leave as text).
10. **Check**: read the test games in the validator, follow single games in the player, compare
    with the rulebook (see [checking a model](#checking-a-model)).

## Decision rules

**Step, action or op?**

- *Step*: a moment in the course of play ("draw a card", "choose an action"). It sits in the flow.
- *Action*: something that can be done, with a condition and an effect. A step chooses from actions.
- *Op*: one elementary operation inside an effect (set, add, draw, roll, choose).

**A list in `do`, or `choose`?**

- The player chooses *which action* (build, trade, pass): `do: [build, trade, pass]` with
  `actor_x` on the step. Each action has its own `pre` and `effect`; the engine offers only the
  legal ones.
- The player chooses *a parameter of one action* (which cell, which card, how many): `choose`
  inside the effect, with the options as a list or an expression.
- The player chooses *a subset* (1 to 3 cards): `choose` with `min` and `max`.

**Zone or variable?** Can you move it, draw it, or point at it on the table? Zone. Is it a number
or a status? Variable. A number per player is a variable with `scope: player`.

**Flow step, trigger or window?**

- It always happens at a fixed moment of the round: flow step.
- It happens *in response to* something ("whenever you take damage", "when a card is revealed"):
  a trigger on an event (`emit` + `triggers`).
- Players may *play something in between* at a moment ("actions may be played after the quest
  phase", "in response you may cast a counterspell"): a priority window.

**Constant or variable?** Does it change during play? Variable. Is it a fixed setting of the game
(target score, number of rounds)? Constant.

**Rule or tactic?** What the rulebook prescribes belongs in the model. How a good player chooses
among legal options belongs in a [tactic](https://tabletwin.com/docs/tactics/).

## Patterns

Each pattern has a minimal snippet, cut from a small model that validates and plays. Snippets leave
out declarations that do not matter for the pattern; the referenced files show the pattern in a
complete model.

### Taking turns

A `loop` with `over_x: players` and `as: <variable>` sets that variable for each player in turn;
the step uses it as `actor_x`. Without `until_x` the loop repeats: after the last player it starts
the next round with the first, round after round, until an end condition fires and ends the game.
The end check comes right after the step, before the loop moves on, so the `as` variable still
points at the player who just moved: "whoever just moved wins" is `who_x: current`.

Inside the action, `actor` is the acting player (the step's `actor_x`): write `gold[actor]` in
`pre_x`, `cost` and `effect`. Flow expressions and end conditions have no `actor`; they use the
loop variable.

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

Declare the `as` variable in `state.vars` (an undeclared one only exists once the loop has set
it). Example: [pig.yaml](https://tabletwin.com/models/docs/examples/pig.yaml).

### Setup: a step without an actor

Setup ("shuffle the deck, deal two cards to each player") is a step without `actor_x`: a system
action. The engine runs it without asking anyone, because there is one action and nothing to
choose. It may roll, draw and shuffle (chance is fine), but it must not need a decision: a
`choose` with two or more options there needs its own `actor_x`.

```yaml
flow:
  id: game
  kind: sequence
  children:
    - {id: setup, kind: step, do: deal}                  # no actor_x
    - id: rounds
      kind: loop
      over_x: players
      as: current
      children: [{id: turn, kind: step, actor_x: current, do: [play, pass_turn]}]
actions:
  deal:
    effect:
      - {op: shuffle, zone: deck}
      - {op: for_each, items_x: players, as: p, body: [{op: draw, from: deck, to: "hand[p]", n: 2}]}
```

### A round that stops halfway (bidding)

A turn loop always completes the round: `until_x` is only tested before the next round. If play
must stop in the middle of a round ("whoever passes loses the auction"), use a `loop` **without**
`over_x`, with your own turn variable that each action passes on, and a flag in `until_x`.

```yaml
state:
  vars:
    bidder: {init: 0}
    bid: {init: 0}
    over: {init: false}
    winner: {init: null}
flow:
  id: auction
  kind: loop
  until_x: over
  children:
    - {id: bid_or_pass, kind: step, actor_x: bidder, do: [raise, pass_out]}
actions:
  raise:
    pre_x: "bid < 5"
    effect:
      - {op: add, var: bid, amount: 1}
      - {op: set, var: bidder, value_x: "1 - bidder"}
  pass_out:
    effect:
      - {op: set, var: winner, value_x: "1 - bidder"}
      - {op: set, var: over, value: true}
end:
  - {id: sold, if_x: over, result: score, returns_x: "[5 - bid if p == winner else 0 for p in players]"}
```

Example: [auction.yaml](https://tabletwin.com/models/docs/examples/auction.yaml).

### Each player once

"Each player may engage one enemy": a `loop` with `over_x: players`, `as: <variable>` and
`once: true`. Without `once` the loop repeats the round until `until_x` holds. `over_x` may be any
list, for example the engaged enemies, with `as: enemy`.

```yaml
- id: engagements
  kind: loop
  over_x: players
  as: p
  once: true
  children:
    - {id: engage, kind: step, actor_x: p, do: [engage, decline]}
```

### Repeat until something happens; push your luck

A `loop` with `until_x`, tested only at the start of each round. Set a flag in an effect and test
it in `until_x`. "Roll or stop" is a step with `do: [roll, stop]` inside such a loop; give an action
that is pointless now a `pre_x`. See the tutorial.

### A grid of cells

A grid of R × C cells can be one zone with one element per cell, for example `init_x: "[None] * 9"`
for 3 × 3 (cell `r * 3 + c`). `place` with `at_x` puts something in a cell (replacing what was
there). The cell is chosen with `choose` and `options_x` that computes the free cells. Fixed
patterns (lines, neighbours) are tables in `constants`, tested with `any(...)` and `all(...)`. A free
cell is `None`; test it with `is None`.

```yaml
constants:
  LINES: [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
  MARKS: [X, O]
state:
  zones:
    board: {kind: grid, init_x: "[None] * 9"}
  vars:
    current: {init: 0}
  derived:
    line_done:
      text: some line holds three equal marks
      x: "any(board[a] is not None and board[a] == board[b] == board[c] for a, b, c in LINES)"
flow:
  id: game
  kind: loop
  over_x: players
  as: current
  children:
    - {id: turn, kind: step, actor_x: current, do: mark}
actions:
  mark:
    effect:
      - {op: choose, options_x: "[i for i in range(9) if board[i] is None]", bind: cell}
      - {op: place, what_x: "MARKS[actor]", to: board, at_x: cell}
end:
  - {id: three, if_x: line_done, result: win, who_x: current}
  - {id: full, if_x: "None not in board", result: draw}
```

The `choose` is inside the effect, so the step needs `actor_x` even though it has one action.
Examples: [tictactoe.yaml](https://tabletwin.com/models/games/tictactoe.yaml),
[neighbours.yaml](https://tabletwin.com/models/docs/examples/neighbours.yaml).

### A board with space: moving, range, line of sight

When distance, paths, sliding or line of sight matter (chess, draughts, dungeon fights), declare
the board in `boards` and track what is where: a variable `{cell: piece}`, or instances with a cell
property. The geometry functions compute the space (`ray` to the first piece, `reachable` with
costs and blockers, `line_of_sight`, `shape` for area attacks). Rules that recur ("is this cell
attacked", "where does this monster move") go into `functions`. Compute expensive things (the legal
moves) once after each move and keep them in a variable instead of recomputing them in every end
condition.

```yaml
boards:
  grid: {kind: square, width: 4, height: 4, neighbours: 4}
state:
  vars:
    pos: {init: "0,0"}
actions:
  walk:
    effect:
      - {op: choose, options_x: "neighbours('grid', pos)", bind: to}
      - {op: set, var: pos, value_x: to}
end:
  - {id: home, if_x: "pos == '3,3'", result: win, who_x: "0"}
```

See [board geometry](https://tabletwin.com/docs/board-geometry/); examples
[chess.yaml](https://tabletwin.com/models/games/chess.yaml) and
[hex-skirmish.yaml](https://tabletwin.com/models/docs/examples/hex-skirmish.yaml).

### Deck, hand, hidden cards, secret choices

- Deck: a zone with `visibility: hidden`, `shuffled: true` and `init_x` with the cards. Drawing
  from a shuffled zone is a chance node; from a zone that is not shuffled you always get the first
  element.
- Hand per player: a zone with `owner: player`, `visibility: owner`; path `hand[i]` in ops, and
  `hand[i]` is a list in expressions.
- Secret choice: `secret: true` on `choose` (or on the step). Show it later with `reveal` and
  `what_x`: everyone (or `visible_to_x`) observes the value; the zone's visibility does not change.
- A card nobody sees (set aside, face down): move or draw it into a zone with
  `visibility: hidden`.

```yaml
constants:
  CARDS: [1, 2, 3, 4, 5, 6, 7, 8]
state:
  zones:
    deck: {kind: stack, visibility: hidden, shuffled: true, init_x: "CARDS"}
    hand: {kind: set, owner: player, visibility: owner}
    played: {kind: set, owner: player, visibility: hidden}
  vars:
    p: {init: 0}
flow:
  id: game
  kind: sequence
  children:
    - {id: deal, kind: step, do: deal}
    - id: secret_turns
      kind: loop
      over_x: players
      as: p
      once: true
      children:
        - {id: play, kind: step, actor_x: p, do: play_card}
    - {id: show, kind: step, do: reveal_all}
actions:
  deal:
    effect:
      - op: for_each
        items_x: players
        as: q
        body:
          - {op: draw, from: deck, to: "hand[q]", n: 2}
  play_card:
    effect:
      - {op: choose, options_x: "hand[actor]", bind: card, secret: true}
      - {op: move, what_x: card, from: "hand[actor]", to: "played[actor]"}
  reveal_all:
    effect:
      - op: for_each
        items_x: players
        as: q
        body:
          - {op: reveal, what_x: "played[q][0]"}
```

Players choose one after the other, but because the choice is secret and the card goes to a hidden
zone, the second player cannot see the first card: this is how simultaneous secret choices are
modelled. Examples: [higher-lower.yaml](https://tabletwin.com/models/docs/examples/higher-lower.yaml),
[kuhn-poker.yaml](https://tabletwin.com/models/games/kuhn-poker.yaml).

### Cards with their own state (instances)

A card, monster or unit that changes during play (damage, exhausted, tokens) is an *instance*: a
dict in a zone with a unique `uid`. Make instances with `create` from a content item (`n: 3` for
three copies, `with_x` for initial state), or let a zone's `init_x` produce dicts. Change a
property with `set`/`add` and `of_x` + `attr`; read it as `target['damage']`.

```yaml
content:
  monsters:
    goblin: {id: goblin, label: Goblin, hp: 3, status: verified}
actions:
  spawn:
    effect:
      - {op: create, what_x: "content['monsters']['goblin']", to: board, n: 2, with_x: "{'damage': 0}"}
  hit:
    effect:
      - {op: let, bind: target, value_x: "board[0]"}
      - {op: add, of_x: target, attr: damage, amount: 2}
      - {op: set, of_x: target, attr: exhausted, value: true}
      - op: if
        cond_x: "target['damage'] >= target['hp']"
        then: [{op: move, what_x: target, from: board, to: discard}]
```

An instance keeps its properties when it moves. If it can come back (reshuffled, redrawn), reset
damage and tokens where it leaves play. Example:
[stack-game.yaml](https://tabletwin.com/models/tests/stack-game.yaml).

### Looking at, ordering and searching a deck

- "Look at the top 3": `look` with `n: 3` and `bind`. Their order is fixed afterwards.
- "Put it on the bottom" or "on top": `move`, `create` or `draw` with `position: bottom` or `top`.
- "Shuffle the deck": `shuffle` with `zone`; what was known on top or at the bottom is unknown
  again.
- "Search your deck for a card of type X": `search` with `filter_x` (the candidate is `it`, or the
  name in `as`), then `move` and usually `shuffle`.

```yaml
actions:
  peek:
    effect:
      - {op: look, from: deck, n: 2, bind: top}
      - {op: move, what_x: "top[0]", from: deck, to: deck, position: bottom}
  dig:
    effect:
      - {op: search, from: deck, filter_x: "it % 2 == 0", bind: found}
      - {op: move, what_x: found, from: deck, to: hand}
      - {op: shuffle, zone: deck}
```

If nothing matches, `search` binds `None` (or `[]` with `min`/`max`), and the `move` after it
fails; guard it with an `if`. Example:
[stack-game.yaml](https://tabletwin.com/models/tests/stack-game.yaml).

### Dice

`roll` with `die: [1, 2, 3, 4, 5, 6]` (or `die_x`) and `bind`. Every face is equally likely; an
uneven die repeats faces (`[0, 1, 1, 2]`). Faces may be text (a coin: `[heads, tails]`) or
instances (`die_x: "hand[p]"` picks a random card from a hand). Several dice: several `roll` ops,
or a `for_each`/`repeat`.

### Score and winner

| Rulebook | `end` |
|---|---|
| One winner | `result: win`, `who_x: <index>`; everyone else gets −1 |
| One loser | `result: loss`, `who_x: <index>`; everyone else gets +1 |
| Points | `result: score`, `returns_x: "[...]"` with one number per player |
| Draw | `result: draw` |
| Solo game | `num_players: 1`, `mode: competitive`, `result: score` with `returns_x: "[score]"` |
| Cooperative win / loss | `mode: cooperative`, `result: win` or `loss` without `who_x`: everyone +1 or −1 |
| A player drops out | `result: eliminate` with `who_x` (index or list); the game goes on |

With two players `[1, -1]`, `[-1, 1]` or `[0, 0]` is the usual score; for a zero-sum game the
returns add up to 0. Returns may be larger than 1 (chips, points). `utility: {min, max}` documents
the range of returns; the engine does not use it. Examples:
[auction.yaml](https://tabletwin.com/models/docs/examples/auction.yaml),
[coop-clock.yaml](https://tabletwin.com/models/tests/coop-clock.yaml).

### Eliminating players

`result: eliminate` adds players to `eliminated`; checking continues with the next condition, so
put the "last one standing" condition after it. Loops do not skip eliminated players by
themselves: filter them in `over_x`. A list in `over_x` is computed at the start of the round, so a
player eliminated halfway still gets that round's turn unless `pre_x` forbids it.

```yaml
flow:
  id: game
  kind: loop
  over_x: "[q for q in players if q not in eliminated]"
  as: p
  children:
    - {id: turn, kind: step, actor_x: p, do: [poke_left, poke_right]}
end:
  - {id: out, if_x: "True", result: eliminate, who_x: "[q for q in players if hp[q] <= 0]"}
  - {id: last, if_x: "len(eliminated) == n_players - 1", result: win, who_x: "[q for q in players if q not in eliminated][0]"}
```

Priority windows do skip eliminated players.

### Reacting to an event (triggers)

An `emit` op marks a moment ("damage"). Triggers with the same `event` react to it. `timing`:
`when`/`before` run before the `impact`, `instead` replaces the impact (the first one that fires),
`after` runs afterwards (default). `optional: true` makes it a `use`/`skip` choice for
`controller_x`.

```yaml
actions:
  hit:
    effect:
      - {op: let, bind: target, value: 1}
      - op: emit
        event: damage
        impact:
          - {op: add, var: "hp[target]", amount: -2}
triggers:
  - id: block
    event: damage
    timing: instead
    if_x: "shield[target] > 0"
    do:
      - {op: add, var: "shield[target]", amount: -1}
  - id: cry_out
    event: damage
    optional: true
    controller_x: target
    do:
      - {op: add, var: "rage[target]", amount: 1}
```

A trigger reads the local names of the place that emits (`target` here). If another place emits
the same event without binding that name, `if_x` fails with a `NameError`: make every emitter bind
the same names, or keep the value in a variable. Example:
[triggers.yaml](https://tabletwin.com/models/tests/triggers.yaml).

### Playing in between (priority windows)

`window: <name>` on a flow node opens a window after that node. Actions with a `speed` that the
window `allows` can be played there; players get priority in turn and play a legal fast action or
pass. The protocol (`priority.protocol`) decides who starts (`first_x`), when the window closes
and how effects resolve: `immediate` or `lifo_stack` (on a stack; last played resolves first).
`cost` happens when played, `effect` when it resolves; `cancel` cancels an item on the stack, and
`stack` can be read in the window (in `pre_x`, `cost` and `effect`).

```yaml
state:
  vars:
    hp: {scope: player, init: 3}
    counters: {scope: player, init: 1}
priority:
  protocol:
    first_x: "0"
    closes: all_pass_in_succession
    resolution: lifo_stack
    after_action: next
  windows:
    response: {label: Responses, allows: [fast]}
flow:
  id: game
  kind: loop
  children:
    - {id: attack, kind: step, do: attack, window: response}
actions:
  bolt:
    speed: fast
    pre_x: "len(stack) == 0"
    cost:
      - {op: let, bind: target, value_x: "1 - actor"}    # chosen when played, kept until it resolves
    effect:
      - {op: add, var: "hp[target]", 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}
      # the topmost item of another player that is not cancelled yet; cost ops see `stack`
      - {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}                   # `cancelled` becomes true only now
```

- Bind a target that is fixed when the action is played in `cost` with `let`. Names bound in `cost`
  are kept until the effect resolves, and `actor` is then the player who played it.
- A stack item's `cancelled` turns true only when the `cancel` op runs, so in the effect: when the
  counter resolves, not when it is played. Until then another counter may target the same item.
  Cancelling it twice does no harm.
- After an item resolves, priority goes back to the player `first_x` gave when the window opened
  (it is not evaluated again).
- Give fast actions a cost or a limit (`counters` here). Free responses that are always legal can
  keep a window open forever (`window does not close` after 10,000 turns).
- The engine checks the end after each effect that resolves, not when the window closes. If the
  result depends on "the window is over", put `end_event: <event>` on the node with the window and
  a trigger on that event that sets a flag: the end check right after the event sees it. A step
  after the window that sets the flag works too.
- A node that only opens a window: `{id: combat, kind: sequence, children: [], window: combat}`.
- `pass` is offered by the engine; do not name a fast action `pass`.

Examples: [ambush.yaml](https://tabletwin.com/models/docs/examples/ambush.yaml),
[spell-duel.yaml](https://tabletwin.com/models/tests/spell-duel.yaml).

### Choosing a subset

`choose` with `min` and `max`: the engine asks for one element at a time, in the fixed order of the
options, plus `done` once `min` is reached, and binds the list of chosen options.

```yaml
- {op: choose, options: [a, b, c, d], min: 1, max: 2, bind: cards}
```

In the player the first question is at `action pick · 1` with the options `a`, `b`, `c`, `d`; after
picking `b`, the next is at `action pick · 2` with `c`, `d`, `done`: only later options remain.
Example: [subset.yaml](https://tabletwin.com/models/tests/subset.yaml).

### Values changed by effects (modifiers)

Declare `modifiers` with `stat`, `layer` and `change_x` (where `value` is the value so far) and read
the value with `modified('stat', base)`. Layers go from low to high. Temporary: the op `modify` with
`until: <event>`. On a content item: active while its `active_x` holds.

```yaml
modifiers:
  - {id: frenzy, stat: attack, layer: 1, if_x: "rage[attacker] > 2", change_x: "value + 2"}
actions:
  strike:
    effect:
      - {op: modify, stat: attack, change_x: "value + 1", until: round_end}
      - {op: add, var: "hp[1 - actor]", amount_x: "-modified('attack', 1, attacker=actor)"}
```

Keyword arguments of `modified()` (`attacker=actor`) are local names in `if_x` and `change_x`.
Example: [modifiers.yaml](https://tabletwin.com/models/tests/modifiers.yaml).

### Until the end of the phase or round

Put `end_event: phase_end` on every phase node (and `end_event: [phase_end, round_end]` on the
last one, or `end_event: round_end` on the round node), and use `until: phase_end` on `modify`. The
engine emits the event when the node is done, after its window. Triggers on that event do what
happens "at the end of the phase".

```yaml
- id: round
  kind: sequence
  end_event: round_end
  children:
    - {id: planning, kind: step, do: plan, end_event: phase_end}
    - {id: combat, kind: step, do: fight, end_event: phase_end}
```

### Permission and costs that depend on cards

"You may only travel here if ...": read a yes/no value with `modified('can_travel', True, card=c)`
in `pre_x` and `options_x`, and let the card set it to `False` with a modifier. The same pattern fits
"cannot draw cards" (`modified('draw_count', n)`) and "does not ready" (`modified('readies', True,
card=c)`). A cost that the card adds is a trigger on an event emitted just before the act
(`traveling_to`).

```yaml
modifiers:
  - {id: no_heal_on_odd_rounds, stat: can_heal, if_x: "round % 2 == 1", change_x: "False"}
actions:
  heal:
    pre_x: "modified('can_heal', True)"
    effect: [{op: add, var: "hp[actor]", amount: 1}]
```

### Campaign: scenarios with lasting state

- What lasts between scenarios: `persist: campaign` on the variable or zone.
- Start of each scenario: the op `reset`; everything without `persist` gets its initial value again
  (`init_x` is recomputed and may read lasting variables).
- A scenario is a `subflow`; an end condition with `scope: <name>` ends only that subflow and sets
  `results['<name>']` to `win`, `loss` or `draw`. `<name>` is the key in `flows`.
- `results['<name>']` is cleared when that subflow starts again. Read it right after the subflow
  with `results.get('<name>')`: a reward step after the subflow node pays out for a won scenario.
- The scenario's inner loop needs no `until_x`: the scoped end conditions end it.

A complete model (one player; `swing` gains gold and costs health, `guard` heals):

```yaml
game: guide-campaign
label: Guide campaign
num_players: 1
state:
  vars:
    scenario_no: {init: 1, persist: campaign}
    wins: {init: 0, persist: campaign}
    gold: {init: 0, persist: campaign}
    hp: {init: 2}
flows:
  scenario:
    id: scenario
    kind: sequence
    children:
      - {id: setup, kind: step, do: setup}
      - id: fight
        kind: loop                                        # no until_x: the scoped end conditions stop it
        children: [{id: swing, kind: step, actor_x: "0", do: [swing, guard]}]
flow:
  id: campaign
  kind: loop
  until_x: "scenario_no > 3"
  children:
    - {id: play_scenario, kind: subflow, ref: scenario}
    - {id: tally, kind: step, do: tally}
actions:
  setup:
    effect: [{op: reset}]                                 # hp back to 2; gold, wins, scenario_no stay
  swing:
    effect:
      - {op: roll, die: [0, 1, 2], bind: loot}
      - {op: add, var: gold, amount_x: loot}
      - {op: add, var: hp, amount: -1}
  guard:
    pre_x: "hp < 2"
    effect: [{op: add, var: hp, amount: 1}]
  tally:                                                  # the reward step after each scenario
    effect:
      - op: if
        cond: the scenario was won
        cond_x: "results.get('scenario') == 'win'"
        then:
          - {op: add, var: wins, amount: 1}
          - {op: add, var: gold, amount: 1}               # reward: 1 extra gold
      - {op: add, var: scenario_no, amount: 1}
end:
  - {id: scenario_won, scope: scenario, if_x: "gold >= 3 * scenario_no", result: win}
  - {id: scenario_lost, scope: scenario, if_x: "hp <= 0", result: loss}
  - {id: campaign_over, if_x: "scenario_no > 3", result: score, returns_x: "[wins]"}
```

The order of events in one scenario: `setup` resets `hp`; after each `swing` or `guard` the end
check runs; when `gold >= 3 * scenario_no` the condition `scenario_won` sets
`results['scenario'] = 'win'` and ends the subflow; the campaign loop continues with `tally`,
which rewards the win; the end check after `tally` ends the game once `scenario_no > 3`.

Examples: [expedition.yaml](https://tabletwin.com/models/docs/examples/expedition.yaml),
[campaign.yaml](https://tabletwin.com/models/tests/campaign.yaml).

### Skill or physics (outcome models)

What is not a rule but skill (a pool shot, a dexterity flick) becomes an outcome model: a
probability distribution over outcomes with `p_x`, which may depend on skill and difficulty. The op
`sample` draws an outcome and runs its effect.

```yaml
constants: {SKILL: 0.6}
outcomes:
  shot:
    skill: true
    derived_x:
      p_pot: "SKILL * (1 - difficulty)"
    outcomes:
      - {id: potted, p: "p_pot", p_x: "p_pot", effect: [{op: add, var: score, amount: 1}]}
      - {id: missed, p: "the rest", p_x: "1 - p_pot"}
actions:
  shoot:
    effect:
      - {op: let, bind: difficulty, value: 0.25}
      - {op: sample, model: shot}
```

Each outcome needs `p` (text, for the validator and the diagram) and `p_x`. Example:
[eightball.yaml](https://tabletwin.com/models/games/eightball.yaml).

### Cards and faces as data (content)

What is printed on cards belongs in `content`: catalogues of items (`id`, `label`, `status`,
`text`, and behaviour). `exec` with `of_x` runs an item's `effect` (`self` is the item).

| Card text | On the item |
|---|---|
| "When revealed", "Forced: after X", "Response: after X" | `triggers` on event X; `optional: true` + `controller_x` for "may" |
| "+2 attack per token", "while active: no cards are drawn" | `modifiers` with `stat` and `if_x` |
| "Action: ..." (played by a player) | `abilities`: an action with `speed`, playable in windows |
| "Sentinel", "Ranged" | `keywords`; the rules ask for them (`'Ranged' in c.get('keywords', '')`) |

```yaml
content:
  cards:
    spider:
      id: spider
      label: Forest Spider
      status: verified
      text: "When revealed: each player loses 1 hp."
      triggers:
        - event: revealed
          if_x: "card['id'] == self['id']"
          do:
            - {op: for_each, items_x: players, as: q, body: [{op: add, var: "hp[q]", amount: -1}]}
    potion:
      id: potion
      label: Potion
      status: verified
      text: "Action: gain 1 hp."
      abilities:
        - {id: drink, speed: action, pre_x: "hp[actor] < 3", effect: [{op: add, var: "hp[actor]", amount: 1}]}
actions:
  reveal:
    effect:
      - {op: create, what_x: "content['cards']['spider']", to: staging, bind: card}
      - {op: emit, event: revealed}
```

- A trigger belongs to the card *type*, not to one copy: select the copy through the local names of
  the event (`card` here). Make sure every place that emits the event binds the same names.
- "At this moment" ("each character currently questing gets −1"): record the `uid`s and pass them
  on, `{op: let, bind: ids, value_x: "[c['uid'] for c in in_play if c.get('questing')]"}` and then
  `modify` with `if_x: "card['uid'] in ids"`. Otherwise cards that arrive later are affected too.
- An ability sees its card as `self`; find the copy in play with
  `[c for c in allies[actor] if c['id'] == self['id']]`.
- `status`: `verified`, `uncertain` or `missing`. `exec` on an item with `status: missing` or
  without `effect` is `NotExecutable`.

Examples, as rules structure in the diagram (their content and source are not published):
[The Lord of the Rings LCG](https://tabletwin.com/diagram/#lotr-lcg) (all card texts as
triggers, modifiers and abilities) and [Too Many Bones](https://tabletwin.com/diagram/#too-many-bones)
(content and open questions at diagram level). Small published examples:
[stack-game.yaml](https://tabletwin.com/models/tests/stack-game.yaml) and
[triggers.yaml](https://tabletwin.com/models/tests/triggers.yaml).

### Rule procedures that cards call

When the same procedure recurs ("reveal an encounter card", "whoever has enough damage is
destroyed", "a full location is explored"), make it a trigger on an event of its own and emit that
event wherever it is needed. Cards then use the same procedure: a travel cost "reveal the top card"
is `{op: emit, event: reveal_card}`, damage from a card ends with `{op: emit, event: damage}`. No
card repeats the rules.

## Pitfalls

1. **Text without `_x` does nothing in the engine.** `pre: "has money"` is for people; without
   `pre_x` the engine stops with `NotExecutable`. For a loop's `until`/`over` and an end
   condition's `if` it is worse: the engine silently ignores them. The validator lists them under
   "What the engine still misses".
2. **Literal or expression.** `value: open` sets the text `'open'`; `value_x: open` reads the
   variable `open`. `amount` must be a number or `amount_x`. In every other field text is a
   description: write a literal string as a quoted expression, `what_x: "'sword'"`.
3. **Players count from 0.** `players` is `[0, 1, ...]`.
4. **End conditions are checked only after a step that ran, after each resolved effect in a
   window, and after each `end_event`**, in the order of `end`; the first that holds decides. Not
   after a cost, not when a window closes, not in loops. A step with no legal action is skipped
   without a check.
5. **A flow that runs out without an end condition** ends with 0 for everyone and reason `flow
   finished without an end condition`. Almost always a bug.
6. **Drawing from a zone that is not shuffled always gives the first element.** For randomness set
   `shuffled: true`. The op `shuffle` only forgets known positions; the randomness is in drawing.
7. **`move` removes the first equal element** and fails if it is not there.
8. **Local names** (`bind` of `choose`, `roll`, `let`) live in the effect of that action, including
   `then`/`else`. A `for_each` body, an `emit` impact, triggers, outcome effects and `exec` work on a
   copy: what they bind is gone afterwards. Use a variable to remember something.
9. **YAML.** Quote expressions that contain `:`, `#`, `[`, `{` or start with a number. Models are
   read as YAML 1.1: `yes`, `no`, `on` and `off` are booleans. As a key (`on:`) that is an error
   (`key true is not text; put it in quotes`); triggers use `event`, a branch uses `switch` as text.
   As a value, quote them when you mean text (`options: ["yes", "no"]`).
10. **One option is not a choice.** The engine takes a single option without asking. That is
    intended, but it changes the number of decisions.
11. **Competitive games need `who_x`** on `win` and `loss`; cooperative games do not. `score` needs
    `returns_x` with exactly one number per player.
12. **A window without fast actions** (no action with a `speed` the window allows) closes at once
    without asking anyone.
13. **Who chooses.** A `choose` with more than one option needs a chooser: `actor_x` on the op, or
    `actor_x` on the step. That holds even if the step has one action.
14. **Names are visible.** Action ids are the options players and agents see. Choose readable,
    stable ids.
15. **A trigger reads the local names of whoever emits.** If another place emits the same event
    without that name, `if_x` fails with a `NameError`.
16. **Instances keep their state.** An instance keeps its properties in a discard pile. If it comes
    back, it still has damage or tokens; reset them where it leaves play.
17. **Text without behaviour counts as a gap.** A content item with `text` and no triggers,
    modifiers, abilities or used keywords is not executable.
18. **The validator does not know your names.** A misspelled variable in an expression passes
    validation and fails as a `NameError` in the test games or in the player. Always read the test
    games and follow a few games.

## Checking a model

Do this after every step of the workflow, not only at the end. Everything runs in the browser;
nothing is uploaded.

| Tool | Use it for |
|---|---|
| [Validator](https://tabletwin.com/validate/) | Paste the model and press Check: errors and warnings (structure, references, syntax, unknown keys), `% executable`, "What the engine still misses", 20 random test games and "The flow as a tree" |
| [Player](https://tabletwin.com/play/) | "Open in the player" in the validator: one game, question by question. Change "Game (seed)" or press "Random game" for other games; play a seat yourself to test one line of play |
| [Diagram](https://tabletwin.com/diagram/) | "Draw your own model": the flow as a track diagram, with markers for choice, chance, hidden information and windows per step |

What to look for:

- The validator shows no errors. Every warning is either a deliberate choice or a bug.
- "Test games: 20 of 20 finished", with the end reasons you expect and a plausible number of
  requests per game. Nothing under "Where games stopped", and no games that did not end within
  5000 requests.
- Every action occurs in a few games; an action that never occurs usually has a wrong `pre_x`.
  Follow games in the player with several seeds.
- Every trigger fires. After each question the player shows the events and the triggers they set
  off; a trigger whose event never appears, or whose effect never shows in the state, usually has a
  wrong `if_x` or listens to an event nobody emits (the validator warns about the latter).
- Test card texts one by one: set up the situation (a small test model whose setup creates it),
  emit the event and check in the player that exactly what the text says happens.
- Known facts match: if the rulebook or the community gives game length or difficulty, compare.
  Win rates over thousands of games cannot be measured on the site yet.
- Put the flow ("The flow as a tree", or the diagram) next to the rulebook: does every heading of
  the course of play appear?

## Checklist before you call a model done

- [ ] `game`, `label`, `num_players`, `mode`, `summary` are set.
- [ ] Every rule in the rulebook appears in the flow, an action, a trigger, a modifier, content, or
      an open question.
- [ ] The validator shows no errors; every warning is understood.
- [ ] The validator shows 100% executable, or only gaps you list in `open_questions` or a `note`.
- [ ] 20 of 20 test games finish, with the reasons you expect.
- [ ] A few games followed in the player, with different seeds, read like the rulebook.
- [ ] Every ambiguity is an `open_questions` entry with a status.
