Compare commits

...
2 Commits
Author SHA1 Message Date
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
hypercross 345832e389 docs: reorganize docs into bgm and status folders
Group the bgm spec cluster under docs/bgm and move dev logs and plans under docs/status, add an overview index, and update cross-references in the docs, README, and source comments.
2026-08-16 11:57:19 +08:00
20 changed files with 217 additions and 43 deletions
+3 -2
View File
@@ -14,8 +14,9 @@ analyze their contents. A lightweight, client-only pnpm monorepo.
| `packages/mesh` | 2D shapes + extrusion into 3D mesh geometry | Isomorphic |
| `packages/shared` | Shared types + zod schemas | Isomorphic |
See [`docs/architecture.md`](docs/architecture.md) for the architecture and
[`docs/implementation-plan.md`](docs/implementation-plan.md) for the plan.
See [`docs/overview.md`](docs/overview.md) for the docs index,
[`docs/architecture.md`](docs/architecture.md) for the architecture, and
[`docs/status/implementation-plan.md`](docs/status/implementation-plan.md) for the plan.
## Setup
@@ -30,7 +30,7 @@ export function tintedColor(base: THREE.Color, tint: THREE.Color): THREE.Color {
* the same image.
*
* These caches live for the session (like drei's global texture cache) and are
* not disposed on unmount; see `docs/full-setup-view.md`.
* not disposed on unmount; see `docs/status/full-setup-view.md`.
*/
const geometryCache = new Map<string, THREE.BufferGeometry>();
+2 -2
View File
@@ -2,8 +2,8 @@
> **Scope:** The system's architecture and dependency graph. For implementation
> details (files, endpoints, build order), see
> [`implementation-plan.md`](./implementation-plan.md). For the rationale behind
> key decisions, see [`decisions.md`](./decisions.md).
> [`status/implementation-plan.md`](./status/implementation-plan.md). For the
> rationale behind key decisions, see [`decisions.md`](./decisions.md).
## Overview
@@ -1,14 +1,14 @@
# bgm-commands
Command execution for [bgm](./bgm-format.md) board games, built into
[`@tts/tabletop`](./bgm-tabletop.md). A command is a unit of scripted
Command execution for [bgm](./format.md) board games, built into
[`@tts/tabletop`](./tabletop.md). A command is a unit of scripted
interaction — focus the camera, wait for a tap, move a part, show a caption —
that runs against the tabletop state store and render layer.
This doc covers **command execution**: the async lifecycle, run contexts, and
tap interaction. The message layer above this — how commands are *declared*
and *fired* (triggers, orchestrators, the message queue) — is specified in
[`bgm-engine.md`](./bgm-engine.md). Commands are async functions registered
[`engine.md`](./engine.md). Commands are async functions registered
with the engine's handler registry; `@tts/tabletop` provides the concrete
commands that mutate the tabletop store and render layer.
@@ -16,7 +16,7 @@ commands that mutate the tabletop store and render layer.
A command is an async function that returns a result. Every command ends in
one of three states, emitted as a message discriminated on the type suffix
(see `bgm-engine.md` §3):
(see `engine.md` §3):
- `:done` — completed normally.
- `:cancel` — interrupted (a newer command superseded it, the user skipped,
@@ -124,7 +124,7 @@ cancel, so a cancelled `wait: tap` never leaks a handler.
## 5. run context
The context a command receives is the handle to everything it can affect. The
engine defines the base `RunContext` (see `bgm-engine.md` §5): `signal`
engine defines the base `RunContext` (see `engine.md` §5): `signal`
(cancellation), `emit`, `wait`, and `enableTrigger`/`disableTrigger`. Tabletop
extends it with the handles commands need to mutate the board:
@@ -142,12 +142,12 @@ interface TabletopRunContext extends RunContext {
The tap detector reads trigger points from a runtime map keyed by part id; it
does not care where they are declared. Declaration (on the part definition, in
a setup, or in a script) is the deferred "how to declare" half and lives in
`bgm-format.md`.
`format.md`.
## Open decisions
- **Where commands are declared** — the `script` role and its schema
(`bgm-format.md`), deferred.
(`format.md`), deferred.
- **Animation** — a general "ease toward target placement" layer (preferred)
vs explicit per-move tweens.
- **Camera**`CameraControls` (drei) vs hand-rolled.
+5 -5
View File
@@ -1,7 +1,7 @@
# bgm-engine
The message layer that drives [bgm](./bgm-format.md) board games, built into
[`@tts/engine`](./architecture.md). It unifies the two halves of scripted
The message layer that drives [bgm](./format.md) board games, built into
[`@tts/engine`](../architecture.md). It unifies the two halves of scripted
interaction — *declaring* what should happen and *executing* it — into a single
reactive loop: **messages** flow through a **queue**, and **handlers** react to
them.
@@ -9,7 +9,7 @@ them.
This doc covers the message model (what flows), the queue and its tick (how it
flows), and the handlers (who reacts). Command *execution* — the async
lifecycle, run contexts, and tap interaction — is specified in
[`bgm-commands.md`](./bgm-commands.md); this doc is the layer above it.
[`commands.md`](./commands.md); this doc is the layer above it.
## package split
@@ -19,7 +19,7 @@ is node-testable in isolation (mirroring `@tts/extract`'s isomorphic, zero-dep
style). It defines the contract — `Message`, the handler registry, `Trigger`,
`Orchestrator`, and `RunContext`.
[`@tts/tabletop`](./bgm-tabletop.md) is one consumer of that contract: it
[`@tts/tabletop`](./tabletop.md) is one consumer of that contract: it
registers the built-in commands (`move`, `focus`, `caption`, `enableSurface`,
...) that mutate the tabletop store and drive the render layer. The engine
never imports tabletop; tabletop depends on the engine for the message types
@@ -94,7 +94,7 @@ it cannot re-enter itself.
### interaction messages
Interaction is the player's input, reported to the engine as messages. Only
tap interaction is supported (see `bgm-commands.md` §4).
tap interaction is supported (see `commands.md` §4).
```ts
interface TapMessage {
+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,6 +1,6 @@
# bgm-tabletop
a r3f based interactive component library to work with [bgm](./bgm-format.md) board games. will be used somewhere in the `web` app's bgm inspector routes.
a r3f based interactive component library to work with [bgm](./format.md) board games. will be used somewhere in the `web` app's bgm inspector routes.
## 1. stack
@@ -47,14 +47,14 @@ the render map is per enabled surface: a piece may appear on more than one enabl
## 4. stacking
the format's stacking strategy (`curve` / `limit` / `align` / `steps` / `tilt` / `zStart` / `zEnd`, see `bgm-format.md` §4) is implemented as a hook, e.g. `useStacking(route.stacking, index, stackSize)`, returning the offset/rotation to apply to a piece: `{ x, y, rotation, z, tilt }`. `x`/`y`/`rotation` come from the `curve`; `z` is the surface-normal height ramped from `zStart` to `zEnd`; `tilt` is the rotation about the card's local Y (long) axis, applied to every part. `PartPlacement` consumes it.
the format's stacking strategy (`curve` / `limit` / `align` / `steps` / `tilt` / `zStart` / `zEnd`, see `format.md` §4) is implemented as a hook, e.g. `useStacking(route.stacking, index, stackSize)`, returning the offset/rotation to apply to a piece: `{ x, y, rotation, z, tilt }`. `x`/`y`/`rotation` come from the `curve`; `z` is the surface-normal height ramped from `zStart` to `zEnd`; `tilt` is the rotation about the card's local Y (long) axis, applied to every part. `PartPlacement` consumes it.
## 5. commands
Scripted interaction — focus, tap-to-advance, move, caption — is built on an
async command layer. See [`bgm-commands.md`](./bgm-commands.md) for command
async command layer. See [`commands.md`](./commands.md) for command
execution (lifecycle, run contexts, tap interaction), and
[`bgm-engine.md`](./bgm-engine.md) for the message layer above it (the queue,
[`engine.md`](./engine.md) for the message layer above it (the queue,
triggers, and orchestrators that declare and fire commands).
## 6. usage
+2 -2
View File
@@ -2,7 +2,7 @@
> **Scope:** The rationale behind key design decisions. For the system's
> architecture, see [`architecture.md`](./architecture.md). For the concrete
> build plan, see [`implementation-plan.md`](./implementation-plan.md).
> build plan, see [`status/implementation-plan.md`](./status/implementation-plan.md).
>
> Each entry records the decision, the context, and the alternatives considered.
> New entries are appended; existing entries are updated only to correct facts,
@@ -275,7 +275,7 @@ group starts (e.g. a `camera` group so a second focus cancels the first).
**Context:** The user wants to script interaction sequences — focus, caption,
title, highlight, tap-to-advance, move, camera away. The state store and
render layer already exist; what's missing is a way to drive them over time
and react to input. Design: [`bgm-commands.md`](./bgm-commands.md).
and react to input. Design: [`bgm/commands.md`](./bgm/commands.md).
**Alternatives considered:** A single monolithic script interpreter. Rejected
— commands as self-contained async units are testable in isolation and let
+26
View File
@@ -0,0 +1,26 @@
# Docs
The documentation is split into the living specs, which describe how the
system works today, and the status/plans, which track development iterations
and go stale as work lands.
## Specs — how the system works
| Doc | Covers |
| --- | --- |
| [`architecture.md`](./architecture.md) | System architecture and the package dependency graph |
| [`decisions.md`](./decisions.md) | Key design decisions and the rationale behind them |
| [`bgm/format.md`](./bgm/format.md) | The board game manifest (bgm) format spec |
| [`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)
| Doc | Covers |
| --- | --- |
| [`status/implementation-plan.md`](./status/implementation-plan.md) | Original build plan |
| [`status/bgm-loader.md`](./status/bgm-loader.md) | bgm loader — what's built, works, missing |
| [`status/bgm-tabletop.md`](./status/bgm-tabletop.md) | bgm tabletop — implementation plan / status |
| [`status/full-setup-view.md`](./status/full-setup-view.md) | Full-setup view plan |
@@ -1,7 +1,7 @@
# bgm Loader — Status
> WIP. What's built, what works, what's missing, and the known issues.
> Spec: [`bgm-format.md`](./bgm-format.md).
> Spec: [`../bgm/format.md`](../bgm/format.md).
## What's built
@@ -30,7 +30,7 @@ A real 52-card deck: a single `card` part expanded by `$variants` into 52 cards,
### `packages/tabletop` — rendering library (new)
A standalone r3f library that renders bgm parts. `PartView`/`PartMesh` build a mesh from a `Part` definition via `@tts/mesh` (size/fillet, face/back sprite UVs, traced `shape` or rect fallback). Proxy calls (`/asset`, `/trace`) default to `@tts/http` handlers and are overridable via `TabletopProvider`. Plan: [`bgm-tabletop-plan.md`](./bgm-tabletop-plan.md).
A standalone r3f library that renders bgm parts. `PartView`/`PartMesh` build a mesh from a `Part` definition via `@tts/mesh` (size/fillet, face/back sprite UVs, traced `shape` or rect fallback). Proxy calls (`/asset`, `/trace`) default to `@tts/http` handlers and are overridable via `TabletopProvider`. Plan: [`bgm-tabletop.md`](./bgm-tabletop.md).
### `packages/http` — shared proxy HTTP (new)
@@ -71,5 +71,5 @@ The vite plugin itself lives in `packages/bgm/src/vite.ts` (exported from `@tts/
- **`$variants` URL paths** — spec mentions file/URL; URLs deferred.
- **zod `SerializedPackage` shape for the emitted JSON** — the plugin emits `SerializedPackage` objects; a zod schema for the emitted module would give runtime validation beyond the ambient `declare module`.
- **`setup` value expansion** — `type` without `id` → all parts of that type is documented but not implemented in the loader (it's a game-state init concern; noted as future).
- **Surface mounting is validated but not resolved**`mount`/`children`/`surfaces` are parsed and validated, but the loader doesn't resolve child→parent relationships or enforce that a setup's `surfaces`/a surface's `children` reference existing surfaces. That's a game-state/rendering concern (see `docs/bgm-tabletop.md`).
- **Surface mounting is validated but not resolved**`mount`/`children`/`surfaces` are parsed and validated, but the loader doesn't resolve child→parent relationships or enforce that a setup's `surfaces`/a surface's `children` reference existing surfaces. That's a game-state/rendering concern (see `../bgm/tabletop.md`).
- Docs for the loader itself (this file is the start).
@@ -1,8 +1,9 @@
# bgm-tabletop — Implementation Plan / Status
> **Scope:** A standalone r3f component library that renders [bgm](./bgm-format.md)
> board games: a state store, surface mounting, part placement with stacking,
> and per-part meshes. Design: [`bgm-tabletop.md`](./bgm-tabletop.md).
> **Scope:** A standalone r3f component library that renders
> [bgm](../bgm/format.md) board games: a state store, surface mounting, part
> placement with stacking, and per-part meshes. Design:
> [`../bgm/tabletop.md`](../bgm/tabletop.md).
> **Status:** items 18 implemented and the full tabletop scene is wired into
> the web app's setup detail route (`/bgm/:id/setups/:type/:setup`). The
> part-inspection route renders `PartView` from the library.
@@ -75,7 +76,7 @@ useful slice and unblocks the web app's part inspection route immediately.
### 3. State store (`state.ts`) ✅
Source-of-truth game state per `bgm-tabletop.md` §2:
Source-of-truth game state per `../bgm/tabletop.md` §2:
```ts
interface GameState {
@@ -96,7 +97,7 @@ interface PartState {
Computed with a selector/memo so the render list is stable. A path's ordered
children (for stacking) are derived from the parts map by sorting on `index`.
- **Assumption**: each piece id is unique on the board (documented in
`bgm-tabletop.md`); the render map is keyed by piece id.
`../bgm/tabletop.md`); the render map is keyed by piece id.
### 4. Setup seeding (`setup.ts`) ✅
@@ -104,7 +105,7 @@ interface PartState {
`Setup` — enables its `surfaces` (or all when omitted) and applies its
ordered `setup` placements (each moves its `parts` to a `path`).
- `setup` value expansion: a bare `type` (no id) expands to all parts of that
type (documented in `bgm-format.md` §3; the loader doesn't do this — it's a
type (documented in `../bgm/format.md` §3; the loader doesn't do this — it's a
game-state init concern, so it lives here).
### 5. Surface mounting (`mount.ts`) ✅
@@ -126,7 +127,7 @@ interface PartState {
### 7. Stacking (`stacking.ts`) ✅
- `useStacking(route.stacking, index, stackSize)``{ x, y, rotation, z, tilt }`.
- Implements the format's positioning process (`bgm-format.md` §4): step
- Implements the format's positioning process (`../bgm/format.md` §4): step
length from curve length / `max(steps, count-1)`, alignment (`start`/`end`/
`center`), and `limit` (`0` all, `n` first n, `-n` last n).
- `z` ramps linearly from `zStart` to `zEnd` across the curve's span; `tilt`
@@ -174,7 +175,7 @@ consumers share them (see Open decisions).
## Commands (not yet implemented)
Scripted interaction is designed in [`bgm-commands.md`](./bgm-commands.md):
Scripted interaction is designed in [`../bgm/commands.md`](../bgm/commands.md):
async commands with `ok`/`cancel`/`error` results, per-invocation run
contexts, fire-and-forget vs self-managed waiting, and tap interaction with
part-local trigger points. Implementation order: types + run-context manager,
@@ -2,8 +2,8 @@
> **Scope:** The concrete build plan — files, endpoints, dependencies, build
> order. For the system's architecture and dependency graph, see
> [`architecture.md`](./architecture.md). For the rationale behind key decisions,
> see [`decisions.md`](./decisions.md).
> [`../architecture.md`](../architecture.md). For the rationale behind key decisions,
> see [`../decisions.md`](../decisions.md).
A lightweight, client-only pnpm monorepo for searching the Tabletop Simulator
Steam Workshop, fetching full TTS save files, and analyzing their contents.
@@ -55,7 +55,16 @@ tts-workshop/
├── .npmrc
├── .env.example # STEAM_API_KEY, PORT
├── docs/
── implementation-plan.md # this file
── overview.md
│ ├── architecture.md
│ ├── decisions.md
│ ├── bgm/
│ │ ├── format.md
│ │ ├── engine.md
│ │ ├── commands.md
│ │ └── tabletop.md
│ └── status/
│ └── implementation-plan.md # this file
├── apps/
│ ├── proxy/
│ │ ├── package.json
+1 -1
View File
@@ -11,7 +11,7 @@
* their `include` patterns, and assembles the package's parts, surfaces,
* and setups.
*
* See docs/bgm-format.md for the format's concrete behavior.
* See docs/bgm/format.md for the format's concrete behavior.
*/
import * as path from 'node:path';
import picomatch from 'picomatch';
+1 -1
View File
@@ -2,7 +2,7 @@
* Parse raw definition files (yaml/json/toml text) into JSON objects.
*
* A def file's document can be either a single JSON object (the root) or a
* list of objects; both are handled per docs/bgm-format.md §3. In list mode,
* list of objects; both are handled per docs/bgm/format.md §3. In list mode,
* each object is a separate definition.
*/
import * as fs from 'node:fs';
+1 -1
View File
@@ -3,7 +3,7 @@
*
* These validate the raw definition objects (after `$variants` expansion)
* and produce the typed `Part` / `Surface` / `Setup` / `PackageDef` values.
* See docs/bgm-format.md for the format's concrete behavior.
* See docs/bgm/format.md for the format's concrete behavior.
*/
import { z } from 'zod';
import type { PackageDef, Part, Setup, Surface } from './types.js';
+1 -1
View File
@@ -5,7 +5,7 @@
* discovered as JSON objects from yaml/json/toml files and from markdown
* code blocks, then assembled into a `Package` (see `collect.ts` / `emit.ts`).
*
* The concrete behavior of the format is described in `docs/bgm-format.md`.
* The concrete behavior of the format is described in `docs/bgm/format.md`.
*/
/** Part value types. */
+1 -1
View File
@@ -2,7 +2,7 @@
* The `$variants` directive: parse a CSV into a typed object array and
* extend the original object with each row.
*
* Per docs/bgm-format.md §1:
* Per docs/bgm/format.md §1:
* - The CSV's first row is the header, the second row is the type declaration
* (`string`, `number`, `string[]`, `[number;number;number;number]`, ...),
* the remaining rows are data.
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Stacking — the format's positioning process (`bgm-format.md` §4).
* Stacking — the format's positioning process (`docs/bgm/format.md` §4).
*
* Given a route's `stacking` strategy and a piece's position in its path's
* stack, compute the offset/rotation to apply. Parts are spread along an SVG