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