feat(client): AI badge and bot-moving turn indicator
Track aiOpponent in game store; show a pill badge in the topbar for AI games, update turn label to "<Brain> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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';
|
||||
});
|
||||
</script>
|
||||
@@ -89,6 +104,11 @@
|
||||
· You: {game.state.you === 'w' ? 'White' : 'Black'}
|
||||
{/if}
|
||||
</span>
|
||||
{#if aiBadgeText}
|
||||
<span class="ai-badge" class:thinking={isBotTurn}>
|
||||
{aiBadgeText}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if game.state.gameStatus === 'waiting'}
|
||||
@@ -141,7 +161,7 @@
|
||||
<div class="banner err">⚠ {game.state.lastError.code}: {game.state.lastError.message}</div>
|
||||
{/if}
|
||||
|
||||
{#if !game.state.opponentConnected && game.state.gameStatus === 'active'}
|
||||
{#if !game.state.opponentConnected && game.state.gameStatus === 'active' && !game.state.aiOpponent}
|
||||
<div class="banner muted">Opponent disconnected — 5 minute grace window.</div>
|
||||
{/if}
|
||||
</aside>
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user