Files
tts-workshop/docs/bgm/state-model.md
T
hypercross 211c151971 docs: add bgm state model design
Describe the format's state model: components as constants, setup as the shape of states, path-to-stack by facing (part affordance x zone restriction), and the stacks-on-paths anchoring scope cut.
2026-08-16 12:38:58 +08:00

6.0 KiB
Raw Blame History

bgm State Model

The format's model of what a board game is, and the shape of the states it can be in. This is the organizing model behind the concrete specs: format.md (the manifest), tabletop.md (the render library), and engine.md / commands.md (scripted interaction).

Status: Design. The loader and render layer implement components and a static setup today; the state-shape framing, zone-facing, and the anchoring scope cut below are proposals to guide the next phase.

1. Components are constants; setup is the shape of states

A bgm game is authored two ways, and they answer different questions:

  • Components (part, surface) describe the constants — the physical pieces and the board geometry that do not change during play. A tile is a 45×45 mm square with a meadow/road/city face; the play board has a draw pile and an 11×11 grid. These are timeless facts about the game, not state.
  • setup describes the shape of the game's states — which parts can be where, how many, and with what facing. It is a schema over the state space, not a concrete snapshot of one run.

The runtime store is then an instance of the setup's shape: the current state of play, a point in the state space the setup describes. This buys three things:

  • Validation — a state is legal iff it matches the setup's shape (on the right paths, right counts, right facing).
  • A contract for the rule engine — legal play is a transition between shapes; the rule layer (layer 4 in the layering vision) reasons over them.
  • A clean boundary — components are timeless; setup is the state space; the store is the current point.

Open: does setup carry the initial state?

A game needs a concrete starting position, not just a schema. The default is that a setup is shape + initial instance — one role that both describes the legal state space and seeds the store. The alternative (schema-only, with the initial state derived from the shape) is explored but not preferred.

2. The state: path → stack × facing

The state space is made of two axes.

Path → stack

Every part lives on a path (a URL-style key like /grid/5/5 or /draw), and multiple parts on a path form an ordered stack (a deck, a pile of meeples, a tile with a meeple on it). Placement is one axis. This is already in use.

facing — part affordance × zone restriction

Facing is the second axis, and it has two distinct sources of constraint:

  • The part declares the physical affordance — the facings the piece physically supports. A card supports face/back/standing; a tile supports face/back but not "tapped".
  • The path (zone) declares the legal facing — what's allowed on that zone. An MTG discard pile requires face-up; a play area allows tapped; a facedown deck requires face-down. The same card is face-up in the discard, tapped in play, face-up in exile — the piece's affordance doesn't change, only the zone's rule does.

A state's facing is legal iff it is both physically possible (part) and zone-legal (path) — the effective set is the intersection.

role: part
type: card
id: basic
facing: [face, back, standing]   # physical affordance

role: surface
type: board
id: main
layout:
  - route: /discard
    facing: [face]               # zone restriction
  - route: /play
    facing: [face, tap]          # zone restriction

Deck / the current facing lives in the store and must be in the intersection.

Open questions

  • Default when a path declares no facing — unrestricted (only the part's affordance bounds it), or a sensible default like face? Lean unrestricted.
  • Is "tapped" a facing or a rotation? In MTG it's a 90° in-plane rotation. Default: fold common rotations into the facing enum (face / back / standing / tap) for schema simplicity; arbitrary rotation is a later extension.
  • Naming — the part-side and path-side are different constraints wearing the same word. Worth distinct terms (capability/typ) so they don't collide.

3. Anchoring scope: stacks-on-paths, not part-to-part networks

Real TTS mods anchor components to each other — a meeple on a tile, a fanned hand, tokens scattered on a board. We are not modeling part-aligned-to-part relative placement networks. Instead:

  • Stacks absorb part-on-part. "Meeple on tile" is a stack [tile, meeple] on a path. Much of TTS's anchoring collapses into the path→stack model.
  • Free relative placement is out of scope — a meeple at an offset on a tile, a fanned hand, arbitrary token scatter are not modeled.

This is a deliberate scope cut. It keeps the state model closed and simple, but it is a fidelity loss for converted games and part of the "with some fixing" cost of the TTS→bgm conversion. Components (layer 1), not the state model, are the place to extend later if needed.

The grid compromise

Because we don't model parts-aligned-to-parts, common layouts are expressed explicitly — and grid layouts become verbose (the Carcassonne board is 121 hand-written row coordinates in grid.csv). The mitigations:

  • Grid shorthand — a declarative grid (cols/rows, cell size, origin) that expands to routes, instead of authored coordinates.
  • Free-placement shorthand — a free/scatter mode for loose collections, when the exact positions don't matter to play.

But there is no general part-to-part network on the roadmap. If a converted game needs it, that is a format extension to design deliberately, not an implicit assumption.

Summary

Concept Role Where
Components Constants — the physical pieces & board geometry part, surface
Setup The shape of the state space (+ initial instance) setup
Store The current state, an instance of the setup @tts/tabletop
Facing Part affordance × zone restriction part + path/zone restriction
Anchoring Stacks-on-paths only; no part networks