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
+24 -4
View File
@@ -361,8 +361,24 @@ Key points:
Substitutes for a human in 2-player auto-match: the matchmaking reaper seats it in an
open game's empty opponent slot when no human has joined within the wait window (§8).
It lives in `internal/robot` and plays as an ordinary seated account through the game
service, so only `internal/engine` imports the solver. It is designed to be
indistinguishable from a person.
service, so only `internal/engine` imports the solver. In the random/auto-match path it is
designed to be indistinguishable from a person.
The same robot serves **two quick-game modes**, chosen by the player on New Game and recorded
on the game as **`games.vs_ai`**: the **random** path above (disguised as a person) and an
**honest-AI** path the player knowingly picks (shown as **🤖**). The mode is a per-game flag,
never derived from the opponent account, so the disguised path is never revealed. In an
honest-AI game the robot keeps its per-game strength (`playToWin`) and margin band but **moves
at once** — no sampled delay, no sleep window, no proactive nudge; chat and nudge are disabled,
the opponent is shown as 🤖 everywhere, and the game records **no statistics** for either seat
(practice, like a guest game). The fast reply is **event-driven**: committing a move (or
creating the game) triggers `robot.DriveGame` immediately via the game service's after-commit /
after-create hook (`game.Service.SetAITrigger`, a func value so the game package never imports
the robot package), with the periodic driver as the fallback. There is **no short move
timeout**; instead the game is created with `turn_timeout_secs = AIInactivityTimeout` (**7
days**) and the existing turn-timeout sweeper resigns the overdue seat — since the robot moves
at once, only the human is ever on the clock, so the per-turn timeout doubles as the
"abandoned after 7 days of inactivity → loss" rule with no new column or sweeper.
The robot keeps **no per-game state**: every choice is derived deterministically
from the game's bag `seed` (a restart-stable FNV-1a mix), so a background driver
@@ -406,8 +422,12 @@ English game the Latin pool.
## 8. Lobby & social
- **Matchmaking**: auto-match drops the player **straight into a real game and lets
them wait inside it**. `Enqueue` (`POST /lobby/enqueue`) opens a game seating the
- **Matchmaking**: a quick game offers **two opponents** on New Game — an **honest AI** (the
default) or a **random** human (§7). The AI choice (`vs_ai` on `POST /lobby/enqueue`) takes the
`Matchmaker.StartVsAI` path, which picks a pooled robot and creates a game **already seated and
active** (`vs_ai`, random seat order); it never enters the open pool, so the reaper below never
touches it. The random choice drops the player **straight into a real game and lets
them wait inside it**: `Enqueue` (`POST /lobby/enqueue`) opens a game seating the
caller with an **empty opponent seat** (status `open`, §9), or — when another player
is already waiting for the same `variant` and per-turn rule — seats the caller into
that open game and starts it; which seat the caller takes is randomised for