Files
Dune/tests/sapho-truth-battle-runtime.test.ts
T
Quintine cdc47ed2e1
Verify and publish checkpoint container / container (push) Successful in 11m19s
Order simultaneous Truthtrances during battle
2026-10-29 02:27:09 +11:00

137 lines
7.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { TERRITORIES } from '../game/board';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { nextOccupyRichese, finishOccupyRichese } from './fixture-ecaz-occupy-richese';
import { act, inventory, player, createSaphoTruthOrderFixture,
truthOrderFrame as frame, playSaphoTruthOrder as play, finishTruthOrder as finish,
finishOriginalTruthQuestions as answer, passFreshDiscard } from './fixture-sapho-truth-order';
const clean = (game: Game) => !game.response && !game.phaseOpening && !game.decision &&
!game.pendingTreacheryDiscard && !game.truthtrance;
function advance(game: Game, until: (game: Game) => boolean): Game {
for (let i = 0; i < 200 && !until(game); i++) game = nextOccupyRichese(game);
assert.ok(until(game), 'The native battle must reach the requested control.');
return game;
}
/** Genuine public Basic setup and paid cache purchase, then two native paid
* shipments and the actual battle choice. The inherited fixture swaps one
* original Truthtrance with another held card so its real Sapho holder can
* ask; the claim supplement swaps the original cached Semuta into Atreides'
* hand. No phase, battle, plan, answer or spice balance is installed. */
function battleSource(mode: 'first' | 'last', semutaClaim = false) {
const f = createSaphoTruthOrderFixture({ mode });
let game = structuredClone(f.before);
if (semutaClaim) {
const claimant = player(game, 'atreides'), cache = game.richeseCache;
assert.ok(cache && claimant.hand.length);
const at = cache.findIndex(card => card.effect === 'semutaDrug');
assert.ok(at >= 0);
const [semuta] = cache.splice(at, 1), [exchange] = claimant.hand.splice(0, 1);
claimant.hand.push(semuta); cache.push(exchange);
}
assert.deepEqual(inventory(game), f.originalCards);
const site = TERRITORIES.find(t => t.id === 'the_great_flat')!;
const sector = site.sectors.find(value => value !== game.storm);
assert.notEqual(sector, undefined);
while (game.phase === 5) {
if (!clean(game)) { game = nextOccupyRichese(game); continue; }
const actor = game.active!;
if (actor === 'atreides' || actor === 'emperor') {
game = act(game, actor, { type: 'ship', territory: site.id, sector, amount: 1 });
game = advance(game, clean);
}
game = act(game, actor, { type: 'endMovement' });
}
game = advance(game, state => state.phase === 6 && clean(state) && !!state.active && !state.battle);
const choice = viewGame(game, game.active!).battleChoices.find(row => row.territory === site.id);
assert.ok(choice);
game = act(game, choice.chooser, { type: 'chooseBattle', territory: site.id,
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
game = advance(game, state => !!state.battle && clean(state) && !state.battle.preparation &&
(!state.battle.preLeader || state.battle.preLeader.closed));
const battle = structuredClone(game.battle);
for (const actor of [f.owner, f.other]) {
const truth = player(game, actor).hand.find(card => card.effect === 'truthtrance');
assert.ok(truth);
game = act(game, actor, { type: 'card', card: truth.id });
}
while (game.truthtrance?.stage === 'priority') {
const seat = game.players.find(row => !game.truthtrance!.passed.includes(row.id));
assert.ok(seat);
game = act(game, seat.id, { type: 'truthPass' });
}
const current = frame(game);
assert.deepEqual([...current.holders].sort(), [f.owner, f.other].sort());
assert.equal(mode === 'first' ? game.truthtrance!.queue.at(-1)!.player : game.truthtrance!.queue[0].player, f.owner);
assert.deepEqual(game.battle, battle);
return { ...f, game, battle, semutaClaim };
}
for (const mode of ['first', 'last'] as const) for (const claim of [false, true])
void test(`Basic battle Truthtrance Sapho ${mode} survives Semuta ${claim ? 'claim' : 'pass'} and the native battle`, () => {
const f = battleSource(mode, claim);
const before = structuredClone(f.game.battle), order = [...f.game.order];
const option = viewGame(f.game, f.owner).saphoTruthOrderChoices.find(row => row.card === f.card && row.mode === mode);
assert.ok(option);
let game = play(f.game, f.owner, f.card, mode);
assert.ok(game.pendingTreacheryDiscard?.reaction);
if (claim) {
const event = game.pendingTreacheryDiscard.batch.event;
assert.equal(viewGame(game, 'atreides').semutaReaction?.canCommit, true);
game = act(game, 'atreides', { type: 'semutaCommit', event });
assert.ok(player(game, 'atreides').hand.some(card => card.id === f.card));
} else game = passFreshDiscard(game);
assert.equal(game.pendingTreacheryDiscard, null);
assert.deepEqual(game.battle, before, 'No battle plans, preparations or original event are replayed or changed.');
assert.deepEqual(game.order, order);
game = finish(game);
assert.equal(mode === 'first' ? game.truthtrance!.queue[0].player : game.truthtrance!.queue.at(-1)!.player, f.owner);
game = answer(game);
assert.equal(game.truthtrance, null);
assert.deepEqual(game.battle, before);
const battle = game.battle!;
for (const actor of [battle.attacker, battle.defender]) {
const leader = player(game, actor).leaders.find(row => !row.dead && !row.usedAt);
assert.ok(leader);
game = act(game, actor, { type: 'battlePlan', dial: 0, support: 0, leader: leader.id });
}
assert.ok(game.battle?.revealed);
game = finishOccupyRichese(game);
assert.equal(game.battle, null);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('All minimal bot profiles can use the Basic battle Truth-order window legally', () => {
const f = battleSource('first');
for (const difficulty of DIFFICULTIES) for (const actor of [f.owner, f.other]) {
const view = viewGame(f.game, actor);
view.players.find(row => row.id === actor)!.bot = difficulty;
const action = botActions(view)[0];
assert.ok(action && (action.type === 'truthOrderPass' || action.type === 'saphoTruthOrder'));
const game = act(f.game, actor, action);
assert.deepEqual(game.battle, f.battle);
}
});
void test('Battle Sapho continuation rejects a changed battle and keeps committed cards reserved', () => {
const f = battleSource('first');
// Controlled reservation probe: put the actual Sapho into the owner's
// sealed weapon slot to exercise the shared reservation guard directly.
const reserved = structuredClone(f.game);
reserved.battle!.plans[f.owner] = { leader: null, dial: 0, support: 0,
weapon: f.card, defense: null };
assert.deepEqual(viewGame(reserved, f.owner).saphoTruthOrderChoices, []);
const pending = play(f.game, f.owner, f.card, 'first');
assert.ok(pending.pendingTreacheryDiscard?.reaction);
const changed = structuredClone(pending);
changed.battle!.event = 'different-battle';
const snapshot = structuredClone(changed);
assert.throws(() => passFreshDiscard(changed));
assert.deepEqual(changed, snapshot);
});