Files
Dune/tests/reinforcements-typed-choice-runtime.test.ts
T
Quintine 1cf90f1328
Verify and publish checkpoint container / container (push) Successful in 11m11s
Allow chosen Reinforcements reserve costs
2026-10-29 04:41:26 +11:00

99 lines
4.4 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { harassCustody, harassWithdrawGame, takeHarassCard } from './fixture-harass-withdraw';
const card = 'ecaz-reinforcements';
/** Genuine Advanced setup and native battle admission; the shared fixture
* stages physical board forces and card custody, preserving every counter. */
function fixture(): Game {
const game = harassWithdrawGame({ advanced: true, factions: ['emperor','atreides','harkonnen'] });
takeHarassCard(game, 'a', card);
harassCustody(game);
return game;
}
function plan(elite?: number, useCard = true): Action {
return { type: 'battlePlan', dial: 1, support: 1, leader: 'emperor-0',
weapon: useCard ? card : null, defense: null,
...(elite === undefined ? {} : { reinforcementsElite: elite }),
} as Action;
}
function second(game: Game): Game {
return applyAction(game, 'd', { type:'battlePlan', dial:1, support:1,
leader:'atreides-0', weapon:null, defense:null });
}
function finish(game: Game): Game {
let next = applyAction(game, 'a', {type:'traitorCall',call:false});
next = applyAction(next, 'd', {type:'traitorCall',call:false});
if (next.decision?.kind === 'moritaniAssassinate')
next = applyAction(next, next.decision.player,
{type:'decision', event:next.decision.event, decline:true});
harassCustody(next);
return next;
}
for (const selected of [0,1,2,3] as const) {
void test(`Reinforcements commits the selected ${selected} elite reserve cost`, () => {
const start = fixture(), initial = structuredClone(start);
const owner = start.players.find(row => row.id === 'a')!;
assert.ok(owner.reserves - owner.elites!.reserves >= 3);
assert.ok(owner.elites!.reserves >= 3);
const one = applyAction(start, 'a', plan(selected));
assert.deepEqual(start, initial);
assert.equal(one.players[0].reserves, owner.reserves, 'Sealing does not spend the cost');
assert.equal(one.players[0].elites!.reserves, owner.elites!.reserves);
assert.deepEqual(viewGame(one, 'd').battle?.plans, {}, 'A sealed plan remains private');
const resolved = finish(second(one));
const after = resolved.players.find(row => row.id === 'a')!;
assert.equal(after.reserves, owner.reserves - 3);
assert.equal(after.elites!.reserves, owner.elites!.reserves - selected);
assert.equal(after.elites!.tanks, owner.elites!.tanks + selected);
assert.equal(resolved.discard.filter(row => row.id === card).length, 1);
assert.equal(resolved.log.filter(row => row.text.includes('used Reinforcements: three reserve forces')).length, 1);
});
}
void test('legacy omitted selection preserves normal-first reserve costs', () => {
const initial = fixture();
const owner = initial.players[0];
const done = finish(second(applyAction(initial, 'a', plan())));
assert.equal(done.players[0].reserves, owner.reserves - 3);
assert.equal(done.players[0].elites!.reserves, owner.elites!.reserves);
});
void test('invalid or irrelevant elite-cost selections reject without mutating the game', () => {
const game = fixture();
for (const action of [plan(-1), plan(4), plan(1.5), plan(Number.NaN), plan(1, false)]) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, 'a', action));
assert.deepEqual(game, before);
}
const restricted = fixture();
restricted.players[0].elites!.reserves = 2;
restricted.players[0].elites!.tanks = 3;
restricted.players[0].reserves = 3;
restricted.players[0].tanks = 12;
harassCustody(restricted);
for (const invalid of [0,3]) {
const before = structuredClone(restricted);
assert.throws(() => applyAction(restricted, 'a', plan(invalid)));
assert.deepEqual(restricted, before);
}
assert.doesNotThrow(() => applyAction(restricted, 'a', plan(2)));
});
void test('all bot profiles retain a legal battle plan at an eligible typed-cost opportunity', () => {
const game = fixture();
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, 'a');
view.players.find(row => row.id === 'a')!.bot = difficulty;
const actions = botActions(view);
const battle = actions.find(action => action.type === 'battlePlan');
assert.ok(battle, `${difficulty} must have a legal battle plan`);
assert.doesNotThrow(() => applyAction(game, 'a', battle));
}
});