import assert from 'node:assert/strict'; import test from 'node:test'; import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; import { advanceTruthAlliance, createTruthAllianceFixture, truthAllianceClean } from './fixture-truth-alliance'; const act = (game: Game, id: string, action: Action) => applyAction(game, id, action); const seat = (game: Game, id: string) => game.players.find(player => player.id === id)!; const remaining = (game: Game, donor: string, recipient: string) => game.coalitionEscrow!.pledges.find(row => row.donor === donor && row.recipient === recipient)?.amount ?? 0; const allSpice = (game: Game) => game.players.reduce((sum, p) => sum + p.spice, 0) + (game.coalitionEscrow?.pledges.reduce((sum, row) => sum + row.amount, 0) ?? 0); /** Genuine Advanced preview and normal Nexus. Only an empty group ledger is * added there; the base fixture selects unopened deal/lot/Spice positions and * scalar lotteries. Pair, invitation, consent and later phases use native * actions. Tests explicitly stage wallets or one conserved Tank force. */ function group() { const source = createTruthAllianceFixture({advanced:true, consent:'none'}); let game = structuredClone(source.game); game.coalitionState = emptyCoalitionState('three-short'); game = act(game, source.asker, {type:'alliance', target:source.opponent}); game = act(game, source.opponent, {type:'alliance', target:source.asker}); const pair = coalitionOf(game.players, game.coalitionState, source.asker); assert.ok(pair); game = act(game, source.asker, {type:'coalitionPropose', groupId:pair.id, entrant:source.target, expectedRevision:game.coalitionState!.revision}); for (const id of [source.opponent, source.target]) { const offer = game.coalitionState!.proposal; assert.ok(offer); game = act(game, id, {type:'coalitionAnswer', event:offer.event, nexus:offer.nexus, turn:offer.turn, accept:true, expectedRevision:game.coalitionState!.revision}); } assert.equal(coalitionOf(game.players, game.coalitionState, source.asker)!.members.length, 3); assert.ok(game.coalitionEscrow); return {game, source}; } function stageWallets(game: Game, payer: string, donor1: string, donor2: string) { seat(game, payer).spice = 2; seat(game, donor1).spice = 10; seat(game, donor2).spice = 10; } function pledge(game: Game, payer: string, donor1: string, donor2: string): Game { game = act(game, donor1, {type:'coalitionPledge', recipient:payer, amount:4}); return act(game, donor2, {type:'coalitionPledge', recipient:payer, amount:3}); } function responses(game: Game): Game { for (let i = 0; game.response && i < 30; i++) { const player = game.players.find(p => !game.response!.passed.includes(p.id)); assert.ok(player); game = act(game, player.id, {type:'passResponse'}); } assert.equal(game.response, null); return game; } void test('Advanced group bid settles both donors once and pays the Emperor', () => { const {game: initial, source} = group(); const payer = source.asker, donor1 = source.opponent, donor2 = source.target; let game = advanceTruthAlliance(initial, state => state.phase === 3 && !!state.auction && truthAllianceClean(state)); for (let count = 0; game.auction && game.auction.active !== payer && count < game.players.length; count++) game = act(game, game.auction.active, {type:'passBid'}); assert.equal(game.auction?.active, payer); stageWallets(game, payer, donor1, donor2); const beforeSpice = allSpice(game), hand = seat(game, payer).hand.length; game = pledge(game, payer, donor1, donor2); assert.equal(viewGame(game, payer).coalitionFunding?.incoming.length, 2); game = act(game, payer, {type:'bid', amount:6, allyPayment:0, coalitionContributions:[{donor:donor1, amount:3}, {donor:donor2, amount:2}]}); for (let count = 0; !game.response && !game.decision && game.auction?.bidder === payer && count < 10; count++) game = act(game, game.auction.active, {type:'passBid'}); if (game.decision?.kind === 'auctionPayment') game = act(game, payer, {type:'decision', karama:false}); if (game.response) { assert.equal(game.response.kind, 'emperorIncome'); game = responses(game); } assert.ok(seat(game, payer).hand.length > hand); assert.equal(seat(game, payer).spice, 1); assert.equal(seat(game, donor1).spice, 6); assert.equal(seat(game, donor2).spice, 11, 'The Emperor donor pays two to the bank and receives the other four.'); assert.equal(remaining(game, donor1, payer), 1); assert.equal(remaining(game, donor2, payer), 1); assert.equal(allSpice(game), beforeSpice - 2, 'The Emperor donor’s own contribution goes to the bank.'); }); void test('Advanced group shipment spends the actual donor pledges and bank invoice', () => { const {game: initial, source} = group(); const payer = source.asker, donor1 = source.opponent, donor2 = source.target; let game = advanceTruthAlliance(initial, state => state.phase === 5 && state.active === payer && truthAllianceClean(state)); stageWallets(game, payer, donor1, donor2); // Select a safe sector without moving any force or changing its inventory. game.storm = 18; const beforeSpice = allSpice(game), reserves = seat(game, payer).reserves; game = pledge(game, payer, donor1, donor2); game = act(game, payer, {type:'ship', territory:'imperial_basin', sector:9, amount:3, elite:0, allyPayment:0, coalitionContributions:[{donor:donor1, amount:3}, {donor:donor2, amount:2}]}); game = responses(game); assert.equal(seat(game, payer).forces['imperial_basin:9'], 3); assert.equal(seat(game, payer).reserves, reserves - 3); assert.equal(seat(game, payer).spice, 1); assert.equal(remaining(game, donor1, payer), 1); assert.equal(remaining(game, donor2, payer), 1); assert.equal(allSpice(game), beforeSpice - 6, 'There is no Guild seat; the invoice goes to the bank.'); }); void test('Advanced Emperor pays a group ally for one extra revival on that ally’s actual Revival turn', () => { const {game: initial, source} = group(); let game = advanceTruthAlliance(initial, state => state.phase === 4 && truthAllianceClean(state) && !!state.revivalOrder?.remaining[0] && state.revivalOrder.remaining[0] !== source.target); const target = game.revivalOrder!.remaining[0], recipient = seat(game, target), emperor = seat(game, source.target); // Move one original ordinary reserve to Tanks and supply a test-bank wallet. assert.ok(recipient.reserves > 0); recipient.reserves--; recipient.tanks++; emperor.spice = 10; const reserves = recipient.reserves, tanks = recipient.tanks; game = act(game, source.target, {type:'emperorRevival', target, amount:1, elite:0}); if (game.response) assert.equal(game.response.kind, 'emperorRevival'); game = responses(game); assert.equal(seat(game, source.target).spice, 8); assert.equal(seat(game, target).reserves, reserves + 1); assert.equal(seat(game, target).tanks, tanks - 1); assert.equal(game.emperorExtra[target], 1); });