168 lines
6.9 KiB
TypeScript
168 lines
6.9 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createGame,
|
|
initializeFactionExpansionsGameForAudit,
|
|
initializeLeaderSkillsGameForAudit,
|
|
joinGame,
|
|
newPlayer,
|
|
normalizeAutomaticGame,
|
|
viewGame,
|
|
type Game,
|
|
} from '../game/engine';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { runBots } from '../game/bots';
|
|
import { traitorDeck } from '../game/traitors';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
|
|
function readyTable(factions: FactionId[], advanced = true, expansions = ['ecaz']) {
|
|
const g = createGame(
|
|
`ECAZLOYALTY${factions.length}${advanced ? 'A' : 'B'}`,
|
|
newPlayer('p0', 'Player 0', factions[0]),
|
|
advanced,
|
|
expansions,
|
|
);
|
|
for (let i = 1; i < factions.length; i++)
|
|
joinGame(g, newPlayer(`p${i}`, `Player ${i}`, factions[i]));
|
|
g.players.forEach((p) => {
|
|
p.ready = true;
|
|
p.bot = 'Easy';
|
|
});
|
|
return g;
|
|
}
|
|
|
|
function setupToTraitors(g: Game) {
|
|
let state = initializeFactionExpansionsGameForAudit(g);
|
|
for (let i = 0; i < 12 && state.setupStage !== 'traitors'; i++) {
|
|
state = runBots(state, 1);
|
|
if (state.status === 'playing') break;
|
|
}
|
|
assert.ok(state.setupStage === 'traitors' || state.status === 'playing');
|
|
return state;
|
|
}
|
|
|
|
function completeSetup(g: Game) {
|
|
let state = g;
|
|
for (let i = 0; i < 240 && state.status === 'setup'; i++)
|
|
state = runBots(state, 1);
|
|
assert.equal(state.status, 'playing', `setup did not complete at ${state.setupStage}`);
|
|
return state;
|
|
}
|
|
|
|
function traitorCustody(g: Game) {
|
|
return [
|
|
...g.players.flatMap((p) => p.traitorChoices),
|
|
...g.players.flatMap((p) => p.traitors),
|
|
...g.players.flatMap((p) => (p.faceDancers ?? []).map((card) => card.leader)),
|
|
...(g.traitorReserve ?? []),
|
|
];
|
|
}
|
|
|
|
void test('Advanced Ecaz creates a private pending marker and selects loyalty after BG prediction', () => {
|
|
let g = readyTable(['beneGesserit', 'ecaz', 'atreides']);
|
|
g = initializeFactionExpansionsGameForAudit(g);
|
|
assert.deepEqual(g.ecazLoyalty, { player: 'p1', card: null });
|
|
assert.equal(viewGame(g, 'p0').ecazLoyalty, null);
|
|
assert.equal(g.setupStage, 'prediction');
|
|
|
|
for (let i = 0; i < 24 && g.setupStage !== 'traitors' && g.status === 'setup'; i++)
|
|
g = runBots(g, 1);
|
|
assert.equal(g.ecazLoyalty!.player, 'p1');
|
|
assert.ok(g.ecazLoyalty!.card);
|
|
assert.deepEqual(viewGame(g, 'p0').ecazLoyalty, g.ecazLoyalty);
|
|
assert.ok(g.log.some((entry) => entry.text.includes('before initial dealing')));
|
|
});
|
|
|
|
void test('Basic and Advanced games without Ecaz have no loyalty marker', () => {
|
|
let basic = readyTable(['ecaz', 'atreides'], false, ['ecaz']);
|
|
basic = initializeFactionExpansionsGameForAudit(basic);
|
|
assert.equal(basic.ecazLoyalty, undefined);
|
|
assert.equal(viewGame(basic, 'p0').ecazLoyalty, null);
|
|
|
|
let advanced = readyTable(['atreides', 'emperor'], true, ['ix']);
|
|
advanced = initializeFactionExpansionsGameForAudit(advanced);
|
|
assert.equal(advanced.ecazLoyalty, undefined);
|
|
assert.equal(viewGame(advanced, 'p0').ecazLoyalty, null);
|
|
});
|
|
|
|
void test('Every legal two-to-six player Ecaz setup removes exactly one native leader from traitor custody', () => {
|
|
const profiles: FactionId[][] = [
|
|
['ecaz', 'atreides'],
|
|
['ecaz', 'atreides', 'emperor'],
|
|
['ecaz', 'atreides', 'emperor', 'fremen'],
|
|
['ecaz', 'atreides', 'emperor', 'fremen', 'beneGesserit'],
|
|
['ecaz', 'atreides', 'emperor', 'fremen', 'beneGesserit', 'harkonnen'],
|
|
];
|
|
for (const factions of profiles) {
|
|
const g = setupToTraitors(readyTable(factions));
|
|
const selected = g.ecazLoyalty!.card;
|
|
assert.ok(selected);
|
|
assert.ok(g.players.find((p) => p.faction === 'ecaz')!.leaders.some((l) => l.id === selected));
|
|
const universe = traitorDeck(g.players).filter((id) => id !== selected);
|
|
const custody = traitorCustody(g);
|
|
assert.equal(custody.includes(selected), false);
|
|
assert.equal(custody.length, universe.length);
|
|
assert.deepEqual([...custody].sort(), [...universe].sort());
|
|
}
|
|
});
|
|
|
|
void test('Harkonnen keeps four dealt traitors and Tleilaxu face-dancer rebuilding excludes loyalty', () => {
|
|
const g = completeSetup(setupToTraitors(readyTable(['ecaz', 'harkonnen', 'tleilaxu', 'atreides'], true, ['ecaz', 'ix'])));
|
|
const harkonnen = g.players.find((p) => p.faction === 'harkonnen')!;
|
|
const tleilaxu = g.players.find((p) => p.faction === 'tleilaxu')!;
|
|
assert.equal(harkonnen.traitors.length, 4);
|
|
assert.deepEqual(harkonnen.traitorChoices, []);
|
|
assert.deepEqual(tleilaxu.traitorChoices, []);
|
|
assert.equal(tleilaxu.faceDancers?.length, 3);
|
|
assert.equal(traitorCustody(g).includes(g.ecazLoyalty!.card!), false);
|
|
assert.deepEqual(
|
|
[...traitorCustody(g)].sort(),
|
|
traitorDeck(g.players, true).filter((id) => id !== g.ecazLoyalty!.card).sort(),
|
|
);
|
|
});
|
|
|
|
void test('all four bot profiles can legally perform Ecaz setup', () => {
|
|
for (const difficulty of DIFFICULTIES) {
|
|
let g = readyTable(['beneGesserit', 'ecaz', 'atreides', 'harkonnen']);
|
|
g.players.forEach((p) => (p.bot = difficulty));
|
|
g = initializeFactionExpansionsGameForAudit(g);
|
|
// Stop at the setup boundary; a bulk bot run may already finish a short game.
|
|
for (let i = 0; i < 200 && g.status === 'setup'; i++)
|
|
g = runBots(g, 1);
|
|
assert.equal(g.status, 'playing', `${difficulty} did not complete setup`);
|
|
assert.ok(g.ecazLoyalty?.card);
|
|
}
|
|
});
|
|
|
|
void test('Leader Skills audit setup remains available for the no-Ecaz profile', () => {
|
|
let g = readyTable(['beneGesserit', 'atreides'], true, ['choam']);
|
|
g = initializeLeaderSkillsGameForAudit(g);
|
|
assert.equal(g.ecazLoyalty, undefined);
|
|
assert.equal(g.setupStage, 'prediction');
|
|
g = runBots(g, 1);
|
|
assert.ok(g.setupStage === 'skillTreachery' || g.setupStage === 'leaderSkills' || g.status === 'playing');
|
|
});
|
|
|
|
void test('JSON save and normalize preserve the selected marker without duplicate loyalty logs', () => {
|
|
const started = setupToTraitors(readyTable(['ecaz', 'atreides', 'emperor']));
|
|
const before = started.ecazLoyalty;
|
|
const loyaltyLogs = started.log.filter((entry) => entry.text.includes('Ecaz Loyalty')).length;
|
|
const restored = normalizeAutomaticGame(JSON.parse(JSON.stringify(started)) as Game);
|
|
assert.deepEqual(restored.ecazLoyalty, before);
|
|
assert.equal(restored.log.filter((entry) => entry.text.includes('Ecaz Loyalty')).length, loyaltyLogs);
|
|
assert.deepEqual(viewGame(restored, 'p0').ecazLoyalty, before);
|
|
});
|
|
|
|
void test('a legacy started state without the marker is not assigned loyalty retroactively', () => {
|
|
const setup = completeSetup(setupToTraitors(readyTable(['ecaz', 'atreides'])));
|
|
const selected = setup.ecazLoyalty!.card!;
|
|
setup.traitorReserve!.push(selected);
|
|
const beforeCustody = traitorCustody(setup).sort();
|
|
const legacy = JSON.parse(JSON.stringify(setup)) as Game;
|
|
delete legacy.ecazLoyalty;
|
|
const restored = normalizeAutomaticGame(legacy);
|
|
assert.equal(restored.ecazLoyalty, undefined);
|
|
assert.equal(viewGame(restored, 'p0').ecazLoyalty, null);
|
|
assert.deepEqual(traitorCustody(restored).sort(), beforeCustody);
|
|
});
|