Stage 5: robot opponent (pool, seed-derived strategy, move driver, matchmaker substitution)
- internal/robot: durable kind='robot' account pool (migration 00004); every per-game and per-turn choice derived deterministically from the game seed (restart-stable FNV mix); a background move driver; margin targeting (band 1-30, closest-to-band); right-skewed [2,90]min delays (median ~10m); opponent-anchored sleep with +/-3h drift; daytime nudge reply + proactive 12h nudge; friend/chat blocked via profile toggles. - engine.Candidates (decoded ranked plays); game.Candidates + RobotTurns; social.LastNudgeAt. - matchmaker: 10s wait then robot substitution (reaper) + Poll delivery seam. - config (BACKEND_ROBOT_DRIVE_INTERVAL, BACKEND_LOBBY_ROBOT_WAIT, BACKEND_LOBBY_REAPER_INTERVAL); main wiring + boot-time pool provisioning. - metrics: robot account_stats (authoritative balance) + robot_games_finished_total OTel counter + per-finish log. - docs: PLAN, ARCHITECTURE, FUNCTIONAL(+ru), TESTING, README; account.go comment. - tests: robot strategy units, matchmaker reaper/Poll, engine.Candidates; inttest robot full-game / substitution / proactive-nudge.
This commit is contained in:
@@ -38,7 +38,7 @@ independent (see ARCHITECTURE §9.1).
|
||||
| 2 | Engine package over scrabble-solver | **done** |
|
||||
| 3 | Game domain (lifecycle, rules, hint, word-check, history+GCG, stats) | **done** |
|
||||
| 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | **done** |
|
||||
| 5 | Robot opponent | todo |
|
||||
| 5 | Robot opponent | **done** |
|
||||
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | todo |
|
||||
| 7 | UI (plain Svelte + Vite, board, lobby, chat, i18n) | todo |
|
||||
| 8 | Telegram integration (bot side-service, deep-link, push) | todo |
|
||||
@@ -311,6 +311,50 @@ Open details: deployment target/host; dashboards; load expectations.
|
||||
(both Go workflows already clone the solver sibling and export
|
||||
`BACKEND_DICT_DIR`).
|
||||
|
||||
- **Stage 5** (interview + implementation):
|
||||
- Scope, as in Stages 1–4: **domain layer, no HTTP** — the robot consumes the
|
||||
public game API as an ordinary seated player (`internal/robot`), so only
|
||||
`internal/engine` still imports the solver. New: `engine.Candidates()` (decoded
|
||||
ranked plays) and a thin `game.Service.Candidates` + `RobotTurns` read.
|
||||
- **Account model** (interview): a pool of **durable accounts**, each a single
|
||||
`identities` row `kind='robot'` (migration `00004` widens the kind CHECK — a
|
||||
CHECK-only change, no jetgen). A curated ~16-name pool in code; `EnsurePool`
|
||||
provisions them idempotently at boot (a hard dependency, like the registry) with
|
||||
`block_chat`/`block_friend_requests` set, which is **all** the friend/DM blocking
|
||||
needs (no special-casing).
|
||||
- **Driver + state** (interview): a background sweeper goroutine
|
||||
(`robot.Service.Run`/`Drive`, mirroring the timeout sweeper); **every per-game
|
||||
and per-turn choice is derived deterministically from the game `seed`** (FNV-1a
|
||||
mix, restart-stable — not `hash/maphash`), so the robot keeps **no extra state**.
|
||||
`playToWin = mix(seed,"win")%100 < 40`; per-turn `delay`; sleep `drift`.
|
||||
- **Timing** (interview): per-move delay `2 + 88·u^k` minutes, `u~U(0,1)`,
|
||||
**k≈3.5 → median ~10 min**, clamped to [2,90]. A daytime nudge on the robot's
|
||||
turn pulls the move into a 2–10 min reply window; the robot proactively nudges
|
||||
after **12 h** idle on the human's turn (reusing `social.Nudge`'s once-per-hour
|
||||
guard; `social.LastNudgeAt` added to detect the human's nudge).
|
||||
- **Sleep** (interview — resolves the §7-vs-`account.go` mismatch): the robot
|
||||
sleeps 00:00–07:00 in the **opponent's timezone shifted by a per-game drift ∈
|
||||
[−3,+3]h** (so its night overlaps the human's rather than running anti-phase),
|
||||
computed on the fly per game — **no profile mutation, no concurrency cap**. The
|
||||
`account.go` away-window comment was corrected accordingly.
|
||||
- **Margin** (interview): pick the candidate whose resulting margin (own+move−opp)
|
||||
is closest to **[1,30]** when playing to win / **[−30,−1]** when playing to lose,
|
||||
tie-broken toward the conservative edge; no legal play → exchange the full rack
|
||||
when the bag can refill it, else pass.
|
||||
- **Substitution** (interview): a matchmaker **reaper** (`Reap`/`RunReaper`)
|
||||
substitutes a pooled robot after a **10 s** wait (`BACKEND_LOBBY_ROBOT_WAIT`),
|
||||
`NewMatchmaker` now takes a `RobotProvider`. A waiter learns of a match — human
|
||||
pairing **or** substitution — through a new `Poll` + results map; production
|
||||
delivery is a **match-found notification** (session/in-app push + side-service),
|
||||
Stage 6/8 — noted in §10.
|
||||
- **Metrics** (interview, 1+2): robots are durable accounts, so `account_stats`
|
||||
is the authoritative, complete balance ground-truth (target ~40% robot wins);
|
||||
an OTel counter (`robot_games_finished_total`, exporter `none` today) and a
|
||||
structured log cover robot-finished games for live observation.
|
||||
- **Config**: `BACKEND_ROBOT_DRIVE_INTERVAL` (30 s), `BACKEND_LOBBY_ROBOT_WAIT`
|
||||
(10 s), `BACKEND_LOBBY_REAPER_INTERVAL` (1 s). No CI change (both Go workflows
|
||||
already clone the solver sibling and export `BACKEND_DICT_DIR`).
|
||||
|
||||
## Deferred TODOs (cross-stage)
|
||||
|
||||
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
|
||||
|
||||
Reference in New Issue
Block a user