feat(client): wire the phantom opponent-model layer into the game view

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude (blind_chess)
2026-05-18 20:45:40 -04:00
parent 816f89be36
commit 313837eb21
+66 -1
View File
@@ -5,6 +5,10 @@
import ModeratorPanel from './ModeratorPanel.svelte';
import CaptureTally from './CaptureTally.svelte';
import PromotionDialog from './PromotionDialog.svelte';
import PhantomPalette from './PhantomPalette.svelte';
import { pieceGlyph } from './pieces.js';
import { phantoms } from './stores/phantoms.svelte.js';
import { phantomDrag } from './stores/phantom-drag.svelte.js';
import type { PromotionType, Square } from '@blind-chess/shared';
interface Props { gameId: string; }
@@ -92,6 +96,37 @@
}
return 'Opponent thinking';
});
const oppColor = $derived<'w' | 'b'>(game.state.you === 'w' ? 'b' : 'w');
// Phantom layer is blind-mode-only and shown only during active play.
const phantomLayerEnabled = $derived(
game.state.mode === 'blind' && game.state.gameStatus === 'active',
);
// The piece type currently being dragged (for the floating ghost), or null.
const dragGhost = $derived.by(() => {
const a = phantomDrag.state.active;
return a && phantomDrag.state.moved ? a.type : null;
});
// Load the phantom layer once `you` is known (blind games only).
let phantomsLoaded = $state(false);
$effect(() => {
if (phantomsLoaded) return;
const you = game.state.you;
if (you && game.state.mode === 'blind') {
untrack(() => phantoms.loadForGame(gameId, you));
phantomsLoaded = true;
}
});
// Drop the phantom layer when the game ends.
$effect(() => {
if (game.state.gameStatus === 'finished') {
untrack(() => phantoms.clearForGame(gameId));
}
});
</script>
<div class="game-layout" class:waiting={game.state.gameStatus === 'waiting'}>
@@ -129,11 +164,16 @@
toMove={game.state.view.toMove}
mode={game.state.mode ?? 'blind'}
highlightingEnabled={game.state.highlightingEnabled}
phantoms={phantomLayerEnabled ? phantoms.state.phantoms : {}}
phantomsEnabled={phantomLayerEnabled}
armedSquare={armedSquare}
touchedSquare={game.state.touchedPiece}
{onArm}
{onCommit}
/>
{#if phantomLayerEnabled}
<PhantomPalette {oppColor} />
{/if}
</div>
<aside class="side">
@@ -170,6 +210,13 @@
{/if}
</div>
{#if dragGhost}
<div
class="drag-ghost piece-{oppColor}"
style="left: {phantomDrag.state.x}px; top: {phantomDrag.state.y}px;"
>{pieceGlyph({ color: oppColor, type: dragGhost })}</div>
{/if}
{#if pendingPromotion && game.state.you}
<PromotionDialog
color={game.state.you}
@@ -221,7 +268,13 @@
color: var(--text-dim);
}
.board-area { display: flex; align-items: center; justify-content: center; }
.board-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
}
.side {
display: flex;
@@ -270,4 +323,16 @@
.waiting-card h2 { margin: 0 0 8px; font-size: 18px; }
.link-row { display: flex; gap: 8px; margin-top: 16px; }
.link { flex: 1; font-family: ui-monospace, monospace; font-size: 13px; }
.drag-ghost {
position: fixed;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 1000;
font-size: 44px;
line-height: 1;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
}
.drag-ghost.piece-w { color: #fafafa; }
.drag-ghost.piece-b { color: #1a1a1a; }
</style>