feat(bgm): add board game manifest loader

Parse yaml/json/toml and markdown code blocks into packages, expanding
$variants via typed-csv and collecting parts, surfaces, and setups by
include patterns. Ships zod validation, a vitest config, and 16 tests.
This commit is contained in:
2026-08-09 18:59:14 +08:00
parent 1cfec40c99
commit c0967ab71f
14 changed files with 1242 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadDefs, collectPackages } from './collect.js';
const fixtureRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '__fixtures__');
describe('collectPackages', () => {
it('collects the harbor package from markdown code blocks', () => {
const defMap = loadDefs('', fixtureRoot);
const packages = collectPackages(defMap, fixtureRoot);
expect(packages).toHaveLength(1);
const harbor = packages[0]!;
expect(harbor.meta).toMatchObject({ id: 'harbor', title: 'Harbor', designer: 'Jane Doe' });
// Two tokens from two yaml blocks sharing a `file=` name.
expect([...harbor.parts.keys()].sort()).toEqual(['token#grain', 'token#wood']);
const wood = harbor.parts.get('token#wood')!;
expect(wood).toMatchObject({
type: 'token',
id: 'wood',
size: [20, 20, 3],
fillet: 2,
});
expect(wood.face).toBe('./assets/tokens.png');
expect(wood.faceCrop).toEqual([1, 0, 5, 2]);
// One surface with a $variants-expanded candidates list.
expect([...harbor.surfaces.keys()]).toEqual(['board#harbor']);
const board = harbor.surfaces.get('board#harbor')!;
expect(board.size).toEqual([300, 200]);
expect(board.layout).toHaveLength(2);
const dock = board.layout[0]!;
expect(dock.route).toBe('/dock/:seat');
expect(dock.candidates).toEqual([
{ seat: '0', x: 40, y: 0, rotation: 0 },
{ seat: '1', x: 40, y: 20, rotation: 0 },
]);
const deck = board.layout[1]!;
expect(deck.route).toBe('/deck');
expect(deck).toMatchObject({ x: -100, y: 0, rotation: 0 });
// One setup.
expect([...harbor.setups.keys()]).toEqual(['game#main']);
const setup = harbor.setups.get('game#main')!;
expect(setup.setup).toEqual({
'/dock/0': 'harbor:token#wood',
'/deck': 'harbor:token#grain',
});
});
it('throws on a duplicate type#id', () => {
const defMap = loadDefs('', fixtureRoot);
// Inject a duplicate part into the map under a new file name.
const tokensKey = [...defMap.defs.keys()].find((k) => k.endsWith('parts/tokens.yaml'))!;
const tokens = defMap.defs.get(tokensKey)!;
defMap.defs.set(tokensKey.replace('tokens.yaml', 'dup.yaml'), [tokens[0]!]);
expect(() => collectPackages(defMap, fixtureRoot)).toThrow(/Duplicate part/);
});
});