Files
Dune/tests/coalition-advanced-funding.test.ts
T
Quintine 599af2475d
Verify and publish checkpoint container / container (push) Canceled after 0s
Route Advanced Emperor allied auction funding
2026-10-29 02:48:11 +11:00

121 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});