48 lines
1.7 KiB
JavaScript
48 lines
1.7 KiB
JavaScript
// @ts-check
|
|
import { GameSession } from '../core/session/session.js'
|
|
|
|
/**
|
|
* In-process transport: runs a `GameSession` locally and emits its events.
|
|
*
|
|
* The Transport interface is intentionally async — `send()` returns a Promise —
|
|
* so a future `RemoteTransport` (server-authoritative) is a drop-in with no
|
|
* change to the client (server-readiness invariant #1). Rendering is driven by
|
|
* `onEvent`, which Local fires synchronously inside `send()` and Remote would
|
|
* fire when the server replies; the client never depends on `send()`'s timing.
|
|
*
|
|
* Only serializable messages cross this boundary (moves in; events + a projected
|
|
* view out) — never the live session/state (invariant #2).
|
|
*/
|
|
export class LocalTransport {
|
|
/**
|
|
* @param {object} rules - a GameRules implementation (e.g. MinesweeperRules)
|
|
* @param {{ seed: number, config: object, clock?: () => number }} opts
|
|
*/
|
|
constructor(rules, opts) {
|
|
this.session = new GameSession(rules, opts)
|
|
this._subs = []
|
|
}
|
|
|
|
/** @param {(payload: { events: object[], view: object, time: number }) => void} cb */
|
|
onEvent(cb) {
|
|
this._subs.push(cb)
|
|
return () => { this._subs = this._subs.filter(s => s !== cb) }
|
|
}
|
|
|
|
/**
|
|
* Apply a move and emit the resulting events/view. Returns a Promise for
|
|
* interface parity with a remote transport.
|
|
* @param {object} move
|
|
* @returns {Promise<{ events: object[], view: object, time: number }>}
|
|
*/
|
|
send(move) {
|
|
const payload = this.session.applyMove(move)
|
|
for (const cb of this._subs) cb(payload)
|
|
return Promise.resolve(payload)
|
|
}
|
|
|
|
status() { return this.session.status() }
|
|
view() { return this.session.view() }
|
|
result() { return this.session.result() }
|
|
elapsed() { return this.session.elapsed() }
|
|
}
|