diff --git a/docs/bgm/state-model.md b/docs/bgm/state-model.md new file mode 100644 index 0000000..0dc6ebf --- /dev/null +++ b/docs/bgm/state-model.md @@ -0,0 +1,137 @@ +# 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`](./format.md) (the manifest), [`tabletop.md`](./tabletop.md) (the +render library), and [`engine.md`](./engine.md) / [`commands.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. + +```yaml +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 | — | \ No newline at end of file diff --git a/docs/overview.md b/docs/overview.md index f99907a..6164960 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -14,6 +14,7 @@ and go stale as work lands. | [`bgm/engine.md`](./bgm/engine.md) | The bgm message layer: queue, triggers, orchestrators | | [`bgm/commands.md`](./bgm/commands.md) | bgm command execution: lifecycle, run contexts, tap interaction | | [`bgm/tabletop.md`](./bgm/tabletop.md) | The r3f tabletop component library | +| [`bgm/state-model.md`](./bgm/state-model.md) | The format's state model: components vs setup, facing, anchoring scope | ## Status & plans (dev logs)