feat(client): capture-tally panel
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import type { CaptureTally, Color, PieceTally, PieceType } from '@blind-chess/shared';
|
||||
import { pieceGlyph } from './pieces.js';
|
||||
|
||||
interface Props {
|
||||
captures: CaptureTally;
|
||||
you: Color;
|
||||
}
|
||||
let { captures, you }: Props = $props();
|
||||
|
||||
const ORDER: PieceType[] = ['q', 'r', 'b', 'n', 'p'];
|
||||
const oppColor = $derived<Color>(you === 'w' ? 'b' : 'w');
|
||||
|
||||
function glyphs(tally: PieceTally, color: Color): { glyph: string; key: string }[] {
|
||||
const out: { glyph: string; key: string }[] = [];
|
||||
for (const t of ORDER) {
|
||||
const n = tally[t] ?? 0;
|
||||
for (let i = 0; i < n; i++) out.push({ glyph: pieceGlyph({ color, type: t }), key: `${t}${i}` });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function total(tally: PieceTally): number {
|
||||
return Object.values(tally).reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
const youTook = $derived(glyphs(captures.byYou, oppColor));
|
||||
const youLost = $derived(glyphs(captures.byOpponent, you));
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
<header>Captures</header>
|
||||
<div class="row">
|
||||
<span class="label">You took</span>
|
||||
<span class="pieces">
|
||||
{#each youTook as g (g.key)}<span class="g g-{oppColor}">{g.glyph}</span>{:else}<span class="muted">—</span>{/each}
|
||||
</span>
|
||||
<span class="n">{total(captures.byYou)}</span>
|
||||
</div>
|
||||
<div class="row lost">
|
||||
<span class="label">Lost</span>
|
||||
<span class="pieces">
|
||||
{#each youLost as g (g.key)}<span class="g g-{you}">{g.glyph}</span>{:else}<span class="muted">—</span>{/each}
|
||||
</span>
|
||||
<span class="n">{total(captures.byOpponent)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
header {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
.row.lost { opacity: 0.7; }
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
width: 56px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pieces { flex: 1; font-size: 20px; line-height: 1; }
|
||||
.g-w { color: #fafafa; }
|
||||
.g-b { color: #1a1a1a; }
|
||||
.n {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 13px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import { game } from './stores/game.svelte.js';
|
||||
import Board from './Board.svelte';
|
||||
import ModeratorPanel from './ModeratorPanel.svelte';
|
||||
import CaptureTally from './CaptureTally.svelte';
|
||||
import PromotionDialog from './PromotionDialog.svelte';
|
||||
import type { PromotionType, Square } from '@blind-chess/shared';
|
||||
|
||||
@@ -137,6 +138,7 @@
|
||||
|
||||
<aside class="side">
|
||||
<ModeratorPanel announcements={game.state.announcements} you={game.state.you} />
|
||||
<CaptureTally captures={game.state.captures} you={game.state.you} />
|
||||
|
||||
<div class="actions">
|
||||
{#if game.state.gameStatus === 'active'}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
Announcement,
|
||||
BoardView,
|
||||
CaptureTally,
|
||||
ClientMessage,
|
||||
Color,
|
||||
ErrorCode,
|
||||
@@ -29,6 +30,7 @@ interface GameStateValue {
|
||||
opponentConnected: boolean;
|
||||
lastError: { code: ErrorCode; message: string; at: number } | null;
|
||||
aiOpponent: { color: Color; brain: 'casual' | 'recon' } | null;
|
||||
captures: CaptureTally;
|
||||
}
|
||||
|
||||
function makeStore() {
|
||||
@@ -49,6 +51,7 @@ function makeStore() {
|
||||
opponentConnected: false,
|
||||
lastError: null,
|
||||
aiOpponent: null,
|
||||
captures: { byYou: {}, byOpponent: {} },
|
||||
});
|
||||
|
||||
function tokenKey(gameId: string) { return `bc:${gameId}`; }
|
||||
@@ -94,6 +97,7 @@ function makeStore() {
|
||||
state.highlightingEnabled = m.highlightingEnabled;
|
||||
state.opponentConnected = m.opponentConnected;
|
||||
state.aiOpponent = m.aiOpponent ?? null;
|
||||
state.captures = m.captures;
|
||||
if (state.gameId) localStorage.setItem(tokenKey(state.gameId), m.token);
|
||||
break;
|
||||
case 'update':
|
||||
@@ -107,6 +111,7 @@ function makeStore() {
|
||||
state.announcements = [...state.announcements, ...m.newAnnouncements];
|
||||
}
|
||||
if (m.aiOpponent !== undefined) state.aiOpponent = m.aiOpponent;
|
||||
state.captures = m.captures;
|
||||
break;
|
||||
case 'peer-status':
|
||||
if (state.you && m.color !== state.you) {
|
||||
|
||||
Reference in New Issue
Block a user