Files
Dune/tests/classic-independent-nexus-battle.test.ts

105 lines
6.5 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { maxCombatDial } from '../game/combat';
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
import {
createClassicIndependentNexusBattleFixture,
revealIndependentNexusBattle, finishIndependentNexusBattle,
independentNexusBattlePlayer as player,
} from './fixture-classic-independent-nexus-battle';
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before, 'A rejected native plan or stale Nexus use cannot change physical custody');
}
void test('native Advanced Emperor Cunning changes five shipped ordinary counters, while independent reserve fees and enemy withdrawal retain their physical sources', () => {
const f = createClassicIndependentNexusBattleFixture();
const before = player(f.game, f.owner), enemy = player(f.game, f.opponent);
assert.equal(player(f.afterShipment, f.owner).reserves, player(f.beforeShipment, f.owner).reserves - 5);
assert.equal(player(f.afterShipment, f.owner).spice, player(f.beforeShipment, f.owner).spice - 5);
assert.equal(f.afterDraw.nexusCards!.cards!.hands[f.owner], 'emperor');
const revealed = revealIndependentNexusBattle(f, { cunning: true });
assert.equal(revealed.nexusCards!.cards!.hands[f.owner], null);
rejected(revealed, f.owner, { type: 'nexusSardaukar', event: revealed.nexusSardaukarLast!.event });
const forces = viewGame(revealed, f.owner).battle!.ownForces!;
assert.equal(forces.normal, 5);
assert.equal(forces.elite, 0);
assert.equal(forces.temporaryElite, 5);
assert.equal(forces.eliteStrength, 2);
assert.equal(forces.freeSupport, false);
assert.equal(maxCombatDial(forces), 10);
assert.equal(revealed.battle!.plans[f.owner].support, 2);
assert.deepEqual(player(revealed, f.owner).elites, before.elites,
'Cunning never replaces the native elite reserve stock or creates elite counters');
const cleanup = finishIndependentNexusBattle(revealed);
const own = player(cleanup, f.owner), other = player(cleanup, f.opponent);
assert.equal(cleanup.lastBattleContext!.winner, f.owner);
assert.equal(own.reserves, before.reserves - 3, 'Reinforcements consumes exactly three ordinary reserve counters');
assert.equal(own.tanks, before.tanks + 5, 'Three reserve fees and two committed temporary-Sardaukar counters are distinct ordinary losses');
assert.equal(own.forces[f.key], 3);
assert.equal(own.spice, before.spice - 2 + 2, 'Support2 is paid before native Carthag Collection2');
assert.deepEqual(own.elites, before.elites);
assert.equal(other.reserves, enemy.reserves + 9, 'Harass returns the nine actual undialed enemy board counters');
assert.equal(other.tanks, enemy.tanks + 1);
assert.equal(other.forces[f.key] ?? 0, 0);
assert.equal(other.spice, enemy.spice - 1);
assert.equal(cleanup.nexusSardaukarHistory!.length, 1);
assert.equal(cleanup.nexusSardaukarHistory![0].casualties!.outcome, 'complete');
assert.equal(cleanup.nexusCards!.cards!.discard.filter(card => card === 'emperor').length, 1);
assert.equal(cleanup.discard.filter(card => card.id === REINFORCEMENTS_CARD).length, 1);
assert.equal(cleanup.discard.filter(card => card.id === HARASS_WITHDRAW_CARD).length, 1);
for (const seat of cleanup.players)
assert.equal(seat.reserves + seat.tanks + Object.values(seat.forces).reduce((sum, count) => sum + count, 0), 20);
assert.equal(cleanup.turn, 2);
assert.equal(cleanup.phase, 7);
assert.equal(viewGame(cleanup, f.owner).nexusSardaukar!.active, false);
});
void test('a genuinely dealt opposing Karama spends Cunning without converting physical fighters, and invalid over-dial plans remain immutable', () => {
const f = createClassicIndependentNexusBattleFixture();
const before = player(f.game, f.owner);
rejected(f.game, f.owner, { type: 'battlePlan', leader: f.ownerLeader,
dial: 11, support: 0, weapon: REINFORCEMENTS_CARD, defense: null });
const revealed = revealIndependentNexusBattle(f, { cunning: true, canceled: true });
const forces = viewGame(revealed, f.owner).battle!.ownForces!;
assert.equal(forces.temporaryElite ?? 0, 0);
assert.equal(maxCombatDial(forces), 5);
assert.equal(revealed.battle!.plans[f.owner].support, 4);
assert.equal(revealed.nexusSardaukarLast!.stage, 'canceled');
const cleanup = finishIndependentNexusBattle(revealed);
const own = player(cleanup, f.owner);
assert.equal(cleanup.lastBattleContext!.winner, f.owner);
assert.equal(own.forces[f.key], 1, 'The same dial now loses four ordinary board counters, not two temporary Sardaukar');
assert.equal(own.tanks, before.tanks + 7);
assert.equal(own.reserves, before.reserves - 3, 'Cancellation cannot refund or duplicate the separate Reinforcements cost');
assert.equal(own.spice, before.spice - 4 + 2, 'Canceled conversion pays support4 before native Carthag Collection2');
assert.deepEqual(own.elites, before.elites);
assert.equal(player(cleanup, f.opponent).reserves, player(f.game, f.opponent).reserves + 9);
assert.equal(cleanup.discard.filter(card => card.id === f.counter).length, 1);
assert.equal(cleanup.nexusCards!.cards!.discard.filter(card => card === 'emperor').length, 1);
assert.equal(cleanup.nexusCards!.cards!.hands[f.owner], null);
assert.equal(cleanup.phase, 7);
});
void test('Basic independent battle preserves ordinary dial losses and free support rather than borrowing the Advanced Cunning advantage', () => {
const f = createClassicIndependentNexusBattleFixture(false);
const before = player(f.game, f.owner);
const offer = viewGame(f.game, f.owner).nexusSardaukar!.offer!;
rejected(f.game, f.owner, { type: 'nexusSardaukar', event: offer.event });
const cleanup = finishIndependentNexusBattle(revealIndependentNexusBattle(f));
const own = player(cleanup, f.owner);
assert.equal(cleanup.lastBattleContext!.winner, f.owner);
assert.equal(own.spice, before.spice);
assert.equal(own.tanks, before.tanks + 7);
assert.equal(own.forces[f.key], 1);
assert.equal(own.reserves, before.reserves - 3);
assert.equal(player(cleanup, f.opponent).reserves, player(f.game, f.opponent).reserves + 9);
assert.equal(player(cleanup, f.opponent).tanks, player(f.game, f.opponent).tanks + 1);
assert.equal(cleanup.nexusCards!.cards!.hands[f.owner], 'emperor', 'The blocked Advanced ability cannot spend a genuinely drawn Basic Nexus card');
assert.equal(cleanup.phase, 7);
});