154 lines
4.8 KiB
TypeScript
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);
|
|
});
|