Docs: Quick reference

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

KeyMeaning
game, labelId (lower-case, hyphens) and display name. Required by the schema
num_playersDefault 2
modecompetitive (default) or cooperative
players, summary, demonstrates, scope, description, utilityFor people only
constants{NAME: value}; read-only
boards, functionsBoard geometry; own pure functions {name: {params: [a, b?], let: {...}, x: expr}}
statezones, vars, derived (below); entity_types is descriptive
flow, flowsMain flow (one node, required); named subflows {name: node}
actions{name: {pre_x, cost, effect, speed}}
triggers, modifiers, priority, outcomes, agents, content, codeReactions, value changes, windows, skill, policies, cards as data, escape hatch
open_questions[{id, question, about, status, answer, source}] (§22)
endList of end conditions (below)

State

Zone field (state.zones.<name>)Meaning
ownerglobal (default, one list) or player (list per player; path hand[p], unindexed is an error)
visibilitypublic (default), owner, hidden: who sees what is drawn into it
shuffledtrue: drawing is a chance node (order unknown except known top/bottom)
init_xInitial list (owner: player: list of lists). Dicts become instances with a uid
persistcampaign: kept by reset
drawn_fromHidden zone dealt from that shuffled zone (so look-ahead players do not peek)
kind, holds, size, label, noteDescriptive
Var field (state.vars.<name>)Meaning
scopeglobal (default, one value) or player (list, path score[p]; a non-list init is copied per player)
init / init_xLiteral (null = None) / expression (sees constants, players, n_players, content, vars declared above; no zones)
persistcampaign: kept by reset
type, range, label, noteDescriptive

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

Flow nodes

KindFieldsDoes
allid (unique), kind, label, note, window, end_eventAfter 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
sequencechildrenChildren in order. children: [] + window = a node that only opens a window
loopchildren, 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
branchcases: [{when_x, children}], switch (V)First case whose when_x is true; when_x: "True" as default
stepdo (action or list), actor_x, secret, visible_to_xLegal = actions whose pre_x holds; none: skipped (no end check); one: runs without asking; several: decision for actor_x. Then cost, effect, end check
subflowref (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.

OpFields (f/f_x = literal or expression)
setvar path, or of_x + attr; value/value_x
addvar path, or of_x + attr; amount/amount_x (+: lists concatenate)
letbind, value/value_x
placewhat/what_x, to, at/at_x (replace index), position (top/bottom/random)
createwhat/what_x (dict → instance), to, n/n_x, with_x (extra state), position, bind (last)
movewhat/what_x (item or list; first equal element), from, to, position
destroywhat/what_x, from (else first zone holding it)
drawfrom, to, n/n_x, visible_to_x (default by destination visibility), position
shufflezone: forget known top/bottom of a shuffled zone
lookfrom, n/n_x, bind (list), actor_x, visible_to_x
searchfrom, bind, filter_x (item it or as), as, actor_x, min/max, visible_to_x
revealwhat/what_x, visible_to_x (default everyone)
rolldie/die_x (faces), bind, visible_to_x
chooseoptions/options_x, bind, actor_x (default actor), secret, visible_to_x, min/max (subset), agent
ifcond_x, then, else
for_eachitems/items_x, as (default it), body (copy of names)
repeatuntil/until_x (tested before each pass), body
emitevent (literal name), impact (ops; triggers may replace it)
samplemodel (name in outcomes)
execof_x (content item with effect; self = item), content (catalogue path, V)
runflow: run a named flow inside the effect
windowname (none: every speed)
canceltarget/target_x: index in stack (0 bottom, −1 top); sets cancelled now
modifystat, change_x (value = so far), layer, if_x, until (event)
resetscope (label): every var/zone without persist back to its initial value
callcode: escape hatch to code outside the model; always NotExecutable (a gap)
sayactor, channel, constraint, bind, agent: always NotExecutable

Windows, triggers, content, outcomes, agents

KeyFields
priority.protocolfirst_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 itemid, 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

FieldMeaning
idReason reported
if_xCondition (without it: never fires)
resultwin, loss, draw, score, eliminate, continue
who_xCompetitive win/loss: one index (a list is an error; several winners: score). eliminate: index or list
returns_xscore: one number per player
scopeName in flows: only while that subflow runs
ResultCompetitiveCooperative
win+1 for who_x, −1 all others+1 all
loss−1 for who_x, +1 all others−1 all
draw0 all0 all
scorereturns_xreturns_x
eliminateadd who_x to eliminated; play and checking go onsame
scoped win/loss/drawresults[scope] = 'win'/'loss'/'draw', the subflow ends, its caller continuessame

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

NameWhere
constants, vars (incl. as loop vars), zones, derived, players, n_players, eliminated, results ({flow: 'win'/'loss'/'draw'}), content, modified, functionsEvery expression (var init_x: no zones, derived or modified)
actorAction 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 / assearch filter_x; for_each body
selfAbilities, exec effects, content triggers, modifiers, active_x
stack, windowWindow actions' pre_x, cost (stack as played on) and effect (stack without the item)
eventTrigger if_x, controller_x, do (plus the emitter's names)
valueModifier change_x (plus modified() keyword args)
options, deciderAgent 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.

ToolWhat you get
ValidatorPaste 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]"}

This page as Markdown