Files
Dune/tests/nexus-inspection-integrity.test.ts

154 lines
4.8 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction,
normalizeAutomaticGame,
viewGame,
type Action,
type Game,
} from '../game/engine';
import {
inspectionNative,
nexusInspectionFixture,
} from './fixture-nexus-inspection';
import {
nexusAllow,
nexusInventory,
nexusPlayer,
nexusReload,
} from './fixture-nexus-cards';
function play(g: Game, mode = 'cunning', field = 'weapon'): Game {
return applyAction(g, 'a', {
type: 'nexusAtreides',
event: g.battle!.event,
mode,
field,
});
}
function answerAction(g: Game): Action {
return {
type: 'nexusPrescienceAnswer',
event: g.battle!.event,
value: null,
};
}
function plan(g: Game, id: string, dial = 0): Action {
return {
type: 'battlePlan',
dial,
leader: nexusPlayer(g, id).leaders[0].id,
};
}
function rejectedEverywhere(g: Game, actor: string, action: Action): void {
const before = JSON.stringify(g);
for (const seat of g.players) {
assert.throws(() => viewGame(g, seat.id), /inspection|inspected|Nexus|Prescience|Cunning/i);
assert.equal(JSON.stringify(g), before);
}
assert.throws(
() => normalizeAutomaticGame(g),
/inspection|inspected|Nexus|Prescience|Cunning/i,
);
assert.equal(JSON.stringify(g), before);
assert.throws(
() => applyAction(g, actor, action),
/inspection|inspected|Nexus|Prescience|Cunning/i,
);
assert.equal(JSON.stringify(g), before);
}
function pendingCunning(): Game {
const g = inspectionNative(nexusInspectionFixture());
const index = g.deck.findIndex((card) => card.effect === 'karama');
assert.ok(index >= 0);
nexusPlayer(g, 'f').hand.push(g.deck.splice(index, 1)[0]);
const pending = play(g);
assert.equal(pending.response?.kind, 'nexusPrescience');
assert.equal(pending.battle!.nexusInspection!.stage, 'response');
nexusInventory(pending);
return pending;
}
void test('deleting the actual Cunning cancellation response cannot silently allow its answer', () => {
const pending = pendingCunning();
const corrupted = nexusReload(pending);
corrupted.response = null;
rejectedEverywhere(corrupted, 'h', answerAction(corrupted));
// Explicitly passing the real response remains a valid recoverable path.
let restored = nexusAllow(normalizeAutomaticGame(nexusReload(pending)));
restored = applyAction(restored, 'h', answerAction(restored));
assert.equal(restored.battle!.nexusInspection!.stage, 'answered');
assert.deepEqual(restored.battle!.nexusInspection!.answers, [null]);
assert.equal(restored.battle!.prescience!.value, 0);
assert.equal(
restored.nexusCards!.cards!.discard.filter((card) => card === 'atreides')
.length,
1,
);
nexusInventory(restored);
});
void test('Cunning cancellation response owner and intent remain bound on every seat read and continuation', () => {
const pending = pendingCunning();
for (const mutate of [
(g: Game) => {
g.response!.owner = 'f';
},
(g: Game) => {
g.response!.intent = 'another-battle';
},
]) {
const corrupted = nexusReload(pending);
mutate(corrupted);
rejectedEverywhere(corrupted, 'h', answerAction(corrupted));
}
});
void test('a completed Cunning inspection cannot change its first native commitment by editing the live value', () => {
let g = nexusAllow(play(inspectionNative(nexusInspectionFixture())));
g = applyAction(g, 'h', answerAction(g));
assert.equal(g.battle!.nexusInspection!.first!.value, 0);
assert.equal(g.battle!.prescience!.value, 0);
const corrupted = nexusReload(g);
corrupted.battle!.prescience!.value = 1;
rejectedEverywhere(corrupted, 'h', plan(corrupted, 'h', 1));
const restored = applyAction(
normalizeAutomaticGame(nexusReload(g)),
'h',
plan(g, 'h'),
);
assert.equal(restored.battle!.plans.h.dial, 0);
assert.equal(viewGame(restored, 'h').battle!.ownCommitments.length, 2);
nexusInventory(restored);
});
void test('a sealed target plan cannot diverge from the real Secret Ally answer before the opponent seals', () => {
let g = nexusInspectionFixture(true);
g = applyAction(g, 'h', plan(g, 'h'));
g = play(g, 'secretAlly', 'dial');
assert.deepEqual(g.battle!.nexusInspection!.answers, [0]);
assert.equal(g.battle!.revealed, false);
assert.deepEqual(viewGame(g, 'a').battle!.plans, {});
assert.deepEqual(viewGame(g, 'f').battle!.nexusInsights, []);
const corrupted = nexusReload(g);
corrupted.battle!.plans.h.dial = 1;
rejectedEverywhere(corrupted, 'a', plan(corrupted, 'a'));
const restored = applyAction(
normalizeAutomaticGame(nexusReload(g)),
'a',
plan(g, 'a'),
);
assert.equal(restored.battle!.revealed, true);
assert.equal(restored.battle!.plans.h.dial, 0);
for (const id of ['a', 'h'])
assert.equal(viewGame(restored, id).battle!.nexusInsights[0].value, 0);
nexusInventory(restored);
});