Files
Dune/tests/nexus-advisor-storm-runtime.test.ts
Quintine 178251ad35 Restore native Ecaz simulation continuation and current rule fixtures
Match original Ecaz sample snapshots to their native Occupy and Advanced assassination state. Preserve older absent-power samples and resume the original scenario instead of rejecting fresh saved output. Update stale fixture reactions and module expectations while retaining meaningful Banker guidance and positive legal commitments. Correct the unsupported rules-completion claim.

Validation: typecheck/lint, 173 affected tests across 16 files, bounded independent source review, and one direct Ecaz continuation completed through 178 accepted actions. Runtime fingerprint 95d009d3957650b08bb24826b70aa5f00a74cdd9992b6a6c222f0665d3e3759c; documentation-only proof update a91ab248109bee74a74373fb544027a2e7cc8f178e26d2c9dad475a9618eb64d. Full rules, comprehensive assurance and deployed acceptance remain open. No saved-room or provider changes.
2026-10-18 10:08:55 +11:00

178 lines
10 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { fighterCount } from '../game/advisors';
import { gameDistance, splitLocation, territory } from '../game/board';
import { quoteBattleBoard } from '../game/board-resolution-quote';
import { quoteAdvisorBattleOffer } from '../game/movement-phase-quote';
import { nexusAdvisorAction } from '../game/nexus-advisor-options';
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
import {
createNexusAdvisorStormFixture, nexusAdvisorStormPlayer,
type NexusAdvisorStormFixture,
} from './fixture-nexus-advisor-storm';
function declaration(f: NexusAdvisorStormFixture, game = f.game): Action {
const view = viewGame(game, f.owner);
const offer = view.nexusAdvisors!.offer!;
const action = nexusAdvisorAction(view, offer.event, [f.territory]);
assert.ok(action, 'The real storm-held advisor territory is a legal whole-territory Cunning selection');
return action;
}
function resources(game: Game) {
return game.players.map(player => ({ id: player.id, spice: player.spice,
reserves: player.reserves, tanks: player.tanks, forces: player.forces,
elites: player.elites, shipped: player.shipped, moved: player.moved,
revived: player.revived, ally: player.ally }));
}
function settle(game: Game): Game {
for (let count = 0; count < 80; count++) {
// Native Guild Betrayal now initializes at fresh setup; the paid polar
// sink shipment opens its acknowledgement window before it may commit.
const betrayal = game.pendingGuildBetrayal;
if (betrayal) {
const seat = betrayal.required.find(id => !betrayal.passed.includes(id))!;
game = applyAction(game, seat, { type: 'guildBetrayalPass', event: betrayal.invoice.event });
continue;
}
if (!game.response && !game.decision && !game.phaseOpening) return game;
const step = nextStrongholdFactionsNativeStep(game);
assert.ok(step, 'Only original responses and decisions may settle the declaration');
game = applyAction(game, step.actor, step.action);
}
throw Error('Original storm advisor response did not settle');
}
function rejectsImmutably(game: Game, actor: string, action: Action): void {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
function assertStormGeometry(f: NexusAdvisorStormFixture, game: Game): void {
const from = `${f.territory}:${f.separated ? 9 : 10}`;
const to = `${f.territory}:${f.separated ? 5 : 10}`;
assert.equal(gameDistance(game, from, to), 0);
assert.ok(gameDistance(game, from, to, key => splitLocation(key).sector === game.storm) > 0);
assert.equal(quoteBattleBoard(game).battles.some(battle => battle.territory === f.territory &&
[battle.attacker, battle.defender].includes(f.owner)), false);
}
function ordinaryStormMove(f: NexusAdvisorStormFixture): Action {
return { type: 'move', forces: { [`${f.territory}:${f.separated ? 9 : 10}`]: 1 },
eliteForces: { [`${f.territory}:${f.separated ? 9 : 10}`]: 0 },
territory: f.separated ? f.territory : 'polar_sink', sector: f.separated ? 5 : 0 };
}
for (const separated of [false, true]) {
const label = separated ? 'real rock groups separated by the storm' : 'real shielded Arrakeen groups under the storm';
void test(`Cunning changes only stance for ${label}, without granting movement or battle`, () => {
const f = createNexusAdvisorStormFixture({ separated });
const original = structuredClone(f.game);
assert.equal(original.advanced, true);
assert.deepEqual(nexusAdvisorStormPlayer(f.afterStorm, f.owner).forces,
nexusAdvisorStormPlayer(f.beforeStorm, f.owner).forces);
assert.deepEqual(nexusAdvisorStormPlayer(f.afterStorm, f.opponent).forces,
nexusAdvisorStormPlayer(f.beforeStorm, f.opponent).forces);
assert.equal(territory(f.territory).type, separated ? 'rock' : 'stronghold');
assert.deepEqual(nexusAdvisorStormPlayer(original, f.owner).advisors![f.territory], { lockedTurn: 1 });
assert.equal(nexusAdvisorStormPlayer(original, f.owner).forces[`${f.territory}:${separated ? 9 : 10}`], separated ? 1 : 2);
assert.equal(fighterCount(nexusAdvisorStormPlayer(original, f.opponent), f.territory), 1);
assert.equal(quoteAdvisorBattleOffer(original).territories.includes(f.territory), false);
assertStormGeometry(f, original);
rejectsImmutably(original, f.owner, ordinaryStormMove(f));
rejectsImmutably(original, f.owner, { ...ordinaryStormMove(f), territory: f.territory,
sector: separated ? 5 : 10, fighters: true });
rejectsImmutably(original, f.owner, { type: 'ship', territory: f.territory,
sector: original.storm, amount: 1, elite: 0, allyPayment: 0 });
const action = declaration(f);
rejectsImmutably(original, f.opponent, action);
rejectsImmutably(original, f.owner, { ...action, event: `${String(action.event)}-stale` });
let game = applyAction(original, f.owner, action);
assert.equal(game.response?.kind, 'nexusAdvisorFlip');
const receipt = structuredClone(game.nexusAdvisorHistory!.at(-1)!.receipt);
assert.equal(receipt.owner, f.owner);
assert.equal(receipt.turn, original.turn);
assert.deepEqual(receipt.selections, [{ territory: f.territory, count: 2,
locations: separated
? [{ location: `${f.territory}:5`, count: 1 }, { location: `${f.territory}:9`, count: 1 }]
: [{ location: `${f.territory}:10`, count: 2 }],
noField: null, stance: { lockedTurn: 1 } }]);
assert.deepEqual(resources(game), resources(original));
assert.deepEqual(nexusAdvisorStormPlayer(game, f.owner).advisors,
nexusAdvisorStormPlayer(original, f.owner).advisors);
assert.equal(game.nexusCards!.cards!.hands[f.owner], null);
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'beneGesserit').length, 1);
rejectsImmutably(game, f.owner, action);
game = settle(game);
assert.equal(game.nexusAdvisorHistory!.at(-1)!.stage, 'completed');
assert.equal(game.nexusAdvisorHistory!.length, 1);
assert.deepEqual(game.nexusAdvisorHistory!.at(-1)!.receipt, receipt);
const expected = structuredClone(nexusAdvisorStormPlayer(original, f.owner).advisors!);
delete expected[f.territory];
assert.deepEqual(nexusAdvisorStormPlayer(game, f.owner).advisors, expected);
assert.deepEqual(resources(game), resources(original));
assert.deepEqual(game.players.map(player => player.hand), original.players.map(player => player.hand));
assert.equal(fighterCount(nexusAdvisorStormPlayer(game, f.owner), f.territory), 2);
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'beneGesserit').length, 1);
assert.equal(game.turn, original.turn);
assert.equal(game.phase, 5);
assert.equal(game.active, f.owner);
assert.equal(game.response, null);
assertStormGeometry(f, game);
rejectsImmutably(game, f.owner, ordinaryStormMove(f));
rejectsImmutably(game, f.owner, action);
const sinkBefore = nexusAdvisorStormPlayer(game, f.owner).forces['polar_sink:0'] ?? 0;
game = applyAction(game, f.owner, { type: 'ship', territory: 'polar_sink', sector: 0,
amount: 1, elite: 0, allyPayment: 0 });
game = settle(game);
assert.equal(nexusAdvisorStormPlayer(game, f.owner).shipped, true);
assert.equal(nexusAdvisorStormPlayer(game, f.owner).moved, 0);
assert.equal(nexusAdvisorStormPlayer(game, f.owner).forces['polar_sink:0'], sinkBefore + 1);
game = applyAction(game, f.owner, { type: 'endMovement' });
assert.notEqual(game.active, f.owner);
});
void test(`the real auction Karama cancels the original batch for ${label}`, () => {
const f = createNexusAdvisorStormFixture({ separated });
const original = structuredClone(f.game), action = declaration(f);
let game = applyAction(original, f.owner, action);
game = applyAction(game, f.canceler, { type: 'card', card: f.cancelerCard, mode: 'cancel' });
game = settle(game);
assert.equal(game.nexusAdvisorHistory!.at(-1)!.stage, 'canceled');
assert.equal(game.nexusAdvisorHistory!.length, 1);
assert.deepEqual(resources(game), resources(original));
assert.deepEqual(game.players.map(player => player.advisors), original.players.map(player => player.advisors));
assert.equal(game.discard.filter(card => card.id === f.cancelerCard).length, 1);
assert.equal(nexusAdvisorStormPlayer(game, f.canceler).hand.some(card => card.id === f.cancelerCard), false);
assert.equal(game.nexusCards!.cards!.hands[f.owner], null);
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'beneGesserit').length, 1);
assert.equal(game.active, f.owner);
assert.equal(game.phase, 5);
assert.equal(fighterCount(nexusAdvisorStormPlayer(game, f.owner), f.territory), 0);
assert.equal(quoteAdvisorBattleOffer(game).territories.includes(f.territory), false);
assertStormGeometry(f, game);
rejectsImmutably(game, f.owner, ordinaryStormMove(f));
rejectsImmutably(game, f.owner, action);
game = applyAction(game, f.owner, { type: 'endMovement' });
assert.notEqual(game.active, f.owner);
});
void test(`all four legal bot policies can use Cunning for ${label}`, () => {
const f = createNexusAdvisorStormFixture({ separated });
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
const view = viewGame(f.game, f.owner);
view.players.find(player => player.id === f.owner)!.bot = difficulty;
const actions = botActions(view);
const action = actions.find(candidate => candidate.type === 'nexusAdvisors');
assert.ok(action, `${difficulty} selects the genuine two-advisor versus one-fighter offer`);
let game = applyAction(f.game, f.owner, action);
assert.equal(game.response?.kind, 'nexusAdvisorFlip');
game = settle(game);
assert.equal(fighterCount(nexusAdvisorStormPlayer(game, f.owner), f.territory), 2);
assert.deepEqual(resources(game), resources(f.game));
assertStormGeometry(f, game);
}
});
}