Files
Dune/tests/fixture-nexus-choam.ts
T

229 lines
6.8 KiB
TypeScript

import assert from 'node:assert/strict';
import { applyAction, type Action, type Game } from '../game/engine';
import { gameDistance, splitLocation, TERRITORIES } from '../game/board';
import { treacheryDeck, type Card } from '../game/cards';
import { richeseCards } from '../game/richese-cards';
import { newRevivalRules } from '../game/revival';
import {
nexusTraitorFixture,
nexusTraitorInventory,
} from './fixture-nexus-traitors';
import { nexusAllow, nexusReload } from './fixture-nexus-cards';
export type NexusChoamEffect =
| 'kulon'
| 'laLaLa'
| 'gamont'
| 'baliset'
| 'jubba';
export const NEXUS_CHOAM_EFFECTS: NexusChoamEffect[] = [
'kulon',
'laLaLa',
'gamont',
'baliset',
'jubba',
];
export type NexusChoamFixture = {
g: Game;
owner: string;
target: string;
observer: string;
cost: Card;
karama?: string;
effect: NexusChoamEffect;
selection: Record<string, unknown>;
};
export function holdNexusChoamCard(g: Game, owner: string, kind: string): Card {
const index = g.deck.findIndex(
(c) => c.kind === kind || c.effect === kind || c.name === kind,
);
assert.ok(index >= 0, `Missing physical ${kind}`);
const card = g.deck.splice(index, 1)[0];
g.players.find((p) => p.id === owner)!.hand.push(card);
return card;
}
/** Final identities, faction roster, Advanced mode and Nexus components precede
* genuine setup. Only conserved later board positions/phase boundaries are
* staged; the public revival, move, storm and Mentat choices use real actions. */
export function nexusChoamFixture(
effect: NexusChoamEffect,
options: {
seatIds?: [string, string, string];
advanced?: boolean;
opponentFaction?: 'guild' | 'emperor' | 'richese';
costKind?: string;
karama?: boolean;
} = {},
): NexusChoamFixture {
const [owner, target, observer] = options.seatIds ?? ['p', 'q', 'r'];
const phase =
effect === 'laLaLa'
? 4
: effect === 'gamont'
? 7
: effect === 'jubba'
? 0
: 5;
let g = nexusTraitorFixture({
ownerFaction: 'choam',
opponentFaction: options.opponentFaction,
advanced: options.advanced ?? true,
seatIds: [owner, target, observer],
phase,
});
const cards = g.nexusCards!.cards!,
index = cards.deck.indexOf('choam');
assert.ok(index >= 0);
cards.deck[index] = cards.hands[owner]!;
cards.hands[owner] = 'choam';
for (const p of g.players) {
Object.assign(p, {
forces: {},
reserves: 20,
tanks: 0,
spice: 20,
moved: 0,
shipped: true,
revived: 0,
});
if (p.elites)
Object.assign(p.elites, {
forces: {},
reserves: p.faction === 'emperor' ? 5 : 3,
tanks: 0,
revived: 0,
});
}
Object.assign(g, {
active: owner,
order: [owner, target, observer],
ready: [],
phaseOpening: null,
response: null,
decision: null,
pendingRevival: null,
choamMarket: null,
movementRemaining: [owner, target, observer],
spice: {},
});
const cost = holdNexusChoamCard(g, owner, options.costKind ?? 'projectile');
const karama =
options.karama === false
? undefined
: holdNexusChoamCard(g, target, 'karama').id;
const p = g.players[0],
q = g.players[1];
let selection: Record<string, unknown> = {};
if (effect === 'laLaLa') {
g.revivalRules = newRevivalRules();
q.tanks = 4;
q.reserves = 16;
g = applyAction(g, target, { type: 'revive', amount: 3 });
assert.equal(g.decision?.kind, 'choamFreeRevival');
selection = { target };
} else if (effect === 'jubba') {
g.storm = 5;
g.stormPending = 3;
p.forces = { 'red_chasm:7': 4 };
p.reserves = 16;
q.forces = { 'red_chasm:7': 3 };
q.reserves = 17;
g.spice = { 'red_chasm:7': 8 };
for (const id of g.order) g = applyAction(g, id, { type: 'ready' });
assert.equal(g.decision?.kind, 'choamStorm');
selection = { territory: 'red_chasm' };
} else if (effect === 'baliset') {
const source = 'red_chasm:7';
const to = TERRITORIES.flatMap((t) =>
t.sectors.map((s) => `${t.id}:${s}`),
).find((key) => {
const loc = splitLocation(key);
return (
loc.territory !== 'red_chasm' &&
loc.sector !== g.storm &&
gameDistance(
g,
source,
key,
(k) => splitLocation(k).sector === g.storm,
) === 1
);
});
assert.ok(to);
p.forces = { [to]: 1 };
p.reserves = 19;
q.forces = { [source]: 3 };
q.reserves = 17;
g.active = target;
g = applyAction(g, target, {
type: 'move',
from: source,
amount: 2,
...splitLocation(to),
});
assert.equal(g.decision?.kind, 'choamMovement');
selection = { target, territory: splitLocation(to).territory };
} else if (effect === 'gamont') {
q.forces = { 'arrakeen:10': 2 };
q.reserves = 18;
for (const id of g.order)
if (g.phase === 7) g = applyAction(g, id, { type: 'ready' });
assert.equal(g.decision?.kind, 'choamMarket');
g = applyAction(g, owner, { type: 'decision', done: true });
for (const id of g.order)
if (!g.ready.includes(id)) g = applyAction(g, id, { type: 'ready' });
assert.equal(g.decision?.kind, 'choamMarket');
g = applyAction(g, owner, { type: 'decision', done: true });
assert.equal(g.decision?.kind, 'choamMentat');
selection = { target, from: 'arrakeen:10', elite: 0 };
} else {
p.forces = { 'red_chasm:7': 3 };
p.reserves = 17;
}
nexusChoamInventory(g);
return { g, owner, target, observer, cost, karama, effect, selection };
}
export function nexusChoamRequest(f: NexusChoamFixture, g = f.g): Action {
return {
type: 'card',
mode: 'choam',
card: f.cost.id,
effect: f.effect,
nexus: JSON.stringify([
'nexusChoam',
g.turn,
g.phase,
f.owner,
f.cost.id,
f.effect,
]),
...f.selection,
};
}
export function nexusChoamInventory(g: Game) {
if (!g.richeseCache) return nexusTraitorInventory(g);
const extra = richeseCards(),
ids = new Set(extra.map((card) => card.id));
assert.deepEqual(
[
...g.deck,
...g.discard,
...g.players.flatMap((p) => p.hand),
...g.richeseCache,
]
.map((card) => card.id)
.sort(),
[...treacheryDeck(g.expansions), ...extra].map((card) => card.id).sort(),
);
// The shared traitor fixture checks the ordinary deck. Above we independently
// include every native Richese component, then reuse its other physical checks.
const ordinary = structuredClone(g);
ordinary.deck = ordinary.deck.filter((card) => !ids.has(card.id));
ordinary.discard = ordinary.discard.filter((card) => !ids.has(card.id));
for (const p of ordinary.players)
p.hand = p.hand.filter((card) => !ids.has(card.id));
nexusTraitorInventory(ordinary);
}
export const nexusChoamAllow = nexusAllow;
export const nexusChoamReload = nexusReload;