How the engine runs a model
The engine runs a model as a sequence of requests. At each request whoever plays (a person in the player, or an agent) answers with one of the options, or chance picks an outcome. The same engine runs the validator's test games and the player, in your browser. This page describes exactly what the engine does with each part of a model, who sees what, how executability is measured and what the validator reports. The keys themselves are listed in the reference; the expression language is in expressions.
Requests
| Request | What it carries | The answer |
|---|---|---|
| Decision | The deciding player, the options, the place (where), and who sees the answer | One of the options |
| Chance | The possible outcomes with their probabilities, the place, and who sees the outcome | One outcome, drawn with those probabilities |
| End | The payoff per player (one number each) and the reason | Nothing: the game is over |
- Who sees the answer is either everyone, or a list of players (an empty list: nobody).
- A decision inside a subset choice (
choosewithmin/max) also knows the item behind each option (nothing fordone), all options of the choice, and what has been picked so far. Tactics read these asopts,poolandchosen. - The reason is the
idof the end condition that ended the game, orflow finished without an end condition. - An option is shown by its label: a dict shows its
label, else itsid, else itsuid, else?. Two instances of the same card therefore look the same to a player.
Places (where)
Every request names the place it comes from. Tactics match rules on these names (at:), and the
player shows the place of every question.
| Place | Where it comes from |
|---|---|
step <id> | Choosing between the legal actions of a step |
action <name> | An op in the effect of action <name> (also an ability on a content item) |
action <name> (cost) | An op in the cost of that action |
trigger <id> | An optional trigger (use / skip), or an op in a trigger's do. A trigger on a content item without its own id uses the item's id |
window <name> | Priority in window <name>: the playable fast actions plus pass. A window opened with window: true (no name) is window None |
content <id> | An op in the effect of a content item run by exec |
Suffixes: a subset choice adds · n (the n-th element being chosen, from 1); an op in the
impact of an emit gets · <event> after the place of the emit; a sample chance adds
· <outcome model>. The ops if, for_each, repeat and the effect of an outcome keep the
place of their parent. Two more names only appear in error messages: end <node id> (emitting an
end_event) and <place> · window (a window op). Decisions inside a window opened by a window
op are still window <name>, and the actions played there are action <name>.
Start of a game
- Constants, as written in the model.
- Variables, in declaration order.
init_xmay read constants,content,players,n_players, board and own functions and the variables declared before it (not zones, not derived values).initis copied as is (nullisNone). Withscope: playera value that is not a list is repeated for every player; a list is taken as the per-player list. - Zones, in declaration order.
init_xgives the contents (forowner: player: a list of lists) and may read every variable. Withoutinit_xa zone is empty (per player: one empty list each). Every dict in the initial contents becomes an instance with its ownuid.
The site always runs a model with the values written in it. To try another value, change the constant in the model and check it again.
Instances. create (and a zone's init_x) turn a dict into an instance: a fresh dict with a
unique uid. A dict that is model data (from content or constants) is not copied whole: its
atomic fields (numbers, text, booleans, null) are copied and the source dict itself is kept under
the key origin (so nested fields stay readable as x['origin']['effect']). Writing an attribute of
model data is an error (... is model data; make an instance with create first).
Running the flow
The engine walks the flow tree depth first.
| Node | What happens |
|---|---|
sequence | Children in order |
loop | Before every round: if until_x is true, the loop ends. With over_x: the list is evaluated at the start of the round; for each value the variable named in as is set and all children run. Without over_x: the children run once per round. With once: true the loop stops after one round. After 10,000 rounds: ExprError: flow <id>: loop does not end |
branch | The children of the first case whose when_x is true; none true: nothing. A case without when_x is NotExecutable |
subflow | Clears results[<ref>], then runs flow flows.<ref> as a scope (see end conditions with scope) |
step | See below |
After a node has finished: if it has window, that priority window opens; then, if it has
end_event, each listed event is emitted in order, with an end check after each. On a loop both
happen once, after the whole loop, not per round.
until_x is only tested at the start of a round, never halfway. over_x is not re-evaluated
during a round: a player who is eliminated halfway still gets a turn in that round unless an action
condition prevents it.
A step
- Evaluate
actor_xif present (otherwise there is no actor). - The legal actions are those in
dowhosepre_xis true, withactorin scope (andselffor an ability). An action withprebut nopre_xisNotExecutable. - No legal action: the step is skipped. No end check.
- One legal action: it is taken without asking. The player lists it under "automatic", with what happened after the previous question.
- Several: a decision for
actoratstep <id>(no actor:NotExecutable: step <id>: choice between actions without actor_x). Withsecret: trueonly the actor sees the choice; withvisible_to_xthe listed players. - Run the action's
cost, then itseffect, with local names{actor}. - End check.
Effects and ops
An effect is a list of ops run top to bottom. Local names (actor, names bound by choose, roll,
let, look, search, create) live in one dict per action run: cost and effect share it,
and so do if branches and repeat bodies. Four places get a copy, so names bound inside are
gone afterwards: a for_each body (per item), the impact and the triggers of an emit, the effect
of an outcome model, and the effect of an exec. Use a variable to remember something across
those boundaries.
Which ops ask something:
| Op | Request |
|---|---|
choose | A decision when there are 2+ options. One option: taken without asking. No options: ExprError (except a subset choice, which then binds [] when min is 0) |
search | A decision among the matches (via choose; secret unless visible_to_x) |
roll | A chance, uniform over the faces |
sample | A chance with the probabilities p_x of the outcome model (must add up to 1 within 1e-6) |
draw from a shuffled zone | A chance, uniform over the cards whose position is not known, unless the top card is known or only one unknown card is left |
look at a shuffled zone | A chance per looked-at card whose position is not yet known |
window | The decisions of the priority window |
emit | use / skip decisions for optional triggers, plus whatever the triggers ask |
Fields: text or expression
For most fields the engine reads <field>_x (an expression) if present, else <field> if it is a
literal that is not text (number, list, bool). Text without _x is a description for people:
the engine stops with NotExecutable: <place>: field '<field>' is text without <field>_x. The
exception is value on set and let, where text is a literal string (value: open sets
'open'). A required field that is missing gives NotExecutable: <place>: field '<field>' is missing.
The engine reads only pre_x, cond_x, if_x (triggers, modifiers, end conditions), when_x,
until_x, over_x, actor_x, who_x, returns_x, first_x, controller_x, change_x,
active_x, filter_x, visible_to_x, with_x, of_x (exec) and p_x as expressions; their
text twins are documentation. A trigger if, a branch case when, an action pre, an if op
cond, exec of and protocol.first without their _x are NotExecutable. A loop's until
or over without _x, and an end condition without if_x, are silently ignored: the loop
runs forever (until an end condition) or not per player, and the end condition never fires.
The validator's gap list ("What the engine still misses") shows all of these.
Deck order
Index 0 of a zone is the top. draw and look take from the top. A zone with shuffled: true has
an unknown order except for a known top and bottom:
- known on top: after
look, or aftermove/create/drawwithposition: top; - known at the bottom: after
move/create/drawwithposition: bottom; - everything else is unordered; a chance decides which card it is only when it is drawn or looked at;
- an item added without
positiongoes between the unknown cards (or at the end if no bottom is known); shuffle(withzone) forgets the known top and bottom. It does not reorder anything: the randomness is in drawing.
Drawing takes the known top first, then a chance over the unknown middle, and only when that is
empty the known bottom. From a zone that is not shuffled, draw always takes element 0. draw
stops early when the source is empty.
Events and triggers
emit appends the event name to the game's event list and runs:
- triggers with
timing: whenorbefore; - triggers with
timing: instead: the first one that fires replaces the impact; - the
impactops, if no instead-trigger fired; - triggers with
timing: after(the default).
Within each phase: mandatory triggers first, then optional ones; within those, model triggers in
model order, then triggers on content items. if_x is evaluated just before the trigger would
fire, so an earlier trigger can switch off a later one. A content trigger only counts while the
item's active_x is true (default: always). An optional trigger asks its controller (controller_x,
else the actor of the emitting place; neither: NotExecutable) use or skip at
trigger <id>. An instead-trigger that fires adds the event instead:<id>.
Triggers see the local names of the place that emitted, plus event (the event name) and, on a
content item, self. A trigger that reads a name another emitting place does not bind fails with
a NameError there. Triggers may emit events; more than 50 nested emits is
ExprError: ... trigger chain too deep. When the emit is done, temporary modifiers whose until
equals this event expire.
A trigger belongs to a content item (the card type), not to one instance: with two copies in
play it still fires once per event. Select the instance in if_x through the emitter's local
names, for example "card['id'] == self['id']".
Modifiers
modified('stat', base, name=value, ...) starts from base and applies every active modifier
for stat: the model's modifiers, those on content items, and temporary ones from modify.
Order: by layer (low first, default 0); within a layer, static modifiers in model order and then
temporary ones in the order they were added. For each: skip it if the item's active_x or the
modifier's if_x is false; else value becomes change_x. Scope: value, the keyword arguments
of modified(), self on a content item, and for modify the local names at the time it ran
(minus stack and self). A modifier without change_x is NotExecutable; modifiers that call
modified() on each other more than 20 deep raise ExprError: modifiers for 'x' refer to each other in a cycle. A modify without until lasts until the next campaign reset.
Priority windows
A window opens after a flow node with window: <name> (or window: true), or through the op
{op: window, name: <name>}. Candidates are all actions and content abilities with a speed that
the window's allows lists (window: true or a window op without name: every speed). The name
must exist in priority.windows.
- No candidates at all: the window closes at once, without a request.
first_xdecides who gets priority first (evaluated once). Without it: the localactor(for awindowop), else player 0.protocol.firstas text withoutfirst_xisNotExecutable.- The player with priority gets a decision at
window <name>between the legal candidates (pre_xtrue withactor= that player,window= the name,stack= the open stack) andpass. A player without a legal candidate passes automatically, without a request. Eliminated players are skipped. - Playing an action: its
costruns at once (placeaction <name> (cost)). Then:resolution: immediate(default) ornested: theeffectruns at once, followed by an end check. (nestedis accepted and currently behaves exactly likeimmediate.)resolution: lifo_stack: the action goes on the stack ({action, actor, cancelled}), eventstack:<name>. The local names of the cost are kept for the effect. Then priority goes to the next player (after_action: next, default) or stays (after_action: same).
- Closing (
closes):all_pass_in_succession(default) closes when every non-eliminated player has passed in a row; any action resets the count.single_pass_each: a player who passes is out for the rest of the window; it closes when all are out. - With
lifo_stack, when the window would close and the stack is not empty, the top item resolves instead: a cancelled item is removed with eventcancelled:<name>and no effect and no end check; otherwise eventresolves:<name>, its effect runs with the player who played it asactor, the names bound in its cost, andstackwithout the item, followed by an end check. Then the pass count is reset and the same first player gets priority again: the valuefirst_xgave when the window opened, not a new evaluation. The window closes only when everyone passes with an empty stack. cancelwithtarget_xmarks stack itemtarget(an index intostack, bottom first, solen(stack) - 1is the top) as cancelled at the moment the op runs: in aneffect, when that effect resolves, not when the action is played. Cancelling an item twice is allowed;Nonedoes nothing. Without an open window:ExprError. To fix the target when the action is played, bind its index incost(cost ops seestack); see reference § cancel.
More than 10,000 turns of priority in one window is ExprError: ... window does not close.
pass is reserved: an action with a speed may not be called pass.
End conditions
The engine checks end at these moments, and only these:
- after every step that ran an action;
- in a window, after every effect that resolves (with
immediate, after every played action); - after each event of an
end_event.
Not after a cost, not after a cancelled stack item, not when a window closes, not at the start or end of a loop round, not when a step is skipped.
The conditions are tested in list order; the first one that fires ends the game (or its subflow)
and the rest are not tested. eliminate and continue never stop the scan:
| Condition | Effect |
|---|---|
no if_x | ignored |
scope: <flow> while that subflow is not running | ignored |
scope: <flow>, result win/loss/draw, if_x true | results['<flow>'] is set to the result, event <flow>:<result>, and that subflow ends; the calling flow continues. Checking stops |
result: eliminate | Every player in who_x (an index or a list) not yet eliminated is added to eliminated (event eliminated:<p>). Checking continues with the next condition |
result: continue | ignored |
other result, if_x false | next condition |
draw | the game ends: 0 for everyone |
score | the game ends with returns_x (one number per player, else ExprError) |
win / loss, cooperative | the game ends: everyone +1 (win) or −1 (loss) |
win / loss, competitive | needs who_x (one index): that player gets +1 (win) or −1 (loss), all others the opposite. A list, tuple or set is an ExprError (it would match no player) |
A scope equal to the id of the main flow means the whole game. scope only changes the meaning
of win, loss and draw; any other result with a scope is only tested while that subflow runs
and then acts as without scope. If the main flow runs out without an end condition, the game ends
with 0 for everyone and reason flow finished without an end condition.
Engine events
Besides the names you emit, the engine adds these to the event list. The player shows them after each question: emitted events with the triggers they set off, the others under "messages".
| Event | When |
|---|---|
<event> | every emit and every end_event |
instead:<trigger id> | an instead-trigger replaced an impact |
stack:<action>, resolves:<action>, cancelled:<action> | priority window with lifo_stack |
<flow>:win, <flow>:loss, <flow>:draw | an end condition with scope ended a subflow |
eliminated:<player> | a player was eliminated |
<model>:<outcome id> | a sample from an outcome model |
exec:<content id> | an exec ran a content item |
destroy:<label> | a destroy |
reset:<scope> | a campaign reset (default scope label scenario) |
Information: who sees what
The engine keeps a list of observations. Each one has a place, a kind, a visibility and a label.
| Event | Who sees the outcome |
|---|---|
| Decision | Everyone; with secret only the chooser; with visible_to_x the listed players |
roll | Everyone, or visible_to_x |
sample | Everyone |
draw | By destination zone: public everyone, owner the owner, hidden nobody; or visible_to_x |
look | The actor (actor_x or the local actor), or visible_to_x; with no actor, everyone |
search | The searcher sees the whole zone; the choice is secret unless visible_to_x |
reveal | Everyone, or visible_to_x. Zone visibility does not change |
What player p knows is everything they observed, in order, with a blank for what they could not
see. In the player, "view as" a player shows the table and the questions as that player saw them:
hidden zones and other players' hands show card backs, and a hidden outcome says who can see it.
A player that looks ahead (see tactics) must not peek. Before it tries options, everything that player cannot know is redistributed:
- Drawing from a shuffled zone is already fair: the order is only fixed when a card is drawn.
- Hidden cards that are already on the table (shadow cards dealt face down) must be marked with
drawn_from: <source zone>on their zone. The contents of every such zone thatpcannot see are shuffled together with the unknown middle part of the source zone, and dealt back. Counts and identities (uid) stay; only which card each one is changes.
shadow: {kind: set, visibility: hidden, drawn_from: encounter_deck}
A tactic (and a policy) reads the full state, hidden zones included. Not reading what the player cannot know is up to the author.
Executability
A model can be partly executable. The validator (and the diagram) measure it in two layers:
- rules: every slot the engine needs in flow, actions, triggers, modifiers, the priority
protocol, outcome models and end conditions has an
_xor a literal. - content: every content item with behaviour or card text (
effect,triggers,modifiers,abilities,keywords,text, orstatus: missing) counts as one slot, executable when it has behaviour, its own ops and triggers have no gaps, and its status is notmissing. An item with onlytextis a gap. Keywords count as behaviour when every keyword occurs in quotes in some rule expression ('Sentinel' in c.get('keywords', '')). Acallto code of kindcontentcounts as a content gap.
executable is the share over both layers together.
The slots in the rules layer:
| Where | Slot is executable when |
|---|---|
action with pre | pre_x exists |
choose | options, min, max (those present) have _x or a non-text literal |
if | cond_x exists |
for_each, repeat | items, until likewise |
set, let | value has _x, a non-text literal, or text that is a plain name (open, face_up) |
add | amount likewise (no text) |
set/add with attr | of likewise |
place, create, move, destroy, reveal | what, and at (place) and n (create) when present |
draw, look | n when present |
roll, cancel | die, target |
modify, modifier | stat and change_x |
call | never (counted as code:<kind>) |
say | never (counted as language) |
trigger with if | if_x exists |
priority.protocol (if any action has a speed) | first_x exists, or no first |
| outcome | p_x exists |
| branch case | when_x exists |
loop with until / over | until_x / over_x exists |
step with several actions, or a choose without actor_x | actor_x on the step (or one action whose choose ops all have actor_x) |
end condition score | if_x and returns_x |
| other end condition | if_x, plus who_x for eliminate and for competitive win/loss without a subflow scope |
Below 100%, the validator lists the missing slots under "What the engine still misses", per kind
(text, code:<kind>, language, and content:... for gaps inside content items) with their
place.
The validator
The validator page shows errors (must be fixed) and warnings (check them: each is either a deliberate choice or a bug). Only a model without errors gets test games.
| Errors | Warnings |
|---|---|
mode not competitive/cooperative; persist not campaign | drawn_from source not shuffled; drawn_from on a public zone |
duplicate flow id; unknown kind or loop order; branch without cases; subflow or run flow missing; window not in priority.windows; step action missing; loop as not declared in state.vars | action never used by a step (unless it has speed or note); named flow never called |
unknown op; zone in from/to/zone not declared; code, outcome model or agent not declared; exec content catalogue missing | say without agent; exec without content or of_x |
expression syntax error in any _x, derived value or function | board never named in an expression; function never called |
invalid board; function name taken; function without x; bad params or let | trigger event never emitted (model and content); optional trigger without controller_x |
trigger timing unknown; resolution/closes unknown; action with speed named pass | speed not allowed by any window; fast action always playable in an opened window (no pre_x, no cost, nothing in its effect an end condition reads) |
end scope not a named flow; end_event not a name; competitive win/loss whose who_x is obviously a list (list/tuple/set literal, comprehension, list(), sorted(), set(), tuple()) | modifier or modify stat never read with modified('stat', ...) |
content ability without id or with a name that is already an action; dice face type not in face_types; outcome model without outcomes or an outcome without p | content item status not verified/uncertain/missing; keyword not used by any rule; ability without speed |
open question without question | open question status not open/assumed/answered |
key not allowed by the JSON schema (unknown key 'x' at actions.y; content is not checked) |
Before that, the page checks that the text is readable YAML ("The YAML cannot be read") and that it
is a model at all, a mapping with a flow ("This is not a model").
The validator does not check that the names in an expression exist; a wrong name shows up as a
NameError when the engine evaluates it. The test games find most of these (under "Where games
stopped"); follow games in the player to find the rest.
What the validator shows
| Part | Meaning |
|---|---|
| Errors and warnings | As above |
executable | Executability over rules and content together, with a bar |
| rules | Executability of the rules layer |
| content | Executability of the content layer: pct (ok/total), or - without content |
| steps | Number of step nodes in the flow |
| ops | Number of ops in actions, triggers, outcome models and content |
| code | Share of call ops among all ops |
| open questions | Number of open_questions |
| What the engine still misses | The gap list, below 100% |
| Test games | 20 games with random choices and random chance: how many finished, the average number of requests, how they ended (per reason), where games stopped (engine errors), and how many did not end within 5000 requests |
| The flow as a tree | The flow as a text tree: nodes, who acts, actions and their pre, windows, end events |
The markers of each step (choice, chance, hidden, window, ...) are shown in the diagram.
Checking and running a model on the site
Everything runs in your browser; nothing is uploaded.
| Tool | What it does |
|---|---|
| Validator | Paste a model (and optionally a view). Errors, warnings including unknown keys, executability, the gaps, the flow as a tree, and 20 random test games. "Start from" loads one of the site's examples |
| Player | After at least one test game finished, "Open in the player" adds your model to the player as an extra game ("Your model: ..."). The player plays one game question by question: the track of the rules, what was asked, what was chosen, what changed, and the table |
| Diagram | "Draw your own model": the rules as a text tree and as a track diagram, with markers per step. See diagram |
In the player:
- Players chooses who plays: one seat by hand ("You play player 0") with the others played by
the computer, everyone by hand on one screen, random players, a heuristic from the model's
agents(policy_x), or always the first option. The built-in games also offer their tactics (tactics); tactics for your own model cannot be loaded on the site yet. - Game (seed) fixes the random numbers: the same seed and the same players give the same game. "Random game" picks another seed.
- View as shows the game as one player saw it.
- The table has two tabs: Board (laid out by the view, or automatically without one) and Zones and values.
- At the end: "End:
" and the payoff per player. An engine error stops the game and shows the message.
Playing thousands of games to measure win rates, comparing rule variants or tactics, and changing constants per run are not yet available on the site.
Error messages
The engine stops with one of two kinds of error. NotExecutable: the model lacks something the engine needs (the site shows it as "Not executable: ..."). ExprError: something went wrong while running, such as an expression that fails or a loop that never ends (the site shows it as "Error: ..."). The validator lists them under "Where games stopped" with the number of test games that stopped there; the player shows the message where the game stopped.
| Message | Meaning | Fix |
|---|---|---|
NotExecutable: action X: pre is text without pre_x | The engine needs an expression | Add pre_x |
NotExecutable: <place>: field 'amount' is text without amount_x | A text field where the engine needs a value | Add the _x field, or a literal |
NotExecutable: step X: choice between actions without actor_x | Several legal actions, nobody to choose | actor_x on the step |
NotExecutable: <place>: choose without actor_x and without an actor on the step | A choose with 2+ options and no chooser | actor_x on the op or the step |
NotExecutable: end condition X: a competitive game needs who_x | A win without a winner | who_x on the condition |
ExprError: end condition X: who_x gives a list ([0, 1]); a competitive win needs one player index ... | who_x gave several players; with a list no player would match | One index; several winners: result: score with returns_x |
ExprError: <place>: window does not close | Players kept playing fast actions (10,000 priority turns in one window) | A pre_x or cost that limits the action (the validator warns: always playable in window) |
NotExecutable: trigger X: optional without controller_x or actor | Nobody to ask use/skip | controller_x on the trigger |
NotExecutable: code 'x' → m:f is not implemented | A call to code outside the model | Model it with ops; a model cannot bring its own code |
ExprError: error evaluating '...': NameError: name 'x' is not defined | Name unknown at that place | See the scope table in expressions; local names do not exist in flow and end |
ExprError: zone 'x' not declared / zone 'x' belongs to a player; write x[player] | Bad zone path | Declare the zone, or index it |
ExprError: variable 'x' not declared | set/add on an undeclared variable | Declare it under state.vars |
ExprError: <place>: move: 'x' is not in <zone> | The item is not in from | Follow the game in the player and look at the zone and the value |
ExprError: flow X: loop does not end | until_x never becomes true (10,000 rounds) | Set a flag in an effect |
ExprError: end condition X: returns_x gives 1 values, expected 2 | returns_x length differs from num_players | One number per player |
ExprError: outcome model X: probabilities add up to 0.900000, not 1 | p_x values do not sum to 1 | Fix the probabilities |
End reason flow finished without an end condition (in the test games and the player) | The main flow ran out | Check end and when it is checked |
N games did not end within 5000 requests: a loop without an end? (validator), "cut off" (player) | A loop that never ends | Check the loop conditions and end conditions |
Limits
| Limit | Value |
|---|---|
Rounds of one loop, iterations of repeat, turns of priority in one window | 10,000 |
| Nested emits (trigger chains) | 50 |
| Nested calls of own functions | 100 |
Nested modified() calls | 20 |
| Requests in one test game of the validator | 5,000 |