# ─────────────────────────────────────────────────────────────
# Tabletwin game model — metamodel v0.9
# One source, two projections:
#   1. flow view     → drawn from `flow`/`flows` + derived markers
#   2. forward model → state, legal_actions, apply (OpenSpiel-style API)
# Executable (v0.3): every text field may have a twin `<field>_x` holding an
# expression in a safe Python subset (see docs/expressions.md). The text stays
# for people and the viewer; the engine reads only `_x`. A model is executable
# once every field the engine needs has an `_x` (or a literal value).
#
# New in v0.2 (the four adapters):
#   a. priority  — named windows + priority protocol (LCGs)
#   b. flows     — subflows and `persist` for campaigns/legacy games
#   c. outcomes  — outcome model as a stand-in for physics (dexterity)
#   d. agents    — `say` op and agent hooks for language and communication
#
# New in v0.3 (executability):
#   `_x` fields, `constants`, `state.derived`, `num_players`, loop `as`,
#   end condition `who_x`, `roll` with `bind`, `place` with `at_x`,
#   outcome model `derived_x`/`p_x`, agent `policy_x`.
#
# New in v0.4 (content as data):
#   `content` layer: cards, dice, faces and skills as data with effects in the
#   same algebra; the `exec` op runs such an effect; `status` per item
#   (verified, uncertain, missing); `code.kind` separates content from
#   algorithms (content, algorithm); `open_questions` records the rule gaps that
#   modelling exposes (status open, assumed, answered).
#
# New in v0.5: `mode` (competitive, cooperative); `eliminate` takes one player
#   out of the game (list `eliminated` in expressions) and play goes on.
#
# New in v0.6 (hidden information, executable):
#   zones per player via path `hand[player]`; who sees each draw, roll and choice follows
#   from the destination zone (public/owner/hidden), `secret` or `visible_to_x`;
#   `reveal` with `what_x` makes something visible; result `score` with `returns_x`.
#   The engine records per player what they observed (information state) and can sample
#   a world consistent with it (resample), so ISMCTS and CFR work.
#
# New in v0.7 (rule mechanics executable):
#   trigger timing (when/before → instead → impact → after) with `impact` on emit; optional
#   triggers as a `use`/`skip` choice for `controller_x`; triggers and modifiers on content
#   via `active_x`; priority windows with a protocol (first_x, closes, resolution
#   immediate/nested/lifo_stack; option `pass`; events stack:X, resolves:X, cancelled:X) and
#   actions with `speed` and `cost`; ops `let`, `window`, `cancel`, `modify`, `reset`,
#   `destroy`; `choose` with min/max (a subset as a series of choices ending with `done`);
#   modifiers with `stat`, `layer`, `change_x`, read with modified(stat, value); end conditions
#   with `scope` (end a subflow, result in `results`); `reset` for campaigns (everything
#   without persist).
#
# New in v0.8 (cards and stacks):
#   instances: dicts in zones with a unique `uid` and their own state (`create` with a dict,
#   `n`, `with_x`); change an attribute with `set`/`add` + `of_x` + `attr`; stack order with a
#   known top and bottom (`look`, `position: top|bottom`, `shuffle`); `search` (look through a
#   zone with a filter); executability in layers: rules, content, code.
#   Card texts: `abilities` (actions on a content item, `self` = the item) and `keywords` on
#   content; `end_event` on flow nodes (phase_end: "until the end of the phase").
#   Players as a separate layer: tactics in their own file (docs/tactics.md, tactic.schema.json;
#   scope names me, pool, chosen; agents tactic:<path> and search:<path>[:n]);
#   `drawn_from` on a hidden zone, so search redistributes what a player cannot know.
#
# New in v0.9 (space):
#   `boards`: boards with geometry, square (4 or 8 neighbours, cells as a1 or x,y) and hex
#   (axial q,r; rectangle, hexagon or loose cells), with `missing` (holes) and `walls` (walls
#   between neighbours). Board functions in expressions: cells, directions, xy, cell, step,
#   leap, ray, neighbours, distance, path, path_cost, reachable (Dijkstra with costs, blocked
#   and through cells), line_of_sight (centre or corner to corner), shape (pattern with
#   rotation). `functions`: own pure functions with parameters, `let` and `x`; they see the
#   state, not the caller's local names. See docs/board-geometry.md.
#   Rule language in English: places step X, action X, action X (cost), window X, end X,
#   content X, trigger X; options pass, done, use, skip.
# ─────────────────────────────────────────────────────────────
metamodel: tabletwin
version: "0.9"

game:
  num_players: int        # (v0.3) number of players for the engine
  utility: "{min, max}?"  # (v0.7) range of a player's return, for OpenSpiel (default −1 … 1); not used by the TypeScript engine
  mode:                   # (v0.5) how end conditions turn into returns
    competitive: "win/loss applies to who_x; the others get the opposite (zero-sum)"
    cooperative: "all players share one outcome: +1 on win, −1 on loss; who_x not needed"

expressions:
  doc: >
    (v0.3) Safe Python subset: arithmetic, comparisons, and/or/not, if-else, lists, tuples,
    dicts, sets, comprehensions, and the functions len, any, all, min, max, sum, range, abs,
    round, sorted, list, tuple, set, dict, enumerate, zip, int, float, str, bool, sigmoid (σ),
    exp, log, sqrt. No attributes except get/count/index/items/keys/values, no names starting
    with an underscore. In scope: constants, variables, zones, derived values, players,
    n_players, eliminated, results, content, and local binds (choose, roll, let, for_each, ...).
    See docs/expressions.md.
  twin_fields: [pre_x, cond_x, if_x, options_x, value_x, amount_x, items_x, until_x, over_x,
                actor_x, what_x, at_x, die_x, n_x, when_x, who_x, p_x, init_x, policy_x,
                returns_x, visible_to_x, first_x, controller_x, change_x, active_x, target_x,
                min_x, max_x, of_x, filter_x, with_x]
  information: >
    (v0.6) Who sees what follows from the model. A draw into a zone with visibility public:
    everyone; owner: only the owner (zone per player, path hand[i]); hidden: nobody.
    Roll and sample: everyone. Choice: everyone, unless secret (only the actor). Each of these
    can be overridden with visible_to_x (list of players). The information state of player i
    is the sequence of observations in which what i did not see appears as '?'.
  functions: >
    (v0.7) modified(stat, value, **context) gives the value after all active modifiers for
    `stat`; results gives the outcome (win, loss, draw) of each finished scoped subflow; in a
    window, actor, window and stack (list of {action, actor, cancelled}) are available.
    (v0.9) With `boards` also the board functions (board name as first argument), and the own
    functions from `functions` under their names.

layers:

  constants:
    doc: (v0.3) Fixed tables and parameters, overridable per experiment.
    entry: {name: value}

  boards:
    doc: (v0.9) Boards with geometry. Fixed during play; what stands on them lives in zones or variables.
    entry:
      kind: [square, hex]
      width: int            # square, and hex as a rectangle (odd rows shifted half a cell right)
      height: int
      names: [xy, letters]  # square: '3,4' or 'e4'
      neighbours: [4, 8]    # square
      radius: int?          # hex: hexagon around 0,0
      cells: "[q,r]?"       # hex: loose cells
      missing: "[cell]?"    # holes: do not exist, block movement and sight
      walls: "[[cell, cell]]?"   # wall between two neighbours

  functions:
    doc: (v0.9) Own pure functions in the expression language, e.g. 'is this square attacked'.
    entry:
      params: "[name]"      # 'name?' = optional (default None)
      let: "{name: expr}?"  # helper values, in order
      x: expr               # the result; sees params, let, constants and the state

  state:
    doc: Everything on the table. Exactly this is what a simulator copies per search node.
    entity_type:
      attrs: map<name, type>
      extends: entity_type?
    zone:
      kind: [grid, hexgrid, graph, plane, stack, set, bag, slot, track]
      owner: [global, player, team, entity]
      visibility:
        public: everyone sees the content
        owner: only the owner sees the content
        hidden: nobody sees the content (face-down pile, shadow card)
      shuffled: bool        # true → drawing from this zone is random (chance node)
      drawn_from: zone?     # (v0.8) hidden zone: the pile its cards come from; search redistributes them
      persist: campaign?    # (b) kept between scenarios; (v0.7) `reset` skips it
      holds: entity_type?
    var:
      scope: [global, player, team, entity]
      type: [int, real, bool, enum, ref, set, list, dict, str]   # descriptive
      init: any
      range: "[min, max]?"
      persist: campaign?    # (b)
      init_x: expr?         # (v0.3)
    derived:
      doc: (v0.3) Named derived values, recomputed on every evaluation.
      entry: {text: string, x: expr}

  flow:
    doc: Hierarchical statechart. This is the layer that gets drawn.
    root: flow             # the main flow
    named: flows           # (b) named subflows: {name: node}
    node:
      id: string
      label: string
      kind:
        sequence: children in fixed order
        loop: repeat the children; optionally per actor (`over` + `order`) and/or `until`
        branch: pick exactly one `case` based on `switch`
        step: leaf; runs `do`. If no action is legal, the step is skipped
        subflow: (b) runs named flow `ref`
      over: expr?
      as: var?              # (v0.3) loop variable set for each actor
      once: bool            # (v0.8) one round over `over`: each value once
      order:
        fixed: fixed order
        clockwise_from_first: clockwise from the first player
        initiative: sorted by initiative value
        simultaneous: at the same time, choices hidden until revealed
        deck_order: order decided by a drawn card
        players_choose: the players decide the order
      until: expr?
      switch: expr?         # branch discriminator (not `on`: YAML 1.1 reads that as true)
      cases: [{when: expr, label: string, children: [node]}]
      ref: flow-name        # subflow only
      actor: expr?
      do: action | [action] # list = the actor picks one legal action (decision node)
      window: window-name | bool   # (a) window after this node; a name refers to priority.windows
      end_event: "event | [event]"  # (v0.8) emitted when the node is done, after the window (e.g. phase_end)
      secret: bool          # (v0.6) only the actor sees the chosen action
      visible_to_x: expr?   # (v0.6) who sees the choice
      note: string?

  priority:
    doc: (a) Who may react when, and how reactions resolve.
    protocol:
      first: expr           # who gets the first chance (start player, lead investigator, active player)
      first_x: expr?        # (v0.7) executable; default the actor, else player 0
      order: [clockwise_from_first, any_order, active_then_clockwise]
      closes: [all_pass_in_succession, single_pass_each]
      resolution:
        immediate: the effect resolves at once, no stack (LotR)
        nested: "'when' reactions interrupt and resolve first (Arkham); the engine runs it like immediate"
        lifo_stack: everything on a stack, last in first out; priority again after each resolution (Magic)
      after_action: "[next, same]?"   # (v0.7) who gets priority after an action; default next
      note: string?
    engine: >
      (v0.7) A window asks player after player: a legal fast action or `pass`. If nobody has a
      legal action, the window closes without a decision. all_pass_in_succession: closes when
      all players pass one after another; single_pass_each: whoever passes is out. With
      lifo_stack the cost is paid at once and the effect goes on the stack; when all players
      pass, the top item resolves (unless cancelled with `cancel`) and `first` gets priority again.
    window:
      label: string
      allows: "[speed]"     # free names, e.g. action, event, fast, reaction; must match action.speed
      note: string?

  actions:
    doc: Legal moves = actions whose `pre` is true.
    action:
      actor: expr?
      pre: expr?
      speed: string?       # (v0.7) playable in windows that allow this speed (allows)
      cost: "[op]?"        # (v0.7) paid at once when played; the effect follows (with lifo_stack only on resolution)
      effect: [op]

  effects:
    doc: A small algebra. An effect is a list of ops (implicit sequence).
    ops:
      place:    [what, to, "at?"]
      move:     [what, from, to]
      create:   [what, to]
      destroy:  [what]
      set:      [var, value]
      add:      [var, amount]
      draw:     [from, to, n]          # chance if zone(from).shuffled
      shuffle:  [zone]                 # chance
      reveal:   [what, "visible_to?"]  # (v0.6) makes something visible; by default to everyone
      roll:     [die, "bind?"]         # chance; uniform over die_x
      choose:   [actor, options, bind, "secret?", "agent?"]  # decision; agent = (d) hook
      if:       [cond, then, "else?"]
      repeat:   [until, body]
      for_each: [items, body]
      emit:     [event]                # hook point for triggers
      call:     [code]                 # escape hatch
      run:      [flow, "args?"]        # (b) run a named subflow from an effect (args: not used by the engine)
      sample:   [model, inputs]        # (c) draw an outcome from an outcome model
      say:      [actor, channel, constraint, bind, agent]   # (d) free speech, not enumerable
      exec:     [of, "content?"]       # (v0.4) run the effect of a content item; content = catalogue
      let:      [bind, value]          # (v0.7) bind a local name (e.g. a target when playing)
      window:   [name]                 # (v0.7) open a priority window from an effect
      cancel:   [target]               # (v0.7) cancel stack item `target` (index in stack)
      modify:   [stat, layer, change, "until?"]  # (v0.7) temporary modifier until event `until`
      reset:    [scope]                # (v0.7) campaign: everything without persist back to its initial value
      look:     [from, n, "bind?"]     # (v0.8) look at the top n; their order is fixed afterwards
      search:   [from, filter, bind]   # (v0.8) look through a zone and pick what matches the filter (filter_x)
    op_details:
      emit: "(v0.7) impact: [op] = what the event itself does; triggers with timing when/before go first, instead replaces, after follows"
      choose: "(v0.7) min/max: choose a subset; the engine asks for each element in fixed order, or 'done'"
      destroy: "(v0.7) remove `what` from `from`, or from the first zone that holds it"
      create: "(v0.8) n, with_x, position, bind; a dict becomes an instance with a uid"
      move: "(v0.8) position: top | bottom | random"
      set_add_attr: "(v0.8) of_x + attr: attribute of an instance"

  outcomes:
    doc: >
      (c) Adapter for what is not a rule but physics or skill. The model replaces the
      world by a probability distribution over outcomes; rules and tactics stay exact.
    model:
      skill: bool           # the outcome depends on the actor's skill
      inputs: map<name, doc>
      params: map<name, value>
      derived: map<name, expr>   # executable as derived_x
      outcomes: [{id, p: expr, effect: [op]}]   # the p's add up to 1 (executable as p_x)

  agents:
    doc: >
      (d) Who decides where the moves cannot sensibly be enumerated. An agent hook ties a
      `say` or `choose` to a player implementation.
    agent:
      kind: [search, heuristic, llm, human]
      roles: [string]
      sees: [zone]          # the agent's information set
      prompt: string?
      policy_x: expr?       # (v0.3) heuristic: returns the label of the chosen option

  triggers:
    doc: Rules that live on cards; they react to `emit` events.
    trigger:
      source: string
      event: string         # not `on`: YAML 1.1 reads that as true
      timing: [before, after, instead, when]   # (v0.7) executable; default after
      optional: bool        # (v0.7) a choice 'use' or 'skip' for the controller
      controller_x: expr?   # (v0.7) who decides on an optional trigger; default the actor
      if: expr?
      do: [op]
      engine: >
        (v0.7) Order per emit: when/before, then instead (the first that fires replaces the
        impact), then the impact, then after. Within each phase mandatory triggers first, then
        optional ones, in model order; triggers on content after those of the model.
    modifier:
      source: string
      stat: string          # (v0.7) which quantity; read with modified('stat', base value)
      layer: int            # order of application, after the MTG layer system; ascending
      target: expr          # text for people
      change: expr
      change_x: expr?       # (v0.7) new value; `value` is the value so far
      if_x: expr?           # (v0.7) only active while this is true
      duration: [permanent, round, turn, while]   # temporary: via `modify` with an until event

  content:
    doc: >
      (v0.4) The game's content as data: what is printed on cards, dice and reference cards.
      Structure (flow) and rules (actions, triggers) stay generic; the content fills them in.
      Every item with an effect can be run with `exec`.
    catalog: {name: {id: item}}       # e.g. gearlocs, dice, encounters
    item:
      id: string
      label: string
      status: [verified, uncertain, missing]
      source: string?
      effect: "[op]?"                 # executable with exec
      faces: "[item]?"                # die: each face is an item with its own effect
      cost: int?
      requires: "[id]?"               # requirements in a skill tree
      triggers: "[trigger]?"          # active while the item is in play (e.g. in a Locked slot)
      modifiers: "[modifier]?"        # likewise
      active_x: expr?                 # (v0.7) when this item's triggers and modifiers apply; `self` = the item
      abilities: "[action]?"          # (v0.8) actions on the card itself (with speed: playable in windows); `self` = the item
      keywords: "string | [string]?"  # (v0.8) keywords; their behaviour lives in the rules
      text: string?                   # card text in your own words; without behaviour the item counts as a gap
      note: string?
    face_notation: >
      A face is a reference to a face type, optionally with a number: `advanced_def 2`.
      The face type's effect uses `#` for that number.

  code:
    doc: Escape hatch. Share of `call` ops among all ops = KPI for the ontology.
    entry: {lang: string, ref: "module:function", doc: string, kind: "[content, algorithm]"}

  open_questions:
    doc: >
      (v0.4) Rule gaps that modelling exposes: places where the source gives no unambiguous
      answer. Basis for a gap report.
    entry: {id: string, about: "path in the model", question: string, source: "string?",
            status: "[open, assumed, answered]?", answer: "string?"}

  end:
    condition:
      scope: flow-name?     # (b) applies to the root or to a subflow (e.g. a scenario within a campaign)
                            # (v0.7) executable: win/loss/draw with scope ends only that subflow
                            # and sets results[scope]; then the calling flow continues
      check: [immediate, end_of_turn, end_of_round, end_of_scenario]
      if: expr
      who_x: expr?          # (v0.3) who wins or loses
      returns_x: expr?      # (v0.6) with result score: list with the return per player
      result: [win, loss, draw, eliminate, continue, score]

# What the flow view derives by itself — none of this is drawn by hand.
# Ops in a subflow called via `run` count for the calling step.
derived_markers:
  choice:      step.do is a list, or a choose, say or search op
  chance:      draw from a zone with shuffled=true, shuffle, roll, look at a shuffled zone, or sample from a model without skill
  skill:       sample from an outcome model with skill=true
  hidden:      choose with secret=true, search, look, or move/draw to an unshuffled zone that is hidden or shows only its owner
  window:      window on the node, or a window op
  trigger:     emit op that at least one trigger listens to
  language:    say op, or choose with an llm agent
  persistent:  set/add on a var with persist, move/draw/place/create into a zone with persist, or a reset op
  code:        call op
