// Hint-count derivation, kept out of the .svelte component so it is unit-testable. // // The badge shows the per-game hint allowance remaining plus the player's global hint wallet. The // view's hints_remaining is the per-game allowance alone; the wallet is global (shared across every // game) and read live from the profile (payments), never the per-game snapshot — so a wallet hint // spent in another game stays reflected here. import type { StateView } from './model'; /** * hintsLeft is the hint count for the badge: the per-game allowance remaining (view.hintsRemaining) * plus the live global wallet balance (passed in from the profile, so it stays correct when a wallet * hint was spent in another game since this view was fetched). */ export function hintsLeft( view: Pick | null, walletBalance: number, ): number { if (!view) return 0; return Math.max(0, view.hintsRemaining) + 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 counts down from a MONOTONIC clock (performance.now()), so a client clock * change cannot skew it: gateStartMono is performance.now() captured when the source's "seconds left" * was received, gateLeftMs is that seconds-left in ms, and monoNow is performance.now() now; the * remaining is gateLeftMs minus the monotonic time elapsed since the anchor. gateStartMono null means * the gate is open (the human's first move, or a non-gated game). The duration comes from the source * (the SERVER clock online, the device clock offline) and is re-fetched on every load, so the wait * both persists across a relaunch and stays immune to a live clock change. */ export function hintGateRemainingMs(gateStartMono: number | null, gateLeftMs: number, monoNow: number): number { if (gateStartMono === null) return 0; return Math.max(0, gateLeftMs - (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); }