Implement Atreides Nexus inspections with durable battle commitments
This commit is contained in:
1 parent
d2a1d63517
commit
d820c8a414
25 files changed
+3171
-182
No files matched your search
@@ -0,0 +1,153 @@
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user