141 lines
9.7 KiB
TypeScript
141 lines
9.7 KiB
TypeScript
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);
|
||
});
|
||
}
|
||
}
|