213 lines
6.3 KiB
TypeScript
213 lines
6.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction,
|
|
createGame,
|
|
initializeNexusGameForAudit,
|
|
initializePairedNexusGameForAudit,
|
|
joinGame,
|
|
newPlayer,
|
|
viewGame,
|
|
type Game,
|
|
type Action,
|
|
} from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { drawNexusCard } from '../game/nexus-cards';
|
|
import { homeworldGameIntegrity } from '../game/homeworld-game';
|
|
import { nexusTraitorInventory } from './fixture-nexus-traitors';
|
|
import {
|
|
enterNexusSpice,
|
|
finishNexusSpice,
|
|
orderNexusSpice,
|
|
nexusAllow,
|
|
nexusReload,
|
|
} from './fixture-nexus-cards';
|
|
|
|
export type NexusRicheseOwner =
|
|
| 'atreides'
|
|
| 'harkonnen'
|
|
| 'emperor'
|
|
| 'fremen'
|
|
| 'guild'
|
|
| 'beneGesserit'
|
|
| 'ixians'
|
|
| 'richese';
|
|
export type NexusRicheseFixtureOptions = {
|
|
seatIds?: [string, string, string];
|
|
ownerFaction?: NexusRicheseOwner;
|
|
opponentFaction?:
|
|
| 'guild'
|
|
| 'harkonnen'
|
|
| 'moritani'
|
|
| 'beneGesserit'
|
|
| 'ixians';
|
|
guild?: boolean;
|
|
advanced?: boolean;
|
|
homeworlds?: boolean;
|
|
spice?: number;
|
|
karama?: boolean;
|
|
};
|
|
export type NexusRicheseFixture = {
|
|
g: Game;
|
|
owner: string;
|
|
target: string;
|
|
observer: string;
|
|
karama?: string;
|
|
};
|
|
|
|
/** Final roster, module choices and production seat IDs precede genuine setup.
|
|
* Conserved reserves are staged only after a real completed Nexus phase. */
|
|
export function nexusRicheseFixture(
|
|
options: NexusRicheseFixtureOptions = {},
|
|
): NexusRicheseFixture {
|
|
const [owner, target, observer] = options.seatIds ?? ['p', 'q', 'r'];
|
|
const ownerFaction = options.ownerFaction ?? 'atreides';
|
|
const opponentFaction =
|
|
options.opponentFaction ??
|
|
((options.guild && ownerFaction !== 'guild') || ownerFaction === 'harkonnen'
|
|
? 'guild'
|
|
: 'harkonnen');
|
|
assert.notEqual(ownerFaction, opponentFaction);
|
|
const observerFaction = ownerFaction === 'richese'
|
|
? 'choam'
|
|
: (['fremen', 'atreides', 'emperor'] as FactionId[])
|
|
.find((f) => f !== ownerFaction && f !== opponentFaction)!;
|
|
let g = createGame(
|
|
'RICHESENEXUS',
|
|
newPlayer(owner, ownerFaction, ownerFaction),
|
|
options.advanced ?? false,
|
|
ownerFaction === 'ixians' || opponentFaction === 'ixians'
|
|
? ['ix']
|
|
: ownerFaction === 'richese' ? ['choam'] : [],
|
|
);
|
|
// The unfinished E3 public deck gate is separate from native Moritani audit
|
|
// setup. Fix the final faction before readiness and genuine initialization.
|
|
joinGame(
|
|
g,
|
|
newPlayer(
|
|
target,
|
|
opponentFaction,
|
|
opponentFaction === 'moritani' ? 'harkonnen' : opponentFaction,
|
|
),
|
|
);
|
|
if (opponentFaction === 'moritani')
|
|
g.players[1] = newPlayer(target, opponentFaction, opponentFaction);
|
|
joinGame(g, newPlayer(observer, observerFaction, observerFaction));
|
|
if (options.homeworlds)
|
|
g = applyAction(g, owner, { type: 'homeworlds', enabled: true });
|
|
g.nexusCards = { cards: null, phase: null };
|
|
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
|
|
g = ownerFaction === 'richese'
|
|
? initializePairedNexusGameForAudit(g)
|
|
: initializeNexusGameForAudit(g);
|
|
for (let step = 0; g.status === 'setup' && step < 100; step++) {
|
|
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,
|
|
`Richese Nexus setup stalled at ${g.setupStage}/${g.decision?.kind}`,
|
|
);
|
|
g = next;
|
|
}
|
|
assert.equal(g.status, 'playing');
|
|
for (const p of g.players) g.deck.push(...p.hand.splice(0));
|
|
if (g.expansions.includes('ix') || ownerFaction === 'richese') {
|
|
for (let step = 0; g.phase === 0 && step < 60; step++) g = fixtureStep(g);
|
|
assert.equal(g.phase, 1);
|
|
orderNexusSpice(g, ['land', 'land']);
|
|
for (let step = 0; g.phase === 1 && step < 60; step++) g = fixtureStep(g);
|
|
assert.equal(g.phase, 2);
|
|
} else {
|
|
g = enterNexusSpice(g);
|
|
orderNexusSpice(g, ['land', 'land']);
|
|
g = finishNexusSpice(g);
|
|
}
|
|
for (const p of g.players) {
|
|
const eliteTotal = p.elites
|
|
? p.elites.reserves +
|
|
p.elites.tanks +
|
|
Object.values(p.elites.forces).reduce((s, n) => s + n, 0)
|
|
: 0;
|
|
Object.assign(p, {
|
|
forces: {},
|
|
reserves: 20,
|
|
tanks: 0,
|
|
spice: 20,
|
|
shipped: false,
|
|
moved: 0,
|
|
});
|
|
if (p.advisors) p.advisors = {};
|
|
if (p.elites)
|
|
Object.assign(p.elites, { forces: {}, reserves: eliteTotal, tanks: 0 });
|
|
}
|
|
const cards = g.nexusCards!.cards!;
|
|
cards.deck = ['richese', ...cards.deck.filter((c) => c !== 'richese')];
|
|
g.nexusCards!.cards = drawNexusCard(cards, owner, g.players, () => 0);
|
|
const karama = options.karama
|
|
? holdNexusRicheseCard(g, target, 'karama').id
|
|
: undefined;
|
|
g.players[0].spice = options.spice ?? 20;
|
|
Object.assign(g, {
|
|
phase: 5,
|
|
active: owner,
|
|
order: [owner, target, observer],
|
|
movementRemaining: [owner, target, observer],
|
|
ready: [],
|
|
response: null,
|
|
decision: null,
|
|
phaseOpening: null,
|
|
spice: {},
|
|
storm: 18,
|
|
hajr: [],
|
|
});
|
|
nexusRicheseInventory(g);
|
|
return { g, owner, target, observer, karama };
|
|
}
|
|
export function holdNexusRicheseCard(g: Game, owner: string, kind: string) {
|
|
const i = g.deck.findIndex((c) => c.kind === kind || c.effect === kind);
|
|
assert.ok(i >= 0);
|
|
const card = g.deck.splice(i, 1)[0];
|
|
g.players.find((p) => p.id === owner)!.hand.push(card);
|
|
return card;
|
|
}
|
|
function fixtureStep(g: Game): Game {
|
|
for (const p of g.players) {
|
|
const v = viewGame(g, p.id);
|
|
v.players.find((s) => s.id === p.id)!.bot = 'Easy';
|
|
const action = botActions(v)[0];
|
|
if (action) return applyAction(g, p.id, action);
|
|
}
|
|
throw new Error(
|
|
`Richese fixture progression stalled at ${g.phase}/${g.decision?.kind}`,
|
|
);
|
|
}
|
|
export function nexusRicheseRequest(
|
|
f: NexusRicheseFixture,
|
|
amount = 5,
|
|
territory = 'arrakeen',
|
|
sector = 10,
|
|
g = f.g,
|
|
): Action {
|
|
return {
|
|
type: 'ship',
|
|
territory,
|
|
sector,
|
|
amount,
|
|
nexus: viewGame(g, f.owner).nexusRichese!.event,
|
|
};
|
|
}
|
|
export function nexusRicheseInventory(g: Game) {
|
|
nexusTraitorInventory(g);
|
|
if (g.homeworlds) homeworldGameIntegrity(g);
|
|
}
|
|
export const nexusRicheseAllow = nexusAllow;
|
|
export const nexusRicheseReload = nexusReload;
|