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.
6.0 KiB
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 likeface? 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 | — |