Files
Dune/tests/discovery-paired-nexus-e2-skills-runtime.test.ts

385 lines
25 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, createGame, handLimit, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { treacheryDeck, spiceDeck } from '../game/cards';
import type { Card } from '../game/cards';
import { richeseCards } from '../game/richese-cards';
import { validGameLocation, territory } from '../game/board';
import { validateDiscoveryState } from '../game/discoveries';
import { validateLeaderSkills } from '../game/leader-skills';
import { validateNexusCards } from '../game/nexus-cards';
import { validateRicheseNoField } from '../game/richese-no-field';
import { ownedTech } from '../game/tech-tokens';
import { placePairedSkillsForce, stagePairedSkillsPlanetologistRoute } from './fixture-paired-choam-nexus-skills';
import {
advanceDiscoveryPairedNexusE2Skills as advance, closeDiscoveryPairedNexusE2Skills as close,
completeDiscoveryPairedNexusE2SkillsSetup as completeSetup,
createDiscoveryPairedNexusE2SkillsFixture as fixture,
discoveryPairedNexusE2SkillsClean as clean,
discoveryPairedNexusE2SkillsCunningAction as pairAction,
discoveryPairedNexusE2SkillsInvoiceAction as invoice,
discoveryPairedNexusE2SkillsPlayer as player,
finishDiscoveryPairedNexusE2SkillsBattle as finishBattle,
initializeDiscoveryPairedNexusE2Skills as initialize,
prepareDiscoveryPairedNexusE2SkillsBattle as prepareBattle,
settleDiscoveryPairedNexusE2Skills as settle,
stepDiscoveryPairedNexusE2Skills as step,
type DiscoveryPairedNexusE2SkillsFixture,
} from './fixture-discovery-paired-nexus-e2-skills';
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
function reject(game: Game, actor: string, action: Action): void {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before, 'Rejected human declarations cannot spend cards, invoice, counters or training.');
}
function custody(game: Game): void {
const cards = [...game.deck, ...game.discard, ...game.players.flatMap(p => p.hand),
...(game.richeseCache ?? []), ...(game.richeseRemoved ?? []),
...(game.auction?.cards.slice(game.auction.index + (game.currentAuctionSale ? 1 : 0)) ?? [])];
assert.deepEqual(cards.map(c => c.id).sort(), [...treacheryDeck(['choam']), ...richeseCards()].map(c => c.id).sort());
validateLeaderSkills(game.leaderSkills!, game.players);
validateNexusCards(game.nexusCards!.cards!, game.players);
validateDiscoveryState(game.discoveries!);
for (const p of game.players) {
assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((n, count) => n + count, 0), 20);
if (p.elites) assert.equal(p.elites.reserves + p.elites.tanks + Object.values(p.elites.forces).reduce((n, count) => n + count, 0), p.faction === 'fremen' ? 3 : 5);
if (p.noField) validateRicheseNoField(p.noField);
}
}
function acquiredCard(f: DiscoveryPairedNexusE2SkillsFixture, buyer: string, matches: (card: Card) => boolean): Card {
const receipt = f.acquisitions.find(a => a.buyer === buyer && matches(a.card));
assert.ok(receipt, 'The intended eligible actor must actually acquire this original card through Bidding.');
const { before, after, card } = receipt, auction = before.auction!;
assert.equal(before.phase, 3);
assert.equal(before.turn, 1);
assert.equal(auction.cards[auction.index].id, card.id);
assert.equal(auction.bidder, buyer);
assert.equal(auction.bid, 1);
assert.ok(player(before, buyer).hand.length < handLimit(player(before, buyer)));
assert.ok(f.afterSetup.deck.some(c => c.id === card.id));
assert.deepEqual(player(after, buyer).hand, [...player(before, buyer).hand, card]);
assert.equal(player(after, buyer).spice, player(before, buyer).spice - 1);
assert.ok(f.actions.some(s => s.actor === buyer && s.action.type === 'bid' && s.action.amount === 1));
return card;
}
const modules = [
{ advanced: false, tech: false, strongholds: false },
{ advanced: false, tech: true, strongholds: false },
{ advanced: true, tech: false, strongholds: false },
{ advanced: true, tech: true, strongholds: false },
{ advanced: true, tech: false, strongholds: true },
{ advanced: true, tech: true, strongholds: true },
] as const;
for (const options of modules) void test(`original paired E2 Discovery/Skills ${JSON.stringify(options)}: paid collection, real end-Mentat entry, Maker and qualified closing draws`, () => {
const f = fixture(options), ending = close(f);
const ordinary = [...f.initial.deck, ...f.initial.players.flatMap(p => p.hand)];
assert.equal(ordinary.length, 35);
assert.deepEqual(ordinary.map(c => c.id).sort(), treacheryDeck(['choam']).map(c => c.id).sort());
assert.equal(f.initial.richeseCache!.length, 10);
assert.equal(f.initial.spiceDeck.length, spiceDeck().length + 7);
assert.equal(f.initial.discoveries!.tokens.length, 8);
assert.equal(f.initial.leaderSkills!.deck.length + Object.values(f.initial.leaderSkills!.offers).reduce((n, o) => n + o.cards.length, 0), 14);
assert.equal(f.initial.nexusCards!.cards!.deck.length, 12);
assert.equal(player(f.previousMarker.after, f.richese).spice, player(f.previousMarker.before, f.richese).spice - 1);
assert.equal(player(f.previousMarker.after, f.richese).reserves, player(f.previousMarker.before, f.richese).reserves);
assert.equal(player(f.collection, f.collector).forces[f.source], 3);
assert.equal(validGameLocation(f.collection, 'shrine', 0), false);
assert.ok(f.firstMentat);
assert.equal(f.firstMentat.before.turn, 1);
assert.equal(f.firstMentat.before.phase, 8);
assert.equal(f.firstMentat.after.turn, 2);
assert.equal(ending.entry.before.phase, 0);
assert.equal(ending.entry.before.decision?.player, f.collector);
assert.equal(player(ending.entry.after, f.collector).forces[f.source] ?? 0, 0);
assert.equal(player(ending.entry.after, f.collector).forces['shrine:0'], 3);
assert.equal(player(ending.entry.after, f.collector).spice, player(ending.entry.before, f.collector).spice);
assert.equal(validGameLocation(ending.game, 'shrine', 0), true);
assert.equal(player(ending.vote, f.choam).forces['hagga_basin:12'] ?? 0, 0);
assert.equal(player(ending.vote, f.choam).tanks, player(ending.entry.before, f.choam).tanks + 2);
assert.equal(ending.ride.before.decision?.player, f.fremen);
assert.equal(player(ending.ride.after, f.fremen).forces['polar_sink:0'], 2);
assert.equal(player(ending.ride.after, f.fremen).reserves, player(ending.ride.before, f.fremen).reserves - 2);
if (options.advanced) assert.equal(player(ending.ride.after, f.fremen).elites!.forces['polar_sink:0'], 1);
assert.equal(ending.alliance.spiceWindow, null);
assert.equal(ending.alliance.spiceResolution, null);
assert.equal(player(ending.alliance, f.guild).ally, f.fremen);
assert.equal(player(ending.alliance, f.fremen).ally, f.guild);
assert.equal(ending.drawing.phase, 1);
for (const draw of ending.draws) {
assert.equal(draw.before.nexusCards!.cards!.hands[draw.step.actor], null);
assert.equal(draw.after.nexusCards!.cards!.hands[draw.step.actor], player(draw.after, draw.step.actor).faction);
reject(draw.after, draw.step.actor, draw.step.action);
}
assert.equal(ending.game.nexusCards!.cards!.hands[f.guild], null);
assert.equal(ending.game.nexusCards!.cards!.hands[f.fremen], null);
assert.deepEqual(ending.game.leaderSkills!.assignments, f.afterSetup.leaderSkills!.assignments);
if (options.strongholds) {
assert.equal(f.firstMentat.before.strongholdCards!.owners.arrakeen, null);
assert.equal(f.firstMentat.after.strongholdCards!.owners.arrakeen, f.choam);
}
custody(ending.game);
});
void test('saved authenticated original skill offers and starting hands survive continuation without demanding a random consumer', () => {
const setup = initialize({ advanced: true, tech: true, strongholds: true,
seatIds: ['human-richese', 'human-choam', 'human-guild', 'human-fremen'] });
const before = structuredClone(setup), continued = initialize({ initial: reload(setup) });
assert.deepEqual(continued, before);
const done = completeSetup(continued);
for (const assignment of done.leaderSkills!.assignments)
assert.ok(before.leaderSkills!.offers[assignment.owner].cards.includes(assignment.skill));
assert.deepEqual(done.players.map(p => [p.id, p.hand]), before.players.map(p => [p.id, p.hand]));
assert.equal(Object.keys(done.leaderSkills!.offers).length, 0);
assert.equal(done.status, 'playing');
assert.equal(done.leaderSkills!.assignments.length, 4);
custody(done);
});
for (const advanced of [false, true]) for (const seats of [2, 3, 4, 5, 6]) void test(`${advanced ? 'Advanced' : 'Basic'} human ${seats}-seat original paired programme trains only original offers with legal native modules`, () => {
const factions = ['richese', 'choam', 'guild', 'fremen', 'emperor', 'atreides'] as const;
const lobby = createGame(`HUMANE2${seats}`, newPlayer('authenticated-r', 'R', 'richese'), advanced, ['choam']);
for (const faction of factions.slice(1, seats)) joinGame(lobby, newPlayer(`authenticated-${faction}`, faction, faction));
const setup = initialize({ initial: lobby, tech: seats >= 3, strongholds: advanced });
const done = completeSetup(setup);
assert.equal(done.players.length, seats);
assert.equal(done.leaderSkills!.assignments.length, seats);
assert.deepEqual(done.players.map(p => p.id), lobby.players.map(p => p.id));
assert.equal(done.status, 'playing');
custody(done);
});
for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} nested signed two-marker Cunning keeps one physical invoice through native responses/Guild and private reserve caps`, () => {
const f = fixture({ advanced, tech: true, strongholds: advanced }), ending = close(f), before = ending.game;
const karama = acquiredCard(f, f.fremen, c => c.effect === 'karama');
assert.ok(player(before, f.fremen).hand.some(c => c.id === karama.id));
const own = player(before, f.richese), previous = own.noField!.tokens.find(t => t.value === 0)!;
const request = pairAction(before, f.richese);
reject(before, f.richese, { ...request, noField: previous.id });
reject(before, f.richese, { ...request, revealedToken: request.noField });
reject(before, f.richese, { ...request, nexus: 'stale-own-pair' });
reject(before, f.richese, { ...request, territory: 'treachery-card-stash', sector: 0 });
reject(before, f.richese, { ...request, sector: 1 });
reject(before, f.richese, { ...request, amount: 8 });
reject(before, f.richese, { ...request, smuggler: true });
for (const cap of [2, 20]) {
const capped = reload(before), p = player(capped, f.richese);
placePairedSkillsForce(capped, f.richese, 'polar_sink:0', p.reserves - cap, f.staging);
const pending = applyAction(capped, f.richese, pairAction(capped, f.richese));
assert.equal(pending.response?.kind, 'richeseNoField');
assert.equal(pending.pendingShipment!.cost, 1);
assert.equal(player(pending, f.richese).spice, p.spice);
assert.equal(player(pending, f.richese).reserves, cap);
const corrupted = reload(pending), holder = player(corrupted, f.fremen);
corrupted.pendingShipment!.cost = 0;
reject(corrupted, holder.id, { type: 'passResponse' });
const retargeted = reload(pending);
retargeted.pendingShipment!.territory = 'arrakeen';
reject(retargeted, holder.id, { type: 'passResponse' });
const guild = advance(pending, g => g.decision?.kind === 'guildShipment' || clean(g));
if (advanced) {
assert.equal(guild.decision?.player, f.guild);
assert.equal(player(guild, f.richese).spice, p.spice);
reject(guild, f.choam, { type: 'decision', allow: true });
reject(guild, f.guild, { type: 'decision', allow: false });
}
const shipped = settle(guild);
assert.equal(player(shipped, f.richese).spice, p.spice - 1);
assert.equal(player(shipped, f.guild).spice, player(capped, f.guild).spice + 1);
assert.equal(player(shipped, f.richese).reserves, cap - Math.min(5, cap));
assert.equal(player(shipped, f.richese).forces['shrine:0'], Math.min(5, cap));
assert.equal(player(shipped, f.richese).noField!.deployed!.tokenId, request.noField);
assert.equal(player(shipped, f.richese).noField!.lastShipped, request.noField);
assert.equal(shipped.nexusCards!.cards!.hands[f.richese], null);
assert.deepEqual(player(shipped, f.richese).noField!.tokens, own.noField!.tokens);
reject(shipped, f.richese, request);
assert.equal(player(settle(shipped), f.guild).spice, player(shipped, f.guild).spice);
custody(shipped);
}
reject(before, f.richese, { ...pairAction(before, f.richese), smuggler: true });
});
void test('actual original Karama holder cancels a nested pair without a fee, counter materialization or ordinary substitute entitlement', () => {
const f = fixture({ advanced: true, tech: true }), before = close(f).game;
const card = acquiredCard(f, f.fremen, c => c.effect === 'karama'), holder = player(before, f.fremen);
assert.ok(holder.hand.some(c => c.id === card.id));
const request = pairAction(before, f.richese), pending = applyAction(reload(before), f.richese, request);
assert.equal(pending.response?.kind, 'richeseNoField');
const stopped = settle(applyAction(pending, holder.id, { type: 'card', mode: 'cancel', card: card.id }));
assert.equal(stopped.nexusRicheseCunningLast!.stage, 'stopped');
assert.equal(stopped.nexusCards!.cards!.hands[f.richese], null);
assert.equal(stopped.discard.filter(c => c.id === card.id).length, 1);
assert.equal(player(stopped, holder.id).hand.some(c => c.id === card.id), false);
for (const p of before.players) {
assert.equal(player(stopped, p.id).spice, p.spice);
assert.deepEqual(player(stopped, p.id).forces, p.forces);
assert.equal(player(stopped, p.id).reserves, p.reserves);
}
assert.equal(player(stopped, f.richese).noField!.lastShipped, player(before, f.richese).noField!.lastShipped);
reject(stopped, f.richese, request);
reject(stopped, f.richese, { type: 'ship', amount: 6, territory: 'shrine', sector: 0, smuggler: true });
custody(stopped);
});
void test('all four minimal policies pass the actual Karama-holder response then consume the native two-marker invoice once', () => {
const f = fixture({ advanced: true, tech: true, strongholds: true }), before = close(f).game;
const card = acquiredCard(f, f.fremen, c => c.effect === 'karama');
const holder = player(before, f.fremen), request = pairAction(before, f.richese);
assert.ok(holder.hand.some(c => c.id === card.id));
for (const difficulty of DIFFICULTIES) {
const pending = applyAction(reload(before), f.richese, request), view = viewGame(pending, holder.id);
assert.equal(pending.response?.kind, 'richeseNoField');
view.players.find(p => p.id === holder.id)!.bot = difficulty;
const action = botActions(view).find(a => a.type === 'passResponse');
assert.ok(action, `${difficulty} must act as the actual eligible Karama holder.`);
const shipped = settle(applyAction(pending, holder.id, action));
assert.equal(player(shipped, f.richese).forces['shrine:0'], 5);
assert.equal(player(shipped, f.richese).spice, player(before, f.richese).spice - 1);
assert.equal(player(shipped, f.guild).spice, player(before, f.guild).spice + 1);
assert.equal(shipped.nexusRicheseCunningLast!.stage, 'shipped');
assert.equal(shipped.nexusCards!.cards!.hands[f.richese], null);
assert.deepEqual(player(shipped, holder.id).hand, player(before, holder.id).hand);
custody(shipped);
}
});
for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} original CHOAM Stash draws its one genuine next card without changing its five-card capacity or training`, () => {
const f = fixture({ advanced, kind: 'stash' }), before = f.collection;
const hand = player(before, f.choam).hand.map(c => c.id), drawn = before.deck[0]; assert.ok(drawn);
assert.equal(player(f.game, f.choam).hand.length, hand.length + 1);
assert.deepEqual(player(f.game, f.choam).hand.map(c => c.id), [...hand, drawn.id]);
assert.equal(f.game.deck.length, before.deck.length - 1);
assert.equal(player(f.game, f.choam).spice, player(before, f.choam).spice);
assert.equal(f.game.discoveries!.tokens.find(t => t.id === f.token)!.status, 'removed');
assert.deepEqual(f.game.leaderSkills!.assignments, before.leaderSkills!.assignments);
reject(f.game, f.choam, { type: 'discovery', token: f.token, reveal: true });
custody(f.game);
});
for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} own CHOAM Cunning consumes actual auction-acquired Special fuel, separate from the trained Smuggler invoice`, () => {
const f = fixture({ advanced, tech: true, strongholds: advanced }), ending = close(f);
const before = advance(ending.movementStart, g => g.phase === 5 && g.active === f.choam && clean(g));
const fuel = acquiredCard(f, f.choam, c => c.kind === 'special' && c.effect !== 'karama');
assert.ok(player(before, f.choam).hand.some(c => c.id === fuel.id));
const offer = viewGame(before, f.choam).choamWorthless!.plays.find(p => p.source === 'nexus' && p.card.id === fuel.id && p.effect === 'kulon');
assert.ok(offer && !offer.blocked && offer.source === 'nexus');
const request: Action = { type: 'card', mode: 'choam', card: fuel.id, effect: 'kulon', nexus: offer.event };
reject(before, f.choam, { ...request, nexus: 'stale-fuel' });
const declared = applyAction(reload(before), f.choam, request);
if (declared.response) assert.equal(declared.response.kind, 'choamWorthless');
assert.equal(declared.nexusCards!.cards!.hands[f.choam], null);
if (declared.response) assert.equal(player(declared, f.choam).hand.some(c => c.id === fuel.id), true);
const fueled = settle(declared);
assert.equal(fueled.discard.filter(c => c.id === fuel.id).length, 1);
assert.equal(player(fueled, f.choam).hand.some(c => c.id === fuel.id), false);
assert.equal(fueled.choamMovement!.bonus, 1);
reject(fueled, f.choam, request);
const shipment = invoice(fueled), paid = settle(applyAction(reload(fueled), f.choam, shipment.action));
assert.equal(player(paid, f.choam).forces[shipment.key], 6);
assert.equal(player(paid, f.choam).reserves, player(fueled, f.choam).reserves - 6);
assert.equal(player(paid, f.choam).spice, player(fueled, f.choam).spice - 5);
assert.equal(player(paid, f.guild).spice, player(fueled, f.guild).spice + 5);
assert.equal(paid.discard.filter(c => c.id === fuel.id).length, 1);
assert.deepEqual(paid.leaderSkills!.assignments, before.leaderSkills!.assignments);
custody(paid);
});
void test('all four minimal card policies spend actual physical fuel for Kulon before a separate ordinary skill move', () => {
const f = fixture({ advanced: true, choamSkill: 'planetologist', tech: true, strongholds: true }), ending = close(f);
const before = advance(ending.movementStart, g => g.phase === 5 && g.active === f.choam && clean(g));
const route = stagePairedSkillsPlanetologistRoute(before, f.choam, f.staging);
reject(before, f.choam, route.action);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(before, f.choam); view.players.find(p => p.id === f.choam)!.bot = difficulty;
const action = botActions(view).find(a => a.type === 'card' && a.mode === 'choam' && a.effect === 'kulon');
assert.ok(action);
const fuel = player(before, f.choam).hand.find(c => c.id === action.card); assert.ok(fuel);
const used = settle(applyAction(reload(before), f.choam, action));
assert.equal(used.discard.filter(c => c.id === fuel.id).length, 1);
assert.equal(player(used, f.choam).hand.some(c => c.id === fuel.id), false);
const moved = settle(applyAction(used, f.choam, route.action));
assert.equal(player(moved, f.choam).forces[route.from] ?? 0, 0);
assert.equal(player(moved, f.choam).forces[route.to], 1);
assert.equal(player(moved, f.choam).moved, 1);
assert.equal(moved.discard.filter(c => c.id === fuel.id).length, 1);
assert.deepEqual(moved.leaderSkills!.assignments, before.leaderSkills!.assignments);
custody(moved);
}
});
void test('all four eligible Suk decisions follow voluntary nested pair reveal and settled physical casualties before optional Tech transfer', () => {
const f = fixture({ advanced: true, tech: true, strongholds: true }), before = close(f).game;
const shipped = settle(applyAction(reload(before), f.richese, pairAction(before, f.richese)));
const battle = prepareBattle(f, shipped);
let game = battle.game;
for (const plan of battle.plans) game = settle(step(game, plan));
const revealed = reload(game), loserTokens = ownedTech(game.techTokens, f.guild);
assert.equal(player(game, f.richese).forces[battle.key], 8);
game = advance(game, g => g.decision?.kind === 'sukRescue');
assert.equal(game.decision?.player, f.richese);
if (game.decision?.kind !== 'sukRescue') throw Error('The genuinely trained battle must reach its actual rescue decision.');
const pending = game, decision = game.decision;
reject(pending, f.guild, { type: 'decision', event: decision.event, choice: 0 });
reject(pending, f.richese, { type: 'decision', event: 'stale-rescue', choice: 0 });
for (const difficulty of DIFFICULTIES) {
const view = viewGame(pending, f.richese); view.players.find(p => p.id === f.richese)!.bot = difficulty;
const action = botActions(view).find(a => a.type === 'decision' && typeof a.choice === 'number'); assert.ok(action && typeof action.choice === 'number');
const rescue = decision.options[action.choice]; assert.ok(rescue);
const done = finishBattle(applyAction(reload(pending), f.richese, action));
const kept = rescue.kept?.kind === 'normal' ? 1 : 0;
assert.equal(player(done, f.richese).forces[battle.key], 6 + kept);
assert.equal(player(done, f.richese).reserves, 12 + rescue.normal - kept);
assert.equal(player(done, f.richese).tanks, 2 - rescue.normal);
assert.equal(player(done, f.guild).tanks, player(revealed, f.guild).tanks + 3);
assert.equal(done.lastBattleContext!.winner, f.richese);
assert.equal(player(done, f.richese).noField!.deployed, null);
assert.deepEqual(done.leaderSkills!.assignments, before.leaderSkills!.assignments);
for (const token of loserTokens) assert.equal(done.techTokens![token].owner, f.richese);
custody(done);
}
});
void test('held Arrakeen support affects only its proper payer leg, not an unrelated nested Shrine entitlement', () => {
const f = fixture({ advanced: true, tech: true, strongholds: true, arrakeenOwner: 'richese' }), ending = close(f);
assert.equal(ending.game.strongholdCards!.owners.arrakeen, f.richese);
const nested = settle(applyAction(reload(ending.game), f.richese, pairAction(ending.game, f.richese)));
const battle = prepareBattle(f, nested);
let game = battle.game;
for (const plan of battle.plans) game = settle(step(game, plan));
const wallet = player(game, f.richese).spice;
const committed = advance(game, g => g.decision?.kind === 'sukRescue');
assert.equal(player(committed, f.richese).spice, wallet - 2, 'Arrakeen held card cannot fund support at another source.');
assert.equal(committed.strongholdCards!.owners.arrakeen, f.richese);
const done = finishBattle(committed);
assert.equal(done.lastBattleContext!.winner, f.richese);
assert.equal(player(done, f.richese).forces['shrine:0'], 7);
custody(done);
});
void test('actual held Arrakeen bank support changes its payer debit without increasing trained dial or rescue', () => {
const f = fixture({ advanced: true, tech: true, strongholds: true, arrakeenOwner: 'richese' }), before = close(f).game;
const key = `arrakeen:${territory('arrakeen').sectors[0]}`, own = player(before, f.richese);
assert.equal(before.strongholdCards!.owners.arrakeen, f.richese);
own.reserves += own.forces[key]; delete own.forces[key];
f.staging.push('Conserve the original Arrakeen claiming counter back to native reserves after its real END Mentat claim; retain the actually held card, never assign custody.');
const shipped = settle(applyAction(reload(before), f.richese, pairAction(before, f.richese, 'arrakeen')));
const battle = prepareBattle(f, shipped);
let game = battle.game;
reject(game, f.richese, { ...battle.plans[0].action, dial: 9, support: 9 });
for (const plan of battle.plans) game = settle(step(game, plan));
const wallet = player(game, f.richese).spice;
const committed = advance(game, g => g.decision?.kind === 'sukRescue');
assert.equal(player(committed, f.richese).spice, wallet, 'Held Arrakeen funds exactly the original declared support leg.');
assert.equal(committed.decision?.player, f.richese);
const done = finishBattle(committed);
assert.equal(done.lastBattleContext!.winner, f.richese);
assert.equal(player(done, f.richese).forces[key], 7);
assert.equal(player(done, f.richese).tanks, 0);
assert.equal(player(done, f.guild).tanks, 3);
assert.equal(done.strongholdCards!.owners.arrakeen, f.richese);
custody(done);
});