Model part face in tabletop state
Replace the path->part-list store with a per-part map keyed by part id, so each placed part carries its path, stack index, and face. Derive each path's ordered children for stacking, and flip face-down parts in PartPlacement.
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Package, Surface } from '@tts/bgm';
|
||||
import { matchRoute, computeSurfacePlacements, computeRenderState, placementKey } from './state.js';
|
||||
import {
|
||||
matchRoute,
|
||||
childrenByPath,
|
||||
computeSurfacePlacements,
|
||||
computeRenderState,
|
||||
placementKey,
|
||||
} from './state.js';
|
||||
|
||||
function makeSurface(overrides: Partial<Surface> = {}): Surface {
|
||||
return {
|
||||
@@ -59,24 +65,39 @@ describe('matchRoute', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('childrenByPath', () => {
|
||||
it('groups parts by path, ordered by index', () => {
|
||||
const parts = {
|
||||
'harbor:card#a': { path: '/deck', index: 1, face: true },
|
||||
'harbor:card#b': { path: '/deck', index: 0, face: false },
|
||||
'harbor:card#c': { path: '/community/0', index: 0, face: true },
|
||||
};
|
||||
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 and stackSize', () => {
|
||||
it('places parts on a matching route with index, stackSize, and face', () => {
|
||||
const surface = makeSurface({
|
||||
layout: [{ route: '/deck', x: -100, y: 0, rotation: 0 }],
|
||||
});
|
||||
const placements = computeSurfacePlacements(surface, {
|
||||
'/deck': ['harbor:card#a', 'harbor:card#b'],
|
||||
'harbor:card#a': { path: '/deck', index: 0, face: true },
|
||||
'harbor:card#b': { path: '/deck', index: 1, face: false },
|
||||
});
|
||||
expect(placements).toHaveLength(2);
|
||||
expect(placements[0]).toMatchObject({ piece: 'harbor:card#a', index: 0, stackSize: 2 });
|
||||
expect(placements[1]).toMatchObject({ piece: 'harbor:card#b', index: 1, stackSize: 2 });
|
||||
expect(placements[0]).toMatchObject({ piece: 'harbor:card#a', index: 0, stackSize: 2, face: true });
|
||||
expect(placements[1]).toMatchObject({ piece: 'harbor:card#b', index: 1, stackSize: 2, face: false });
|
||||
});
|
||||
|
||||
it('drops parts with no matching route', () => {
|
||||
const surface = makeSurface({ layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }] });
|
||||
const placements = computeSurfacePlacements(surface, {
|
||||
'/deck': ['harbor:card#a'],
|
||||
'/elsewhere': ['harbor:card#b'],
|
||||
'harbor:card#a': { path: '/deck', index: 0, face: true },
|
||||
'harbor:card#b': { path: '/elsewhere', index: 0, face: true },
|
||||
});
|
||||
expect(placements).toHaveLength(1);
|
||||
expect(placements[0]!.piece).toBe('harbor:card#a');
|
||||
@@ -94,7 +115,9 @@ describe('computeSurfacePlacements', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
const placements = computeSurfacePlacements(surface, { '/dock/0': ['harbor:boat#fleet'] });
|
||||
const placements = computeSurfacePlacements(surface, {
|
||||
'harbor:boat#fleet': { path: '/dock/0', index: 0, face: true },
|
||||
});
|
||||
expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, rotation: 1 });
|
||||
});
|
||||
|
||||
@@ -110,29 +133,21 @@ describe('computeSurfacePlacements', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
const placements = computeSurfacePlacements(surface, { '/dock/0': ['harbor:boat#fleet'] });
|
||||
const placements = computeSurfacePlacements(surface, {
|
||||
'harbor:boat#fleet': { path: '/dock/0', index: 0, face: true },
|
||||
});
|
||||
expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, stacking: { tilt: 2 } });
|
||||
});
|
||||
|
||||
it('keeps the same piece on two paths as distinct placements', () => {
|
||||
it('orders a path by index regardless of insertion order', () => {
|
||||
const surface = makeSurface({
|
||||
layout: [
|
||||
{ route: '/deck', x: 0, y: 0, rotation: 0 },
|
||||
{ route: '/community/:slot', x: 0, y: 0, rotation: 0 },
|
||||
],
|
||||
layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }],
|
||||
});
|
||||
// The deck expands to every card (including `as`); the flop also places `as`.
|
||||
const placements = computeSurfacePlacements(surface, {
|
||||
'/deck': ['poker:card#as', 'poker:card#kh'],
|
||||
'/community/0': ['poker:card#as'],
|
||||
'harbor:card#b': { path: '/deck', index: 1, face: true },
|
||||
'harbor:card#a': { path: '/deck', index: 0, face: true },
|
||||
});
|
||||
expect(placements).toHaveLength(3);
|
||||
const deck = placements.filter((p) => p.path === '/deck');
|
||||
const flop = placements.filter((p) => p.path === '/community/0');
|
||||
expect(deck).toHaveLength(2);
|
||||
expect(flop).toHaveLength(1);
|
||||
// The same piece on two paths yields distinct placement keys.
|
||||
expect(placementKey(deck[0]!)).not.toBe(placementKey(flop[0]!));
|
||||
expect(placements.map((p) => p.piece)).toEqual(['harbor:card#a', 'harbor:card#b']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,7 +155,7 @@ describe('computeRenderState', () => {
|
||||
it('only includes enabled surfaces', () => {
|
||||
const state = {
|
||||
surfaces: { 'board#harbor': true, 'hud#hand': false },
|
||||
paths: { '/deck': ['harbor:card#a'] },
|
||||
parts: { 'harbor:card#a': { path: '/deck', index: 0, face: true } },
|
||||
};
|
||||
pkg.surfaces.set(
|
||||
'board#harbor',
|
||||
@@ -153,14 +168,11 @@ describe('computeRenderState', () => {
|
||||
});
|
||||
|
||||
describe('placementKey', () => {
|
||||
it('is unique per surface, path, and piece', () => {
|
||||
const a = { surface: 'board#harbor', path: '/deck', piece: 'harbor:card#a' } as never;
|
||||
const b = { surface: 'board#harbor', path: '/deck', piece: 'harbor:card#b' } as never;
|
||||
const c = { surface: 'hud#hand', path: '/deck', piece: 'harbor:card#a' } as never;
|
||||
// The same piece on two paths of the same surface is a distinct placement.
|
||||
const d = { surface: 'board#harbor', path: '/community/0', piece: 'harbor:card#a' } as never;
|
||||
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));
|
||||
expect(placementKey(a)).not.toBe(placementKey(d));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user