import test from 'node:test'; import assert from 'node:assert/strict'; import { applyAction, normalizeAutomaticGame, viewGame, type Action, type Game, } from '../game/engine'; import { nexusGuildSecretAllyFixture, nexusGuildSecretAllyRequest, nexusGuildSecretAllyInventory, nexusGuildSecretAllyReload, holdGuildSecretAllyCard, nexusGuildSecretAllianceFixture, } from './fixture-nexus-guild-secret-ally'; import { nexusAllow } from './fixture-nexus-cards'; import { homeworldContext } from '../game/homeworld-game'; import { quoteNativeReserveWithdrawal } from '../game/homeworld-native-reserves'; import { observeHomeworldOccupation } from '../game/homeworld-occupation-history'; function rejected(g: Game, owner: string, action: Action) { const before = structuredClone(g); assert.throws(() => applyAction(g, owner, action)); assert.deepEqual(g, before); } function stable(g: Game) { nexusGuildSecretAllyInventory(g); for (const p of g.players) assert.deepEqual( viewGame(nexusGuildSecretAllyReload(g), p.id), viewGame(g, p.id), ); assert.deepEqual( normalizeAutomaticGame(nexusGuildSecretAllyReload(g)), nexusGuildSecretAllyReload(g), ); } void test('Guild Secret Ally charges the native tariff once without a Guild response and retains ordinary faction movement', () => { for (const advanced of [false, true]) for (const desert of [false, true]) { const f = nexusGuildSecretAllyFixture({ advanced }); const action = nexusGuildSecretAllyRequest(f.g, f.owner, { type: 'ship', territory: desert ? 'hagga_basin' : 'arrakeen', sector: desert ? 12 : 10, amount: 5, }); let g = applyAction(f.g, f.owner, action); assert.equal(g.players[0].spice, desert ? 15 : 17); assert.equal(g.players[0].reserves, 15); assert.equal(g.players[0].shipped, true); assert.equal(g.players[0].moved, 0); assert.equal(g.response, null); assert.equal(g.decision, null); assert.equal(g.nexusCards!.cards!.hands[f.owner], null); assert.equal( g.nexusCards!.cards!.discard.filter((c) => c === 'guild').length, 1, ); rejected(g, f.owner, action); g = applyAction(nexusGuildSecretAllyReload(g), f.owner, { type: 'move', from: desert ? 'hagga_basin:12' : 'arrakeen:10', territory: desert ? 'arsunt' : 'hagga_basin', sector: 12, amount: 5, }); assert.equal(g.players[0].moved, 1); stable(g); } }); void test('Guild Secret Ally cross and return preserve actual Emperor normal and Sardaukar counters', () => { for (const returns of [false, true]) { const f = nexusGuildSecretAllyFixture({ ownerFaction: 'emperor', advanced: true, }); const p = f.g.players[0]; p.reserves -= 5; p.elites!.reserves -= 2; p.forces['arrakeen:10'] = 5; p.elites!.forces['arrakeen:10'] = 2; stable(f.g); const action = nexusGuildSecretAllyRequest(f.g, f.owner, { type: 'guildShip', from: 'arrakeen:10', territory: returns ? 'reserves' : 'carthag', sector: returns ? 0 : 11, amount: 5, elite: 2, }); const g = applyAction(f.g, f.owner, action); assert.equal(g.players[0].spice, 17); assert.equal(g.players[0].reserves, returns ? 20 : 15); assert.equal(g.players[0].elites!.reserves, returns ? 5 : 3); assert.equal(g.players[0].forces['arrakeen:10'], undefined); if (!returns) { assert.equal(g.players[0].forces['carthag:11'], 5); assert.equal(g.players[0].elites!.forces['carthag:11'], 2); } rejected(g, f.owner, action); stable(g); } }); void test('Fremen paid Guild reserve shipment has no free-reinforcement range or storm privilege', () => { const f = nexusGuildSecretAllyFixture({ ownerFaction: 'fremen', advanced: true, homeworlds: true, }); rejected(f.g, f.owner, { type: 'ship', territory: 'arrakeen', sector: 10, amount: 5, }); const action = nexusGuildSecretAllyRequest(f.g, f.owner, { type: 'ship', territory: 'arrakeen', sector: 10, amount: 5, elite: 2, }); const storm = structuredClone(f.g); storm.storm = 10; rejected(storm, f.owner, action); rejected( f.g, f.owner, nexusGuildSecretAllyRequest(f.g, f.owner, { type: 'guildShip', from: 'reserves', territory: 'arrakeen', sector: 10, amount: 5, }), ); const g = applyAction(f.g, f.owner, action); assert.equal(g.players[0].spice, 17); assert.equal(g.players[0].reserves, 15); assert.equal(g.players[0].elites!.reserves, 1); assert.equal(g.players[0].elites!.forces['arrakeen:10'], 2); stable(g); const ordinary = nexusGuildSecretAllyFixture({ ownerFaction: 'fremen', advanced: true, }); const free = applyAction(ordinary.g, ordinary.owner, { type: 'ship', territory: 'the_great_flat', sector: 15, amount: 5, }); assert.equal(free.players[0].spice, 20); assert.equal(free.nexusCards!.cards!.hands[ordinary.owner], 'guild'); stable(free); }); void test('Guild Secret Ally reserve and interplanetary shipments withdraw exact native Homeworld types', () => { for (const interplanetary of [false, true]) { const f = nexusGuildSecretAllyFixture({ ownerFaction: 'emperor', advanced: true, homeworlds: true, }); const sources = { 'homeworld:emperor': { normal: 3, elite: 0 }, 'homeworld:emperor:salusa': { normal: 0, elite: 2 }, }; const action = nexusGuildSecretAllyRequest( f.g, f.owner, interplanetary ? { type: 'homeworldShip', event: viewGame(f.g, f.owner).homeworldShipment!.event, destination: 'homeworld:harkonnen', sources, } : { type: 'ship', territory: 'arrakeen', sector: 10, amount: 5, elite: 2, homeworldSources: sources, }, ); const g = applyAction(f.g, f.owner, action); assert.equal(g.players[0].spice, 17); assert.equal(g.players[0].reserves, 15); assert.equal(g.players[0].elites!.reserves, 3); if (interplanetary) assert.deepEqual( g.homeworlds!.custody!.visitors['homeworld:harkonnen'][f.owner], { normal: 3, elite: 2 }, ); else assert.equal(g.players[0].elites!.forces['arrakeen:10'], 2); stable(g); } }); void test('Guild Secret Ally rejects foreign, stale, duplicated and concealed requests without revealing another hand', () => { const f = nexusGuildSecretAllyFixture(); const request = nexusGuildSecretAllyRequest(f.g, f.owner); for (const id of [f.target, f.observer]) { assert.equal(viewGame(f.g, id).nexusGuildSecretAlly, null); rejected(f.g, id, request); } rejected(f.g, f.owner, { ...request, nexus: 'stale' }); rejected(f.g, f.owner, { ...request, noField: 'hidden' }); const g = applyAction(f.g, f.owner, request); for (const p of g.players) assert.equal( Object.hasOwn(viewGame(g, p.id), 'nexusGuildSecretHistory'), false, ); stable(g); }); void test('a binding Truthtrance answer can rely on privately held Guild pricing and then fulfill the original shipment', () => { const f = nexusGuildSecretAllyFixture({ spice: 3 }); const card = holdGuildSecretAllyCard(f.g, f.target, 'truthtrance'); let g = applyAction(f.g, f.target, { type: 'card', card: card.id }); while (g.truthtrance?.stage === 'priority') g = applyAction( g, g.players.find((p) => !g.truthtrance!.passed.includes(p.id))!.id, { type: 'truthPass' }, ); g = applyAction(g, f.target, { type: 'truthAsk', question: { kind: 'shipment', target: f.owner, territory: 'arrakeen', minimum: 5, }, }); assert.deepEqual(viewGame(g, f.owner).truthShipmentAnswers, ['yes', 'no']); g = applyAction(g, f.owner, { type: 'truthAnswer', answer: 'yes' }); const witness = viewGame(g, f.owner).shipmentCompletion!; assert.ok( witness.actions.some( (a) => a.type === 'ship' && a.nexus === viewGame(g, f.owner).nexusGuildSecretAlly!.event, ), ); for (const id of [f.target, f.observer]) assert.equal(viewGame(g, id).shipmentCompletion, null); rejected(g, f.owner, { type: 'endMovement' }); rejected( g, f.owner, nexusGuildSecretAllyRequest(g, f.owner, { type: 'ship', territory: 'arrakeen', sector: 10, amount: 4, }), ); g = applyAction( nexusGuildSecretAllyReload(g), f.owner, nexusGuildSecretAllyRequest(g, f.owner), ); assert.equal(g.players[0].spice, 0); assert.equal(g.players[0].reserves, 15); assert.equal(g.shipmentPromises![0].fulfilled, true); stable(g); }); void test('a genuine Moritani arrival alliance preserves completed Guild Secret Ally history and subsequent native movement', () => { const f = nexusGuildSecretAllianceFixture(); let g = applyAction(f.g, f.owner, nexusGuildSecretAllyRequest(f.g, f.owner)); assert.equal(g.pendingTerrorEntry?.stage, 'offer'); const before = structuredClone(g.players[0]), history = structuredClone(g.nexusGuildSecretHistory); g = nexusAllow( applyAction(g, f.target, { type: 'decision', alliance: true }), ); stable(g); g = applyAction(nexusGuildSecretAllyReload(g), f.owner, { type: 'decision', accept: true, }); assert.equal(g.players[0].ally, f.target); assert.equal(g.players[1].ally, f.owner); assert.deepEqual(g.nexusGuildSecretHistory, history); assert.equal(g.players[0].spice, before.spice); assert.equal(g.players[0].reserves, before.reserves); stable(g); g = applyAction(g, f.owner, { type: 'move', from: 'arrakeen:10', territory: 'hagga_basin', sector: 12, amount: 5, }); assert.equal(g.players[0].forces['hagga_basin:12'], 5); g = applyAction(g, f.owner, { type: 'endMovement' }); assert.notEqual(g.active, f.owner); stable(g); }); void test('Guild Secret Ally returns typed counters to the holder own native Homeworld reserves under Homeworlds', () => { const f = nexusGuildSecretAllyFixture({ ownerFaction: 'emperor', advanced: true, homeworlds: true, }); const p = f.g.players[0]; // Withdraw the staged board group through the real native-reserve pipeline so // Homeworld custody stays conserved; the Emperor splits normal/Sardaukar. const withdrawal = quoteNativeReserveWithdrawal( homeworldContext(f.g), f.g.homeworlds!.custody!, p.id, { normal: 3, elite: 2 }, ); const seat = withdrawal.players.find((s) => s.id === p.id)!; p.reserves = seat.reserves; p.elites!.reserves = seat.eliteReserves; f.g.homeworlds!.custody = withdrawal.state; // Mirror the engine's observeOccupation so the qualification history stays // consistent with the staged custody change. if (f.g.homeworlds!.historyVersion === 1 && f.g.homeworldOccupationHistory) f.g.homeworldOccupationHistory = observeHomeworldOccupation( f.g.homeworldOccupationHistory, homeworldContext(f.g), f.g.homeworlds!.custody!, f.g.turn, 'change', `homeworld-change-${f.g.homeworldOccupationHistory.sources[0].event}-${f.g.homeworldOccupationHistory.sources.length}`, ); p.forces['arrakeen:10'] = 5; p.elites!.forces['arrakeen:10'] = 2; stable(f.g); const action = nexusGuildSecretAllyRequest(f.g, f.owner, { type: 'guildShip', from: 'arrakeen:10', territory: 'reserves', sector: 0, amount: 5, elite: 2, }); const g = applyAction(f.g, f.owner, action); assert.equal(g.players[0].spice, 17); assert.equal(g.players[0].reserves, 20); assert.equal(g.players[0].elites!.reserves, 5); assert.equal(g.players[0].forces['arrakeen:10'], undefined); rejected(g, f.owner, action); stable(g); });