Files
Dune/tests/coalition-three-short.test.ts
T
Quintine ef92ddf2b9
Verify and publish checkpoint container / container (push) Canceled after 0s
Add three-member short alliance prototype
2026-10-29 02:31:50 +11:00

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