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:
@@ -24,3 +24,29 @@ export function hintsLeft(
|
||||
const allowance = Math.max(0, view.hintsRemaining - view.walletBalance);
|
||||
return allowance + Math.max(0, walletBalance);
|
||||
}
|
||||
|
||||
/** HINT_GATE_MS is the idle time a vs_ai player must be stuck on a turn before a hint unlocks. */
|
||||
export const HINT_GATE_MS = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* hintGateRemainingMs returns how long (in milliseconds) until a vs_ai game's idle hint unlocks — 0
|
||||
* once it is available. It is measured from a MONOTONIC clock (performance.now()), never a wall-clock
|
||||
* timestamp, so a device clock change — by the user or an auto-sync — can neither open nor freeze the
|
||||
* gate. gateStartMono is the performance.now() captured when the current turn's wait began; null
|
||||
* means the gate is open (the human's first move, or not a gated game). The vs_ai hint is unlimited
|
||||
* and wallet-free; this idle gate is an anti-frustration aid that unlocks only after a stuck turn.
|
||||
*/
|
||||
export function hintGateRemainingMs(
|
||||
gateStartMono: number | null,
|
||||
monoNow: number,
|
||||
gateMs: number = HINT_GATE_MS,
|
||||
): number {
|
||||
if (gateStartMono === null) return 0;
|
||||
return Math.max(0, gateMs - (monoNow - gateStartMono));
|
||||
}
|
||||
|
||||
/** hintLockMinutes rounds a remaining-milliseconds gap up to whole minutes for the "available in N
|
||||
* min." toast, so a gate that is still closed never reads 0. */
|
||||
export function hintLockMinutes(remainingMs: number): number {
|
||||
return Math.ceil(remainingMs / 60000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user