diff --git a/PRERELEASE.md b/PRERELEASE.md index 78b1345..d0b81dc 100644 --- a/PRERELEASE.md +++ b/PRERELEASE.md @@ -31,6 +31,7 @@ the edge before prod. Each phase maps back to the owner's raw pre-release TODO l | OW | Open auto-match: enter the game at once and wait inside it (robot after 90–180 s) | owner ad-hoc | **done** | | DA | Dictionary admin: online release-archive upload → word-diff preview → install/activate; versioned dict volume; active version persisted in DB; resident label = release tag | owner ad-hoc | **done** | | AB | Manual account block (admin suspension): permanent/temporary with an editable en+ru reason picklist; a block forfeits the player's active games + cancels their open ones; a backend gate refuses a blocked account with **403 `account_blocked`**; the UI shows a terminal blocked screen and stops all push/poll; manual unblock; temporary blocks self-expire (migration `00003`) | owner ad-hoc | **done** | +| AI | Honest AI opponent in quick game: an explicit 🤖 AI / 👤 random selector (AI default); the robot is seated and moves at once; 7-day inactivity loss (the per-turn timeout reused); chat/nudge disabled, no statistics; the opponent is shown as 🤖 everywhere | owner ad-hoc | **done** | | → | Stage 18 — prod contour deploy | — | see [`PLAN.md`](PLAN.md) | ## Key findings (these reshaped the raw list — read before starting a phase) @@ -466,3 +467,47 @@ Then Stage 18. `lobby.tournaments` / `lobby.soon`). No backend/wire/history/GCG change. - **No schema/wire change → no contour DB wipe.** Bake-back: `docs/UI_DESIGN.md`, `docs/FUNCTIONAL.md` (+`_ru`). Regression gate: UI `check` + unit + build + bundle budget + e2e (Chromium & WebKit). + +- **AI — Honest AI opponent in quick game** (owner ad-hoc, not on the raw TODO list): a second quick-game + opponent the player *knowingly* chooses, distinct from the disguised robot of the random/open path + (which is kept as-is). New Game's quick-game mode replaces the "auto-match" subtitle with a two-button + selector **🤖 AI / 👤 Random player** (the `.seg`/`.opt` segmented style, AI the default); for AI the + move-clock line reads "Loss after 7 days of inactivity" and the "searching" hint is hidden. + - **Locked decisions (interview):** AI move is **event-driven** (the robot replies the instant the + player's move commits; the 30 s driver is the fallback); AI games **do not touch `account_stats`** + (practice, like guests); the **Stage 5 strength logic is reused unchanged** (`playToWin` 40 % from the + seed + margin band); **no per-move timeout — a 7-day inactivity loss** instead; the 7-day line lives on + the New Game screen (the in-game screen has no move-clock line); chat + nudge **disabled**, word-check + kept, add-friend never drawn, opponent shown as **🤖** everywhere. + - **The 7-day rule reuses the existing per-turn timeout:** an AI game is created with + `turn_timeout_secs = AIInactivityTimeout` (7 days) and the existing 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 *is* + the abandon rule (no new column, no new sweeper). + - **One game flag drives everything:** `games.vs_ai` (edited into the R1 baseline — pre-release, so a + contour DB wipe after merge). It is set **only** on AI-started games, so a robot-filled random game keeps + `vs_ai=false` and the disguised opponent is never revealed; the UI derives 🤖 / the gates **from the flag, + never from the opponent account**. New backend path `Matchmaker.StartVsAI` (picks a pooled robot via the + existing `Pick`, creates an **active** seated game via `game.Service.Create`, random seat order) — the AI + request never enters the open pool, so the open-game reaper never touches it. The robot driver gains a + `vs_ai` branch (no sleep, no proactive nudge, zero delay) and a focused `DriveGame`/`TriggerMove` fast + path wired from the game service's after-create/after-commit hook (`SetAITrigger`, a func value so the + game package never imports the robot package). Chat/nudge gated by a new `social` `VsAI` check + (`ErrGameVsAI` → 409 `ai_game`); statistics skipped in `commit` when `vs_ai`. + - **Wire:** `EnqueueRequest` += `vs_ai`, `GameView` += `vs_ai` (trailing FB fields, regenerated Go + TS), + threaded through the backend DTO, the gateway transcode and the `pkg/wire` + `notify` builders. + - **Tests:** `lobby` unit (StartVsAI seats a robot + flags the game; empty pool leaves no game); backend + integration (`ai_game_test.go`: active+seated+vs_ai+7-day clock, robot moves immediately, stats skipped, + 7-day timeout resigns the human, chat/nudge rejected); UI codec round-trip (`vs_ai` on enqueue + game + view); e2e (an AI game shows 🤖, no "searching", chat disabled, the dictionary still works) + the + existing quick-match e2e updated to pick **Random player** (the default is now AI). + - **Schema/wire change → a contour DB wipe** after merge (`DROP SCHEMA backend CASCADE` + restart, the + R1/R3 pattern). Bake-back: `docs/ARCHITECTURE.md`, `docs/FUNCTIONAL.md` (+`_ru`), `docs/UI_DESIGN.md`, + `backend/README.md`, Go Doc comments. + - **Post-review refinements (owner, same PR):** (1) the **GCG export labels the robot seat "AI"** rather + than its human-like pool name (`ExportGCG` overrides the name via `accounts.IsRobot`; the in-app 🤖 is + unchanged); (2) honest-AI games **emit no `your_turn`** — the robot replies instantly, so the signal + would arrive with the move and be pointless; `opponent_moved` still advances the UI; (3) the **admin + console surfaces the AI flag** — a **🤖 column** in `/games` and an "AI game" line on the game card + (`GameRow`/`GameDetailView` gain `VsAI`); (4) `games_started_total` / `games_abandoned_total` gain a + **`vs_ai`** attribute and the Grafana *Game domain* dashboard splits started/abandoned into **human** + and **AI** panels. diff --git a/backend/README.md b/backend/README.md index 5c7bc03..849da24 100644 --- a/backend/README.md +++ b/backend/README.md @@ -59,6 +59,16 @@ elapsed, and the waiting starter is told an opponent took the seat by an in-app **opponent_joined** push (carrying their refreshed game state) that fills the opponent card and re-enables resign and chat in place. +The same robot also backs an **honest-AI quick game** (`games.vs_ai`), the alternative to the random +path that the player chooses on New Game. `Matchmaker.StartVsAI` picks a pooled robot and creates a +game **already seated and active** (random seat order) — it never enters the open pool. The robot +driver has a `vs_ai` branch (no sleep, no proactive nudge, zero delay) plus a focused +`DriveGame`/`TriggerMove` fast path wired from the game service's after-create/after-commit hook +(`SetAITrigger`), so the robot replies the instant the player moves. AI games keep the same strength +(`playToWin`), have **no per-move timeout** (`turn_timeout_secs = AIInactivityTimeout`, 7 days, so an +abandoned game is lost after a week of inactivity — only the human is ever on the clock), record **no +statistics** (skipped in `commit`), and disable chat/nudge (`social` `VsAI` → `ErrGameVsAI`). + The backend opens to the edge. The route groups gain their first handlers (`internal/server/handlers_*.go`): gateway-only session endpoints under `/api/v1/internal` (Telegram/guest/email login → mint, resolve, revoke) and a diff --git a/backend/cmd/backend/main.go b/backend/cmd/backend/main.go index a0185d8..00003a8 100644 --- a/backend/cmd/backend/main.go +++ b/backend/cmd/backend/main.go @@ -178,6 +178,9 @@ func run(ctx context.Context, cfg config.Config, logger *zap.Logger) error { if err := robots.EnsurePool(ctx); err != nil { return fmt.Errorf("provision robot pool: %w", err) } + // Honest-AI fast path: a move in a vs_ai game triggers the robot's reply at once + // (the periodic driver below is the fallback). Set after the pool is provisioned. + games.SetAITrigger(robots.TriggerMove) go robots.Run(ctx, cfg.Robot.DriveInterval) logger.Info("robot driver started", zap.Duration("interval", cfg.Robot.DriveInterval)) diff --git a/backend/internal/adminconsole/templates/pages/game_detail.gohtml b/backend/internal/adminconsole/templates/pages/game_detail.gohtml index 068b1a6..08316b1 100644 --- a/backend/internal/adminconsole/templates/pages/game_detail.gohtml +++ b/backend/internal/adminconsole/templates/pages/game_detail.gohtml @@ -7,6 +7,7 @@
  • Variant {{.Variant}}
  • Dictionary {{.DictVersion}}
  • Status {{.Status}}{{if .EndReason}} ({{.EndReason}}){{end}}
  • +
  • AI game {{if .VsAI}}🤖 yes{{else}}no{{end}}
  • Players {{.Players}}
  • To move seat {{.ToMove}}
  • Moves {{.MoveCount}}
  • diff --git a/backend/internal/adminconsole/templates/pages/games.gohtml b/backend/internal/adminconsole/templates/pages/games.gohtml index 4a958fd..e6ab08e 100644 --- a/backend/internal/adminconsole/templates/pages/games.gohtml +++ b/backend/internal/adminconsole/templates/pages/games.gohtml @@ -8,11 +8,11 @@ finished - + {{range .Items}} - -{{else}}{{end}} + +{{else}}{{end}}
    GameVariantStatusPlayersUpdated
    GameVariantStatus🤖PlayersUpdated
    {{.ID}}{{.Variant}}{{.Status}}{{.Players}}{{.UpdatedAt}}
    no games
    {{.ID}}{{.Variant}}{{.Status}}{{if .VsAI}}🤖{{end}}{{.Players}}{{.UpdatedAt}}
    no games