# Board Game Manifest — Technical Reference > The concrete behavior of the board game manifest (bgm) format. > > Definitions can live in JSON/YAML/TOML files or in markdown code blocks. In > codeblock mode, each code block is a virtual definition file, named relative > to the current markdown file. --- ## 1. json features ### The `$variants` directive For objects with a `$variants` key, the value is a CSV. Parse it into an object array with `typed-csv`, extend the original object with each row, and return the array. ```yaml job: 'hero' $variants: ./heroes.csv ``` ```csv name,parents string,string[] clark,[jonathan;martha] bruce,[] ``` ```json [ { "job": "hero", "name": "clark", "parents": ["jonathan", "martha"] }, { "job": "hero", "name": "bruce", "parents": [] } ] ``` ### Inline vs file `$variants` can be a file/URL path *or* an inline CSV string. If the value contains a newline it is inline CSV; otherwise it is a path. In YAML a block scalar (`|`) is the natural way to write inline CSV; in JSON you'd use `\n`. ```yaml $variants: | id,name,faceCrop string,string,[number;number;number;number] fish,Fish,[0;0;5;2] grain,Grain,[1;0;5;2] wood,Wood,[2;0;5;2] ``` ### CSV conventions CSV is parsed with `typed-csv`: - The first row is the header, the second row is the type declaration (`string`, `number`, `string[]`, ...), and the remaining rows are data. - Rows are validated against a zod schema derived from the type row. - **`crop` inside a CSV cell** uses `;` as the element separator (`[0;0;5;2]`), because `,` is the CSV delimiter. `typed-csv` loads it into an array with value `[0,0,5,2]`. --- ## 2. Definition discovery Definitions are organized in **packages**. A loader loads a package declaration, then uses its `include` paths to find the definitions. ### Code blocks as virtual files A code block is a virtual definition file. To give it a name — so `include:` and `$variants` paths can resolve against it — add a `file=` segment to the code block's info string. The name is relative to the current markdown file: ````md ```yaml file=parts/cargo.yaml ... ``` ```csv file=parts/cargo.csv ... ``` ```` - A block with `file=` is addressable by that path. - A block without `file=` is auto-named `./${hash}.yaml`, where `hash` is derived from its content. This makes every yaml block naturally discoverable by the default `include: ./**/*.yaml`. Identical blocks dedupe to the same hash. - The `file=` name is what `$variants: ./cargo.csv` and `include: parts/*.yaml` resolve against. When there is a real file in that path, the codeblock wins. - `file=` implies the file type from its extension; the language tag is optional and only for editor highlighting. - **Hash vs explicit `file=`:** a hashed name is for auto-discovery, not for referencing. To point at a specific yaml block by name, give it an explicit `file=`; otherwise its name is content-derived and unstable. ### include `include` is a list of git-style path patterns — the defs that make up the package. **Defaults to `./**/*.yaml`**, so all yaml in the same and sub folders is discovered with no configuration. This also matches the package declaration itself, which is fine — it's the package, not a part. Patterns are resolved **relative to the package declaration's own directory**, not the games root. So a package declared in `carcassonne/carcassonne.md` with the default `./**/*.yaml` only picks up yaml under `carcassonne/` — it never absorbs defs from a sibling game. To reach outside its folder, a package can use a `../`-relative pattern or an absolute-from-root pattern (e.g. `**/shared/*.yaml`). --- ## 3. Roles json objects in yaml blocks are handled if they have a `role:` field for either - `package` - `part` - `surface` - `setup` a valid object can either be the root or in the list of the yaml block. for all roles except package, `type` and `id` are needed. `type#id` is used for identification so that combo must be unique in the package. ### package The package is the container for a game's definitions. It is declared with a `role: package` object: ```yaml role: package id: harbor title: Harbor designer: Jane Doe players: 2 language: en ``` - `role`: for block discovery. - `id`: package identification. - `title` — game name. - `include` — the defs that make up the package (see §2). - Optional metadata: `designer`, `development` (artist/developer), `publisher`, `players` (player count), `language`. ### part A part is a game component. It is identified by a `package:type#id` string, placed on the board via `setup`, and visualized by routes. #### part value types - `image` — a url to an image. - `crop` — a tuple `[col, row, cols, rows]`. Divides the image into a grid and picks the cell at `[col, row]` with size `[width/cols, height/rows]`. Negative `cols` flips the rendered image. - `size` — a tuple `[width, height, depth]` in mm units. #### part props - `face` — `sprite`. Used for texture. - `faceCrop` — `crop` for `face`. - `back` — `sprite`. Used for texture. Defaults to `face`. - `backCrop` — `crop` for `back`. - `shape` — `sprite`. Traced for its profile to create the mesh for the part. Defaults to the full rect of the back image. - `size` — `size`. The token is scaled to fit in the box. The x/y aspect ratio is kept, but not z (thickness). - `fillet` — number in mm. Used to fillet the shape. Defaults to `0`. #### example ```yaml role: part type: token id: wood face: ./assets/tokens.png faceCrop: [1, 0, 5, 2] back: ./assets/tokens.png backCrop: [3, 0, 5, 2] shape: ./assets/token-shape.png size: [20, 20, 3] fillet: 2 ``` A `wood` token: the `face` and `back` sprites come from the same sheet, `faceCrop`/`backCrop` picking different cells of the `5×2` grid. The shape is traced from `token-shape.png`, sized `20×20×3` mm with a `2` mm fillet. ### surface A `surface` is a **view** over the state store, purely for **visual rendering**. It has a reference `size` (`[width, height]` in mm) and a `layout` list of routes. The size is a reference — it may be scaled to fit larger or smaller tables. It does not affect part placement; placement lives in the state store (see §4). A surface need not cover every part — parts with no matching route on this surface are simply not shown. A surface also declares how it is **mounted**: as the root table surface, on a HUD area, or as a child of another surface. `mount` is always an object, with `x`, `y`, and `rotation` (defaulting to `0`) anchoring it like a route. The `kind` selects the mount type: - `table` — the root table surface (default). - `hud` — mounted to a HUD area, e.g. a player's hand. - `child` — mounted relative to a parent surface. A surface lists its `children` (`type#id` refs) so a surface can be repeated, like a player board; each child is mounted relative to its parent's anchor. ```yaml type: board id: harbor role: surface size: [300, 200] mount: kind: table x: 0 y: 0 rotation: 0 children: - board#player layout: - route: /dock/:seat candidates: $variants: ./seats.csv - route: /deck x: -100 y: 0 rotation: 0 ``` ```yaml type: hud id: hand role: surface size: [200, 100] mount: kind: hud area: bottom-left ``` ```yaml type: board id: player role: surface size: [200, 200] mount: kind: child x: 100 y: 50 rotation: 0 ``` ### setup `setup` seeds the state store: the enabled surfaces and the part placement. Each valid game state is a valid setup. ```yaml role: setup type: game id: main surfaces: - board#harbor - hud#hand setup: /dock/0: harbor:boat#fleet /deck: harbor:card ``` `surfaces` lists the surfaces enabled at the start. A surface not listed is disabled and not rendered. When `surfaces` is omitted, all surfaces are enabled. The value on a setup path can be either a string, or a string list. The string can either be a one part string, or a type without an id. When id is omitted, it expands to all parts in that type during game state initialization. --- ## 4. Concepts ### Game state The board's state is a **state store**: the set of **enabled surfaces** and a map from path to a **stack** of parts. It is the authoritative record of which surfaces are in play and where every part is placed. A path is a URL path with named params, like `/dock/1`. A part is identified by a `package:type#id` string. A surface is enabled or disabled; a disabled surface is not rendered. Setup seeds the enabled set (see §3), and it changes at runtime as the game progresses (e.g. enabling the main board after an expansion-chooser scene). ### Routing A route is a **visualization route**: it maps a part to a location on a surface. Routes match the keys of the state store, but they are defined by a surface and need not cover every placed part — a part with no matching route on a given surface is simply not shown there. Routes exist only for game parts; a surface is not a part and never appears on a route. A route matches all parts on the path; the placement of each individual part on the stack is a separate concern. A route is an express-style URL path with named params, plus the `x`, `y`, and `rotation` of its anchor. Routes are defined in a **list**, not a map, so the same route path may appear more than once: ```yaml layout: - route: /dock/:seat x: 40 y: 0 rotation: 0 - route: /deck x: -100 y: 0 rotation: 0 ``` ### Candidates To match a class of routes against a list of positions, keep a single route with its param and give it a `candidates` array to match `:param` against, each candidate carrying its own `x`/`y`/`rotation`: ```yaml layout: - route: /dock/:seat candidates: $variants: ./seats.csv ``` ```csv seat,x,y,rotation string,number,number,number 0,40,0,0 1,40,20,0 ``` The router should select only the first candidate with all params matched against its props — the fields in the candidate's CSV row (e.g. `:seat` matches the candidate's `seat` value). A candidate inherits the route's `x`, `y`, `rotation`, and `stacking`, and may override any of them with its own values. When no candidates match, the whole route fails to match. ### Stacking When multiple parts live on a path, only the top (last) one shows by default. To override this, add stacking strategies: ```yaml layout: - route: /deck x: -100 y: 0 rotation: 0 stacking: curve: M 0 0 C 20 -20 40 -20 60 0 limit: 5 align: center steps: 4 tilt: 0.1 zStart: 0 zEnd: 30 ``` - `curve` — an SVG path string to spread the content along, relative to the anchor `x`, `y`, `rotation`. - `limit` — how many parts to display. `0` shows all, `3` shows the first 3, `-3` shows the last 3. - `align` — `start`, `end`, or `center` of the curve. - `steps` — the maximum number of parts per curve length unit. Defaults to `1`. See the positioning process below. - `tilt` — rotation in degrees applied to every shown part about the card's local Y (long) axis. It applies even without a `curve`, so a bare `tilt` rotates a straight pile. Defaults to `1` when not specified. - `zStart` / `zEnd` — the height (surface-normal) in mm at the start and end of the `curve`. The stack ramps linearly between them across its span, lifting it in 3D. Requires a `curve`. #### positioning process 1. **Determine the step length.** It is `curve length / max(steps, # of parts on path − 1)`. 2. **Determine the alignment.** It places the span of `step length × (# of parts − 1)` on the curve. 3. **Place each part.** Part `#0` is at the start, the last part at the end, each `step length` apart. 4. **Lift each part.** The part's height is `zStart + (zEnd − zStart) × u`, where `u` is its normalized position along the `curve`. 5. **Tilt each part.** Every part is rotated `tilt` about its local Y (long) axis. ### Edge cases - Object with no matching route → **not placed on this surface**. The game state is still valid — the part simply isn't visualized. A surface is a view over the state store, not a mirror of it, and may show only a subset (e.g. a player's hand on the HUD). - Route with no matching object → empty, fine. - Multiple routes match one path -> first route wins. - Multiple parts on one path → **stack** (see §4 Stacking). One route wins for all parts on a path, and the stacking strategy decides what's shown (it may drop parts that are not dropped on other matching routes).