129 lines
6.9 KiB
TypeScript
129 lines
6.9 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame } from '../game/engine';
|
|
import type { GameView } from '../game/engine';
|
|
import { botActions, runBots } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { DEFAULT_ROOM_CONTROL } from '../lib/room-control';
|
|
import { createHarkonnenNexusBetrayalFixture } from './fixture-nexus-harkonnen-betrayal';
|
|
|
|
const scenarios = [
|
|
{ advanced: false, remote: false },
|
|
{ advanced: true, remote: false },
|
|
{ advanced: false, remote: true },
|
|
{ advanced: true, remote: true },
|
|
] as const;
|
|
|
|
void test('every minimal policy uses the actual legal cancellation without any rival private estimates', () => {
|
|
for (const difficulty of DIFFICULTIES) for (const scenario of scenarios) {
|
|
const f = createHarkonnenNexusBetrayalFixture(scenario);
|
|
const own = viewGame(f.game, f.holder);
|
|
own.players.find(p => p.id === own.me)!.bot = difficulty;
|
|
for (const rival of own.players.filter(p => p.id !== own.me)) {
|
|
delete rival.hand;
|
|
delete rival.traitors;
|
|
delete rival.spice;
|
|
}
|
|
const actions = botActions(own);
|
|
assert.deepEqual(actions, [{ type: 'nexusHarkonnenBetrayalUse', event: f.event }]);
|
|
const result = applyAction(f.game, f.holder, actions[0]);
|
|
assert.equal(viewGame(result, f.holder).nexusHarkonnenBetrayalReaction, null);
|
|
assert.equal(result.nexusCards!.cards!.hands[f.holder], null);
|
|
assert.equal(result.players.find(p => p.id === f.provider)!.traitors.includes(f.identity), false);
|
|
assert.equal(result.pendingNexusHarkonnenReplacement!.identity, f.identity);
|
|
assert.equal(result.nexusCards!.cards!.discard.filter(card => card === 'harkonnen').length, 1);
|
|
}
|
|
});
|
|
|
|
void test('every policy passes an irrelevant face and waits after its acknowledgement or outside public membership', () => {
|
|
for (const difficulty of DIFFICULTIES) for (const scenario of scenarios) {
|
|
const f = createHarkonnenNexusBetrayalFixture({ ...scenario, face: 'richese', secondFace: 'guild', receiverCount: 2 });
|
|
let game = f.game;
|
|
for (const seat of game.players) {
|
|
const own = viewGame(game, seat.id);
|
|
own.players.find(p => p.id === own.me)!.bot = difficulty;
|
|
const reaction = own.nexusHarkonnenBetrayalReaction;
|
|
if (!reaction) break;
|
|
const actions = botActions(own);
|
|
if (!reaction.canPass || reaction.hasPassed) {
|
|
assert.deepEqual(actions, []);
|
|
continue;
|
|
}
|
|
assert.deepEqual(actions, [{ type: 'nexusHarkonnenBetrayalPass', event: f.event }]);
|
|
game = applyAction(game, seat.id, actions[0]);
|
|
const refreshed = viewGame(structuredClone(game), seat.id);
|
|
refreshed.players.find(p => p.id === refreshed.me)!.bot = difficulty;
|
|
if (refreshed.nexusHarkonnenBetrayalReaction) assert.deepEqual(botActions(refreshed), []);
|
|
}
|
|
assert.equal(viewGame(game, f.holder).nexusHarkonnenBetrayalReaction, null);
|
|
assert.equal(game.nexusCards!.cards!.hands[f.holder], 'richese');
|
|
assert.equal(game.players.find(p => p.id === f.provider)!.traitors.includes(f.identity), true);
|
|
assert.equal(game.pendingNexusHarkonnenReplacement ?? null, null);
|
|
}
|
|
});
|
|
|
|
void test('canonical response permissions and room scheduling override hints under all four policies', () => {
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const f = createHarkonnenNexusBetrayalFixture();
|
|
const own: GameView = viewGame(f.game, f.holder);
|
|
own.players.find(p => p.id === own.me)!.bot = difficulty;
|
|
for (const reason of ['blocked', 'canUse'] as const) {
|
|
const blocked = structuredClone(own);
|
|
if (reason === 'blocked') blocked.nexusHarkonnenBetrayalReaction!.blocked = 'A committed promise reserves this Nexus card.';
|
|
else blocked.nexusHarkonnenBetrayalReaction!.canUse = false;
|
|
assert.deepEqual(botActions(blocked), [{ type: 'nexusHarkonnenBetrayalPass', event: f.event }]);
|
|
}
|
|
for (const reason of ['canPass', 'hasPassed', 'event', 'paused', 'closed', 'automatic', 'finished', 'human', 'phaseOpening'] as const) {
|
|
const blocked = structuredClone(own);
|
|
if (reason === 'canPass') blocked.nexusHarkonnenBetrayalReaction!.canPass = false;
|
|
else if (reason === 'hasPassed') blocked.nexusHarkonnenBetrayalReaction!.hasPassed = true;
|
|
else if (reason === 'event') blocked.nexusHarkonnenBetrayalReaction!.event = '';
|
|
else if (reason === 'paused' || reason === 'closed') blocked.roomControl = { ...DEFAULT_ROOM_CONTROL, [reason]: true };
|
|
else if (reason === 'automatic') blocked.automaticContinuationPending = true;
|
|
else if (reason === 'human') {
|
|
const seat = blocked.players.find(p => p.id === blocked.me)!;
|
|
delete seat.bot;
|
|
delete seat.autopilot;
|
|
} else if (reason === 'phaseOpening') blocked.phaseOpening = { passed: [] };
|
|
else blocked.status = 'finished';
|
|
assert.deepEqual(botActions(blocked), []);
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('the original native allied counter remains the bot decision before any Nexus cancellation', () => {
|
|
for (const difficulty of DIFFICULTIES) for (const advanced of [false, true]) {
|
|
const f = createHarkonnenNexusBetrayalFixture({ remote: true, advanced });
|
|
assert.ok(f.beforeNativeCounter.response);
|
|
const native = f.beforeNativeCounter;
|
|
for (const seat of native.players) {
|
|
const own = viewGame(native, seat.id);
|
|
own.players.find(p => p.id === own.me)!.bot = difficulty;
|
|
assert.equal(own.nexusHarkonnenBetrayalReaction, null);
|
|
for (const action of botActions(own)) {
|
|
assert.notEqual(action.type, 'nexusHarkonnenBetrayalUse');
|
|
assert.notEqual(action.type, 'nexusHarkonnenBetrayalPass');
|
|
// Exercise actual counter legality, rather than inventing a second gate.
|
|
const result = applyAction(native, seat.id, action);
|
|
assert.equal(result.nexusCards!.cards!.hands[f.holder], 'harkonnen');
|
|
assert.equal(result.pendingNexusHarkonnenReplacement ?? null, null);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('paced native bot execution uses or acknowledges every required seat without unrelated fallthrough', () => {
|
|
for (const difficulty of DIFFICULTIES) for (const scenario of scenarios) for (const face of ['harkonnen', 'richese'] as const) {
|
|
const f = createHarkonnenNexusBetrayalFixture({ ...scenario, face, secondFace: 'guild', receiverCount: 2 });
|
|
let game = f.game;
|
|
for (const id of f.required) game = applyAction(game, id, { type: 'setAutopilot', difficulty });
|
|
for (let step = 0; step < f.required.length && viewGame(game, f.holder).nexusHarkonnenBetrayalReaction; step++) {
|
|
game = runBots(game, 1);
|
|
}
|
|
assert.equal(viewGame(game, f.holder).nexusHarkonnenBetrayalReaction, null);
|
|
assert.equal(game.nexusCards!.cards!.hands[f.holder], face === 'harkonnen' ? null : 'richese');
|
|
assert.equal(game.players.find(p => p.id === f.provider)!.traitors.includes(f.identity), face !== 'harkonnen');
|
|
assert.equal(game.pendingNexusHarkonnenReplacement?.identity ?? null, face === 'harkonnen' ? f.identity : null);
|
|
}
|
|
});
|