101 lines
5.2 KiB
TypeScript
101 lines
5.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
answerCoalitionAdmission, beginCoalitionAdmission, coalitionOf, emptyCoalitionState,
|
|
leaveCoalition, type CoalitionSeat, type CoalitionState, type CoalitionTransition,
|
|
} from '../game/coalition-alliance';
|
|
|
|
function seats(): CoalitionSeat[] {
|
|
return [{ id: 'a', ally: 'b', allySinceTurn: 2 }, { id: 'b', ally: 'a', allySinceTurn: 2 },
|
|
{ id: 'c', ally: null }, { id: 'd', ally: null }];
|
|
}
|
|
|
|
function apply(players: CoalitionSeat[], transition: CoalitionTransition): CoalitionSeat[] {
|
|
return players.map(player => {
|
|
const result = { ...player };
|
|
if (Object.hasOwn(transition.allyUpdates, player.id)) result.ally = transition.allyUpdates[player.id];
|
|
if (Object.hasOwn(transition.allySinceUpdates, player.id)) {
|
|
const turn = transition.allySinceUpdates[player.id];
|
|
if (turn === null) delete result.allySinceTurn;
|
|
else result.allySinceTurn = turn;
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
|
|
function begin(players: CoalitionSeat[], state: CoalitionState, entrant: string, event: string): CoalitionTransition {
|
|
const group = coalitionOf(players, state, 'a'); assert.ok(group);
|
|
return beginCoalitionAdmission(players, state, { event, nexus: 'nexus', turn: 5,
|
|
groupId: group.id, entrant, proposer: 'a', expectedRevision: state.revision });
|
|
}
|
|
|
|
function answer(players: CoalitionSeat[], state: CoalitionState, actor: string, event: string): CoalitionTransition {
|
|
return answerCoalitionAdmission(players, state, { event, nexus: 'nexus', turn: 5,
|
|
actor, accept: true, expectedRevision: state.revision });
|
|
}
|
|
|
|
function triple(variant: CoalitionState['variant']) {
|
|
const players = seats(), original = emptyCoalitionState(variant);
|
|
const offered = begin(players, original, 'c', 'first');
|
|
const approved = answer(players, offered.state, 'b', 'first');
|
|
const completed = answer(players, approved.state, 'c', 'first');
|
|
return { players: apply(players, completed), state: completed.state, original, offered, approved };
|
|
}
|
|
|
|
void test('three-short preserves its saved variant and the actual invitation revision across unanimous admission', () => {
|
|
const { players, state, original, offered, approved } = triple('three-short');
|
|
assert.deepEqual(original, { version: 1, variant: 'three-short', revision: 0, groups: [], proposal: null, usedEvents: [] });
|
|
assert.equal(offered.state.variant, 'three-short');
|
|
assert.deepEqual(offered.state.proposal?.approvals, ['a']);
|
|
assert.equal(approved.state.variant, 'three-short');
|
|
assert.deepEqual(approved.state.proposal?.approvals, ['a', 'b']);
|
|
assert.equal(state.variant, 'three-short');
|
|
assert.equal(state.revision, 3);
|
|
assert.deepEqual(state.usedEvents, ['first']);
|
|
assert.deepEqual(coalitionOf(players, JSON.parse(JSON.stringify(state)) as CoalitionState, 'a')?.members, ['a', 'b', 'c']);
|
|
assert.equal(state.proposal, null);
|
|
});
|
|
|
|
void test('three-short refuses a fourth member while unlimited preserves its separate six-member capacity', () => {
|
|
for (const variant of [undefined, 'three-short'] as const) {
|
|
const { players, state } = triple(variant), before = structuredClone({ players, state });
|
|
assert.throws(() => begin(players, state, 'd', 'second'));
|
|
assert.deepEqual({ players, state }, before);
|
|
}
|
|
const { players, state } = triple('unlimited');
|
|
const offer = begin(players, state, 'd', 'second');
|
|
let next = answer(players, offer.state, 'b', 'second');
|
|
next = answer(players, next.state, 'c', 'second');
|
|
next = answer(players, next.state, 'd', 'second');
|
|
assert.equal(next.state.variant, 'unlimited');
|
|
assert.deepEqual(coalitionOf(apply(players, next), next.state, 'a')?.members, ['a', 'b', 'c', 'd']);
|
|
});
|
|
|
|
void test('three-short departure retains variant and rejects a stale invitation revision or replayed event', () => {
|
|
const { players, state } = triple('three-short');
|
|
const before = structuredClone({ players, state });
|
|
const left = leaveCoalition(players, state, { actor: 'c', expectedRevision: state.revision });
|
|
const reduced = apply(players, left);
|
|
assert.equal(left.state.variant, 'three-short');
|
|
assert.equal(left.state.revision, state.revision + 1);
|
|
const group = coalitionOf(reduced, left.state, 'a'); assert.ok(group);
|
|
const request = { event: 'first', nexus: 'nexus', turn: 5, groupId: group.id,
|
|
entrant: 'c', proposer: 'a', expectedRevision: left.state.revision };
|
|
assert.throws(() => beginCoalitionAdmission(reduced, left.state, request));
|
|
assert.throws(() => beginCoalitionAdmission(reduced, left.state, {
|
|
...request, event: 'fresh', expectedRevision: state.revision,
|
|
}));
|
|
const again = beginCoalitionAdmission(reduced, left.state, { ...request, event: 'fresh' });
|
|
assert.equal(again.state.variant, 'three-short');
|
|
assert.deepEqual(again.state.usedEvents, ['first', 'fresh']);
|
|
assert.deepEqual({ players, state }, before);
|
|
});
|
|
|
|
void test('older missing variants keep their exact shape and invalid variant strings are rejected', () => {
|
|
const legacy = emptyCoalitionState();
|
|
assert.equal(Object.hasOwn(legacy, 'variant'), false);
|
|
assert.deepEqual(JSON.parse(JSON.stringify(legacy)), legacy);
|
|
const invalid = { ...legacy, variant: 'short' } as unknown as CoalitionState;
|
|
assert.throws(() => coalitionOf(seats(), invalid, 'a'));
|
|
});
|