Files
Dune/tests/coalition-unlimited-membership.test.ts
T
Quintine 9cb65a21f5
Verify and publish checkpoint container / container (push) Canceled after 0s
Prototype unlimited alliance variant
2026-10-29 00:33:42 +11:00

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