feat(bgm): model setup as ordered placements

Replace the path->parts setup map with an ordered list of placements, each moving its parts to a path. Entries apply in order, so a part listed in a later placement ends up on that placement's path.
This commit is contained in:
2026-08-10 11:27:46 +08:00
parent bc418b2c73
commit c2693277ac
6 changed files with 36 additions and 12 deletions
@@ -103,6 +103,8 @@ surfaces:
- board#harbor - board#harbor
- board#player - board#player
setup: setup:
/dock/0: harbor:token#wood - path: /dock/0
/deck: harbor:token#grain parts: harbor:token#wood
- path: /deck
parts: harbor:token#grain
``` ```
@@ -48,5 +48,6 @@ role: setup
type: game type: game
id: main id: main
setup: setup:
/factory/0: azul:tile#blue - path: /factory/0
parts: azul:tile#blue
``` ```
@@ -53,6 +53,8 @@ role: setup
type: game type: game
id: main id: main
setup: setup:
/dock/0: harbor:token#wood - path: /dock/0
/deck: harbor:token#grain parts: harbor:token#wood
- path: /deck
parts: harbor:token#grain
``` ```
+4 -4
View File
@@ -55,10 +55,10 @@ describe('collectPackages', () => {
expect([...harbor.setups.keys()]).toEqual(['game#main']); expect([...harbor.setups.keys()]).toEqual(['game#main']);
const setup = harbor.setups.get('game#main')!; const setup = harbor.setups.get('game#main')!;
expect(setup.surfaces).toEqual(['board#harbor', 'board#player']); expect(setup.surfaces).toEqual(['board#harbor', 'board#player']);
expect(setup.setup).toEqual({ expect(setup.setup).toEqual([
'/dock/0': 'harbor:token#wood', { path: '/dock/0', parts: 'harbor:token#wood' },
'/deck': 'harbor:token#grain', { path: '/deck', parts: 'harbor:token#grain' },
}); ]);
}); });
it('scopes include patterns to the package declaration directory', () => { it('scopes include patterns to the package declaration directory', () => {
+8 -1
View File
@@ -60,11 +60,18 @@ const surfaceSchema = z.object({
layout: z.array(route), layout: z.array(route),
}); });
const setupValue = z.union([z.string(), z.array(z.string())]);
const setupPlacement = z.object({
path: z.string(),
parts: setupValue,
});
const setupSchema = z.object({ const setupSchema = z.object({
type: z.string().min(1), type: z.string().min(1),
id: z.string().min(1), id: z.string().min(1),
surfaces: z.array(z.string()).optional(), surfaces: z.array(z.string()).optional(),
setup: z.record(z.string(), z.union([z.string(), z.array(z.string())])), setup: z.array(setupPlacement),
}); });
const packageSchema = z.object({ const packageSchema = z.object({
+14 -2
View File
@@ -157,13 +157,25 @@ export interface Surface {
export type SetupValue = string | string[]; export type SetupValue = string | string[];
/** Seeds the state store: the enabled surfaces and a map from path to parts. */ /**
* One setup placement: move `parts` to `path`. Entries are applied in order,
* so a part listed in a later entry ends up on that entry's path.
*/
export interface SetupPlacement {
/** The path key to place the parts on. */
path: string;
/** Parts to place: a part id, a bare type (expands to all of that type), or a list of either. */
parts: SetupValue;
}
/** Seeds the state store: the enabled surfaces and an ordered list of placements. */
export interface Setup { export interface Setup {
type: string; type: string;
id: string; id: string;
/** Surfaces (`type#id` refs) enabled at the start; omitted = all enabled. */ /** Surfaces (`type#id` refs) enabled at the start; omitted = all enabled. */
surfaces?: string[]; surfaces?: string[];
setup: Record<string, SetupValue>; /** Ordered placements; each moves its parts to its path. */
setup: SetupPlacement[];
} }
export type Role = 'package' | 'part' | 'surface' | 'setup'; export type Role = 'package' | 'part' | 'surface' | 'setup';