Files
Dune/tests/nexus-core-world-battles-runtime.test.ts
T

141 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { applyAction, viewGame } from '../game/engine';
import { combatArmy } from '../game/combat-location';
import { casualtyOptions, maxCombatDial, maxCombatSupport } from '../game/combat';
import { homeworldContext, homeworldGameIntegrity } from '../game/homeworld-game';
import { homeworldPopulations } from '../game/homeworld-population';
import { reactionAction } from './fixture-classic-independent-nexus-reactions';
import {
advanceWorldNexusBattle, createWorldNexusEmperorBattle, createWorldNexusHarkonnenBattle,
revealWorldNexusPlans, worldNexusClean, worldNexusPlayer,
} from './fixture-nexus-core-world-battles';
for (const salusa of ['high', 'low'] as const) {
void test(`original own-Kaitain Cunning with ${salusa} Salusa spends support but kills only physical ordinary counters`, () => {
const f = createWorldNexusEmperorBattle({ salusa, skills: true, tech: true, strongholds: true });
let game = f.game;
const emperor = worldNexusPlayer(game, 'emperor'), guild = worldNexusPlayer(game, 'guild');
const beforeSpice = emperor.spice, beforeStars = structuredClone(emperor.elites);
const beforeSalusa = structuredClone(game.homeworlds!.custody!.salusa);
const populations = homeworldPopulations(homeworldContext(game), game.homeworlds!.custody!);
const home = populations.find(home => home.location === f.location)!;
assert.deepEqual([home.population, home.side, home.nativeBattleStrength], [7, 'high', 2]);
assert.deepEqual(beforeSalusa, { normal: 8, elite: salusa === 'high' ? 5 : 1 });
assert.equal(populations.find(home => home.card === 'salusa_secundus')!.side, salusa);
assert.equal(f.trainer!.leader, f.leader);
assert.equal(game.battle!.leaderSkillHidden!.emperor, true);
assert.equal(f.plans[0].action.leader, f.trainer!.leader);
// Arrakeen and Tuek's Sietch were actually earned at END Mentat; neither
// becomes generic off-planet strength or support in this native battle.
assert.equal(game.strongholdCards!.owners.arrakeen, 'atreides');
assert.equal(game.strongholdCards!.owners.tueks_sietch, 'guild');
assert.deepEqual(viewGame(game, 'emperor').battle!.strongholdEffects, { guild: null, emperor: null });
const offer = viewGame(game, 'emperor').nexusSardaukar!.offer!;
assert.equal(offer.blocked, null);
game = reactionAction(game, 'emperor', { type: 'nexusSardaukar', event: offer.event });
game = advanceWorldNexusBattle(game, state => worldNexusClean(state));
const forces = viewGame(game, 'emperor').battle!.ownForces!;
assert.deepEqual([forces.normal, forces.elite, forces.temporaryElite], [7, 0, 5]);
assert.equal(!!forces.eliteFreeSupport, salusa === 'high');
assert.equal(maxCombatDial(forces), 12);
assert.equal(maxCombatSupport(forces), salusa === 'high' ? 2 : 7);
assert.ok(casualtyOptions(forces, 4, salusa === 'high' ? 0 : 2).some(loss => loss.normal === 2 && loss.elite === 0));
// High Salusa's five real stars grant free elite support, so only the two
// ordinary non-temporary counters can pay (shared cap 2). Low Salusa lifts
// the shared counter cap to 7, but the desert export has already left the
// Emperor 2 spice, so the live plan bound stays 0–2 and anything above it
// is rejected unchanged.
const badSupport = salusa === 'high' ? 3 : 8;
assert.throws(() => applyAction(game, 'emperor', { ...f.plans[0].action, support: badSupport }));
assert.throws(() => applyAction(game, 'emperor', { type: 'nexusSardaukar', event: offer.event }));
game = revealWorldNexusPlans(game, f.plans);
assert.deepEqual(viewGame(game, 'emperor').battle!.traitorVoters, ['emperor']);
assert.throws(() => applyAction(game, 'guild', { type: 'traitorCall', call: false }));
game = advanceWorldNexusBattle(game, state => !state.battle && !state.homeworldBattleLoss && worldNexusClean(state));
assert.equal(game.lastBattleContext!.winner, 'emperor');
assert.equal(game.lastBattleContext!.result, 'normal');
assert.deepEqual(combatArmy(game, 'emperor', f.location), { normal: 5, elite: 0 });
assert.deepEqual(combatArmy(game, 'guild', f.location), { normal: 0, elite: 0 });
assert.equal(worldNexusPlayer(game, 'emperor').tanks, emperor.tanks + 2);
assert.equal(worldNexusPlayer(game, 'guild').tanks, guild.tanks + 3);
assert.equal(worldNexusPlayer(game, 'emperor').spice, beforeSpice - (salusa === 'high' ? 0 : 2));
assert.deepEqual(worldNexusPlayer(game, 'emperor').elites, beforeStars);
assert.deepEqual(game.homeworlds!.custody!.salusa, beforeSalusa);
const record = game.nexusSardaukarHistory!.at(-1)!;
assert.equal(record.casualties!.outcome, 'complete');
assert.ok(record.casualties!.options.every(loss => loss.elite === 0));
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'emperor').length, 1);
assert.equal(viewGame(game, 'emperor').nexusSardaukar!.active, false);
homeworldGameIntegrity(game);
});
}
for (const advanced of [false, true]) {
for (const cancel of [false, true]) {
void test(`${advanced ? 'Advanced all14/phase' : 'Basic'} Giedi Prime native call ${cancel ? 'is canceled with retirement and replacement' : 'retains its original traitor outcome'}`, () => {
const f = createWorldNexusHarkonnenBattle({ advanced, skills: advanced, tech: advanced, strongholds: advanced });
let game = revealWorldNexusPlans(f.game, f.plans);
const native = worldNexusPlayer(game, 'harkonnen'), invader = worldNexusPlayer(game, 'atreides');
const home = homeworldPopulations(homeworldContext(game), game.homeworlds!.custody!).find(home => home.location === f.location)!;
assert.deepEqual([home.population, home.side, home.nativeBattleStrength], [10, 'high', 2]);
assert.deepEqual(viewGame(game, 'atreides').battle!.traitorVoters, ['harkonnen']);
assert.throws(() => applyAction(game, 'atreides', { type: 'traitorCall', call: true }));
if (advanced) {
assert.equal(game.strongholdCards!.owners.carthag, 'harkonnen');
assert.deepEqual(viewGame(game, 'harkonnen').battle!.strongholdEffects, { harkonnen: null, atreides: null });
assert.equal(game.battle!.plans.harkonnen.leader, f.trainer!.leader);
}
game = reactionAction(game, 'harkonnen', { type: 'traitorCall', call: true });
const event = game.pendingNexusHarkonnenBetrayal!.source.event;
assert.equal(viewGame(game, 'atreides').nexusHarkonnenBetrayalReaction!.canUse, true);
assert.equal(game.pendingNexusHarkonnenBetrayal!.source.declaration.identity, f.identity);
game = reactionAction(game, 'atreides', { type: cancel ? 'nexusHarkonnenBetrayalUse' : 'nexusHarkonnenBetrayalPass', event });
if (cancel) {
assert.equal(worldNexusPlayer(game, 'harkonnen').traitors.includes(f.identity), false);
assert.equal(game.traitorReserve!.filter(identity => identity === f.identity).length, 1);
assert.equal(game.pendingNexusHarkonnenReplacement!.identity, f.identity);
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'harkonnen').length, 1);
}
let captive: string | undefined;
if (advanced) {
game = advanceWorldNexusBattle(game, state => state.decision?.kind === 'captureOffer' && !state.response);
assert.equal(game.decision!.kind, 'captureOffer');
game = reactionAction(game, 'harkonnen', { type: 'decision', accept: true });
game = advanceWorldNexusBattle(game, state => state.decision?.kind === 'capturedLeader' && !state.response);
assert.ok(game.decision?.kind === 'capturedLeader');
captive = game.decision.leader;
assert.equal(worldNexusPlayer(game, 'atreides').leaders.find(leader => leader.id === captive)!.capturedBy, 'harkonnen');
const spice = worldNexusPlayer(game, 'harkonnen').spice;
game = reactionAction(game, 'harkonnen', { type: 'decision', mode: 'execute' });
assert.equal(worldNexusPlayer(game, 'harkonnen').spice, spice + 2);
const executed = worldNexusPlayer(game, 'atreides').leaders.find(leader => leader.id === captive)!;
assert.equal(executed.dead, true);
assert.equal(executed.deaths, 2);
assert.equal(executed.capturedBy, undefined);
}
const replacementTop = cancel ? game.traitorReserve![0] : null;
game = advanceWorldNexusBattle(game, state => state.phase === 8 && worldNexusClean(state));
assert.equal(game.lastBattleContext!.territory, f.location);
assert.equal(game.lastBattleContext!.winner, 'harkonnen');
assert.equal(game.lastBattleContext!.result, cancel ? 'normal' : 'traitor');
assert.deepEqual(combatArmy(game, 'harkonnen', f.location), { normal: 10, elite: 0 });
assert.deepEqual(combatArmy(game, 'atreides', f.location), { normal: 0, elite: 0 });
assert.equal(worldNexusPlayer(game, 'harkonnen').tanks, native.tanks);
assert.equal(worldNexusPlayer(game, 'atreides').tanks, invader.tanks + 1);
assert.equal(game.pendingNexusHarkonnenReplacement, null);
assert.equal(game.nexusHarkonnenReplacementHistory!.length, cancel ? 1 : 0);
if (cancel) {
assert.equal(game.nexusHarkonnenReplacementHistory![0].drawn, replacementTop);
assert.equal(worldNexusPlayer(game, 'harkonnen').traitors.length, 4);
} else {
assert.equal(game.nexusCards!.cards!.hands.atreides, 'harkonnen');
assert.ok(worldNexusPlayer(game, 'harkonnen').revealedTraitors!.includes(f.identity));
assert.equal(worldNexusPlayer(game, 'atreides').leaders.find(leader => leader.id === f.identity)!.dead, true);
}
assert.throws(() => applyAction(game, 'atreides', { type: 'nexusHarkonnenBetrayalUse', event }));
homeworldGameIntegrity(game);
});
}
}