import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createMatchEngine } from '../src/shared/match-engine.js'; function setup(mode: 'bot' | 'friend' = 'friend') { let now = 1000; const engine = createMatchEngine({ matchId: 'test-match', mode, selfIndex: 0, clock: { now: () => now }, random: () => 0 }); return { engine, at: (value: number) => { now = value; } }; } test('countdown and turn deadlines gate actions without a server', () => { const { engine, at } = setup(); engine.start(1000); assert.equal(engine.view().phase, 'countdown'); assert.equal(engine.lockAction('charge'), false); at(3999); assert.equal(engine.beginTurn(), false); at(4000); assert.equal(engine.beginTurn(), true); assert.equal(engine.view().deadline, 7000); assert.equal(engine.lockAction('blast'), false); assert.equal(engine.lockAction('charge'), true); assert.equal(engine.lockAction('guard'), false); at(8000); assert.equal(engine.lockAction('charge'), false); engine.resolve(['charge', 'charge']); assert.deepEqual(engine.view().energy, [1, 1]); assert.equal(engine.view().phase, 'revealing'); at(9199); assert.equal(engine.beginTurn(), false); at(9200); assert.equal(engine.beginTurn(), true); assert.equal(engine.view().turnId, 2); }); test('a turn resolves once and a hit ends the game', () => { const { engine, at } = setup(); engine.start(1000); at(4000); engine.beginTurn(); at(8000); engine.resolve(['charge', 'charge']); assert.throws(() => engine.resolve(['charge', 'charge']), /NOT_SELECTING/); at(9200); engine.beginTurn(); at(13200); engine.resolve(['blast', 'charge']); assert.equal(engine.view().finished?.winnerIndex, 0); assert.equal(engine.view().finished?.reason, 'hit'); assert.equal(engine.lockAction('charge'), false); assert.equal(engine.beginTurn(), false); }); test('only verified missing choices count as timeouts; technical abort leaves outcome separate', () => { const { engine, at } = setup(); engine.start(1000); at(4000); engine.beginTurn(); at(8000); engine.resolve([null, 'charge']); assert.equal(engine.view().finished?.winnerIndex, 1); assert.equal(engine.view().finished?.reason, 'timeout'); const other = setup(); other.engine.start(1000); other.at(4000); other.engine.beginTurn(); other.at(8000); other.engine.resolve([null, null]); assert.equal(other.engine.view().finished?.reason, 'double_timeout'); const aborted = setup(); aborted.engine.abort('connection_failed'); assert.equal(aborted.engine.view().aborted, true); assert.equal(aborted.engine.view().phase, 'finished'); }); test('checkpoints are copies and restore cannot overwrite match identity', () => { const { engine, at } = setup(); engine.start(1000); at(4000); engine.beginTurn(); engine.lockAction('charge'); const snapshot = engine.checkpoint(); snapshot.state.energy[0] = 99; assert.equal(engine.view().energy[0], 0); const clean = engine.checkpoint(); const resumed = setup(); resumed.at(4000); resumed.engine.restore(clean); assert.equal(resumed.engine.view().ownAction, 'charge'); assert.throws(() => resumed.engine.restore({ ...clean, matchId: 'other' }), /MATCH_MISMATCH/); }); test('bot commits from public energy before human submission', () => { const { engine, at } = setup('bot'); engine.start(1000); at(4000); engine.beginTurn(); assert.equal(engine.botAction(), 'charge'); engine.lockAction('guard'); assert.equal(engine.botAction(), 'charge'); at(8000); engine.resolve([engine.view().ownAction, engine.botAction()]); assert.deepEqual(engine.view().energy, [0, 1]); }); test('confirmed reveal timing and recovery keep energy while discarding an unconfirmed choice', () => { const { engine, at } = setup(); engine.start(1000); at(4000); engine.beginTurn(); at(8000); engine.resolve(['charge', 'charge']); engine.present(8500); assert.equal(engine.view().reveal?.revealEndsAt, 9700); const confirmed = engine.checkpoint(); at(9700); engine.beginTurn(); engine.lockAction('blast'); at(11000); engine.resumeTurn(confirmed, 11000); assert.deepEqual(engine.view().energy, [1, 1]); assert.equal(engine.view().turnId, 2); assert.equal(engine.view().ownAction, null); assert.equal(engine.view().deadline, 14000); }); test('rollback and recovery keep published revisions increasing so the view accepts restored states', () => { const { engine, at } = setup(); engine.start(); const confirmed = engine.checkpoint(); at(4000); engine.beginTurn(); engine.lockAction('charge'); engine.presence(false); const before = engine.view().version; engine.restore(confirmed); engine.abort('connection_failed'); assert.ok(engine.view().version > before); const other = setup(); other.engine.start(); const start = other.engine.checkpoint(); other.at(4000); other.engine.beginTurn(); other.engine.lockAction('charge'); other.engine.presence(false); const previous = other.engine.view().version; other.engine.resumeTurn(start, 5000); assert.ok(other.engine.view().version > previous); });