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.
This commit is contained in:
2026-08-16 12:38:58 +08:00
parent 345832e389
commit 211c151971
2 changed files with 138 additions and 0 deletions
+137
View File
@@ -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 | — |
+1
View File
@@ -14,6 +14,7 @@ and go stale as work lands.
| [`bgm/engine.md`](./bgm/engine.md) | The bgm message layer: queue, triggers, orchestrators | | [`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/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/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) ## Status & plans (dev logs)