41 lines
1.5 KiB
JavaScript
41 lines
1.5 KiB
JavaScript
// @ts-check
|
|
|
|
/**
|
|
* Deterministically re-run a submitted game from `{ seed, config, log }` and
|
|
* report the authoritative outcome. A server calls this at submit time to verify
|
|
* a score without trusting the client: it recomputes the result and time from the
|
|
* moves, and rejects logs that don't terminate or whose timestamps aren't
|
|
* monotonic. (The "verifiable-replay" anti-cheat path — no live server needed.)
|
|
*
|
|
* @param {{ init: Function, apply: Function, status: Function }} rules
|
|
* @param {{ seed: number, config: object, log: Array<{ move: object, t: number }> }} submission
|
|
* @returns {{ status: string, time: number, valid: boolean, reason: string | null }}
|
|
*/
|
|
export function replay(rules, { seed, config, log }) {
|
|
let state = rules.init(seed, config)
|
|
let monotonic = true
|
|
let prevT = -Infinity
|
|
let t0 = null
|
|
let tEnd = null
|
|
|
|
for (const { move, t } of log) {
|
|
if (t < prevT) monotonic = false
|
|
prevT = t
|
|
const before = rules.status(state)
|
|
state = rules.apply(state, move).state
|
|
const after = rules.status(state)
|
|
if (before === 'fresh' && after !== 'fresh' && t0 === null) t0 = t
|
|
if ((after === 'won' || after === 'lost') && tEnd === null) tEnd = t
|
|
}
|
|
|
|
const status = rules.status(state)
|
|
const terminal = status === 'won' || status === 'lost'
|
|
const time = t0 !== null && tEnd !== null ? tEnd - t0 : 0
|
|
const valid = monotonic && terminal
|
|
const reason = valid
|
|
? null
|
|
: !monotonic
|
|
? 'non-monotonic timestamps'
|
|
: 'log does not reach a terminal state'
|
|
return { status, time, valid, reason }
|
|
}
|