Files
Dune/tests/salusa-free-sardaukar-runtime.test.ts
T

179 lines
9.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { baseDeck } from '../game/cards';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { forceRevivalQuote } from '../game/revival';
import {
holdRevivalCard, homeworldRevivalFixture,
revivalInventory, revivalPlayer,
} from './fixture-homeworld-revival';
import { reachAdvancedRevivalOrderActor } from './fixture-advanced-revival-order';
import {
createSalusaGholaFixture, salusaGholaArmy, salusaGholaInventory,
} from './fixture-salusa-ghola';
const emperor = (game: Game) => revivalPlayer(game, 'e');
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
/** Real Advanced Homeworld setup and the existing actual Charity/auction
* continuation. The fixture itself stages Charity and Fremen positions. This
* supplement explicitly moves conserved Imperial reserves into Tanks: retain
* 1 or 2 original stars on Salusa, put the other stars and 2 ordinary forces
* into Tanks. It does not claim natural casualties or a played-card purchase. */
function source(salusaStars: 1 | 2 = 1) {
let game = homeworldRevivalFixture({ advanced: true });
const p = emperor(game), deadStars = 5 - salusaStars;
assert.equal(p.reserves, 20);
assert.equal(p.elites!.reserves, 5);
assert.equal(game.homeworlds!.custody!.salusa!.elite, 5);
p.reserves -= deadStars + 2;
p.tanks += deadStars + 2;
p.elites!.reserves -= deadStars;
p.elites!.tanks += deadStars;
game.homeworlds!.custody!.salusa!.elite = salusaStars;
const ghola = holdRevivalCard(game, 'e', 'ghola');
const karama = holdRevivalCard(game, 'e', 'karama');
revivalInventory(game);
game = reachAdvancedRevivalOrderActor(game, 'e');
assert.equal(game.phase, 4);
assert.ok(!game.revivalOrder || game.revivalOrder.remaining[0] === 'e');
revivalInventory(game);
return { game, ghola, karama };
}
/** This is a separately controlled legacy-shaped Advanced battleless Revival
* with no Homeworld module; the staged physical Tanks match the Low source. */
function withoutWorlds() {
const player = newPlayer('e', 'Emperor', 'emperor');
const game = createGame('SALUSANOWORLD', player, true);
game.players.push(newPlayer('f', 'Fremen', 'fremen'));
Object.assign(game, { status: 'playing', phase: 4, turn: 2, active: null,
order: ['e', 'f'], ready: [], storm: 18, deck: baseDeck() });
Object.assign(player, { reserves: 14, tanks: 6, spice: 20,
elites: { reserves: 1, tanks: 4, forces: {}, revived: 0 } });
return game;
}
void test('low Salusa makes a normal Sardaukar return paid, leaving the free allowance for an ordinary force', () => {
const { game: initial } = source();
const original = structuredClone(emperor(initial));
const view = viewGame(initial, 'e');
assert.equal(typeof view.revival.sardaukarFreeBlock, 'string');
assert.equal(view.revival.freeRemaining, 1);
const quote = forceRevivalQuote(initial, emperor(initial), 1, 1);
assert.equal(quote.free, 0);
assert.equal(quote.cost, 2);
const poor = structuredClone(initial);
emperor(poor).spice = 1; // Controlled insufficient-funds boundary.
reject(poor, 'e', { type: 'revive', amount: 1, elite: 1 });
let game = applyAction(reload(initial), 'e', { type: 'revive', amount: 1, elite: 1 });
assert.equal(emperor(game).spice, original.spice - 2);
assert.equal(emperor(game).freeForcesRevived, 0);
assert.equal(emperor(game).revived, 1);
assert.equal(emperor(game).elites!.revived, 1);
assert.equal(game.homeworlds!.custody!.salusa!.elite, 2);
assert.equal(viewGame(game, 'e').revival.sardaukarFreeBlock, null);
assert.equal(viewGame(game, 'e').revival.freeRemaining, 1);
game = applyAction(reload(game), 'e', { type: 'revive', amount: 1, elite: 0 });
assert.equal(emperor(game).spice, original.spice - 2);
assert.equal(emperor(game).freeForcesRevived, 1);
assert.equal(emperor(game).revived, 2);
revivalInventory(game);
});
void test('a mixed Low group spends free allowance on normal forces and pays for its Sardaukar', () => {
const { game: initial } = source();
const quote = forceRevivalQuote(initial, emperor(initial), 2, 1);
assert.equal(quote.free, 1);
assert.equal(quote.cost, 2);
const before = structuredClone(emperor(initial));
const game = applyAction(reload(initial), 'e', { type: 'revive', amount: 2, elite: 1 });
assert.equal(emperor(game).spice, before.spice - 2);
assert.equal(emperor(game).tanks, before.tanks - 2);
assert.equal(emperor(game).elites!.tanks, before.elites!.tanks - 1);
assert.equal(emperor(game).freeForcesRevived, 1);
assert.equal(emperor(game).revived, 2);
assert.equal(game.homeworlds!.custody!.salusa!.elite, 2);
revivalInventory(game);
});
void test('actual native Low Salusa battle cannot revive its earned dead Sardaukar with Ghola', () => {
const f = createSalusaGholaFixture();
assert.deepEqual(salusaGholaArmy(f.game), { normal: 0, elite: 1 });
assert.equal(f.game.players.find(player => player.id === f.actor)!.elites!.tanks, 1);
const view = viewGame(f.game, f.actor);
assert.equal(view.ghola.eliteRemaining, 0);
assert.equal(typeof view.ghola.eliteBlock, 'string');
reject(f.game, f.actor, f.action);
assert.ok(f.game.players.find(player => player.id === f.actor)!.hand.some(card => card.id === f.card));
assert.deepEqual(salusaGholaInventory(f.game), f.original);
});
void test('Low Ghola and Emperor special reject free stars while retaining normal-force alternatives', () => {
const { game: initial, ghola, karama } = source();
reject(initial, 'e', { type: 'card', card: ghola, amount: 1, elite: 1 });
reject(initial, 'e', { type: 'card', mode: 'special', card: karama, amount: 1, elite: 1 });
assert.notEqual(emperor(initial).specialKaramaUsed, true);
assert.equal(initial.discard.some(card => card.id === karama || card.id === ghola), false);
const gholaNormal = applyAction(reload(initial), 'e', { type: 'card', card: ghola, amount: 1, elite: 0 });
assert.equal(emperor(gholaNormal).tanks, emperor(initial).tanks - 1);
assert.equal(emperor(gholaNormal).elites!.tanks, emperor(initial).elites!.tanks);
assert.equal(gholaNormal.discard.filter(card => card.id === ghola).length, 1);
const specialNormal = applyAction(reload(initial), 'e', { type: 'card', mode: 'special', card: karama, amount: 1, elite: 0 });
assert.equal(emperor(specialNormal).tanks, emperor(initial).tanks - 1);
assert.equal(emperor(specialNormal).elites!.tanks, emperor(initial).elites!.tanks);
assert.equal(emperor(specialNormal).specialKaramaUsed, true);
assert.equal(specialNormal.discard.filter(card => card.id === karama).length, 1);
revivalInventory(gholaNormal);
revivalInventory(specialNormal);
});
void test('Salusa at the high threshold permits free Ghola and three-star special revival again', () => {
const { game: initial, ghola, karama } = source(2);
assert.equal(viewGame(initial, 'e').revival.sardaukarFreeBlock, null);
assert.equal(forceRevivalQuote(initial, emperor(initial), 1, 1).cost, 0);
const gholaGame = applyAction(reload(initial), 'e', { type: 'card', card: ghola, amount: 1, elite: 1 });
assert.equal(gholaGame.homeworlds!.custody!.salusa!.elite, 3);
const specialGame = applyAction(reload(initial), 'e', { type: 'card', mode: 'special', card: karama, amount: 3, elite: 3 });
assert.equal(specialGame.homeworlds!.custody!.salusa!.elite, 5);
assert.equal(emperor(specialGame).elites!.revived, 0);
assert.equal(emperor(specialGame).specialKaramaUsed, true);
revivalInventory(gholaGame);
revivalInventory(specialGame);
});
void test('legacy Advanced games without Homeworlds retain a free normal Sardaukar return', () => {
const initial = withoutWorlds();
assert.equal(viewGame(initial, 'e').revival.sardaukarFreeBlock, null);
assert.equal(forceRevivalQuote(initial, emperor(initial), 1, 1).free, 1);
const game = applyAction(initial, 'e', { type: 'revive', amount: 1, elite: 1 });
assert.equal(emperor(game).spice, 20);
assert.equal(emperor(game).freeForcesRevived, 1);
assert.equal(emperor(game).elites!.reserves, 2);
assert.equal(game.homeworlds, undefined);
});
void test('all minimal bot profiles use only legal Low revival and Ghola proposals', () => {
const { game } = source();
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, 'e');
view.players.find(player => player.id === 'e')!.bot = difficulty;
const actions = botActions(view);
assert.ok(actions.length, `${difficulty} must have a legal continuation`);
for (const action of actions) {
if (action.type === 'card' && (action.mode === 'special' ||
view.ghola.cards.includes(String(action.card))))
assert.equal(action.elite ?? 0, 0, `${difficulty} cannot propose a free star`);
assert.doesNotThrow(() => applyAction(game, 'e', action), `${difficulty}: ${JSON.stringify(action)}`);
}
}
});