115 lines
3.5 KiB
TypeScript
115 lines
3.5 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction,
|
|
normalizeAutomaticGame,
|
|
viewGame,
|
|
type Game,
|
|
} from '../game/engine';
|
|
import { baseDeck } from '../game/cards';
|
|
import { richeseCards } from '../game/richese-cards';
|
|
import {
|
|
nexusInspectionFixture,
|
|
inspectionNative,
|
|
inspectionInventory,
|
|
} from './fixture-nexus-inspection';
|
|
import { nexusAllow, nexusPlayer, nexusReload } from './fixture-nexus-cards';
|
|
|
|
const residual = 'richese-residual-poison';
|
|
function inventory(g: Game): string[] {
|
|
return [
|
|
...g.deck,
|
|
...g.discard,
|
|
...(g.richeseCache ?? []),
|
|
...g.players.flatMap((p) => p.hand),
|
|
]
|
|
.map((c) => c.id)
|
|
.sort();
|
|
}
|
|
function answeredCunning(): Game {
|
|
let g = inspectionNative(
|
|
nexusInspectionFixture(false, 'a', { richese: true }),
|
|
'dial',
|
|
0,
|
|
);
|
|
// Conserved acquisition seam: move the canonical Residual from the cache
|
|
// initialized by the genuine Richese setup. No purchase is claimed here.
|
|
assert.ok(g.richeseCache);
|
|
const index = g.richeseCache.findIndex((c) => c.id === residual);
|
|
nexusPlayer(g, 'a').hand.push(g.richeseCache.splice(index, 1)[0]);
|
|
assert.equal(viewGame(g, 'a').residualPoison!.blocked, null);
|
|
g = nexusAllow(
|
|
applyAction(g, 'a', {
|
|
type: 'nexusAtreides',
|
|
event: g.battle!.event,
|
|
mode: 'cunning',
|
|
field: 'weapon',
|
|
}),
|
|
);
|
|
g = applyAction(g, 'h', {
|
|
type: 'nexusPrescienceAnswer',
|
|
event: g.battle!.event,
|
|
value: null,
|
|
});
|
|
assert.equal(g.battle!.nexusInspection!.stage, 'answered');
|
|
assert.equal(g.battle!.nexusInspection!.mode, 'cunning');
|
|
inspectionInventory(g);
|
|
return g;
|
|
}
|
|
|
|
void test('answered Cunning publicly blocks Residual independently of hidden funding or Ghola, before RNG or mutation', (t) => {
|
|
const original = answeredCunning();
|
|
assert.deepEqual(
|
|
inventory(original),
|
|
[...baseDeck(), ...richeseCards()].map((c) => c.id).sort(),
|
|
);
|
|
const reasons: string[] = [];
|
|
for (const hasGhola of [false, true]) {
|
|
const g = nexusReload(original),
|
|
target = nexusPlayer(g, 'h');
|
|
const index = g.deck.findIndex((c) =>
|
|
hasGhola ? c.effect === 'ghola' : c.kind === 'projectile',
|
|
);
|
|
assert.ok(index >= 0);
|
|
target.hand.push(g.deck.splice(index, 1)[0]);
|
|
target.spice = hasGhola ? 0 : 20;
|
|
const before = nexusReload(g),
|
|
beforeCards = inventory(g);
|
|
const reason = viewGame(g, 'a').residualPoison!.blocked;
|
|
assert.ok(reason);
|
|
assert.match(reason, /two inspected elements.*ruling/i);
|
|
reasons.push(reason);
|
|
for (const p of g.players) assert.doesNotThrow(() => viewGame(g, p.id));
|
|
assert.deepEqual(normalizeAutomaticGame(nexusReload(g)), g);
|
|
let randomCalls = 0;
|
|
const random = t.mock.method(crypto, 'getRandomValues', () => {
|
|
randomCalls++;
|
|
throw Error('Rejected action must not draw a victim');
|
|
});
|
|
try {
|
|
assert.throws(
|
|
() =>
|
|
applyAction(g, 'a', {
|
|
type: 'card',
|
|
card: residual,
|
|
target: 'h',
|
|
event: g.battle!.event,
|
|
}),
|
|
/two inspected elements.*ruling/i,
|
|
);
|
|
} finally {
|
|
random.mock.restore();
|
|
}
|
|
assert.equal(randomCalls, 0);
|
|
assert.deepEqual(g, before);
|
|
assert.deepEqual(inventory(g), beforeCards);
|
|
assert.equal(
|
|
nexusPlayer(g, 'a').hand.filter((c) => c.id === residual).length,
|
|
1,
|
|
);
|
|
assert.equal(g.battle!.prescience!.value, 0);
|
|
assert.deepEqual(g.battle!.nexusInspection!.answers, [null]);
|
|
}
|
|
assert.equal(reasons[0], reasons[1]);
|
|
});
|