Quick reference
Everything needed to write a model, on one page. Details: reference, expressions, engine; workflow and more patterns: guide. Every snippet below validates and plays: paste it into the validator 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 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) |
end | List of end conditions (below) |
State
Zone field (state.zones.<name>) | Meaning |
|---|---|
owner | global (default, one list) or player (list per player; path hand[p], unindexed is an error) |
visibility | public (default), owner, hidden: who sees what is drawn into it |
shuffled | true: drawing is a chance node (order unknown except known top/bottom) |
init_x | Initial list (owner: player: list of lists). Dicts become instances with a uid |
persist | campaign: kept by reset |
drawn_from | Hidden zone dealt from that shuffled zone (so look-ahead players do not peek) |
kind, holds, size, label, note | Descriptive |
Var field (state.vars.<name>) | Meaning |
|---|---|
scope | global (default, one value) or player (list, path score[p]; a non-list init is copied per player) |
init / init_x | Literal (null = None) / expression (sees constants, players, n_players, content, vars declared above; no zones) |
persist | campaign: kept by reset |
type, range, label, note | Descriptive |
state.derived: {name: "expr"} or {name: {text, x}}, computed on use; may use derived values
declared above.
Flow nodes
| Kind | Fields | Does |
|---|---|---|
| all | id (unique), kind, label, note, window, end_event | After the node's own work: its window opens, then each end_event is emitted with an end check after each. On a loop: once, after the whole loop |
sequence | children | Children in order. children: [] + window = a node that only opens a window |
loop | children, over_x, as, until_x, once, order (V) | Before each round: stop if until_x. Per value of over_x (evaluated each round) set var as, run children. With over_x and no until_x it repeats round after round until an end condition fires. once: true: one round. No over_x: children once per round. 10,000 rounds: error |
branch | cases: [{when_x, children}], switch (V) | First case whose when_x is true; when_x: "True" as default |
step | do (action or list), actor_x, secret, visible_to_x | Legal = actions whose pre_x holds; none: skipped (no end check); one: runs without asking; several: decision for actor_x. Then cost, effect, end check |
subflow | ref (name in flows) | Runs that flow; clears results[ref]; scoped end conditions apply |
Declare the as variable in state.vars (a validator error otherwise: it would only exist once the
loop has started, and reset would skip it).
Flow expressions see no local names: use the as variable. A step without actor_x is a system
step: fine if it never needs a decision.
Actions and ops
Action: pre_x (legal when; default always), cost (ops run when played), effect (ops),
speed (playable in windows that allow it). In pre_x, cost and effect, actor is the
acting player. Names bound in cost stay for effect. Fields accepting literals:
value amount what at n die options min max items until target of; any other text needs _x.
value text in set/let is a string literal.
| Op | Fields (f/f_x = literal or expression) |
|---|---|
set | var path, or of_x + attr; value/value_x |
add | var path, or of_x + attr; amount/amount_x (+: lists concatenate) |
let | bind, value/value_x |
place | what/what_x, to, at/at_x (replace index), position (top/bottom/random) |
create | what/what_x (dict → instance), to, n/n_x, with_x (extra state), position, bind (last) |
move | what/what_x (item or list; first equal element), from, to, position |
destroy | what/what_x, from (else first zone holding it) |
draw | from, to, n/n_x, visible_to_x (default by destination visibility), position |
shuffle | zone: forget known top/bottom of a shuffled zone |
look | from, n/n_x, bind (list), actor_x, visible_to_x |
search | from, bind, filter_x (item it or as), as, actor_x, min/max, visible_to_x |
reveal | what/what_x, visible_to_x (default everyone) |
roll | die/die_x (faces), bind, visible_to_x |
choose | options/options_x, bind, actor_x (default actor), secret, visible_to_x, min/max (subset), agent |
if | cond_x, then, else |
for_each | items/items_x, as (default it), body (copy of names) |
repeat | until/until_x (tested before each pass), body |
emit | event (literal name), impact (ops; triggers may replace it) |
sample | model (name in outcomes) |
exec | of_x (content item with effect; self = item), content (catalogue path, V) |
run | flow: run a named flow inside the effect |
window | name (none: every speed) |
cancel | target/target_x: index in stack (0 bottom, −1 top); sets cancelled now |
modify | stat, change_x (value = so far), layer, if_x, until (event) |
reset | scope (label): every var/zone without persist back to its initial value |
call | code: escape hatch to code outside the model; always NotExecutable (a gap) |
say | actor, channel, constraint, bind, agent: always NotExecutable |
Windows, triggers, content, outcomes, agents
| Key | Fields |
|---|---|
priority.protocol | first_x (default local actor, else 0; evaluated once), closes (all_pass_in_succession/single_pass_each), resolution (immediate/nested/lifo_stack), after_action (next/same) |
priority.windows.<name> | allows: [speed, ...], label, note |
triggers[] | id, event, timing (before/when/instead/after), optional + controller_x, if_x, do |
modifiers[] | stat, change_x, layer, if_x; read with modified('stat', base, k=v) |
| content item | id, label, status (verified/uncertain/missing), text, effect, triggers, modifiers, abilities (actions with speed), keywords, active_x |
outcomes.<m> | derived_x, outcomes: [{id, p, p_x, effect}] (sum 1), skill |
agents.<a> | policy_x returns an option label (options, decider in scope) |
Window run: priority from first_x, then index + 1 (eliminated skipped). A player with legal
candidates picks one or pass; otherwise passes silently. immediate: cost + effect + end check
at once. lifo_stack: cost at once, item {action, actor, cancelled} on stack; when all pass,
the top resolves (cancelled: dropped, no check; else effect with its actor and cost names, then
end check), and priority goes back to the player first_x gave when the window opened (not
re-evaluated). All pass with an empty stack: the window closes, no end check. cancel in an
effect marks its target only when that effect resolves; cancelling twice is harmless. Give each
fast action a pre_x or cost that runs out: an always-legal one can keep the window open
forever (validator warning always playable in window).
End conditions
| Field | Meaning |
|---|---|
id | Reason reported |
if_x | Condition (without it: never fires) |
result | win, loss, draw, score, eliminate, continue |
who_x | Competitive win/loss: one index (a list is an error; several winners: score). eliminate: index or list |
returns_x | score: one number per player |
scope | Name in flows: only while that subflow runs |
| Result | Competitive | Cooperative |
|---|---|---|
win | +1 for who_x, −1 all others | +1 all |
loss | −1 for who_x, +1 all others | −1 all |
draw | 0 all | 0 all |
score | returns_x | returns_x |
eliminate | add who_x to eliminated; play and checking go on | same |
scoped win/loss/draw | results[scope] = 'win'/'loss'/'draw', the subflow ends, its caller continues | same |
Checked in list order; the first that fires ends the game (eliminate and continue never stop
the scan). Other results with a scope act unscoped while the subflow runs (a scoped score ends
the game). When: after each step that ran an action; in a window after each played action
(immediate) or each resolved, uncancelled item (lifo_stack); after each end_event. Never
mid-effect, after a cost, when a window closes, or at loop boundaries. Flow ran out: 0 for all.
Names in scope
| Name | Where |
|---|---|
constants, vars (incl. as loop vars), zones, derived, players, n_players, eliminated, results ({flow: 'win'/'loss'/'draw'}), content, modified, functions | Every expression (var init_x: no zones, derived or modified) |
actor | Action pre_x/cost/effect (step actor; in a window who has priority; on resolution who played it); triggers fired from there. Not in flow or end |
bound names (bind, let, for_each as) | Later ops of the same action run; cost → effect. for_each, emit, exec, sample get a copy |
it / as | search filter_x; for_each body |
self | Abilities, exec effects, content triggers, modifiers, active_x |
stack, window | Window actions' pre_x, cost (stack as played on) and effect (stack without the item) |
event | Trigger if_x, controller_x, do (plus the emitter's names) |
value | Modifier change_x (plus modified() keyword args) |
options, decider | Agent policy_x |
Built-ins: len any all min max sum range abs round sorted list tuple set dict enumerate zip int float str bool sigmoid exp sqrt log; methods .get .items .keys .values (dict), .count .index
(list, tuple, text). No assignment, lambda, _names, |, f-strings, comments.
Places and option labels
Place (where; tactics at:) | From |
|---|---|
step <id> | Choice between a step's actions (options: action names) |
action <name> / action <name> (cost) | Ops in an action's effect / cost |
window <name> (window None for window: true) | Priority: legal fast actions + pass |
trigger <id> | Optional trigger (use/skip) and its ops |
content <id> | Ops of an exec'd item |
<place> · k, <place> · <model>, <place> · <event> | k-th pick of a subset (done ends it), a sample, an emit impact |
Other options are labelled by value: text as is, a dict by its label, else id, else uid.
Checking your model
Everything runs in the browser; nothing is uploaded.
| Tool | What you get |
|---|---|
| Validator | Paste the model (and optionally a view), 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 | "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 | "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.
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.
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').
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).
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]"}