import assert from 'node:assert/strict'; import test from 'node:test'; import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance'; import { applyAction, createGame, newPlayer } from '../game/engine'; import { createTraitorDeclaration, validateTraitorDeclaration, validateTraitorDeclarations, type TraitorDeclarationContext } from '../game/traitor-declarations'; /** The ledger is staged because the lobby does not yet expose group play. * Pair formation and unanimous admission use real Nexus actions. The battle * plans and Harkonnen's known identity are staged for this receipt quote. */ function source(group = true): TraitorDeclarationContext { let game = createGame('GROUPTRAITOR', newPlayer('h', 'Harkonnen', 'harkonnen'), false); game.players.push(newPlayer('a', 'Atreides', 'atreides'), newPlayer('e', 'Emperor', 'emperor'), newPlayer('g', 'Guild', 'guild')); Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true, order:['h','a','e','g'], ready:[], storm:18, response:null, decision:null, phaseOpening:null}); game.coalitionState = emptyCoalitionState(); game = applyAction(game, 'h', {type:'alliance', target:'a'}); game = applyAction(game, 'a', {type:'alliance', target:'h'}); if (group) { const pair = coalitionOf(game.players, game.coalitionState, 'h'); assert.ok(pair); game = applyAction(game, 'h', {type:'coalitionPropose', groupId:pair.id, entrant:'e', expectedRevision:game.coalitionState!.revision}); for (const id of ['a','e']) { const proposal = game.coalitionState!.proposal!; game = applyAction(game, id, {type:'coalitionAnswer', event:proposal.event, nexus:proposal.nexus, turn:proposal.turn, accept:true, expectedRevision:game.coalitionState!.revision}); } assert.deepEqual(coalitionOf(game.players, game.coalitionState, 'h')!.members, ['a','e','h']); } return {event:'group-battle', attacker:'a', defender:'g', status:'playing', advanced:false, expansions:[], coalitionState:game.coalitionState, plans:{a:{leader:'a-leader'}, e:{leader:'e-leader'}, g:{leader:'g-leader'}}, players:game.players.map(player => ({id:player.id, faction:player.faction, ally:player.ally, ...(player.allySinceTurn === undefined ? {} : {allySinceTurn:player.allySinceTurn}), traitors:player.id === 'h' ? ['g-leader'] : []})), heroLeaderIds:[]}; } void test('a Harkonnen group declaration binds each actual ally, opponent and identity', () => { for (const ally of ['a','e']) { const game = source(); game.attacker = ally; const before = structuredClone(game); const receipt = createTraitorDeclaration(game, 'h'); assert.deepEqual({voter:receipt.voter, beneficiary:receipt.beneficiary, target:receipt.target, leader:receipt.leader, identity:receipt.identity}, {voter:'h', beneficiary:ally, target:'g', leader:'g-leader', identity:'g-leader'}); validateTraitorDeclaration(game, receipt); validateTraitorDeclarations(game, {h:true}, {h:receipt}, 1); assert.deepEqual(game, before); } }); void test('a group declaration rejects retargeted battle, leader, identity and receipt', () => { const game = source(); const receipt = createTraitorDeclaration(game, 'h'); for (const mutate of [ (value: TraitorDeclarationContext) => { value.attacker = 'e'; }, (value: TraitorDeclarationContext) => { value.defender = 'e'; }, (value: TraitorDeclarationContext) => { value.plans.g.leader = 'replacement'; }, (value: TraitorDeclarationContext) => { value.plans.g.kwisatz = true; }, (value: TraitorDeclarationContext) => { value.event = 'another-battle'; }, ]) { const changed = structuredClone(game); mutate(changed); assert.throws(() => validateTraitorDeclaration(changed, receipt)); } for (const field of ['beneficiary','target','leader','identity','signature'] as const) { const changed = {...receipt, [field]:'incorrect'}; assert.throws(() => validateTraitorDeclaration(game, changed)); } const noIdentity = structuredClone(game); noIdentity.players.find(player => player.id === 'h')!.traitors = []; assert.throws(() => createTraitorDeclaration(noIdentity, 'h')); validateTraitorDeclaration(noIdentity, receipt); }); void test('group declaration is unavailable in unsupported profiles or ambiguous group battles', () => { const game = source(); for (const patch of [{advanced:true}, {expansions:['ix']}, {homeworlds:{}}, {nexusCards:{}}, {leaderSkills:{}}, {techTokens:{}}, {strongholdCards:{}}, {ecazTreachery:true}, {discoveryEnabled:true}]) { assert.throws(() => createTraitorDeclaration({...game, ...patch}, 'h')); } const ambiguous = {...game, defender:'e'}; assert.throws(() => createTraitorDeclaration(ambiguous, 'h')); }); void test('the legacy Harkonnen pair retains implicit allied beneficiary', () => { const game = source(false); const receipt = createTraitorDeclaration(game, 'h'); assert.equal(receipt.beneficiary, 'a'); assert.equal(receipt.target, 'g'); validateTraitorDeclaration(game, receipt); });