import { describe, expect, it } from 'vitest'; import type { Package, Surface } from '@tts/bgm'; import { matchRoute, childrenByPath, computeSurfacePlacements, computeRenderState, placementKey, } from './state.js'; function makeSurface(overrides: Partial = {}): Surface { return { type: 'board', id: 'harbor', layout: [], ...overrides, }; } const pkg: Package = { meta: { id: 'harbor' }, parts: new Map(), surfaces: new Map([ ['board#harbor', makeSurface()], ['hud#hand', makeSurface({ type: 'hud', id: 'hand' })], ]), setups: new Map(), }; describe('matchRoute', () => { it('matches a literal path', () => { const route = { route: '/deck', x: 0, y: 0, rotation: 0 }; expect(matchRoute(route, '/deck')).toEqual({ candidate: undefined }); expect(matchRoute(route, '/other')).toBeNull(); }); it('matches a :param against a candidate', () => { const route = { route: '/dock/:seat', x: 0, y: 0, rotation: 0, candidates: [ { seat: '0', x: 40, y: 0 }, { seat: '1', x: 40, y: 20 }, ], }; expect(matchRoute(route, '/dock/1')).toEqual({ candidate: { seat: '1', x: 40, y: 20 } }); }); it('fails when no candidate matches the param', () => { const route = { route: '/dock/:seat', x: 0, y: 0, rotation: 0, candidates: [{ seat: '0', x: 40, y: 0 }], }; expect(matchRoute(route, '/dock/9')).toBeNull(); }); it('fails on length mismatch', () => { const route = { route: '/deck', x: 0, y: 0, rotation: 0 }; expect(matchRoute(route, '/deck/extra')).toBeNull(); }); }); describe('childrenByPath', () => { it('groups parts by path, ordered by index', () => { const parts = { 'harbor:card#a': { path: '/deck', index: 1, facing: 'face' }, 'harbor:card#b': { path: '/deck', index: 0, facing: 'back' }, 'harbor:card#c': { path: '/community/0', index: 0, facing: 'standing' }, }; expect(childrenByPath(parts)).toEqual({ '/deck': ['harbor:card#b', 'harbor:card#a'], '/community/0': ['harbor:card#c'], }); }); }); describe('computeSurfacePlacements', () => { it('places parts on a matching route with index, stackSize, and facing', () => { const surface = makeSurface({ layout: [{ route: '/deck', x: -100, y: 0, rotation: 0 }], }); const placements = computeSurfacePlacements(surface, { 'harbor:card#a': { path: '/deck', index: 0, facing: 'face' }, 'harbor:card#b': { path: '/deck', index: 1, facing: 'back' }, }); expect(placements).toHaveLength(2); expect(placements[0]).toMatchObject({ piece: 'harbor:card#a', index: 0, stackSize: 2, facing: 'face' }); expect(placements[1]).toMatchObject({ piece: 'harbor:card#b', index: 1, stackSize: 2, facing: 'back' }); }); it('drops parts with no matching route', () => { const surface = makeSurface({ layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }] }); const placements = computeSurfacePlacements(surface, { 'harbor:card#a': { path: '/deck', index: 0, facing: 'face' }, 'harbor:card#b': { path: '/elsewhere', index: 0, facing: 'face' }, }); expect(placements).toHaveLength(1); expect(placements[0]!.piece).toBe('harbor:card#a'); }); it('uses the candidate anchor for a :param route', () => { const surface = makeSurface({ layout: [ { route: '/dock/:seat', x: 0, y: 0, rotation: 0, candidates: [{ seat: '0', x: 40, y: 5, rotation: 1 }], }, ], }); const placements = computeSurfacePlacements(surface, { 'harbor:boat#fleet': { path: '/dock/0', index: 0, facing: 'face' }, }); expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, rotation: 1 }); }); it('keeps the candidate stacking alongside its anchor', () => { const surface = makeSurface({ layout: [ { route: '/dock/:seat', x: 0, y: 0, rotation: 0, candidates: [{ seat: '0', x: 40, y: 5, stacking: { tilt: 2 } }], }, ], }); const placements = computeSurfacePlacements(surface, { 'harbor:boat#fleet': { path: '/dock/0', index: 0, facing: 'face' }, }); expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, stacking: { tilt: 2 } }); }); it('orders a path by index regardless of insertion order', () => { const surface = makeSurface({ layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }], }); const placements = computeSurfacePlacements(surface, { 'harbor:card#b': { path: '/deck', index: 1, facing: 'face' }, 'harbor:card#a': { path: '/deck', index: 0, facing: 'face' }, }); expect(placements.map((p) => p.piece)).toEqual(['harbor:card#a', 'harbor:card#b']); }); }); describe('computeRenderState', () => { it('only includes enabled surfaces', () => { const state = { surfaces: { 'board#harbor': true, 'hud#hand': false }, parts: { 'harbor:card#a': { path: '/deck', index: 0, facing: 'face' } }, }; pkg.surfaces.set( 'board#harbor', makeSurface({ layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }] }), ); const placements = computeRenderState(pkg, state); expect(placements).toHaveLength(1); expect(placements[0]!.surface).toBe('board#harbor'); }); }); describe('placementKey', () => { it('is unique per surface and piece', () => { const a = { surface: 'board#harbor', piece: 'harbor:card#a' } as never; const b = { surface: 'board#harbor', piece: 'harbor:card#b' } as never; const c = { surface: 'hud#hand', piece: 'harbor:card#a' } as never; expect(placementKey(a)).not.toBe(placementKey(b)); expect(placementKey(a)).not.toBe(placementKey(c)); }); });