Files
Dune/tests/coalition-alliance.test.ts
Quintine cea310b030
Verify and publish checkpoint container / container (push) Successful in 10m14s
Prototype charity Truthtrance and coalition rule foundations
2026-10-28 21:58:30 +11:00

254 lines
15 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
answerCoalitionAdmission, areCoalitionAllies, beginCoalitionAdmission, coalitionAllies,
coalitionEdgeTurn, coalitionGroups, 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 next = {...player};
if (Object.hasOwn(transition.allyUpdates, player.id)) next.ally = transition.allyUpdates[player.id];
if (Object.hasOwn(transition.allySinceUpdates, player.id)) {
const value = transition.allySinceUpdates[player.id];
if (value === null) delete next.allySinceTurn;
else next.allySinceTurn = value;
}
return next;
});
}
function begin(players = seats(), proposer = 'a') {
const state = emptyCoalitionState();
const group = coalitionOf(players, state, 'a')!;
return beginCoalitionAdmission(players, state, {event: 'invite-1', nexus: 'nexus-1', turn: 5,
groupId: group.id, entrant: 'c', proposer, expectedRevision: 0});
}
function answer(players: CoalitionSeat[], state: CoalitionState, actor: string, accept = true) {
return answerCoalitionAdmission(players, state, {event: 'invite-1', nexus: 'nexus-1', turn: 5,
actor, accept, expectedRevision: state.revision});
}
function triple(players = seats()) {
const first = begin(players);
const second = answer(players, first.state, 'b');
const accepted = answer(players, second.state, 'c');
return {players: apply(players, accepted), transition: accepted, original: players};
}
void test('legacy reciprocal pairs are derived without inventing an unknown age', () => {
const original = seats(), before = structuredClone(original);
const group = coalitionOf(original, undefined, 'a'); assert.ok(group);
assert.deepEqual(group.members, ['a', 'b']);
assert.equal(group.formedTurn, 2); assert.equal(coalitionEdgeTurn(original, undefined, 'a', 'b'), 2);
assert.deepEqual(coalitionAllies(original, undefined, 'a'), ['b']);
assert.equal(areCoalitionAllies(original, undefined, 'a', 'b'), true);
assert.equal(areCoalitionAllies(original, undefined, 'a', 'c'), false);
assert.equal(areCoalitionAllies(original, undefined, 'a', 'a'), false);
assert.deepEqual(original, before);
delete original[1].allySinceTurn;
assert.equal(coalitionEdgeTurn(original, undefined, 'a', 'b'), null);
assert.equal(areCoalitionAllies(original, undefined, 'a', 'b'), true);
assert.equal(coalitionOf(original, undefined, 'c'), null);
});
void test('a real pair and free entrant need all three distinct approvals', () => {
const players = seats(), original = structuredClone(players);
const first = begin(players); assert.deepEqual(first.state.proposal?.approvals, ['a']);
assert.deepEqual(first.allyUpdates, {}); assert.deepEqual(players, original);
const second = answer(players, first.state, 'b');
assert.deepEqual(second.state.proposal?.approvals, ['a', 'b']);
assert.deepEqual(second.allyUpdates, {});
const accepted = answer(players, second.state, 'c');
assert.equal(accepted.state.proposal, null);
assert.deepEqual(accepted.allyUpdates, {a: null, b: null, c: null});
const next = apply(players, accepted);
assert.deepEqual(coalitionAllies(next, accepted.state, 'a'), ['b', 'c']);
assert.deepEqual(coalitionAllies(next, accepted.state, 'b'), ['a', 'c']);
assert.equal(coalitionEdgeTurn(next, accepted.state, 'a', 'b'), 2);
assert.equal(coalitionEdgeTurn(next, accepted.state, 'a', 'c'), 5);
assert.equal(coalitionEdgeTurn(next, accepted.state, 'b', 'c'), 5);
assert.deepEqual(players, original);
});
void test('the free entrant may propose, but both existing members must independently approve', () => {
const players = seats(), first = begin(players, 'c');
const second = answer(players, first.state, 'a');
assert.ok(second.state.proposal); assert.deepEqual(second.state.proposal.approvals, ['c', 'a']);
const accepted = answer(players, second.state, 'b');
assert.equal(accepted.state.proposal, null);
assert.deepEqual(coalitionOf(apply(players, accepted), accepted.state, 'c')?.members, ['a', 'b', 'c']);
});
void test('any participant can decline before commitment; event replay and duplicate consent are rejected', () => {
const players = seats(), first = begin(players), snapshot = structuredClone(first.state);
assert.throws(() => answer(players, first.state, 'a'));
assert.throws(() => answer(players, first.state, 'd'));
assert.throws(() => answerCoalitionAdmission(players, first.state, {event: 'wrong', nexus: 'nexus-1',
turn: 5, actor: 'b', accept: true, expectedRevision: first.state.revision}));
assert.deepEqual(first.state, snapshot);
const declined = answer(players, first.state, 'a', false);
assert.equal(declined.state.proposal, null); assert.deepEqual(declined.allyUpdates, {});
assert.throws(() => answer(players, declined.state, 'b'));
assert.throws(() => beginCoalitionAdmission(players, declined.state, {event: 'invite-1', nexus: 'nexus-1', turn: 5,
groupId: coalitionOf(players, declined.state, 'a')!.id, entrant: 'c', proposer: 'a', expectedRevision: declined.state.revision}));
assert.deepEqual(players, seats());
});
void test('stale revision, changed Nexus and changed original membership never change their inputs', () => {
const players = seats(), first = begin(players), before = structuredClone({players, state: first.state});
for (const update of [{expectedRevision: 0}, {nexus: 'different'}, {turn: 6}])
assert.throws(() => answerCoalitionAdmission(players, first.state, {event: 'invite-1', nexus: 'nexus-1', turn: 5,
actor: 'b', accept: true, expectedRevision: first.state.revision, ...update}));
const changed = structuredClone(players); changed[0].ally = null; changed[1].ally = null;
assert.throws(() => answer(changed, first.state, 'b'));
assert.deepEqual({players, state: first.state}, before);
});
void test('leaving a triple preserves the remaining pair, stable ID and exact pairwise formation turn', () => {
for (const actor of ['a', 'b', 'c']) {
const {players, transition} = triple();
const before = structuredClone({players, state: transition.state});
const original = coalitionOf(players, transition.state, actor)!;
const remaining = original.members.filter(member => member !== actor);
const expectedAge = coalitionEdgeTurn(players, transition.state, remaining[0], remaining[1]);
const left = leaveCoalition(players, transition.state, {actor, expectedRevision: transition.state.revision});
const after = apply(players, left);
const pair = coalitionOf(after, left.state, remaining[0]); assert.ok(pair);
assert.equal(pair.id, original.id); assert.deepEqual(pair.members, remaining);
assert.equal(coalitionEdgeTurn(after, left.state, remaining[0], remaining[1]), expectedAge);
assert.equal(after.find(seat => seat.id === remaining[0])!.ally, remaining[1]);
assert.equal(after.find(seat => seat.id === remaining[1])!.ally, remaining[0]);
assert.equal(coalitionOf(after, left.state, actor), null);
assert.deepEqual({players, state: transition.state}, before);
}
});
void test('leaving a legacy pair dissolves it without changing unrelated groups', () => {
const players: CoalitionSeat[] = [...seats(), {id: 'e', ally: 'f', allySinceTurn: 3},
{id: 'f', ally: 'e', allySinceTurn: 3}];
const left = leaveCoalition(players, undefined, {actor: 'a', expectedRevision: 0});
const after = apply(players, left);
assert.equal(coalitionOf(after, left.state, 'a'), null);
assert.equal(coalitionOf(after, left.state, 'b'), null);
assert.equal(coalitionEdgeTurn(after, left.state, 'e', 'f'), 3);
});
void test('a missing legacy edge date stays unknown when the old pair is restored', () => {
const initial = seats(); delete initial[0].allySinceTurn; delete initial[1].allySinceTurn;
const {players, transition} = triple(initial);
assert.equal(coalitionEdgeTurn(players, transition.state, 'a', 'b'), null);
assert.equal(coalitionEdgeTurn(players, transition.state, 'a', 'c'), 5);
const left = leaveCoalition(players, transition.state, {actor: 'c', expectedRevision: transition.state.revision});
assert.equal(left.allySinceUpdates.a, null); assert.equal(left.allySinceUpdates.b, null);
const restored = apply(players, left);
assert.equal(coalitionEdgeTurn(restored, left.state, 'a', 'b'), null);
assert.equal(areCoalitionAllies(restored, left.state, 'a', 'b'), true);
});
void test('invalid saved membership, malformed edges and fake scalar triple pairs are rejected', () => {
const initial = seats();
const dangling = structuredClone(initial); dangling[1].ally = null;
assert.throws(() => coalitionGroups(dangling, undefined));
const {players, transition} = triple();
assert.throws(() => coalitionGroups(initial, transition.state));
const duplicate = structuredClone(transition.state);
duplicate.groups.push({...structuredClone(duplicate.groups[0]), id: 'duplicate'});
assert.throws(() => coalitionGroups(players, duplicate));
const missing = structuredClone(transition.state); missing.groups[0].edges.pop();
assert.throws(() => coalitionGroups(players, missing));
const wrongAge = structuredClone(transition.state); wrongAge.groups[0].edges[0].formedTurn = -1;
assert.throws(() => coalitionGroups(players, wrongAge));
});
void test('saved invitations reject unrelated seats, duplicate approvals and state changed under the offer', () => {
const players = seats(), first = begin(players);
for (const corrupt of [
(state: CoalitionState) => { state.proposal!.approvals.push('d'); },
(state: CoalitionState) => { state.proposal!.approvals.push('a'); },
(state: CoalitionState) => { state.proposal!.original.edges[0].formedTurn = 4; },
(state: CoalitionState) => { state.usedEvents = []; },
]) {
const broken = structuredClone(first.state); corrupt(broken);
assert.throws(() => coalitionGroups(players, broken));
assert.throws(() => answer(players, broken, 'b'));
}
});
void test('unequal legacy scalar ages survive joining and restoring the original pair', () => {
for (const dates of [[2, undefined], [undefined, 3], [2, 3]] as const) {
for (const persistedPair of [false, true]) {
const initial = seats();
for (const [index, date] of dates.entries()) {
if (date === undefined) delete initial[index].allySinceTurn;
else initial[index].allySinceTurn = date;
}
const derived = coalitionOf(initial, undefined, 'a')!;
assert.equal(derived.edges[0].formedTurn, null);
// An older persisted pair may predate the per-seat metadata.
const oldPair = structuredClone(derived); delete oldPair.edges[0].legacySince;
const state = {...emptyCoalitionState(), groups: persistedPair ? [oldPair] : []};
const first = beginCoalitionAdmission(initial, state, {event: 'invite-1', nexus: 'nexus-1', turn: 5,
groupId: derived.id, entrant: 'c', proposer: 'a', expectedRevision: 0});
const second = answer(initial, first.state, 'b');
const accepted = answer(initial, second.state, 'c');
const players = apply(initial, accepted);
assert.equal(coalitionEdgeTurn(players, accepted.state, 'a', 'b'), null);
const left = leaveCoalition(players, accepted.state, {actor: 'c', expectedRevision: accepted.state.revision});
const restored = apply(players, left);
assert.equal(restored[0].allySinceTurn, dates[0]);
assert.equal(restored[1].allySinceTurn, dates[1]);
assert.equal(Object.hasOwn(restored[0], 'allySinceTurn'), dates[0] !== undefined);
assert.equal(Object.hasOwn(restored[1], 'allySinceTurn'), dates[1] !== undefined);
assert.equal(coalitionEdgeTurn(restored, left.state, 'a', 'b'), null);
assert.equal(coalitionOf(restored, left.state, 'a')!.id, derived.id);
}
}
});
void test('maximum-length seat IDs with escaped characters can be admitted and persisted', () => {
const a = '\u0001'.repeat(256), b = '\u0002'.repeat(256);
const players: CoalitionSeat[] = [{id: a, ally: b, allySinceTurn: 2},
{id: b, ally: a, allySinceTurn: 2}, {id: 'entrant', ally: null}];
const group = coalitionOf(players, undefined, a)!;
assert.ok(group.id.length > 256);
const first = beginCoalitionAdmission(players, undefined, {event: 'long-id', nexus: 'nexus', turn: 5,
groupId: group.id, entrant: 'entrant', proposer: a, expectedRevision: 0});
const second = answerCoalitionAdmission(players, first.state, {event: 'long-id', nexus: 'nexus', turn: 5,
actor: b, accept: true, expectedRevision: first.state.revision});
const accepted = answerCoalitionAdmission(players, second.state, {event: 'long-id', nexus: 'nexus', turn: 5,
actor: 'entrant', accept: true, expectedRevision: second.state.revision});
const joined = apply(players, accepted);
assert.equal(coalitionOf(joined, accepted.state, a)!.id, group.id);
const left = leaveCoalition(joined, accepted.state, {actor: 'entrant', expectedRevision: accepted.state.revision});
assert.equal(coalitionOf(apply(joined, left), left.state, a)!.id, group.id);
});
void test('an older unknown edge retains matching saved scalar dates without inventing a shared date', () => {
const initial = seats();
initial[0].allySinceTurn = 2; initial[1].allySinceTurn = 2;
const oldPair = structuredClone(coalitionOf(initial, undefined, 'a')!);
oldPair.formedTurn = null; oldPair.edges[0].formedTurn = null;
const state = {...emptyCoalitionState(), groups: [oldPair]};
const first = beginCoalitionAdmission(initial, state, {event: 'old-pair', nexus: 'nexus-1', turn: 5,
groupId: oldPair.id, entrant: 'c', proposer: 'a', expectedRevision: 0});
const second = answerCoalitionAdmission(initial, first.state, {event: 'old-pair', nexus: 'nexus-1', turn: 5,
actor: 'b', accept: true, expectedRevision: first.state.revision});
const accepted = answerCoalitionAdmission(initial, second.state, {event: 'old-pair', nexus: 'nexus-1', turn: 5,
actor: 'c', accept: true, expectedRevision: second.state.revision});
const joined = apply(initial, accepted);
assert.equal(coalitionEdgeTurn(joined, accepted.state, 'a', 'b'), null);
const left = leaveCoalition(joined, accepted.state, {actor: 'c', expectedRevision: accepted.state.revision});
const restored = apply(joined, left);
assert.equal(restored[0].allySinceTurn, 2);
assert.equal(restored[1].allySinceTurn, 2);
assert.equal(coalitionEdgeTurn(restored, left.state, 'a', 'b'), null);
});