74842dc6242511706ac2e14bbd52fbebe31cd15f
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
82648a4398 |
fix(game): show granted/bought hints in-game — finish the D31 wire removal
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m58s
The in-game hint badge ignored the payments hint wallet: loading a game clobbered app.profile.hintBalance with the deprecated StateView.wallet_balance (zeroed in the D31 domain removal but left in the protocol as a dead 0, then synced over the real balance at game load). Finish the removal honestly. StateView carries the per-game allowance alone (hints_remaining); the purchasable wallet lives solely on the profile and the client adds it (lib/hints). Removed StateView.wallet_balance across every layer — backend StateView/DTO, notify.PlayerState (live events), gateway StateResp + wire.StateView, the client model/codec/mock/localgame — and dropped the now-unused wallet arg from hintsRemaining. The FBS field is tombstoned `(deprecated)` (not deleted) so the vtable slots after it stay stable across a rolling deploy; no accessor is generated. HintResult keeps wallet_balance (the real post-spend payments balance the client adopts into the profile). The StateView type no longer has walletBalance, so the clobber cannot return without a compile error. Tests: hintsRemaining (2-arg); hints.hintsLeft (allowance + live wallet, no strip); the game state/hint integration (allowance-only HintsRemaining); gateway transcode; FBS regen. |
||
|
|
1c06d1d0d1 |
feat(payments): chip wallet, store-compliance gate and benefit application
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
Stand up the internal chip/benefit mechanic behind the narrow payments interface: context-aware balances and benefits, an atomic chip spend, admin grants as zero-price value sales, the one-directional store-compliance gate (VK/TG same- origin only, web draws direct→vk→tg, VK-iOS frozen, untrusted fail-closed), and per-origin hint and no-ads application with term stacking. Reads are served from an in-process, account-keyed write-through cache (mirroring the suspension gate), so hot paths issue no query to the payments schema. Flip the online-game hint wallet and the ad-banner suppression from the deprecated accounts.hint_balance / paid_account columns to the payments benefit (a hint balance no longer suppresses the banner — only a no-ads benefit does), and fold chip segments and benefits by origin on account merge, inside the merge tx. Add the GET/POST /api/v1/user/wallet edge chain (REST → Connect → FlatBuffers) plus its codec unit test; no wallet UI yet. Bring the frozen owner decisions log into the repo at docs/PAYMENTS_DECISIONS_ru.md (it was untracked under .vscode) and reference it from PLAN.md; record the read-cache design and the present-sources interface in PLAN.md and docs/PAYMENTS.md (+ RU mirror). |
||
|
|
ecb21bd218 |
perf(backend): cut evaluate's DB round-trips; load the game in one query
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m18s
EvaluatePlay (the hottest gameplay call, fired on every tile placement) now uses the warm live-game cache directly: an active game stays cached (mutated in place across moves, evicted only on finish), so the cached engine game and its immutable seat list answer the membership check and the score with no DB read. The cold path (eviction / first load) still loads and validates via the store. The seat list is cached alongside the engine game for the membership fast path. GetGame also folds its two round-trips (game, then seats) into one LEFT JOIN, preserving the contract (same Game, a seatless game still returns empty seats, seat order kept) — one round-trip for every remaining caller. Measured at 500 players: evaluate p99 halves (200 -> 100 ms) and the per-op query count drops. It does NOT cut postgres CPU — that is write-bound (per-move CommitMove plus draft upserts and journal replays), the cheap indexed GetGame reads were never its bottleneck, and postgres runs with headroom (~1.5 of 2 cores). So this is a latency / query-volume optimization, not a DB-CPU one. Regression cover: a non-player evaluate against a warm game asserts the cached-seat membership path; the integration suite exercises GetGame's join across every game op. |
||
|
|
8793bd34f2 |
feat(stats): best-move word, moves & hint-share, and a hint-count fix (#81)
The statistics screen gains real depth, plus a hint-count bug fix found along the way. - Best move per variant: the screen shows the actual best-move word (drawn as game tiles; a wildcard shows its letter but no value), broken down by game variant, empty variants omitted. New account_best_move table, written at game finish. - Moves & hint share: two new lifetime tiles — the player's play count and the share of plays that used a hint — from summed account_stats counters (moves, hints_used). Honest-AI games are excluded, like the rest of the stats. - Hint-count fix: the in-game hint badge no longer goes stale across games. The global wallet now rides the wire apart from the per-game allowance (wallet_balance on StateView/HintResult/StatsView), so the client reads the live wallet rather than a per-game snapshot; game_players.hints_used now counts every hint (allowance + wallet), its true per-game total. - Account merge: sums the new moves/hints_used counters and merges the per-variant best moves (higher score kept), which it previously dropped. - Admin: the user card shows Moves and Hints used. - UI polish: tab/label wording, game-over text, and e2e selectors hardened against label changes. All wire additions are trailing (backward-compatible). Docs (ARCHITECTURE, FUNCTIONAL +ru, UISN_DESIGN) updated in step. |
||
|
|
92f48a3b12 |
Backend infers play direction; UI previews words and gates submit on legality
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 44s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m9s
A single tile that only extended a word perpendicular to the client-declared direction was rejected: the UI always sent dir=H for one-tile plays (the dirOverride/Controls toggle was orphaned in the Stage 7 game rework), so placing "А" above "БАК" to form "АБАК" failed the solver's main-word-length check even though the word is in the dictionary. Make the backend infer a play's orientation from the placed tiles and the board (internal/engine.resolveDirection): two or more tiles by the line they share, a lone tile by the axis it abuts (longer word wins, horizontal on a tie). Direction becomes an output, not an input: drop dir from the SubmitPlay/Eval wire requests and add it to EvalResult. Journal replay keeps trusting the stored "H"/"V" (SubmitPlayDir) so a rebuilt game matches the one committed. UI: stop computing/sending direction; the preview now shows the words a move forms with its total score (game.previewWords); the make-move control is disabled until the play is confirmed legal; the "your turn" label hides while tiles are pending. Delete the orphaned Controls.svelte. Regenerate the FlatBuffers bindings (Go + TS) and update the gateway transcode and the loadtest edge client to the new contract. Bake the decision into ARCHITECTURE.md (§5/§9.1), FUNCTIONAL.md (+ _ru) and the backend README. |
||
|
|
c6e0dac940 |
R6(c): centralize shared integration-test fixtures in helpers.go
Move the cross-file integration fixtures — the service constructors (newGameService/newSocialService/newRobotService/newMatchmaker), the game-assembly helpers (newMirror/newGameWithSeats/newDraftGame), account provisioning (provisionAccount/provisionGuest) and the stats reader — out of the domain test files (newGameService alone was used by 10 files) into a single backend/internal/inttest/helpers.go. Helpers used by a single file stay local. Pure relocation: the helper bodies are unchanged, no test logic changes; the imports the moves left unused are pruned. go vet -tags=integration is clean. |
||
|
|
8881214213 |
R6(a): de-stage code, docs, READMEs; split stage6_test
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN) references from comments, doc-comments, service READMEs, the current-state docs (ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the .fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage history. - Rename the only stage-named identifiers: registerStage8 -> registerSocialOps, registerStage11 -> registerLinkOps (gateway transcode). - Split stage6_test.go: TestEmailLoginFlow -> email_test.go, TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go. - Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments). go build/vet/gofmt clean across modules; integration typecheck and pnpm check green. |
||
|
|
26aa154547 |
R1: schema & naming reset — squash migrations, rename variants
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Squash the 12 goose migrations into one 00001_baseline.sql (there is no prod data; verified schema-identical to the chain via a pg_dump diff + the green integration suite) and rename the game-variant labels english/russian_scrabble/erudit -> scrabble_en/scrabble_ru/erudit_ru across the backend, the FlatBuffers wire values and the UI. dawg filenames and the Go enum identifiers are unchanged; the i18n display keys are kept. Adds PRERELEASE.md (the R1-R7 pre-release tracker), linked from CLAUDE.md. Contour DB wipe and the scrabble-dictionary tidy are follow-ups. |
||
|
|
10412fee8e |
Stage 17 round 5 — backend/correctness bug fixes
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Failing after 12s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Failing after 1s
CI / deploy (pull_request) Has been skipped
- Resign on the opponent's turn: engine ResignSeat(seat) resigns a specific seat (not just toMove); game.Resign bypasses the turn check and forfeits the actor's seat. - Quick-match cancel was a UI no-op (only stopped polling): add the full path (REST /lobby/cancel -> gateway lobby.cancel -> client) and clear the matchmaker's pending result on Cancel, so a cancelled search is dequeued (no 'already queued', no later robot-substituted game). NewGame dequeues on cancel and on abandon. - Lobby win/loss: result.ts ranked by score, so a 0-0 resignation read as a win. The winner now takes rank 1 and the viewer is placed from rank 2 — matching the game-detail screen. - Friend request to a robot: robots no longer block requests; the request stays pending and expires (friendRequestTTL), mirroring a human who ignores it. - Nudge cooldown: ErrNudgeTooSoon now maps to a distinct nudge_too_soon code with a correct message; the chat nudge button disables during the hourly cooldown; the nudge note reads 'Waiting for your move!' (button keeps the Nudge action label). Tests: engine/service off-turn resign, matchmaker cancel-clears-result, friend-to-robot inttest, result.ts 0-0 resignation, nudge_too_soon mapping. |
||
|
|
90eaf4964b |
Stage 13: alphabet on the wire (UI alphabet-agnostic, TODO-4)
Tests · Go / test (push) Successful in 10s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Successful in 19s
Tests · Go / test (pull_request) Successful in 9s
Tests · Integration / integration (pull_request) Successful in 12s
Tests · UI / test (pull_request) Successful in 19s
Live play now exchanges per-variant alphabet indices instead of concrete letters (rack out; submit-play, evaluate, exchange, word-check in). The client caches each variant's (index, letter, value) table behind StateRequest.include_alphabet and renders the rack and blank chooser from it, dropping the hardcoded value/alphabet tables. History, the durable journal and GCG stay decoded concrete characters (ARCHITECTURE §9.1, unchanged). - pkg/fbs: new AlphabetEntry + PlayTile; StateView.rack -> [ubyte] + alphabet; StateRequest.include_alphabet; SubmitPlay/Eval tiles -> [PlayTile]; Exchange tiles + CheckWord word -> [ubyte] (committed Go + TS regenerated). - engine: AlphabetTable + a cached per-variant codec (LetterForIndex/EncodeRack/ DecodeTiles/DecodeWord) + BlankIndex sentinel; Go parity test. - backend server edge maps index<->letter (new thin game.Service.GameVariant); game.Service domain methods, engine.Game and the robot keep one letter-based play path. The gateway forwards indices verbatim (no alphabet table). - ui: lib/alphabet.ts in-memory cache; codec encodes/decodes indices; premiums.ts is geometry-only; the mock seeds a fixture table; the UI normalises display to upper case (codec + cache), leaving placement/board/checkword unchanged. Parity moved to the Go engine.AlphabetTable test; premiums.ts loses its value tables. Discharges TODO-4. |
||
|
|
cf66ed7e26 |
Stage 9: Telegram integration (connector side-service, Mini App, out-of-app push)
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 12s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
New platform/telegram connector (own container, bot token only there): - go-telegram/bot long-poll loop: /start deep-links + Mini App launch button. - gRPC API pkg/proto/telegram/v1 (Telegram service): ValidateInitData, Notify (renders a localized message + deep-link button), SendToUser/SendToGameChannel (admin, wired in Stage 10). Generic methods are platform-agnostic (external_id). - Bot API base override for Telegram's test environment; Dockerfile + compose (VPN sidecar, no public ingress); README. Gateway: - initData validation relocated from the gateway into the connector; the gateway calls ValidateInitData over gRPC (GATEWAY_CONNECTOR_ADDR), drops the bot token, and deletes internal/auth. - Out-of-app push: runPushPump routes events whose recipient has no live in-app stream to connector.Notify, gated by /internal/push-target + the in-app-only flag (race-free de-dup); HasSubscribers added to the push hub. Backend: - Migration 00007 accounts.notifications_in_app_only (default true) + jetgen. - ProvisionTelegram seeds a new account's language/display name from the launch fields; IdentityExternalID reverse lookup; /internal/push-target handler. UI: - Telegram Mini App launch: detect initData, apply themeParams, authTelegram, route the deep-link start_param (g/i/f); /telegram/ guard redirects outside Telegram. Vite relative base + telegram-web-app.js. In-app-only profile toggle; share-to-Telegram link for a friend code. Vitest + Playwright coverage. Wire/docs/CI: fbs Profile/UpdateProfileRequest gain notifications_in_app_only (Go + TS); go.work uses ./platform/telegram; go-unit.yaml covers it; PLAN, ARCHITECTURE, FUNCTIONAL (+ru), UI_DESIGN, READMEs updated. |
||
|
|
d733ce3119 |
Stage 8: UI social/account/history surfaces
Wire the deferred Stage 7 surfaces end-to-end (UI -> gateway transcode -> backend REST -> existing domain services): friends (incl. one-time friend codes), per-user blocks, friend-game invitations, profile editing + email binding, the statistics screen, and the in-game history + GCG export. Friends gain two add paths (interview decision, a deliberate plan change): one-time 6-digit codes (friend_codes table, 12h TTL, single-use, rate-limited redeem); and play-gated requests (shared game required) where an explicit decline is permanent, an ignored request lapses after 30 days, and a code bypasses a decline. Migration 00006 widens friendships_status_chk and adds friend_codes. Lobby notification badge is poll + push: a new generic `notify` event drives it live; the client polls on open/focus. Language stays a single Settings control that writes through to the durable account's preferred_language. GCG export is finished-only (game.ErrGameActive) and shares/downloads the .gcg file. Tests: backend unit + inttest (friend gate/decline/code, ListInvitations, GetStats, GCG gate), gateway transcode round-trips + notify constructor, UI vitest (codecs, win-rate, share choice) + Playwright social specs. Docs: PLAN (Stage 8 done + refinements + TODO-5), ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN, TESTING, module READMEs. |
||
|
|
0284c9b83a |
Stage 7 (wip): tests + UI CI
- Vitest units: board replay, placement machine, premium parity, i18n key parity, FlatBuffers codec round-trips (19 tests) - Playwright smoke (mock transport): guest -> lobby -> board -> place tile -> preview - ui-test.yaml workflow: check/unit/build + bundle-size budget (67.5KB gzip < 100KB) + chromium e2e - gateway transcode tests for games.list (seat display_name), pass, hint - backend integration test for game.ListForAccount |
||
|
|
751e74b14f |
Stage 3: game domain (lifecycle, journal+cache, hint, word-check, GCG, stats)
internal/game drives the engine over a single match and owns everything the
engine does not: event-sourced persistence (a games row + an append-only decoded
move journal, with the live engine.Game kept warm in a cache and rebuilt by
replay on a miss), the play/pass/exchange/resign transitions with
validate-at-submit scoring, an unlimited score/legality preview, the hint
(per-game allowance + profile wallet), the word-check tool with complaint
capture, per-player game state, history and GCG export (Poslfit dialect), and a
background turn-timeout sweeper that auto-resigns overdue turns honouring each
player's daily away window. Like Stages 1-2 it is a service/store layer with no
HTTP; the gateway surface lands in Stage 6.
Engine: additive decoded domain API (Direction, SubmitPlay/SubmitExchange/
EvaluatePlay/HintView/Hand, MoveRecord.{Dir,MainRow,MainCol}, Registry.Lookup,
ParseVariant) so internal/game never imports scrabble-solver; and a Resign fix
so the resigner keeps their score yet never wins (the other player wins a
two-player game). Timeout reuses Resign.
Persistence: migration 00002 adds games, game_players, game_moves, complaints,
account_stats and extends accounts with away_start/away_end/hint_balance; go-jet
regenerated. account gained SpendHint. Config adds BACKEND_DICT_DIR (required),
BACKEND_DICT_VERSION, BACKEND_GAME_TIMEOUT_SWEEP_INTERVAL, BACKEND_GAME_CACHE_TTL;
main loads the registry at boot (hard dependency) and starts the sweeper.
Tests: engine resign + decoded-API tests; game unit tests (GCG, away-window
boundaries, hint budget, cache, keyed mutex, payload); inttest integration
(lifecycle, replay equivalence, timeout sweep with away grace, resign stats,
hint policy, word-check/complaint, per-game-lock). Docs (PLAN, ARCHITECTURE,
FUNCTIONAL +_ru, TESTING, README) updated.
|