feat(hint): idle-gated wallet-free vs_ai hint on a monotonic clock (B4)
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
A vs_ai hint is now unlimited and wallet-free, but idle-gated as an anti-frustration
aid: it unlocks only after the player has been stuck ~30 min on a turn (timed from the
robot's last move; the human's first move, before the robot has played, is exempt).
While gated the hint button carries a small lock badge and a tap shows the remaining
minutes ('Available in N min.'); the lock lifts live at the mark.
The gate is enforced CLIENT-SIDE against a MONOTONIC clock (performance.now()), never a
wall-clock timestamp: a device clock the player controls (or an auto-sync) must not be
able to open or freeze it. The wait is therefore session-scoped -- a reload restarts it
(there is no tamper-proof way to carry idle time across a relaunch without a wall clock).
An online vs_ai game will gate the same way but from the server's clock (a follow-up).
- lib/hints.ts: HINT_GATE_MS + pure hintGateRemainingMs (monotonic) + hintLockMinutes;
removed the wall-clock hintRemainingMs. Unit-tested red->green.
- Game.svelte: the monotonic gate (hintGateStart/monoNow, armed on the turn change), a
vs_ai hint button (plain: no confirm, no count; lock badge + gated tap -> toast), a
live 10s tick.
- localgame: removed the wall-clock hint gate and the now-dead robotLastMoveAtUnix field
from source.ts/serialize.ts (the client is authoritative); hint() just serves the top
move.
- i18n game.hintLockedIn.
- offline.spec.ts: assert the first move is un-gated and the lock arms after the robot moves.
- Docs FUNCTIONAL(+_ru) + ARCHITECTURE; bundle budget 114->115 (game-screen feature).
This commit is contained in:
@@ -59,9 +59,15 @@ describe('LocalSource', () => {
|
||||
expect(events.some((e) => e.kind === 'opponent_moved')).toBe(true);
|
||||
});
|
||||
|
||||
it('gates the hint until 30 minutes since the robot last moved', async () => {
|
||||
it('does not wall-clock-gate the hint (the idle gate is client-side and monotonic)', async () => {
|
||||
const src = await newSource('local:g3', 7n);
|
||||
await expect(src.hint('local:g3')).rejects.toMatchObject({ code: 'hint_not_ready' });
|
||||
// hint() serves the engine's top move (or no_hint when the tiny sample dictionary has none for
|
||||
// this rack), but never the old timestamp gate 'hint_not_ready' — that moved to the client
|
||||
// (lib/hints, measured with performance.now()) so a device clock change cannot defeat it.
|
||||
await src.hint('local:g3').then(
|
||||
(h) => expect(h.move).toBeDefined(),
|
||||
(e) => expect((e as { code: string }).code).toBe('no_hint'),
|
||||
);
|
||||
});
|
||||
|
||||
it('records decoded moves in the history', async () => {
|
||||
|
||||
Reference in New Issue
Block a user