Files
Dune/tests/moritani-entry-overlays.test.ts
T
2026-09-08 07:37:52 +10:00

150 lines
5.0 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, createGame, newPlayer, type Game } from '../game/engine';
import { baseDeck } from '../game/cards';
import { createTerrorState, placeTerror } from '../game/moritani-terror';
function hold(g: Game, id: string, name: string) {
const index = g.deck.findIndex((c) => c.name === name);
assert.ok(index >= 0);
const card = g.deck.splice(index, 1)[0];
g.players.find((p) => p.id === id)!.hand.push(card);
return card.id;
}
function entered(remainingRide: boolean) {
const g = createGame('OVERLAY1', newPlayer('m', 'Moritani', 'moritani'));
g.players.push(
newPlayer('f', 'Fremen', 'fremen'),
newPlayer('a', 'Atreides', 'atreides'),
);
Object.assign(g, {
status: 'playing',
advanced: true,
phase: 1,
turn: 2,
storm: 18,
order: ['f', 'm', 'a'],
nexus: true,
active: null,
});
g.deck = baseDeck();
const fremen = g.players[1];
fremen.forces = {
'imperial_basin:10': 3,
...(remainingRide ? { 'hagga_basin:11': 1 } : {}),
};
fremen.reserves = remainingRide ? 16 : 17;
hold(g, 'f', 'Karama');
g.moritaniTerror = createTerrorState(() => 0);
g.moritaniTerror = placeTerror(
g.moritaniTerror,
g.moritaniTerror.tokens.find((t) => t.kind === 'robbery')!.id,
'arrakeen',
1,
);
g.decision = { kind: 'wormRide', player: 'f', territory: 'imperial_basin' };
g.wormRides = remainingRide ? ['hagga_basin'] : [];
return applyAction(g, 'f', {
type: 'decision',
accept: true,
territory: 'arrakeen',
sector: 10,
forces: { 'imperial_basin:10': 2 },
});
}
function summon(g: Game) {
const card = g.players[1].hand.find((c) => c.effect === 'karama')!.id;
return applyAction(g, 'f', {
type: 'card',
mode: 'special',
card,
territory: 'broken_land',
});
}
function passNexus(state: Game) {
let g = state;
for (const p of state.players) g = applyAction(g, p.id, { type: 'ready' });
return g;
}
const reload = (g: Game): Game => JSON.parse(JSON.stringify(g));
for (const remaining of [true, false]) {
void test(`summoned worm during Terror opens its Nexus before ${remaining ? 'the remaining ride' : 'advancing the phase'}`, () => {
let g = entered(remaining);
const custody = structuredClone(g.players[1].forces);
const pending = structuredClone(g.pendingTerrorEntry);
g = summon(g);
assert.equal(g.summonedWorm, null);
assert.deepEqual(g.pendingTerrorEntry, pending);
assert.equal(g.summonedNexusBeforeRides, true);
g = applyAction(reload(g), 'm', { type: 'decision', decline: true });
assert.equal(g.phase, 1);
assert.equal(g.nexus, true);
assert.equal(g.decision, null);
assert.equal(g.pendingTerrorEntry, null);
assert.equal(g.summonedNexusBeforeRides, false);
assert.deepEqual(g.ready, []);
assert.deepEqual(g.players[1].forces, custody);
assert.ok(g.players[1].specialKaramaUsed);
assert.equal(g.discard.filter((c) => c.effect === 'karama').length, 1);
// A negotiation action must now be available, rather than blocked by a pending ride.
g = applyAction(g, 'f', { type: 'alliance', target: 'a' });
g = passNexus(g);
if (remaining) {
assert.deepEqual(g.decision, {
kind: 'wormRide',
player: 'f',
territory: 'hagga_basin',
});
assert.deepEqual(g.wormRides, []);
} else {
assert.equal(g.phase, 2);
assert.equal(g.nexus, false);
}
assert.deepEqual(g.players[1].forces, custody);
});
}
void test('Truthtrance consuming Robbery overflow finishes Terror but still opens the deferred summoned Nexus', () => {
let g = entered(true);
const truth = hold(g, 'm', 'Truthtrance');
hold(g, 'a', 'Shield');
g.players[0].hand.push(...g.deck.splice(0, 3));
g = summon(g);
g = applyAction(g, 'm', { type: 'decision', reveal: true });
g = applyAction(g, 'm', { type: 'decision', choice: 'card' });
assert.equal(g.pendingTerrorEntry?.stage, 'discard');
assert.equal(g.players[0].hand.length, 5);
const cardsAfterDraw = g.deck.length;
g = applyAction(g, 'm', { type: 'card', card: truth });
for (const id of ['f', 'a']) g = applyAction(g, id, { type: 'truthPass' });
g = applyAction(g, 'm', {
type: 'truthAsk',
question: {
kind: 'fact',
target: 'a',
fact: { kind: 'hand', name: 'Shield' },
},
});
g = applyAction(reload(g), 'a', { type: 'truthAnswer', answer: 'yes' });
assert.equal(g.truthtrance, null);
assert.equal(g.players[0].hand.length, 4);
assert.equal(g.pendingTerrorEntry, null);
assert.equal(g.decision, null);
assert.equal(g.nexus, true);
assert.equal(g.phase, 1);
assert.deepEqual(g.wormRides, ['hagga_basin']);
assert.equal(g.deck.length, cardsAfterDraw);
assert.equal(g.discard.filter((c) => c.id === truth).length, 1);
assert.equal(
g.moritaniTerror!.tokens.filter((t) => t.status === 'removed').length,
1,
);
g = passNexus(g);
assert.deepEqual(g.decision, {
kind: 'wormRide',
player: 'f',
territory: 'hagga_basin',
});
});