Stage 17: bake decisions into PLAN, ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN, READMEs; mark stage done
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 26s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 26s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
- PLAN: Stage 17 Refinements entry + caveats resolved summary + tracker done - ARCHITECTURE §7 (move-number robot timing, composed variant-aware names), §10 (move event to the actor too), §11 (game_move_duration metric + offline admin per-user analytics), §14 (current branch model, path-conditional CI + gate, connector liveness) - FUNCTIONAL(+ru): robot draws language-appropriate names - UI_DESIGN: screen transitions, Telegram theme/nav, ad-banner accent, players plaque + history drawer - backend README: robot timing/names refinements
This commit is contained in:
+55
-24
@@ -300,10 +300,16 @@ 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
|
||||
(`robot.Service.Run`, mirroring the turn-timeout sweeper) recomputes the same
|
||||
behaviour on every scan and after a restart — the same philosophy as journal
|
||||
replay. A pool of durable accounts — each a `kind='robot'` identity (§4),
|
||||
provisioned at startup with chat and friend requests blocked — backs the
|
||||
human-like name pool; those two profile toggles are all the friend/DM blocking
|
||||
requires (there is no DM surface; chat is per-game).
|
||||
replay. A pool of durable accounts — each a `kind='robot'` identity (§4), keyed
|
||||
`robot-<lang>-<index>` and provisioned at startup with chat and friend requests
|
||||
blocked — backs the human-like names; those two profile toggles are all the
|
||||
friend/DM blocking requires (there is no DM surface; chat is per-game). Names are
|
||||
**composed per language** from a first-name pool (32 full + 32 colloquial forms) and
|
||||
a surname pool (gender-agreed for Russian) in one of three forms (first only /
|
||||
first + surname initial / first + full surname), deterministically per pool slot so
|
||||
they stay stable across restarts. Substitution is **variant-aware**: a Russian game
|
||||
(Russian Scrabble or Эрудит) draws a Russian-named robot with at most ~20% Latin, an
|
||||
English game the Latin pool.
|
||||
|
||||
- **Balance**: at game start it decides once whether to play to win, with
|
||||
`P(play-to-win) ≈ 0.40` (so the human wins ≈ 60%), derived from the seed.
|
||||
@@ -313,13 +319,15 @@ requires (there is no DM surface; chat is per-game).
|
||||
(playing to lose) is closest to a small band (**1–30 points**), rather than
|
||||
always the maximum; with no legal play it exchanges a full rack when the bag can
|
||||
refill it, else passes.
|
||||
- **Timing**: per-move delay sampled from a right-skewed distribution (short
|
||||
delays frequent, median ≈ 10 min), clamped to **[2, 90] minutes**; it
|
||||
- **Timing**: the per-move delay is **move-number-aware** — a right-skewed sample
|
||||
(exponent k=4, short delays frequent) from a band that interpolates from
|
||||
**[1, 5] min** at the first move to **[10, 90] min** by ~28 moves, so openings are
|
||||
quick and the endgame can run long, clamped to **[1, 90] minutes**; it
|
||||
**sleeps 00:00–07:00** anchored to the **opponent's** profile timezone with a
|
||||
per-game drift of **±3 h** (fallback UTC), so its night overlaps the human's
|
||||
rather than running anti-phase; on a daytime nudge it replies within
|
||||
**2–10 minutes**; it proactively nudges the human after **12 hours** idle
|
||||
(subject to the once-per-hour chat limit).
|
||||
rather than running anti-phase; on a daytime nudge it replies near the move's lower
|
||||
band; it proactively nudges the human after **12 hours** idle (subject to the
|
||||
once-per-hour chat limit).
|
||||
- **Observability**: robot accounts accrue ordinary statistics (§9) — the
|
||||
authoritative balance metric (target ≈ 40% robot wins) — and a
|
||||
`robot_games_finished_total` OTel counter plus a per-finish log give a live view.
|
||||
@@ -451,7 +459,9 @@ services); a single backend→gateway **gRPC server-stream** (`Push.Subscribe`,
|
||||
`pkg/proto/push/v1`) carries every event, and the gateway fans them out by
|
||||
`user_id` to each client's Connect `Subscribe` stream while the app is open. The
|
||||
catalog is **your-turn** and **opponent-moved** (emitted from the game commit, so
|
||||
robot-driver and timeout-sweeper moves emit too), **chat-message** and **nudge**
|
||||
robot-driver and timeout-sweeper moves emit too; opponent-moved goes to **every seat,
|
||||
including the mover**, so the mover's own other devices and their lobby refresh — it is
|
||||
in-app only, so the actor gets no out-of-app push for their own move), **chat-message** and **nudge**
|
||||
(from the social service), **match-found** (from the matchmaker, §8), and **notify**
|
||||
(Stage 8 — a lightweight "re-poll" signal carrying a sub-kind: friend-request,
|
||||
friend-added, invitation or game-started; emitted on a friend-request and invitation
|
||||
@@ -499,13 +509,21 @@ promotions) is future work and would deliver short markdown messages (text + lin
|
||||
client-measured RTT piggybacked on the next request is a later enhancement.
|
||||
- Domain/operational metrics (Stage 12), recorded through the meter and invisible
|
||||
until an exporter is configured: histograms `game_replay_duration` (journal
|
||||
rebuild on a cache miss) and `game_move_validate_duration`; counters
|
||||
`games_started_total`, `games_abandoned_total` (a turn-timeout seat drop),
|
||||
`chat_messages_total` (`kind` = message/nudge) and `robot_games_finished_total`;
|
||||
an observable gauge `game_cache_active`; the gateway `edge_request_duration`
|
||||
(the UI-perceived roundtrip, by `message_type`/`result`); and Go runtime/heap
|
||||
metrics. Game-scoped metrics carry a `variant` attribute
|
||||
rebuild on a cache miss), `game_move_validate_duration` and `game_move_duration`
|
||||
(Stage 17 — a seat's think time per committed move, attributed by `variant` and a
|
||||
`phase` of opening/middle/endgame; it aggregates **all** seats including robots,
|
||||
whose synthetic timing dominates the tail, so per-human analysis lives in the admin
|
||||
console, below); counters `games_started_total`, `games_abandoned_total` (a
|
||||
turn-timeout seat drop), `chat_messages_total` (`kind` = message/nudge) and
|
||||
`robot_games_finished_total`; an observable gauge `game_cache_active`; the gateway
|
||||
`edge_request_duration` (the UI-perceived roundtrip, by `message_type`/`result`);
|
||||
and Go runtime/heap metrics. Game-scoped metrics carry a `variant` attribute
|
||||
(english/russian_scrabble/erudit).
|
||||
- Per-user move-time analytics (Stage 17) are **offline**, derived in the admin
|
||||
console from the move journal (`game_moves.created_at` deltas, the first move from
|
||||
the game's creation), not Prometheus labels (which an `account_id` would explode):
|
||||
the user list shows each account's min/avg/max think time, and the user-detail page
|
||||
draws a zero-JS inline-SVG chart of min/mean/max by the player's move number.
|
||||
- User metrics (Stage 16): a backend counter `accounts_created_total` (`kind` =
|
||||
telegram/email/guest; robots are a provisioned pool, not users, and are excluded)
|
||||
and a gateway **in-memory** observable gauge `active_users` (`window` = 24h/7d) —
|
||||
@@ -579,14 +597,27 @@ Two contours, two secret/variable prefixes (`TEST_` / `PROD_`):
|
||||
|
||||
## 14. CI & branches
|
||||
|
||||
- Trunk is **`master`**; feature work happens on `feature/*` branches merged via
|
||||
PR with a green CI gate (from Stage 1 onward — the genesis commit necessarily
|
||||
lands on `master`).
|
||||
- `.gitea/workflows/` holds the CI. `go-unit.yaml` runs gofmt/vet/build/unit-test
|
||||
on Go changes; `integration.yaml` runs the Postgres-backed tests behind the
|
||||
`integration` build tag (testcontainers `postgres:17-alpine`, Ryuk disabled,
|
||||
serial). Further workflows (ui-test, deploy) are added with the components they
|
||||
cover.
|
||||
- **Two long-lived branches** (Stage 16): **`development`** is the integration
|
||||
trunk and **`master`** the production trunk; `feature/*` branches are cut from
|
||||
`development` and PR back into it (the genesis commit necessarily landed on
|
||||
`master`). A commit to a `feature/*` branch triggers nothing.
|
||||
- A single `.gitea/workflows/ci.yaml` (Gitea has no cross-workflow `needs`) runs the
|
||||
suite on a PR into `development`/`master` and on a push to `development`. Its
|
||||
`unit` (gofmt/vet/build/unit-test), `integration` (Postgres-backed `integration`
|
||||
tag, testcontainers `postgres:17-alpine`, Ryuk off, serial) and `ui`
|
||||
(check/unit/build/bundle-budget/e2e) jobs are **path-conditional** (Stage 17 — a
|
||||
`changes` job filters by changed paths), and an always-running **`gate`** job
|
||||
aggregates them (passing when each succeeded or was **skipped**) and is the single
|
||||
branch-protection required check (`CI / gate`), so a path-skipped job never blocks
|
||||
a merge.
|
||||
- A gated **`deploy`** job auto-rolls the **test contour** on a PR into — or a push
|
||||
to — `development` (`docker compose up -d --build` on the runner host), then probes
|
||||
the gateway (`GET /`) **and the Telegram connector's liveness** (Stage 17 —
|
||||
`docker inspect`: running, not restarting, stable restart count, with a
|
||||
VPN-handshake grace period, since the connector has no public ingress and a
|
||||
crash-loop is otherwise invisible). A PR into `master` is test-only; the prod
|
||||
deploy is the manual Stage 18 workflow. Secrets/variables are prefixed
|
||||
`TEST_`/`PROD_` per contour.
|
||||
- The engine consumes `scrabble-solver` as a **published, versioned module**
|
||||
(`gitea.iliadenisov.ru/developer/scrabble-solver`, pinned in `backend/go.mod`); both Go
|
||||
workflows set `GOPRIVATE=gitea.iliadenisov.ru/*` so go fetches it directly from this Gitea
|
||||
|
||||
Reference in New Issue
Block a user