163 lines
5.0 KiB
TypeScript
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');
|
|
});
|