From 06bd144f7ccc9496aa19196e48b3a472147c8228 Mon Sep 17 00:00:00 2001 From: "claude (blind_chess)" Date: Tue, 28 Apr 2026 14:26:25 -0400 Subject: [PATCH] feat(client): AI badge and bot-moving turn indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track aiOpponent in game store; show a pill badge in the topbar for AI games, update turn label to " is moving…" on the bot's turn, and suppress the disconnected-opponent banner when the opponent is a bot. Co-Authored-By: Claude Sonnet 4.6 --- packages/client/src/lib/Game.svelte | 38 ++++++++++++++++++- packages/client/src/lib/stores/game.svelte.ts | 4 ++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/client/src/lib/Game.svelte b/packages/client/src/lib/Game.svelte index 265146d..3a25dc5 100644 --- a/packages/client/src/lib/Game.svelte +++ b/packages/client/src/lib/Game.svelte @@ -61,6 +61,17 @@ setTimeout(() => copied = false, 1500); } + const isBotTurn = $derived( + !!game.state.aiOpponent + && game.state.gameStatus === 'active' + && game.state.view?.toMove === game.state.aiOpponent.color, + ); + + const aiBadgeText = $derived.by(() => { + if (!game.state.aiOpponent) return null; + return game.state.aiOpponent.brain === 'casual' ? 'Casual bot' : 'gemma4 recon'; + }); + const turnLabel = $derived.by(() => { if (game.state.gameStatus === 'finished') { const reason = game.state.endReason; @@ -74,6 +85,10 @@ if (game.state.gameStatus === 'waiting') return 'Waiting for opponent…'; if (!game.state.you) return '…'; if (game.state.view?.toMove === game.state.you) return 'Your turn'; + if (game.state.aiOpponent) { + const name = game.state.aiOpponent.brain === 'casual' ? 'Casual bot' : 'gemma4 recon'; + return `${name} is moving…`; + } return 'Opponent thinking'; }); @@ -89,6 +104,11 @@ · You: {game.state.you === 'w' ? 'White' : 'Black'} {/if} + {#if aiBadgeText} + + {aiBadgeText} + + {/if} {#if game.state.gameStatus === 'waiting'} @@ -141,7 +161,7 @@ {/if} - {#if !game.state.opponentConnected && game.state.gameStatus === 'active'} + {#if !game.state.opponentConnected && game.state.gameStatus === 'active' && !game.state.aiOpponent} {/if} @@ -221,6 +241,22 @@ .banner .row { display: flex; gap: 8px; margin-top: 8px; } .banner .row button { flex: 1; } + .ai-badge { + font-size: 12px; + font-weight: 600; + padding: 4px 10px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--panel); + color: var(--text-dim); + white-space: nowrap; + } + .ai-badge.thinking { + color: var(--accent); + border-color: var(--accent); + background: rgba(211, 84, 0, 0.07); + } + .waiting-card { background: var(--panel); border: 1px solid var(--border); diff --git a/packages/client/src/lib/stores/game.svelte.ts b/packages/client/src/lib/stores/game.svelte.ts index b3c1a62..06b1691 100644 --- a/packages/client/src/lib/stores/game.svelte.ts +++ b/packages/client/src/lib/stores/game.svelte.ts @@ -28,6 +28,7 @@ interface GameStateValue { winner: Color | null; opponentConnected: boolean; lastError: { code: ErrorCode; message: string; at: number } | null; + aiOpponent: { color: Color; brain: 'casual' | 'recon' } | null; } function makeStore() { @@ -47,6 +48,7 @@ function makeStore() { winner: null, opponentConnected: false, lastError: null, + aiOpponent: null, }); function tokenKey(gameId: string) { return `bc:${gameId}`; } @@ -91,6 +93,7 @@ function makeStore() { state.mode = m.mode; state.highlightingEnabled = m.highlightingEnabled; state.opponentConnected = m.opponentConnected; + state.aiOpponent = m.aiOpponent ?? null; if (state.gameId) localStorage.setItem(tokenKey(state.gameId), m.token); break; case 'update': @@ -103,6 +106,7 @@ function makeStore() { if (m.newAnnouncements.length) { state.announcements = [...state.announcements, ...m.newAnnouncements]; } + if (m.aiOpponent !== undefined) state.aiOpponent = m.aiOpponent; break; case 'peer-status': if (state.you && m.color !== state.you) {