Files
Dune/tests/duke-truthtrance.test.ts
2026-09-08 07:37:52 +10:00

163 lines
5.0 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction,
createGame,
newPlayer,
viewGame,
type Action,
type Game,
} from '../game/engine';
import { baseDeck } from '../game/cards';
import {
acquireDuke,
createDukeVidal,
DUKE_VIDAL_ID,
} from '../game/duke-vidal';
function fixture() {
const g = createGame('DUKETRUTH', newPlayer('m', 'Moritani', 'moritani'));
g.players.push(newPlayer('e', 'Emperor', 'emperor'));
g.status = 'playing';
g.phase = 6;
g.turn = 2;
g.active = 'm';
g.order = ['e', 'm'];
g.storm = 18;
g.deck = baseDeck();
for (const p of g.players) {
p.forces = { 'arrakeen:10': 2 };
p.reserves = 18;
p.spice = 20;
p.hand = [];
p.traitors = [];
}
const index = g.deck.findIndex((card) => card.effect === 'truthtrance');
g.players[1].hand.push(g.deck.splice(index, 1)[0]);
g.dukeVidal = acquireDuke(createDukeVidal(), 'm', 2, 'moritani');
g.battle = {
territory: 'arrakeen',
attacker: 'm',
defender: 'e',
plans: {},
revealed: false,
traitorCalls: {},
prepared: true,
};
return g;
}
function priority(state = fixture()) {
let g = applyAction(state, 'e', {
type: 'card',
card: state.players[1].hand[0].id,
});
while (g.truthtrance?.stage === 'priority') {
const p = g.players.find((p) => !g.truthtrance!.passed.includes(p.id))!;
g = applyAction(g, p.id, { type: 'truthPass' });
}
return g;
}
const question: Action = {
type: 'truthAsk',
question: {
kind: 'battlePlan',
target: 'm',
claim: { kind: 'leader', leader: DUKE_VIDAL_ID },
},
};
function reject(state: Game, id: string, action: Action) {
const before = structuredClone(state);
assert.throws(() => applyAction(state, id, action));
assert.deepEqual(state, before);
}
void test('Truthtrance asks and logs a Duke battle-plan claim, then binds the answer to the actual legal shared-disc plan', () => {
const initial = fixture();
const native = initial.players.map((p) => structuredClone(p.leaders));
const asked = applyAction(priority(initial), 'e', question);
assert.equal(asked.truthtrance?.stage, 'answer');
assert.ok(asked.log.at(-1)!.text.includes('Duke Prad Vidal'));
assert.equal(asked.log.at(-1)!.text.includes(DUKE_VIDAL_ID), false);
assert.ok(viewGame(asked, 'm').truthBattleAnswers?.includes('yes'));
assert.equal(viewGame(asked, 'e').truthBattleAnswers, null);
const answered = applyAction(JSON.parse(JSON.stringify(asked)), 'm', {
type: 'truthAnswer',
answer: 'yes',
});
assert.equal(answered.truthtrance, null);
assert.equal(answered.truthHistory!.at(-1)!.answer, 'yes');
reject(answered, 'm', {
type: 'battlePlan',
dial: 0,
leader: answered.players[0].leaders[0].id,
});
const planned = applyAction(answered, 'm', {
type: 'battlePlan',
dial: 0,
leader: DUKE_VIDAL_ID,
});
assert.equal(planned.battle!.plans.m.leader, DUKE_VIDAL_ID);
assert.equal(planned.dukeVidal!.controller, 'm');
assert.deepEqual(
planned.players.map((p) => p.leaders),
native,
);
assert.ok(planned.players.every((p) => !p.traitors.includes(DUKE_VIDAL_ID)));
});
void test('an unavailable Duke remains a valid battle identity question but cannot be promised as a legal plan', () => {
const initial = fixture();
initial.dukeVidal!.controller = null;
initial.dukeVidal!.source = null;
initial.dukeVidal!.acquiredTurn = null;
const asked = applyAction(priority(initial), 'e', question);
assert.ok(asked.log.at(-1)!.text.includes('Duke Prad Vidal'));
assert.equal(viewGame(asked, 'm').truthBattleAnswers?.includes('yes'), false);
assert.ok(viewGame(asked, 'm').truthBattleAnswers?.includes('no'));
reject(asked, 'm', { type: 'truthAnswer', answer: 'yes' });
const answered = applyAction(asked, 'm', {
type: 'truthAnswer',
answer: 'no',
});
assert.equal(answered.truthtrance, null);
});
void test('adding Duke to battle claims does not permit a nonexistent Duke traitor fact or absent shared disc', () => {
const g = priority();
reject(g, 'e', {
type: 'truthAsk',
question: {
kind: 'fact',
target: 'm',
fact: { kind: 'traitor', leader: DUKE_VIDAL_ID },
},
});
const absent = fixture();
delete absent.dukeVidal;
reject(priority(absent), 'e', question);
});
void test('native leader battle and traitor questions retain their original validation and readable log labels', () => {
const g = priority();
const leader = g.players[0].leaders[0];
const battle = applyAction(g, 'e', {
type: 'truthAsk',
question: {
kind: 'battlePlan',
target: 'm',
claim: { kind: 'leader', leader: leader.id },
},
});
assert.ok(battle.log.at(-1)!.text.includes(leader.name));
const traitor = applyAction(g, 'e', {
type: 'truthAsk',
question: {
kind: 'fact',
target: 'm',
fact: { kind: 'traitor', leader: leader.id },
},
});
assert.ok(traitor.log.at(-1)!.text.includes(leader.name));
assert.equal(viewGame(traitor, 'm').truthAnswer, 'no');
});