188 lines
6.4 KiB
TypeScript
188 lines
6.4 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame, type GameView } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import {
|
|
nexusTraitorBotActions,
|
|
nexusTraitorDrawAction,
|
|
nexusTraitorReturnAction,
|
|
} from '../game/nexus-traitor-options';
|
|
import {
|
|
nexusTraitorDraw,
|
|
nexusTraitorFixture,
|
|
nexusTraitorInventory,
|
|
} from './fixture-nexus-traitors';
|
|
|
|
void test('all four profiles complete actual Cunning and Face Dancer exchanges with conserved custody', () => {
|
|
for (const ownerFaction of ['harkonnen', 'tleilaxu'] as const) {
|
|
const initial = nexusTraitorFixture({ ownerFaction });
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const v = viewGame(initial, 'p');
|
|
v.players[0].bot = difficulty;
|
|
const before = structuredClone(v);
|
|
const begin = botActions(v);
|
|
assert.equal(begin.length, 1);
|
|
assert.equal(begin[0].type, 'nexusTraitorDraw');
|
|
assert.deepEqual(v, before);
|
|
const drawn = applyAction(initial, 'p', begin[0]);
|
|
const pending = viewGame(drawn, 'p');
|
|
pending.players[0].bot = difficulty;
|
|
const returns = botActions(pending);
|
|
assert.equal(returns.length, 1);
|
|
assert.equal(returns[0].type, 'nexusTraitorReturn');
|
|
const cards = returns[0].cards as string[];
|
|
assert.equal(cards.length, ownerFaction === 'harkonnen' ? 1 : 2);
|
|
assert.equal(new Set(cards).size, cards.length);
|
|
assert.deepEqual(
|
|
returns[0],
|
|
nexusTraitorReturnAction(
|
|
pending,
|
|
pending.nexusTraitors!.pending!.event,
|
|
cards,
|
|
),
|
|
);
|
|
const settled = applyAction(drawn, 'p', returns[0]);
|
|
nexusTraitorInventory(settled);
|
|
assert.equal(viewGame(settled, 'p').nexusTraitors!.pending, null);
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('pending exchange outranks preserved controls and automatic flags while Truth outranks exchange and Nullentropy', () => {
|
|
const g = nexusTraitorDraw(nexusTraitorFixture());
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const v = viewGame(g, 'p');
|
|
v.players[0].bot = difficulty;
|
|
v.automaticContinuationPending = true;
|
|
v.decision = { kind: 'nullentropy', player: 'p' };
|
|
assert.equal(botActions(v)[0]?.type, 'nexusTraitorReturn');
|
|
v.truthtrance = {
|
|
stage: 'priority',
|
|
queue: [],
|
|
passed: [],
|
|
question: null,
|
|
};
|
|
assert.deepEqual(botActions(v), [{ type: 'truthPass' }]);
|
|
v.truthtrance.passed = ['p'];
|
|
assert.deepEqual(botActions(v), []);
|
|
}
|
|
for (const id of ['q', 'r']) {
|
|
const v = viewGame(g, id);
|
|
v.players.find((p) => p.id === id)!.bot = 'Brutal';
|
|
Object.defineProperty(v.nexusTraitors!.pending!, 'choices', {
|
|
get() {
|
|
throw new Error('Private exchange choices read');
|
|
},
|
|
});
|
|
assert.deepEqual(botActions(v), []);
|
|
}
|
|
});
|
|
|
|
/** Ranking inputs here are explicit authorized projections. Actual exchange
|
|
* execution/custody is covered above; these staged views isolate preferences. */
|
|
function rankingFixture(): GameView {
|
|
const g = nexusTraitorFixture({ phase: 6 });
|
|
g.battle = {
|
|
event: 'ranking-battle',
|
|
territory: 'arrakeen',
|
|
attacker: 'p',
|
|
defender: 'q',
|
|
prepared: true,
|
|
plans: {},
|
|
revealed: false,
|
|
traitorCalls: {},
|
|
};
|
|
const v = viewGame(g, 'p');
|
|
v.nexusTraitors = {
|
|
offer: null,
|
|
pending: {
|
|
event: 'ranking-exchange',
|
|
owner: 'p',
|
|
mode: 'cunning',
|
|
count: 1,
|
|
choices: [
|
|
v.players[0].leaders[0].id,
|
|
v.players[1].leaders[0].id,
|
|
v.players[2].leaders[0].id,
|
|
].map((id) => ({ id, revealed: false, drawn: false })),
|
|
},
|
|
};
|
|
return v;
|
|
}
|
|
|
|
void test('ranking reads only authorized choices and public leaders, preserving a revealed opposing traitor', () => {
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const v = rankingFixture();
|
|
v.players[0].bot = difficulty;
|
|
const pending = v.nexusTraitors!.pending!;
|
|
const own = pending.choices[0].id;
|
|
const opposing = pending.choices[1].id;
|
|
const other = pending.choices[2].id;
|
|
for (const player of v.players.slice(1))
|
|
for (const key of ['hand', 'traitors', 'faceDancers', 'spice'])
|
|
Object.defineProperty(player, key, {
|
|
get() {
|
|
throw new Error(`Read opponent ${key}`);
|
|
},
|
|
});
|
|
const b = v.battle!;
|
|
Object.defineProperty(b, 'plans', {
|
|
configurable: true,
|
|
get() {
|
|
throw new Error('Read unrevealed plans');
|
|
},
|
|
});
|
|
assert.deepEqual(botActions(v), [
|
|
{ type: 'nexusTraitorReturn', event: pending.event, cards: [own] },
|
|
]);
|
|
// A public dead leader is expendable when no own identity is offered.
|
|
pending.choices = pending.choices.slice(1);
|
|
v.players[2].leaders[0].dead = true;
|
|
assert.deepEqual(botActions(v), [
|
|
{ type: 'nexusTraitorReturn', event: pending.event, cards: [other] },
|
|
]);
|
|
Object.defineProperty(b, 'plans', {
|
|
value: { q: { leader: other, dial: 0, weapon: null, defense: null } },
|
|
writable: true,
|
|
});
|
|
b.revealed = true;
|
|
assert.deepEqual(botActions(v), [
|
|
{ type: 'nexusTraitorReturn', event: pending.event, cards: [opposing] },
|
|
]);
|
|
}
|
|
});
|
|
|
|
void test('optional bot Cunning waits for an unreplied public traitor call or Mentat; authoritative blocks remain binding', () => {
|
|
const v = rankingFixture();
|
|
v.nexusTraitors = {
|
|
offer: { event: 'offer', mode: 'cunning', draw: 1, blocked: null },
|
|
pending: null,
|
|
};
|
|
const b = v.battle!;
|
|
assert.deepEqual(nexusTraitorBotActions(v), []);
|
|
b.revealed = true;
|
|
b.traitorVoters = ['p', 'q'];
|
|
b.traitorSubmitted = [];
|
|
assert.deepEqual(nexusTraitorBotActions(v), [
|
|
{ type: 'nexusTraitorDraw', event: 'offer', mode: 'cunning' },
|
|
]);
|
|
b.traitorSubmitted = ['p'];
|
|
assert.deepEqual(nexusTraitorBotActions(v), []);
|
|
v.phase = 8;
|
|
assert.equal(nexusTraitorBotActions(v)[0]?.type, 'nexusTraitorDraw');
|
|
v.decision = { kind: 'nullentropy', player: 'p' };
|
|
assert.deepEqual(nexusTraitorBotActions(v), []);
|
|
assert.ok(nexusTraitorDrawAction(v, 'offer', 'cunning'));
|
|
v.decision = null;
|
|
v.nexusTraitors.offer!.blocked = 'Exchange unavailable.';
|
|
assert.deepEqual(nexusTraitorBotActions(v), []);
|
|
const secret = viewGame(
|
|
nexusTraitorFixture({ ownerFaction: 'tleilaxu' }),
|
|
'p',
|
|
);
|
|
assert.equal(nexusTraitorBotActions(secret)[0]?.type, 'nexusTraitorDraw');
|
|
secret.phase = 7;
|
|
assert.deepEqual(nexusTraitorBotActions(secret), []);
|
|
});
|