import assert from 'node:assert/strict'; import test from 'node:test'; import { emptyCoalitionState, type CoalitionEdge } from '../game/coalition-alliance'; import { coalitionBattleBeneficiary } from '../game/coalition-battle-preparation'; import { createCoalitionWormProtection, currentCoalitionWormAlly, decideCoalitionWormProtection, protectedCoalitionWormAllies, resolveCoalitionWormProtection, validateCoalitionWormProtection, } from '../game/coalition-worm-protection'; import { projectFremenGroupRevivalGrant, quoteFremenRevivalGrant } from '../game/fremen-revival-grant'; import { createGame, newPlayer, type Game } from '../game/engine'; /** Staged unlimited ledger; lobby entry and actual admission are covered by * integration tests. Every alliance edge has its original public turn. */ function source(members: string[]): Game { const game = createGame('GROUPPOWERS', newPlayer('f', 'Fremen', 'fremen'), false); game.players.push(newPlayer('a', 'Atreides', 'atreides'), newPlayer('b', 'Bene Gesserit', 'beneGesserit'), newPlayer('e', 'Emperor', 'emperor'), newPlayer('g', 'Guild', 'guild'), newPlayer('h', 'Harkonnen', 'harkonnen')); Object.assign(game, {status:'playing', phase:1, turn:3, order:['f','e','g','a','h','b']}); const sorted = [...members].sort(); const edges: CoalitionEdge[] = []; for (let i = 0; i < sorted.length; i++) for (let j = i + 1; j < sorted.length; j++) edges.push({members:[sorted[i], sorted[j]], formedTurn:2}); game.coalitionState = {...emptyCoalitionState(), variant:'unlimited' as const, groups:[{id:'group', members:sorted, formedTurn:2, edges}]}; for (const seat of game.players) { seat.forces = {}; seat.reserves = 20; seat.tanks = 0; seat.ally = null; } return game; } void test('one worm resolves five independently selected allied protections in storm order', () => { const game = source(['f','a','b','e','g','h']); for (const seat of game.players.filter(player => player.id !== 'f')) { seat.forces = {'red_chasm:7':3}; seat.reserves = 17; } let frame = createCoalitionWormProtection(game, 'f', 'red_chasm', 'worm-event'); assert.ok(frame); assert.deepEqual(frame.order, ['e','g','a','h','b']); for (const [ally, outcome] of [['e','allowed'], ['g','canceled'], ['a','declined'], ['h','allowed'], ['b','allowed']] as const) { assert.equal(currentCoalitionWormAlly(game, frame), ally); frame = decideCoalitionWormProtection(game, frame, 'f', 'worm-event', ally, outcome !== 'declined'); if (outcome !== 'declined') frame = resolveCoalitionWormProtection(game, frame, 'f', 'worm-event', ally, outcome === 'canceled'); } assert.equal(frame.stage, 'complete'); assert.deepEqual(protectedCoalitionWormAllies(game, frame), ['e','h','b']); assert.deepEqual(frame.outcomes.map(row => row.outcome), ['allowed','canceled','declined','allowed','allowed']); const stale = structuredClone(frame); stale.order = [...stale.order].reverse(); assert.throws(() => validateCoalitionWormProtection(game, stale)); }); void test('Fremen can independently select each of five allied revival recipients', () => { const game = source(['f','a','b','e','g','h']); game.phase = 4; const grants = projectFremenGroupRevivalGrant(game, 'f'); assert.ok(grants); assert.deepEqual(grants.map(row => row.recipient).sort(), ['a','b','e','g','h']); for (const ally of ['a','b','e','g','h']) { assert.deepEqual(quoteFremenRevivalGrant(game, 'f', ally), {turn:3,owner:'f',recipient:ally}); game.freeRevival.push(ally); assert.throws(() => quoteFremenRevivalGrant(game, 'f', ally)); } assert.throws(() => quoteFremenRevivalGrant(game, 'f', 'f')); game.advanced = true; assert.equal(projectFremenGroupRevivalGrant(game, 'f')?.length, 5); }); void test('group battle preparation selects any single member and rejects two allied sides', () => { const game = source(['f','a','b','e']); game.phase = 6; for (const ally of ['f','b','e']) assert.equal(coalitionBattleBeneficiary(game, 'a', ally, 'g'), ally); assert.equal(coalitionBattleBeneficiary(game, 'a', 'e', 'b'), null); assert.equal(coalitionBattleBeneficiary(game, 'a', 'g', 'h'), null); const advanced = {...game, advanced:true}; assert.equal(coalitionBattleBeneficiary(advanced, 'a', 'e', 'g'), 'e'); });