From d9d38c2bee7e61d568549171353f2d904ba3764f Mon Sep 17 00:00:00 2001 From: hypercross Date: Sun, 9 Aug 2026 22:46:06 +0800 Subject: [PATCH] fix(tabletop): key placements by surface, path, and piece A piece can appear on more than one path of the same surface (e.g. the poker deck expands to every card while the flop also places the ace), so the render key must include the path to stay unique. --- packages/tabletop/src/state.test.ts | 32 ++++++++++++++++--- packages/tabletop/src/state.ts | 7 ++-- .../tabletop/src/surfaces/HudSurfaceView.tsx | 4 +-- .../src/surfaces/WorldSurfaceView.tsx | 4 +-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/tabletop/src/state.test.ts b/packages/tabletop/src/state.test.ts index 741686a..d06e6b0 100644 --- a/packages/tabletop/src/state.test.ts +++ b/packages/tabletop/src/state.test.ts @@ -97,6 +97,27 @@ describe('computeSurfacePlacements', () => { const placements = computeSurfacePlacements(surface, { '/dock/0': ['harbor:boat#fleet'] }); expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, rotation: 1 }); }); + + it('keeps the same piece on two paths as distinct placements', () => { + const surface = makeSurface({ + layout: [ + { route: '/deck', x: 0, y: 0, rotation: 0 }, + { route: '/community/:slot', 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'], + }); + 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]!)); + }); }); describe('computeRenderState', () => { @@ -116,11 +137,14 @@ describe('computeRenderState', () => { }); 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; + 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; expect(placementKey(a)).not.toBe(placementKey(b)); expect(placementKey(a)).not.toBe(placementKey(c)); + expect(placementKey(a)).not.toBe(placementKey(d)); }); }); \ No newline at end of file diff --git a/packages/tabletop/src/state.ts b/packages/tabletop/src/state.ts index fa84326..17ca2c9 100644 --- a/packages/tabletop/src/state.ts +++ b/packages/tabletop/src/state.ts @@ -22,6 +22,8 @@ export interface GameState { export interface Placement { /** Surface id (`type#id`). */ surface: string; + /** The path key this placement came from. */ + path: string; /** The matched route. */ route: Route; /** The matched candidate, when the route has `:param`s. */ @@ -97,6 +99,7 @@ export function computeSurfacePlacements(surface: Surface, paths: Record {own.map((p) => ( - + ))} {node.children.map((child) => ( diff --git a/packages/tabletop/src/surfaces/WorldSurfaceView.tsx b/packages/tabletop/src/surfaces/WorldSurfaceView.tsx index 631664e..e2b6dd0 100644 --- a/packages/tabletop/src/surfaces/WorldSurfaceView.tsx +++ b/packages/tabletop/src/surfaces/WorldSurfaceView.tsx @@ -7,7 +7,7 @@ * it's never rendered. */ import type { Package } from '@tts/bgm'; -import { useRenderState } from '../state.js'; +import { useRenderState, placementKey } from '../state.js'; import { PartPlacement } from '../placement.js'; import type { MountNode } from '../mount.js'; @@ -23,7 +23,7 @@ export function SurfaceNode({ pkg, node }: { pkg: Package; node: MountNode }) { return ( {own.map((p) => ( - + ))} {node.children.map((child) => (