89 lines
4.9 KiB
TypeScript
89 lines
4.9 KiB
TypeScript
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, 8000);
|
|
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, 15000);
|
|
});
|
|
|
|
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);
|
|
});
|