121 lines
6.9 KiB
TypeScript
121 lines
6.9 KiB
TypeScript
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);
|
||
});
|