Initial commit: ssessesse ki battle
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { GAME_CHECKPOINT_KEY } from '../../constants/recording.js';
|
||||
import { createMatchEngine } from '../shared/match-engine.js';
|
||||
import type { ActiveMatch, LocalMatchState, MatchCheckpoint, RecordAck, ResultSubmission } from '../shared/p2p-types.js';
|
||||
import type { ActionId } from '../shared/types.js';
|
||||
import type { SessionStorage } from './result-reporter.js';
|
||||
import { createFriendMatch, type GamePeer } from './friend-match.js';
|
||||
|
||||
export interface GameClock { now(): number; setTimeout(fn: () => void, ms: number): any; clearTimeout(id: any): void }
|
||||
export interface ControllerConfig {
|
||||
activeMatch: ActiveMatch; gameSessionId: string; selfIndex: 0 | 1;
|
||||
reporter: { submit(data: ResultSubmission): Promise<RecordAck> }; storage: SessionStorage;
|
||||
clock?: GameClock; random?: () => number; onRematch?: () => Promise<void>;
|
||||
peer?: GamePeer;
|
||||
}
|
||||
export interface ControllerCheckpoint { gameSessionId: string; activeMatch: ActiveMatch; checkpoint: MatchCheckpoint; epoch: number; confirmed?: MatchCheckpoint }
|
||||
const defaultClock: GameClock = { now: Date.now, setTimeout: (fn, ms) => setTimeout(fn, ms), clearTimeout: id => clearTimeout(id) };
|
||||
export function createMatchController(config: ControllerConfig) {
|
||||
const clock = config.clock ?? defaultClock;
|
||||
let offset = 0;
|
||||
const engine = createMatchEngine({ ...config.activeMatch, selfIndex: config.selfIndex, clock: { now: () => clock.now() + offset }, random: config.random });
|
||||
let friend: ReturnType<typeof createFriendMatch> | undefined;
|
||||
const listeners = new Set<(state: LocalMatchState) => void>();
|
||||
let timer: any, destroyed = false, started = false, recording = false;
|
||||
function publish() {
|
||||
if (destroyed) return;
|
||||
const checkpoint: ControllerCheckpoint = { gameSessionId: config.gameSessionId, activeMatch: config.activeMatch, checkpoint: engine.checkpoint(), epoch: 0, ...friend?.snapshot() };
|
||||
config.storage.setItem(GAME_CHECKPOINT_KEY, JSON.stringify(checkpoint));
|
||||
for (const listener of listeners) listener({ ...engine.view(), appearances: config.activeMatch.appearances });
|
||||
}
|
||||
async function record() {
|
||||
const state = engine.view(); if (!state.finished || state.finished.saved || recording || destroyed) return;
|
||||
recording = true;
|
||||
const outcome = state.aborted ? 'aborted' : state.finished.winnerIndex === null ? 'draw' : state.finished.winnerIndex === config.selfIndex ? 'win' : 'loss';
|
||||
try {
|
||||
const ack = await config.reporter.submit({ ...config.activeMatch, outcome, reason: state.finished.reason });
|
||||
if (!destroyed) { engine.saved(ack.stats); publish(); }
|
||||
} catch { if (!destroyed) { engine.saveFailed(); publish(); } }
|
||||
finally { recording = false; }
|
||||
}
|
||||
function tick() {
|
||||
if (destroyed) return;
|
||||
clock.clearTimeout(timer);
|
||||
let state = engine.view();
|
||||
if (state.phase === 'countdown' && clock.now() >= state.startsAt) engine.beginTurn(state.startsAt);
|
||||
else if (state.phase === 'revealing' && clock.now() >= state.reveal!.revealEndsAt) engine.beginTurn(state.reveal!.revealEndsAt);
|
||||
state = engine.view();
|
||||
if (state.phase === 'selecting' && clock.now() >= state.deadline) engine.resolve([state.ownAction, engine.botAction()]);
|
||||
publish(); state = engine.view();
|
||||
if (state.phase === 'finished') { void record(); return; }
|
||||
const next = state.phase === 'countdown' ? state.startsAt : state.phase === 'selecting' ? state.deadline : state.reveal!.revealEndsAt;
|
||||
timer = clock.setTimeout(tick, Math.max(0, next - clock.now()));
|
||||
}
|
||||
return {
|
||||
async start() {
|
||||
if (started) return; started = true;
|
||||
let saved: ControllerCheckpoint | undefined;
|
||||
try { saved = JSON.parse(config.storage.getItem(GAME_CHECKPOINT_KEY) ?? 'null') ?? undefined; } catch { /* new game */ }
|
||||
if (saved?.gameSessionId !== config.gameSessionId || saved.activeMatch.matchId !== config.activeMatch.matchId) saved = undefined;
|
||||
if (saved) engine.restore(saved.checkpoint);
|
||||
if (saved?.checkpoint.state.phase === 'finished' && (config.activeMatch.mode === 'bot' || saved.confirmed?.state.phase === 'finished' || saved.checkpoint.state.aborted)) {
|
||||
publish(); await record(); return;
|
||||
}
|
||||
if (config.activeMatch.mode === 'friend') {
|
||||
if (!config.peer) throw new Error('PEER_REQUIRED');
|
||||
friend = createFriendMatch({ matchId: config.activeMatch.matchId, slot: config.selfIndex, engine, peer: config.peer, clock,
|
||||
setOffset: value => { offset = value; }, changed: publish, finished: () => { void record(); }, saved });
|
||||
await friend.start(); return;
|
||||
}
|
||||
if (!saved) engine.start();
|
||||
tick();
|
||||
},
|
||||
async submitAction(action: ActionId) { if (friend?.recovering) return false; const accepted = engine.lockAction(action); if (accepted) publish(); return accepted; },
|
||||
async leave() {
|
||||
if (friend) { await friend.leave(); await record(); return; }
|
||||
clock.clearTimeout(timer);
|
||||
if (engine.view().phase !== 'finished') engine.finish(config.selfIndex === 0 ? 1 : 0, 'forfeit');
|
||||
publish(); await record();
|
||||
},
|
||||
async rematch() {
|
||||
if (!engine.view().finished?.saved) return;
|
||||
await config.onRematch?.();
|
||||
if (!destroyed) friend?.requestRematch();
|
||||
},
|
||||
view: () => ({ ...engine.view(), appearances: config.activeMatch.appearances }),
|
||||
subscribe(listener: (state: LocalMatchState) => void) { listeners.add(listener); listener({ ...engine.view(), appearances: config.activeMatch.appearances }); return () => { listeners.delete(listener); }; },
|
||||
retrySave: record,
|
||||
destroy() { destroyed = true; clock.clearTimeout(timer); friend?.destroy(); listeners.clear(); },
|
||||
};
|
||||
}
|
||||
export type MatchController = ReturnType<typeof createMatchController>;
|
||||
Reference in New Issue
Block a user