Files
Dune/tests/fixture-nexus-guild-cunning.ts

176 lines
5.6 KiB
TypeScript

import assert from 'node:assert/strict';
import { applyAction, viewGame, type Game, type Action } from '../game/engine';
import {
nexusRicheseFixture,
holdNexusRicheseCard,
type NexusRicheseFixtureOptions,
} from './fixture-nexus-richese';
import { nexusTraitorInventory } from './fixture-nexus-traitors';
import { homeworldGameIntegrity } from '../game/homeworld-game';
import { nexusAllow, nexusReload } from './fixture-nexus-cards';
import { orderNexusSpice } from './fixture-nexus-cards';
import { botActions } from '../game/bots';
export type NexusGuildCunningFixture = {
g: Game;
owner: string;
target: string;
observer: string;
karama?: string;
};
/** CONTROLLED historical Movement boundary after genuine Guild setup. The
* inherited fixture stages wallets, reserve pools and phase5; retire its old
* phase2 Charity frame consistently with that declared jump. Exchange physical
* Nexus identities before Cunning; no grant, progress or completed Charity turn
* is presented as native history. */
export function nexusGuildCunningFixture(
options: Pick<
NexusRicheseFixtureOptions,
| 'seatIds'
| 'advanced'
| 'homeworlds'
| 'spice'
| 'karama'
| 'opponentFaction'
> = {},
): NexusGuildCunningFixture {
const f = nexusRicheseFixture({ ...options, ownerFaction: 'guild' });
assert.equal(f.g.phase, 5);
delete f.g.charityOrder;
const cards = f.g.nexusCards!.cards!,
index = cards.deck.indexOf('guild');
assert.ok(index >= 0);
assert.equal(cards.hands[f.owner], 'richese');
cards.deck[index] = 'richese';
cards.hands[f.owner] = 'guild';
nexusGuildCunningInventory(f.g);
return f;
}
/** Allow only the already declared shipment and its ordinary response chain. */
export function settleGuildCunningShipment(g: Game): Game {
if (g.decision?.kind === 'guildShipment')
g = applyAction(g, g.decision.player, { type: 'decision', allow: true });
return nexusAllow(g);
}
export function originalGuildCunningTurn(
f: NexusGuildCunningFixture,
move = true,
g = f.g,
): Game {
g = settleGuildCunningShipment(
applyAction(g, f.owner, {
type: 'ship',
territory: 'arrakeen',
sector: 10,
amount: 5,
}),
);
if (move)
g = applyAction(g, f.owner, {
type: 'move',
from: 'arrakeen:10',
territory: 'hagga_basin',
sector: 12,
amount: 5,
});
nexusGuildCunningInventory(g);
return g;
}
export function nexusGuildCunningRequest(
f: NexusGuildCunningFixture,
g = f.g,
): Action {
return {
type: 'endMovement',
nexus: viewGame(g, f.owner).nexusGuildCunning!.offer!.event,
};
}
export function holdGuildCunningCard(g: Game, owner: string, kind: string) {
const held = g.players
.find((p) => p.id === owner)!
.hand.find((c) => c.kind === kind || c.effect === kind);
return held ?? holdNexusRicheseCard(g, owner, kind);
}
export function nexusGuildCunningInventory(g: Game) {
// The shared card census also checks a pre-Homeworld aggregate force total.
// Count foreign visitors in that detached census; validate actual typed
// Homeworld custody against the unchanged game separately.
const census = structuredClone(g);
for (const p of census.players)
p.reserves += Object.values(g.homeworlds?.custody?.visitors ?? {}).reduce(
(sum, visitors) => {
const group = visitors[p.id];
return sum + (group?.normal ?? 0) + (group?.elite ?? 0);
},
0,
);
nexusTraitorInventory(census);
if (g.homeworlds) homeworldGameIntegrity(g);
}
export const nexusGuildCunningReload = nexusReload;
export const nexusGuildCunningAllow = nexusAllow;
/** Place an actual native Terror token at the first Mentat, then complete the
* next Storm/Spice/auction/Revival before returning the Guild's real turn. */
export function nexusGuildCunningAllianceFixture(
seatIds?: [string, string, string],
): NexusGuildCunningFixture {
const f = nexusGuildCunningFixture({ seatIds, opponentFaction: 'moritani' });
// Reserve this actual card before the intervening real auction can distribute
// all copies. The later scenario reuses this owned identity rather than
// assuming a needed card remains in the randomly shuffled draw pile.
holdGuildCunningCard(f.g, f.owner, 'hajr');
let g = f.g;
while (g.phase === 5) g = applyAction(g, g.active!, { type: 'endMovement' });
assert.equal(g.phase, 7);
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
assert.equal(g.decision?.kind, 'moritaniPlacement');
const token = g.moritaniTerror!.tokens.find((t) => t.kind === 'robbery')!;
g = nexusAllow(
applyAction(g, f.target, {
type: 'decision',
token: token.id,
territory: 'carthag',
}),
);
let ordered = false;
for (
let step = 0;
!(
g.turn === 2 &&
g.phase === 5 &&
!g.response &&
!g.decision &&
!g.phaseOpening
) && step < 180;
step++
) {
if (g.turn === 2 && g.phase === 1 && !ordered) {
orderNexusSpice(g, ['land', 'land']);
ordered = true;
}
let next: Game | undefined;
for (const p of g.players) {
const v = viewGame(g, p.id);
v.players.find((s) => s.id === p.id)!.bot = 'Easy';
const a = botActions(v)[0];
if (a) {
next = applyAction(g, p.id, a);
break;
}
}
assert.ok(
next,
`Guild alliance fixture stalled at ${g.turn}/${g.phase}/${g.decision?.kind}`,
);
g = next;
}
assert.equal(g.turn, 2);
assert.equal(g.phase, 5);
while (g.active !== f.owner)
g = applyAction(g, g.active!, { type: 'endMovement' });
assert.equal(g.nexusCards!.cards!.hands[f.owner], 'guild');
nexusGuildCunningInventory(g);
return { ...f, g };
}