import test from 'node:test'; import assert from 'node:assert/strict'; import { createGame, newPlayer } from '../game/engine'; import { quoteBattleBoard, quoteCoalitionAlliedSeparation } from '../game/board-resolution-quote'; import { territoryEntryBlock } from '../game/occupancy'; import type { CoalitionState } from '../game/coalition-alliance'; function fixture() { const g = createGame('COALBOARD', newPlayer('a', 'Atreides', 'atreides'), false); g.players.push(newPlayer('b', 'Emperor', 'emperor'), newPlayer('c', 'Fremen', 'fremen'), newPlayer('d', 'Harkonnen', 'harkonnen')); Object.assign(g, { advanced: false, turn: 4, storm: 18, order: ['a', 'b', 'c', 'd'] }); for (const p of g.players) { p.forces = {}; p.ally = null; } const coalitionState: CoalitionState = { version: 1, revision: 1, proposal: null, usedEvents: [], groups: [{ id: 'abc', members: ['a', 'b', 'c'], formedTurn: 2, edges: [ { members: ['a', 'b'], formedTurn: 2 }, { members: ['a', 'c'], formedTurn: 4 }, { members: ['b', 'c'], formedTurn: 4 }, ] }] }; return { g, coalitionState }; } void test('coalition battles exclude every ally while preserving separate battles against outsiders', () => { const { g, coalitionState } = fixture(); for (const p of g.players) p.forces = { 'red_chasm:7': 1 }; const before = structuredClone({ g, coalitionState }); assert.deepEqual(quoteBattleBoard({ ...g, coalitionState }).battles, [ { territory: 'red_chasm', attacker: 'a', defender: 'd' }, { territory: 'red_chasm', attacker: 'b', defender: 'd' }, { territory: 'red_chasm', attacker: 'c', defender: 'd' }, ]); assert.equal(quoteBattleBoard(g).battles.length, 6); assert.deepEqual({ g, coalitionState }, before); }); void test('entry checks each coalition ally and retains polar, temporary shipment and capacity rules', () => { const { g, coalitionState } = fixture(); g.players[1].forces = { 'red_chasm:7': 1 }; g.players[2].forces = { 'arrakeen:10': 1 }; const block = 'You cannot enter a territory occupied by your ally.'; assert.equal(territoryEntryBlock(g.players, 'a', 'red_chasm', false, false, coalitionState), block); assert.equal(territoryEntryBlock(g.players, 'a', 'arrakeen', false, false, coalitionState), block); assert.equal(territoryEntryBlock(g.players, 'a', 'red_chasm'), null); assert.equal(territoryEntryBlock(g.players, 'a', 'red_chasm', false, true, coalitionState), null); g.players[1].forces['polar_sink:0'] = 1; assert.equal(territoryEntryBlock(g.players, 'a', 'polar_sink', false, false, coalitionState), null); g.players[3].forces = { 'arrakeen:10': 1 }; assert.equal(territoryEntryBlock(g.players, 'a', 'arrakeen', false, true, coalitionState), 'A stronghold cannot contain three occupying factions.'); }); void test('Basic separation follows each alliance edge and preserves old formation age', () => { const { g, coalitionState } = fixture(); g.players[0].forces = { 'red_chasm:7': 1, 'arrakeen:10': 1 }; g.players[1].forces = { 'red_chasm:7': 1 }; g.players[2].forces = { 'arrakeen:10': 1 }; const context = { ...g, coalitionState, remaining: ['a'] }; const before = structuredClone(context); assert.deepEqual(quoteCoalitionAlliedSeparation(context, 'a'), ['red_chasm']); assert.deepEqual(quoteCoalitionAlliedSeparation({ ...context, remaining: ['a', 'b'] }, 'a'), []); assert.deepEqual(new Set(quoteCoalitionAlliedSeparation({ ...context, turn: 5 }, 'a')), new Set(['red_chasm', 'arrakeen'])); assert.deepEqual(context, before); coalitionState.groups[0].edges[0].formedTurn = null; assert.deepEqual(quoteCoalitionAlliedSeparation(context, 'a'), ['red_chasm']); }); void test('legacy pair timing remains unchanged and unsupported coalition combinations fail closed', () => { const { g, coalitionState } = fixture(); g.players[0].forces = { 'red_chasm:7': 1 }; g.players[1].forces = { 'red_chasm:7': 1 }; g.players[0].ally = 'b'; g.players[1].ally = 'a'; g.players[0].allySinceTurn = 4; g.players[1].allySinceTurn = 4; assert.deepEqual(quoteCoalitionAlliedSeparation({ ...g, remaining: ['a'] }, 'a'), []); g.players[1].allySinceTurn = 2; assert.deepEqual(quoteCoalitionAlliedSeparation({ ...g, remaining: ['a'] }, 'a'), ['red_chasm']); assert.equal(territoryEntryBlock(g.players, 'a', 'red_chasm'), 'You cannot enter a territory occupied by your ally.'); g.players[0].ally = null; g.players[1].ally = null; assert.doesNotThrow(() => quoteBattleBoard({ ...g, advanced: true, coalitionState })); g.players[2] = newPlayer('c', 'Ecaz', 'ecaz'); assert.throws(() => territoryEntryBlock(g.players, 'a', 'red_chasm', false, false, coalitionState), /do not yet support Ecaz/); }); void test('a malformed null coalition ledger cannot turn a group into enemies', () => { const { g } = fixture(); for (const p of g.players) p.forces = { 'red_chasm:7': 1 }; const invalid = null as unknown as CoalitionState; assert.throws(() => quoteBattleBoard({ ...g, coalitionState: invalid }), /ledger cannot be null/); assert.throws(() => territoryEntryBlock(g.players, 'a', 'red_chasm', false, false, invalid), /ledger cannot be null/); assert.throws(() => territoryEntryBlock(g.players, 'a', 'red_chasm', true, false, invalid), /ledger cannot be null/); });