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
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:
+24
-4
@@ -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
|
||||
|
||||
+14
-2
@@ -84,8 +84,14 @@ Russian → Scrabble + Erudite; a bilingual service shows all three, and the web
|
||||
unrestricted). Variants are shown by their **display name** — both Scrabble variants read
|
||||
"Scrabble"/"Скрэббл" and Erudit reads "Erudite"/"Эрудит" (by the interface language), and
|
||||
the same name titles the in-game screen. This gates only **starting** a new game — both auto-match and a friend
|
||||
invitation — so a player still sees and plays existing games of any language. Auto-match
|
||||
(always 2 players) drops you **straight into the game and you wait inside it**: if it is your turn you
|
||||
invitation — so a player still sees and plays existing games of any language.
|
||||
|
||||
**Quick game** lets you choose your opponent — an **AI** (the default) or a **random player**.
|
||||
With **AI** you start at once against a 🤖 that joins and replies immediately: there is no waiting,
|
||||
chat and nudge are off, add-friend is never shown, and the word-check tool still works; instead of a
|
||||
per-move clock you lose only after **7 days without a move** (so you can abandon an AI game freely,
|
||||
and the AI itself never runs out of time). Choosing a **random player** is auto-match
|
||||
(always 2 players), which drops you **straight into the game and you wait inside it**: if it is your turn you
|
||||
can already move, otherwise you watch your tiles. While no opponent has joined, the opponent card (and
|
||||
the game's row in the lobby) reads **"searching for opponent"**, and resign, chat and nudge are
|
||||
unavailable. Another player searching the same variant and rule joins your game; failing that, a robot
|
||||
@@ -142,6 +148,12 @@ carries a human-like, language-appropriate name (a Russian game draws mostly Rus
|
||||
names); it does not chat, and **silently ignores friend requests** — a request to a
|
||||
robot stays pending and expires, exactly like a human who never responds.
|
||||
|
||||
The same robot also backs the **honest-AI quick game** the player chooses directly (above). There
|
||||
it makes no pretence: it is shown as **🤖** everywhere, joins and moves at once (no thinking time
|
||||
or night pause), keeps the same strength (it still plays to win only about 40% of the time), and
|
||||
chat, nudge and add-friend are off. AI games are **practice** — they never count toward a player's
|
||||
statistics.
|
||||
|
||||
### Social: friends, block, chat, nudge
|
||||
Become friends in two ways: redeem a **one-time code** the other player issues (six
|
||||
digits, valid for twelve hours), or send a **request to someone you have played
|
||||
|
||||
+13
-1
@@ -87,7 +87,14 @@ nudge) приходят от бота **этой партии** — по язы
|
||||
читаются как «Scrabble»/«Скрэббл», а Erudit — «Erudite»/«Эрудит» (по языку интерфейса),
|
||||
и это же имя выносится в заголовок экрана игры. Это ограничивает только **старт** новой игры — и авто-подбор, и
|
||||
приглашение друга, — поэтому игрок по-прежнему видит и играет существующие игры на
|
||||
любом языке. Авто-подбор (всегда 2 игрока) сразу **помещает вас в игру, и вы ждёте соперника прямо
|
||||
любом языке.
|
||||
|
||||
**Быстрая игра** даёт выбрать соперника — **ИИ** (по умолчанию) или **случайного игрока**. С **ИИ**
|
||||
вы сразу начинаете против 🤖, который присоединяется и отвечает мгновенно: ожидания нет, чат и nudge
|
||||
выключены, «добавить в друзья» не показывается, а проверка слова работает; вместо лимита на ход вы
|
||||
проигрываете только после **7 дней без хода** (так что ИИ-партию можно спокойно забросить, а сам ИИ
|
||||
никогда не «просрочит» ход). Выбор **случайного игрока** — это авто-подбор (всегда 2 игрока), который
|
||||
сразу **помещает вас в игру, и вы ждёте соперника прямо
|
||||
в ней**: если ваш ход — вы уже можете ходить, иначе просто рассматриваете свои фишки. Пока соперник не
|
||||
присоединился, на карточке соперника (и в строке игры в лобби) написано **«Поиск соперника...»**, а
|
||||
сдача, чат и nudge недоступны. Другой игрок, ищущий тот же вариант и правило, присоединяется к вашей
|
||||
@@ -146,6 +153,11 @@ nudge) приходят от бота **этой партии** — по язы
|
||||
**молча игнорирует заявки в друзья** — заявка роботу остаётся в ожидании и истекает,
|
||||
ровно как у человека, который не отвечает.
|
||||
|
||||
Тот же робот стоит и за **честной игрой против ИИ**, которую игрок выбирает напрямую (выше). Там он
|
||||
не притворяется: везде показан как **🤖**, присоединяется и ходит сразу (без раздумий и ночной
|
||||
паузы), сохраняет ту же силу (по-прежнему играет на победу лишь примерно в 40% партий), а чат, nudge
|
||||
и «добавить в друзья» выключены. Партии с ИИ — это **тренировка**: они не идут в статистику игрока.
|
||||
|
||||
### Социальное: друзья, блок, чат, nudge
|
||||
Подружиться можно двумя способами: погасить **одноразовый код**, который выпускает
|
||||
другой игрок (шесть цифр, действует двенадцать часов), либо отправить **заявку
|
||||
|
||||
+8
-2
@@ -227,7 +227,9 @@ IV 🏅; active games show Your move 🟢 / Opponent's move ⏳; invitations use
|
||||
Decline for an invitee and a waiting/Cancel state for the inviter; a single-word-rule
|
||||
invitation adds a **"One word per turn"** line to the card. Creating a game lives in
|
||||
`NewGame.svelte`: **"Play with friends"** (pick invitees, then variant / move time /
|
||||
hints) and **auto-match** (random opponent). The auto-match variant plaques are
|
||||
hints) and **quick game**. Quick game opens with a two-button **🤖 AI / 👤 Random player**
|
||||
segmented select (the `.seg`/`.opt` style, **AI the default**) in place of the old subtitle — AI
|
||||
is an honest robot you knowingly play, Random player is auto-match against a human. The variant plaques are
|
||||
**mutually-exclusive selects** — a tap **highlights** one (an accent inset border) instead
|
||||
of starting a game; a lone offered variant is pre-selected, and a bottom **Start game**
|
||||
button (disabled until a variant is chosen) confirms. For a **Russian** variant (either
|
||||
@@ -237,7 +239,11 @@ IV 🏅; active games show Your move 🟢 / Opponent's move ⏳; invitations use
|
||||
opponent's score card (and the game's lobby row) reads the localized **"searching for opponent"**
|
||||
placeholder, the add-friend 🤝 is hidden, and resign and the chat's send/nudge are disabled; an
|
||||
**opponent_joined** push restores them in place when a human or robot takes the seat, and a line
|
||||
under Start game notes the wait can take a while (the app may be closed meanwhile).
|
||||
under Start game notes the wait can take a while (the app may be closed meanwhile). With **AI**
|
||||
selected there is no wait: the move-clock line instead reads **"Loss after 7 days of inactivity"**
|
||||
and the "searching" hint is hidden; the game starts already seated, the opponent shows as **🤖**
|
||||
everywhere (score card, lobby row, turn line), the add-friend 🤝 is never drawn, and the chat's
|
||||
send field and 🛎️ nudge stay **disabled** while the 🔎 dictionary word-check keeps working.
|
||||
- **Statistics** (`screens/Stats.svelte`, the lobby 📊 tab): a 2-column grid of stat
|
||||
cards (wins / losses / draws / games / win-rate / best game / best move) — pure
|
||||
numbers, no charts.
|
||||
|
||||
Reference in New Issue
Block a user