96 lines
5.5 KiB
TypeScript
96 lines
5.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
beginCoalitionAdmission, answerCoalitionAdmission, coalitionAllies, coalitionGroups,
|
|
coalitionOf, emptyCoalitionState, leaveCoalition,
|
|
type CoalitionSeat, type CoalitionState, type CoalitionTransition,
|
|
} from '../game/coalition-alliance';
|
|
|
|
function initial() {
|
|
const seats: CoalitionSeat[] = [
|
|
{id:'a', ally:'b', allySinceTurn:1}, {id:'b', ally:'a', allySinceTurn:2},
|
|
...['c','d','e','f'].map(id => ({id, ally:null})),
|
|
];
|
|
return {seats, state:emptyCoalitionState('unlimited')};
|
|
}
|
|
function apply(seats: CoalitionSeat[], transition: CoalitionTransition): CoalitionState {
|
|
for (const seat of seats) {
|
|
if (Object.hasOwn(transition.allyUpdates, seat.id)) seat.ally = transition.allyUpdates[seat.id];
|
|
if (Object.hasOwn(transition.allySinceUpdates, seat.id)) {
|
|
const value = transition.allySinceUpdates[seat.id];
|
|
if (value === null) delete seat.allySinceTurn;
|
|
else seat.allySinceTurn = value;
|
|
}
|
|
}
|
|
return transition.state;
|
|
}
|
|
function invite(seats: CoalitionSeat[], state: CoalitionState, entrant: string, turn: number): CoalitionState {
|
|
const group = coalitionOf(seats, state, 'a'); assert.ok(group);
|
|
const event = `invite-${entrant}-${turn}`;
|
|
state = apply(seats, beginCoalitionAdmission(seats, state, {event, nexus:`nexus-${turn}`, turn,
|
|
groupId:group.id, entrant, proposer:'a', expectedRevision:state.revision}));
|
|
const participants = [...group.members.filter(member => member !== 'a'), entrant];
|
|
for (const actor of participants) {
|
|
const previous = coalitionOf(seats, state, 'a')!.members;
|
|
state = apply(seats, answerCoalitionAdmission(seats, state, {event, nexus:`nexus-${turn}`,
|
|
turn, actor, accept:true, expectedRevision:state.revision}));
|
|
if (actor !== entrant) assert.deepEqual(coalitionOf(seats, state, 'a')!.members, previous,
|
|
'The existing group does not expand before every participant approves.');
|
|
}
|
|
return state;
|
|
}
|
|
|
|
void test('unlimited membership grows to six with unanimous approval and retained edge ages', () => {
|
|
const {seats} = initial(); let state = emptyCoalitionState('unlimited');
|
|
for (const [index, entrant] of ['c','d','e','f'].entries()) state = invite(seats, state, entrant, index + 3);
|
|
const group = coalitionOf(seats,state,'a'); assert.ok(group);
|
|
assert.deepEqual(group.members,['a','b','c','d','e','f']);
|
|
assert.equal(group.edges.length,15);
|
|
assert.deepEqual(group.edges.find(edge => edge.members.join('') === 'ab')?.legacySince,[1,2]);
|
|
assert.equal(group.edges.find(edge => edge.members.join('') === 'ad')?.formedTurn,4);
|
|
assert.deepEqual(coalitionAllies(seats,state,'a'),['b','c','d','e','f']);
|
|
assert.ok(seats.every(seat => seat.ally === null));
|
|
assert.equal(coalitionGroups(seats, JSON.parse(JSON.stringify(state))).length,1);
|
|
const before = structuredClone(state);
|
|
assert.throws(() => beginCoalitionAdmission(seats,state,{event:'full',nexus:'n',turn:8,
|
|
groupId:group.id,entrant:'f',proposer:'a',expectedRevision:state.revision}));
|
|
assert.deepEqual(state,before);
|
|
});
|
|
|
|
void test('departures preserve surviving edges and restore the original pair scalar ages', () => {
|
|
const {seats} = initial(); let state = emptyCoalitionState('unlimited');
|
|
state = invite(seats,state,'c',3); state = invite(seats,state,'d',4);
|
|
state = apply(seats,leaveCoalition(seats,state,{actor:'d',expectedRevision:state.revision}));
|
|
let group = coalitionOf(seats,state,'a'); assert.ok(group);
|
|
assert.deepEqual(group.members,['a','b','c']); assert.equal(group.edges.length,3);
|
|
assert.equal(group.edges.find(edge => edge.members.join('') === 'ac')?.formedTurn,3);
|
|
state = apply(seats,leaveCoalition(seats,state,{actor:'c',expectedRevision:state.revision}));
|
|
group = coalitionOf(seats,state,'a'); assert.ok(group);
|
|
assert.deepEqual(group.members,['a','b']);
|
|
assert.equal(seats[0].ally,'b'); assert.equal(seats[1].ally,'a');
|
|
assert.equal(seats[0].allySinceTurn,1); assert.equal(seats[1].allySinceTurn,2);
|
|
assert.equal(seats[2].ally,null); assert.equal(seats[3].ally,null);
|
|
state = invite(seats,state,'d',5);
|
|
assert.deepEqual(coalitionOf(seats,state,'a')?.members,['a','b','d']);
|
|
assert.deepEqual(coalitionOf(seats,state,'a')?.edges.find(edge => edge.members.join('') === 'ab')?.legacySince,[1,2]);
|
|
});
|
|
|
|
void test('decline, stale answers and an omitted variant preserve the original alliance', () => {
|
|
const {seats} = initial(); let state = emptyCoalitionState('unlimited');
|
|
state = invite(seats,state,'c',3);
|
|
const group = coalitionOf(seats,state,'a'); assert.ok(group);
|
|
state = apply(seats,beginCoalitionAdmission(seats,state,{event:'invite-d',nexus:'n',turn:4,
|
|
groupId:group.id,entrant:'d',proposer:'d',expectedRevision:state.revision}));
|
|
const before = structuredClone(state);
|
|
assert.throws(() => answerCoalitionAdmission(seats,state,{event:'invite-d',nexus:'n',turn:4,
|
|
actor:'a',accept:true,expectedRevision:state.revision - 1}));
|
|
assert.deepEqual(state,before);
|
|
state = apply(seats,answerCoalitionAdmission(seats,state,{event:'invite-d',nexus:'n',turn:4,
|
|
actor:'b',accept:false,expectedRevision:state.revision}));
|
|
assert.equal(state.proposal,null); assert.deepEqual(coalitionOf(seats,state,'a')?.members,['a','b','c']);
|
|
const defaultState = {...state}; delete defaultState.variant;
|
|
assert.throws(() => beginCoalitionAdmission(seats,defaultState,{event:'new',nexus:'n',turn:4,
|
|
groupId:group.id,entrant:'d',proposer:'a',expectedRevision:defaultState.revision}));
|
|
assert.throws(() => coalitionGroups(seats,{...state,variant:'invalid'} as unknown as CoalitionState));
|
|
});
|