Files
ssessesse-ki-battle/tests/match-engine.test.ts
T

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, 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);
});