import { Action } from '../rules/Action'; import { GameEvent } from '../rules/GameEvent'; import { GameState, Phase, PlayerId } from '../rules/GameState'; import { apply, createGame, currentActor, GameOptions } from '../rules/DurakRules'; import { PlayerView, projectView } from '../bot/PlayerView'; export type MatchListener = (event: GameEvent) => void; /** * One match in progress. * * A thin stateful shell over the pure reducer: it remembers the current state, * hands out player views, and broadcasts events. Still free of any engine * dependency — the game screen holds one of these, and so does the headless * soak test. */ export class Match { private _state: GameState; private _listeners: MatchListener[] = []; private readonly _history: GameEvent[] = []; constructor(options: GameOptions) { const { state, events } = createGame(options); this._state = state; this._history.push(...events); } /** The full state. The UI must not show a player anything they cannot see — use `viewFor`. */ get state(): GameState { return this._state; } /** Every event since the deal, oldest first. */ get history(): readonly GameEvent[] { return this._history; } get finished(): boolean { return this._state.phase === Phase.Finished; } /** The fool, or null while the game runs and on the rare mutual finish. */ get loser(): PlayerId | null { return this._state.loser; } get currentActor(): PlayerId | null { return currentActor(this._state); } viewFor(player: PlayerId): PlayerView { return projectView(this._state, player); } /** * Applies an action and broadcasts what happened. Throws `IllegalActionError` * if the action is not permitted, leaving the match untouched. */ perform(action: Action): GameEvent[] { const result = apply(this._state, action); this._state = result.state; this._history.push(...result.events); for (const event of result.events) { for (const listener of this._listeners) { listener(event); } } return result.events; } /** Subscribes to the event stream. Returns a function that unsubscribes. */ subscribe(listener: MatchListener): () => void { this._listeners.push(listener); return () => { const i = this._listeners.indexOf(listener); if (i >= 0) this._listeners.splice(i, 1); }; } /** Drops every listener. Call when tearing a match screen down. */ clearListeners(): void { this._listeners.length = 0; } }