cozy-games/packages/mnswpr/core/session/session.js
2026-07-04 07:49:46 +02:00

72 lines
2.3 KiB
JavaScript

// @ts-check
/**
* Layer 1 — owns lifecycle, the (injected) clock, and the move log; delegates all
* game meaning to an injected `rules` object. This is where timing authority
* lives: on the client `clock` is `Date.now` (cosmetic); on a server it is the
* server's clock (authoritative). The session never calls a wall clock itself.
* Future home: @cozy-games/game-session.
*
* @typedef {{ init: Function, apply: Function, status: Function, project: Function }} Rules
*/
export class GameSession {
/**
* @param {Rules} rules
* @param {{ seed: number, config: object, clock?: () => number }} opts
*/
constructor(rules, { seed, config, clock = () => 0 }) {
this.rules = rules
this.clock = clock
this.state = rules.init(seed, config)
/** @type {Array<{ move: object, t: number }>} */
this._log = []
this._t0 = null
this._tEnd = null
}
/**
* Apply a move: stamp it, fold it through the rules, and return the projected
* view + events + authoritative elapsed time.
* @param {object} move
*/
applyMove(move) {
const t = this.clock()
const before = this.rules.status(this.state)
const { state, events } = this.rules.apply(this.state, move)
this.state = state
this._log.push({ move, t })
const after = this.rules.status(state)
// Timer starts on the first move that leaves 'fresh' (the opening reveal).
if (before === 'fresh' && after !== 'fresh' && this._t0 === null) this._t0 = t
if ((after === 'won' || after === 'lost') && this._tEnd === null) this._tEnd = t
return { events, view: this.rules.project(state), time: this.elapsed() }
}
status() {
return this.rules.status(this.state)
}
view() {
return this.rules.project(this.state)
}
log() {
return this._log.slice()
}
/** Authoritative elapsed ms: first move → terminal move (or → now if ongoing). */
elapsed() {
if (this._t0 === null) return 0
const end = this._tEnd !== null ? this._tEnd : this.clock()
return end - this._t0
}
/** The signed-off result, or null while the game is still in progress. */
result() {
const status = this.status()
if (status !== 'won' && status !== 'lost') return null
return { status, time: this.elapsed(), seed: this.state.seed, config: this.state.config, log: this.log() }
}
}