Files
Dune/tests/nexus-sardaukar-homeworld.test.ts

198 lines
6.7 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction,
createGame,
initializeNexusGameForAudit,
joinGame,
newPlayer,
normalizeAutomaticGame,
viewGame,
type Game,
} from '../game/engine';
import { botActions } from '../game/bots';
import { drawNexusCard } from '../game/nexus-cards';
import { homeworldGameIntegrity } from '../game/homeworld-game';
import { maxCombatDial, maxCombatSupport } from '../game/combat';
import {
enterNexusSpice,
orderNexusSpice,
finishNexusSpice,
nexusAllow,
nexusReload,
} from './fixture-nexus-cards';
import {
beginNexusSardaukar,
prepareSardaukarBattle,
nexusSardaukarInventory,
} from './fixture-nexus-sardaukar';
/** Both modules and the final roster precede the genuine setup initializer.
* Later board positions conserve all counters; Salusa is an explicit subset
* of existing reserves, including the low case's four stars at Kaitain. */
function fixture(salusa: 1 | 2 | 5 = 5) {
let g = createGame('SARDHOME', newPlayer('p', 'Emperor', 'emperor'), true);
joinGame(g, newPlayer('q', 'Guild', 'guild'));
joinGame(g, newPlayer('r', 'Fremen', 'fremen'));
g = applyAction(g, 'p', { type: 'homeworlds', enabled: true });
g.nexusCards = { cards: null, phase: null };
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
g = initializeNexusGameForAudit(g);
for (let step = 0; g.status === 'setup' && step < 80; step++) {
let next: Game | undefined;
for (const p of g.players) {
const view = viewGame(g, p.id);
view.players.find((player) => player.id === p.id)!.bot = 'Easy';
const action = botActions(view)[0];
if (action) {
next = applyAction(g, p.id, action);
break;
}
}
assert.ok(next, 'Combined setup must progress through real choices.');
g = next;
}
assert.equal(g.status, 'playing');
for (const p of g.players) g.deck.push(...p.hand.splice(0));
g = enterNexusSpice(g);
orderNexusSpice(g, ['land', 'land']);
g = finishNexusSpice(g);
for (const p of g.players) {
p.forces = {};
p.reserves = 20;
p.tanks = 0;
p.spice = 20;
if (p.elites) {
p.elites.forces = {};
p.elites.reserves = p.faction === 'emperor' ? 5 : 3;
p.elites.tanks = 0;
}
}
g.players[0].forces = { 'pasty_mesa:5': 7 };
g.players[0].reserves = 13;
g.players[1].forces = { 'pasty_mesa:5': 2 };
g.players[1].reserves = 18;
g.homeworlds!.custody!.salusa = { normal: 0, elite: salusa };
const cards = g.nexusCards!.cards!;
cards.deck = ['emperor', ...cards.deck.filter((card) => card !== 'emperor')];
g.nexusCards!.cards = drawNexusCard(cards, 'p', g.players, () => 0);
Object.assign(g, {
phase: 6,
phaseOpening: null,
response: null,
decision: null,
active: 'p',
order: ['p', 'q', 'r'],
ready: [],
storm: 18,
});
// This conserved controlled Battle setup skips the actual Charity turns;
// their ordered frame does not belong to the newly staged phase.
delete g.charityOrder;
g = applyAction(g, 'p', {
type: 'chooseBattle',
territory: 'pasty_mesa',
target: 'q',
});
for (const id of ['p', 'q'])
if (g.battle!.preLeader && !g.battle!.preLeader.closed)
g = applyAction(g, id, {
type: 'battlePreparationReady',
event: g.battle!.event,
});
g = prepareSardaukarBattle(g);
inventory(g);
return g;
}
function inventory(g: Game) {
homeworldGameIntegrity(g);
nexusSardaukarInventory(g);
}
function activate(g: Game) {
return nexusAllow(beginNexusSardaukar(g));
}
function plan(g: Game, support: number) {
const leader = [...g.players[0].leaders].sort(
(a, b) => b.strength - a.strength,
)[0];
return { type: 'battlePlan', dial: 12, support, leader: leader.id };
}
function resolve(g: Game, support: number) {
g = applyAction(g, 'p', plan(g, support));
g = applyAction(g, 'q', {
type: 'battlePlan',
dial: 0,
support: 0,
leader: g.players[1].leaders[0].id,
});
for (const id of ['p', 'q'])
g = applyAction(g, id, { type: 'traitorCall', call: false });
if (g.decision?.kind === 'battleLosses')
g = applyAction(g, 'p', { type: 'decision', choice: 0 });
return g;
}
void test('real high-Salusa Cunning pays only two normal support and all seven losses remain ordinary counters', () => {
const original = fixture();
let g = activate(nexusReload(original));
const forces = viewGame(g, 'p').battle!.ownForces!;
assert.deepEqual(
[forces.normal, forces.elite, forces.temporaryElite],
[7, 0, 5],
);
assert.equal(forces.eliteFreeSupport, true);
assert.equal(maxCombatDial(forces), 12);
assert.equal(maxCombatSupport(forces), 2);
assert.deepEqual(g.homeworlds!.custody!.salusa, { normal: 0, elite: 5 });
const before = nexusReload(g);
assert.throws(() => applyAction(g, 'p', plan(g, 3)));
assert.deepEqual(g, before);
for (const p of g.players)
assert.deepEqual(viewGame(nexusReload(g), p.id), viewGame(g, p.id));
g = resolve(g, 2);
assert.equal(g.players[0].tanks, 7);
assert.equal(g.players[0].spice, original.players[0].spice - 2);
assert.equal(g.players[0].elites!.tanks, 0);
assert.equal(g.players[0].elites!.reserves, 5);
assert.deepEqual(g.homeworlds!.custody!.salusa, { normal: 0, elite: 5 });
assert.equal(g.players[0].reserves, original.players[0].reserves);
assert.deepEqual(normalizeAutomaticGame(nexusReload(g)), nexusReload(g));
inventory(g);
});
void test('one Salusa star stays low despite five temporary Sardaukar and requires all seven support payments', () => {
const original = fixture(1);
let g = activate(nexusReload(original));
const forces = viewGame(g, 'p').battle!.ownForces!;
assert.equal(forces.temporaryElite, 5);
assert.equal(!!forces.eliteFreeSupport, false);
assert.equal(maxCombatDial(forces), 12);
assert.equal(maxCombatSupport(forces), 7);
const before = nexusReload(g);
assert.throws(() => applyAction(g, 'p', plan(g, 2)));
assert.deepEqual(g, before);
g = resolve(g, 7);
assert.equal(g.players[0].tanks, 7);
assert.equal(g.players[0].spice, original.players[0].spice - 7);
assert.equal(g.players[0].elites!.tanks, 0);
assert.equal(g.players[0].elites!.reserves, 5);
assert.deepEqual(g.homeworlds!.custody!.salusa, { normal: 0, elite: 1 });
inventory(g);
});
void test('the actual Salusa one-to-two threshold controls the shared profile without counting stars at Kaitain', () => {
for (const [salusa, support] of [
[1, 7],
[2, 2],
] as const) {
const g = activate(fixture(salusa));
assert.equal(
maxCombatSupport(viewGame(g, 'p').battle!.ownForces!),
support,
);
assert.equal(g.players[0].elites!.reserves, 5);
assert.equal(g.homeworlds!.custody!.salusa!.elite, salusa);
inventory(g);
}
});