feat: honest AI opponent in quick game
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s

New Game's quick game gains an explicit opponent selector — 🤖 AI (default)
or 👤 Random player. AI starts a game seated with a pooled robot that joins
and moves at once: no per-move timeout (a 7-day inactivity loss reusing the
turn-timeout sweeper), chat/nudge disabled, no statistics, the opponent shown
as 🤖 everywhere. The random path (disguised robot) is unchanged.

Driven by one game flag (games.vs_ai), set only on AI-started games so the
disguised path is never revealed; Matchmaker.StartVsAI seats the robot
directly (no open pool); the robot replies event-driven via the game service's
after-commit/after-create hook. Wire: vs_ai on EnqueueRequest + GameView.
This commit is contained in:
Ilia Denisov
2026-06-15 20:14:24 +02:00
parent 91d5c341ef
commit aa765a0c06
56 changed files with 901 additions and 86 deletions
+8 -3
View File
@@ -12,6 +12,7 @@
canNudge = false,
waiting = false,
nudgeOnCooldown = false,
vsAi = false,
onsend,
onnudge,
}: {
@@ -34,6 +35,9 @@
// are disabled (there is no one to message or hurry yet).
waiting?: boolean;
nudgeOnCooldown?: boolean;
// vsAi disables both controls in an honest-AI game: the opponent is a robot, so there is
// nobody to message or hurry. The fields still show (turn-driven), but disabled.
vsAi?: boolean;
onsend: (text: string) => void;
onnudge: () => void;
} = $props();
@@ -67,9 +71,10 @@
maxlength="60"
placeholder={t('chat.placeholder')}
bind:value={text}
onkeydown={(e) => e.key === 'Enter' && !busy && send()}
disabled={vsAi}
onkeydown={(e) => e.key === 'Enter' && !busy && !vsAi && send()}
/>
<button class="iconbtn" onclick={send} disabled={busy || waiting || !connection.online} aria-label={t('chat.send')}>⬆️</button>
<button class="iconbtn" onclick={send} disabled={busy || waiting || vsAi || !connection.online} aria-label={t('chat.send')}>⬆️</button>
{:else if myTurn}
<!-- Own turn, the one allowed message already sent: a caption holds the row until the
next turn (no nudge — there is no one to hurry on your own turn). -->
@@ -77,7 +82,7 @@
{:else if canNudge}
<!-- A flex:1 caption keeps the nudge pinned right whether or not the cooldown text shows. -->
<span class="cooldown">{nudgeOnCooldown ? t('chat.awaitingReply') : ''}</span>
<button class="iconbtn" onclick={onnudge} disabled={busy || waiting || nudgeOnCooldown || !connection.online} aria-label={t('chat.nudgeAction')}>🛎️</button>
<button class="iconbtn" onclick={onnudge} disabled={busy || waiting || nudgeOnCooldown || vsAi || !connection.online} aria-label={t('chat.nudgeAction')}>🛎️</button>
{/if}
</div>
</div>