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 { game } from './stores/game.svelte.js';
|
||||||
import Board from './Board.svelte';
|
import Board from './Board.svelte';
|
||||||
import ModeratorPanel from './ModeratorPanel.svelte';
|
import ModeratorPanel from './ModeratorPanel.svelte';
|
||||||
|
import CaptureTally from './CaptureTally.svelte';
|
||||||
import PromotionDialog from './PromotionDialog.svelte';
|
import PromotionDialog from './PromotionDialog.svelte';
|
||||||
import type { PromotionType, Square } from '@blind-chess/shared';
|
import type { PromotionType, Square } from '@blind-chess/shared';
|
||||||
|
|
||||||
@@ -137,6 +138,7 @@
|
|||||||
|
|
||||||
<aside class="side">
|
<aside class="side">
|
||||||
<ModeratorPanel announcements={game.state.announcements} you={game.state.you} />
|
<ModeratorPanel announcements={game.state.announcements} you={game.state.you} />
|
||||||
|
<CaptureTally captures={game.state.captures} you={game.state.you} />
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if game.state.gameStatus === 'active'}
|
{#if game.state.gameStatus === 'active'}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
Announcement,
|
Announcement,
|
||||||
BoardView,
|
BoardView,
|
||||||
|
CaptureTally,
|
||||||
ClientMessage,
|
ClientMessage,
|
||||||
Color,
|
Color,
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
@@ -29,6 +30,7 @@ interface GameStateValue {
|
|||||||
opponentConnected: boolean;
|
opponentConnected: boolean;
|
||||||
lastError: { code: ErrorCode; message: string; at: number } | null;
|
lastError: { code: ErrorCode; message: string; at: number } | null;
|
||||||
aiOpponent: { color: Color; brain: 'casual' | 'recon' } | null;
|
aiOpponent: { color: Color; brain: 'casual' | 'recon' } | null;
|
||||||
|
captures: CaptureTally;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeStore() {
|
function makeStore() {
|
||||||
@@ -49,6 +51,7 @@ function makeStore() {
|
|||||||
opponentConnected: false,
|
opponentConnected: false,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
aiOpponent: null,
|
aiOpponent: null,
|
||||||
|
captures: { byYou: {}, byOpponent: {} },
|
||||||
});
|
});
|
||||||
|
|
||||||
function tokenKey(gameId: string) { return `bc:${gameId}`; }
|
function tokenKey(gameId: string) { return `bc:${gameId}`; }
|
||||||
@@ -94,6 +97,7 @@ function makeStore() {
|
|||||||
state.highlightingEnabled = m.highlightingEnabled;
|
state.highlightingEnabled = m.highlightingEnabled;
|
||||||
state.opponentConnected = m.opponentConnected;
|
state.opponentConnected = m.opponentConnected;
|
||||||
state.aiOpponent = m.aiOpponent ?? null;
|
state.aiOpponent = m.aiOpponent ?? null;
|
||||||
|
state.captures = m.captures;
|
||||||
if (state.gameId) localStorage.setItem(tokenKey(state.gameId), m.token);
|
if (state.gameId) localStorage.setItem(tokenKey(state.gameId), m.token);
|
||||||
break;
|
break;
|
||||||
case 'update':
|
case 'update':
|
||||||
@@ -107,6 +111,7 @@ function makeStore() {
|
|||||||
state.announcements = [...state.announcements, ...m.newAnnouncements];
|
state.announcements = [...state.announcements, ...m.newAnnouncements];
|
||||||
}
|
}
|
||||||
if (m.aiOpponent !== undefined) state.aiOpponent = m.aiOpponent;
|
if (m.aiOpponent !== undefined) state.aiOpponent = m.aiOpponent;
|
||||||
|
state.captures = m.captures;
|
||||||
break;
|
break;
|
||||||
case 'peer-status':
|
case 'peer-status':
|
||||||
if (state.you && m.color !== state.you) {
|
if (state.you && m.color !== state.you) {
|
||||||
|
|||||||
Reference in New Issue
Block a user