00d1bd33e37035fe156b6830b6113bb2d0ba85b3
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7fc1301b31 |
feat(hint): unify the vs_ai idle hint online + offline (server-enforced, monotonic)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m28s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
Online vs_ai hints were broken: #207 made the vs_ai hint button always-enabled and wallet-free (assuming all vs_ai = the offline idle-gate), but the backend still served online vs_ai from the allowance/wallet, so over-clicking hit ErrNoHintsLeft -> a generic error toast. Now online vs_ai uses the SAME idle-gate model as offline. Backend: Hint() for a vs_ai game skips the allowance/wallet and increments no hints_used (owner: vs_ai counts toward no hint statistic), and is idle-gated from the SERVER clock -- it returns ErrHintLocked (code hint_locked) until the robot's last move + 30 min, else serves the top move. GameState/StateView expose hint_unlock_left_seconds (server-computed seconds remaining; 0 for a human game / first move / not-your-turn). Pure helper hintUnlockLeftSeconds unit-tested. Wire: StateView gains hint_unlock_left_seconds (FlatBuffers, additive); pkg/wire + gateway transcode carry it (round-trip test). Client: the gate counts down from a MONOTONIC clock (performance.now()) anchored to the source's seconds-left when it lands (on load from the view; to the full window when the robot moves), so a client clock change cannot skew it and a relaunch re-reads a fresh value. The vs_ai hint button (online + offline) shows the lock + toast; doHint handles the hint_locked backstop by re-syncing. Replaces #207's absolute hintUnlockAtMs on the wire/model/delta (the offline record keeps the absolute for persistence; the view exposes seconds-left). Docs FUNCTIONAL(+_ru)/ARCHITECTURE updated. Verified: go build/vet + game/server/transcode tests (+ new hintUnlockLeftSeconds); ui check 0 / unit 490 / e2e 198 (one pre-existing webkit offline flake) / app entry 114.2/115. |
||
|
|
ff486c80f8 |
feat(hint): persist the vs_ai idle-hint wait (wall-clock + read sanitiser)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m28s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
Per the owner's call, the idle-hint gate now PERSISTS across leaving and reopening the app, instead of the session-scoped monotonic clock: the unlock is a wall-clock instant (hintUnlockAtMs) stamped from the robot's reply, stored on the local record, carried on the game view + the opponent_moved move delta, and read through a sanitiser that caps it at now + the window. So: - the wait survives a relaunch (a stuck turn is not forgotten); - a device clock set BACK cannot freeze the gate (the cap bounds the remaining to the window and self-heals on the next read); - a clock set FORWARD just opens the hint early -- accepted as harmless for a solo game. - lib/hints.ts hintGateRemainingMs now takes the unlock instant + clamps to the window. - localgame: re-add hintUnlockAtMs to the record/meta; stamp it off the robot's reply; sanitise on read (a shared hintUnlock helper feeds stateView + the opponent_moved event). - gamedelta + PushEvent: the move delta carries hintUnlockAtMs so the view stays fresh. - Game.svelte: hintUnlockAt derives from the view; the tick + lock + toast unchanged. - offline.spec.ts: also assert the lock survives a reload (the wait persisted). - Docs FUNCTIONAL(+_ru)/ARCHITECTURE updated (persist + sanitiser, not monotonic-resets). |
||
|
|
56dbf86472 |
feat(lobby): keep lobby/game caches fresh from any screen + invitation delta channel
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 48s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
Builds on the cross-screen cache work: the global stream handler now keeps both caches current no matter which screen is mounted, and invitations become a live delta channel so the lobby's invitations list is fresh from any screen too. Client (boundary already started): - advanceCached now also folds opponent_joined into a not-currently-viewed game's cache via a new pure reducer applyOpponentJoined (extracted and reused by the mounted game board), so opening an open game that filled while you were elsewhere is flash-free. - patchLobbyInvitation upserts a still-pending invitation and removes a terminal one (started/declined/cancelled/expired); the global notify handler calls it on the invitation / invitation_update sub-kinds. Invitations delta channel (no wire/gateway/connector change — the notification already carries the full invitation with id/status/game_id end to end): - notify: a new in-app-only NotifyInvitationUpdate sub + NotificationInvitationUpdate constructor (shares encoding with NotificationInvitation). The Telegram connector renders no message for it, so a decline/cancel never becomes an out-of-app push. - lobby: emit the changed invitation to every participant on respond (accept/decline), on the final accept's game start, and on cancel — so each participant's lobby patches its list in place. The authoritative list holds only pending invitations, so the client's pending-vs-terminal rule matches it exactly. Tests: applyOpponentJoined + patchLobbyInvitation unit tests (TDD), the NotificationInvitationUpdate encoding unit test, and integration assertions that decline/cancel/accept publish invitation_update to every participant. Full local suite green (backend unit+integration, UI check/unit/build/bundle/e2e). Docs: ARCHITECTURE §10 (notify catalog + in-app-only note) and UI_DESIGN updated. |
||
|
|
cf70e6b1fc |
fix(ui): keep lobby/game caches fresh across screens (no stale-status flash)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 48s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 58s
The per-screen in-memory caches (lobbycache, gamecache) were refreshed only by the screen that owns them while it was mounted, so a state change that crossed screens left the other screen's cache stale and it visibly redrew on the next navigation: - game -> lobby: the player's own move advanced the game cache but not the lobby snapshot, and an own move carries no self-directed push event, so returning to the lobby painted the pre-move status until the background refresh corrected it. - lobby -> game (and from any other screen): an opponent's move / game-over refreshed the lobby (while mounted) but never the per-game cache, so opening that game flashed the pre-move board. Make cache freshness cross-screen, owned by the single global stream handler that runs for every live event regardless of the mounted screen: - patchLobbyGame upserts the affected game's GameView into the lobby snapshot; the global handler calls it on opponent_moved / game_over / opponent_joined and on a match_found / game_started seed (so a game started elsewhere is present too). The game board still mirrors the player's own move and its own load() — the two updates no live event carries. - advanceCached (a pure wrapper over the existing delta reducers) advances a not-currently-viewed game's cache from opponent_moved / game_over; the game in view is skipped so its mounted board stays the sole owner (no double apply). End-state behaviour is unchanged (the background refresh always reconciled); this removes the transient stale frame. Unit-tested patchLobbyGame and advanceCached; docs/UI_DESIGN.md updated. |
||
|
|
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. |
||
|
|
41a642ef97 |
R4: push enrichment — events carry a state delta, kill the last poll
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Enrich the in-app live stream into a delta channel so the UI renders a move from the event without a follow-up game.state, and make the matchmaking poll a stream-down fallback. - pkg/fbs: trailing fields on opponent_moved (move+game+bag_len), your_turn (move_count), match_found (state), game_over (game), notify (account/invitation/state), MoveResult (rack+bag_len); regenerate Go + TS. - backend: notify owns the FB encoding (encode.go + payload.go input structs); game/lobby/social map their domain types in. emitMove builds the move delta; game.Service.InitialState feeds match_found/game_started the recipient's initial StateView; friends/invitations notify carry their account/invitation. The move-commit response (submit_play/pass/exchange/resign) returns the actor's refilled rack + bag size. - gateway: MoveResult transcode carries rack+bag_len. - ui: pure lib/gamedelta.ts reducer advances the per-game cache keyed on move_count (idempotent + gap-safe); app.svelte seeds the cache on match_found/game_started; Game.svelte applies the delta (commit/pass/exchange/resign drop their load()); NewGame polls only while app.streamAlive is false. - docs: ARCHITECTURE §10, FUNCTIONAL(+ru), backend/gateway/ui READMEs; PRERELEASE R4 marked done + Refinements. |