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:
@@ -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 | — |
|
||||
Reference in New Issue
Block a user