136 lines
7.9 KiB
TypeScript
136 lines
7.9 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { botActions, runBots } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { discoveryStormActions } from '../game/discovery-storm-options';
|
|
import { tableActionOwner } from '../game/table-turn';
|
|
import { createTestingStationContestFixture } from './fixture-testing-station-contest';
|
|
import { createSaphoStationPriorityFixture, SAPHO, stationCards, stationSeat } from './sapho-station-priority-fixture';
|
|
|
|
function finishDiscard(game: Game): Game {
|
|
if (!game.pendingTreacheryDiscard?.reaction) return game;
|
|
const event = game.pendingTreacheryDiscard.batch.event;
|
|
for (const player of game.players) game = applyAction(game, player.id, {type:'semutaPass', event});
|
|
return game;
|
|
}
|
|
|
|
function rejectedWithoutMutation(game: Game, player: string, action: {type:string; [key:string]:unknown}) {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, player, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
|
|
for (const advanced of [false, true]) void test(
|
|
`${advanced ? 'Advanced' : 'Basic'} Station waits for a human Sapho holder after the earlier AI passes`, () => {
|
|
const fixture = createSaphoStationPriorityFixture(advanced);
|
|
let game = fixture.game;
|
|
const priority = game.saphoStationPriority;
|
|
assert.ok(priority, 'A real Station opening creates the neutral priority window');
|
|
assert.equal(priority.event, game.ecologicalStorm!.event);
|
|
assert.deepEqual(priority.passed, []);
|
|
assert.equal(viewGame(game, fixture.first).saphoStationPriority?.canPass, true);
|
|
assert.equal(viewGame(game, fixture.holder).saphoStationPriority?.canPass, true);
|
|
for (const player of game.players) assert.equal(tableActionOwner(viewGame(game, player.id)), null);
|
|
|
|
const normalChoice = {type:'decision', event:game.ecologicalStorm!.event, delta:0};
|
|
assert.throws(() => applyAction(game, fixture.first, normalChoice), /Sapho|priority/i);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, fixture.first);
|
|
view.players.find(player => player.id === fixture.first)!.bot = difficulty;
|
|
const actions = botActions(view);
|
|
assert.ok(actions.some(action => action.type === 'saphoStationPass' && action.event === priority.event), difficulty);
|
|
assert.ok(!actions.some(action => action.type === 'decision' && action.event === game.ecologicalStorm!.event), difficulty);
|
|
}
|
|
game = applyAction(game, fixture.first, {type:'saphoStationPass', event:priority.event});
|
|
assert.deepEqual(game.saphoStationPriority?.passed, [fixture.first]);
|
|
const afterBotView = viewGame(game, fixture.first);
|
|
assert.equal(afterBotView.saphoStationPriority?.canPass, false);
|
|
assert.equal(afterBotView.saphoStationPriority?.waiting, true);
|
|
assert.ok(!botActions(afterBotView).some(action => action.type === 'decision' && action.event === game.ecologicalStorm!.event));
|
|
assert.throws(() => applyAction(game, fixture.first, normalChoice), /Sapho|priority/i);
|
|
rejectedWithoutMutation(game, fixture.first, {type:'saphoStationPass', event:priority.event});
|
|
rejectedWithoutMutation(game, fixture.holder, {type:'saphoStationPass', event:'stale-station-event'});
|
|
rejectedWithoutMutation(game, 'richese', {type:'saphoStationPass', event:priority.event});
|
|
for (const player of game.players) assert.equal(tableActionOwner(viewGame(game, player.id)), null);
|
|
|
|
const option = viewGame(game, fixture.holder).saphoOptions.find(row => row.scope === 'testingStation' && row.mode === 'first');
|
|
assert.ok(option, 'The human retains their real Sapho option');
|
|
game = applyAction(game, fixture.holder, {type:'card', card:SAPHO,
|
|
scope:option.scope, event:option.event, mode:option.mode});
|
|
assert.ok(!game.saphoStationPriority);
|
|
assert.deepEqual(game.ecologicalStorm?.queue?.order, [...fixture.order].reverse());
|
|
assert.equal(game.ecologicalStorm?.owner, fixture.holder);
|
|
game = finishDiscard(game);
|
|
assert.equal(game.discard.filter(card => card.id === SAPHO).length, 1);
|
|
const choice = discoveryStormActions(viewGame(game, fixture.holder)).find(action => action.delta === 0);
|
|
assert.ok(choice);
|
|
game = applyAction(game, fixture.holder, choice);
|
|
assert.equal(game.ecologicalStorm?.queue?.choices.length, 1);
|
|
assert.deepEqual(stationCards(game).map(card => card.id).sort(), fixture.beforeCards);
|
|
});
|
|
|
|
for (const advanced of [false, true]) void test(
|
|
`${advanced ? 'Advanced' : 'Basic'} bot scheduler passes once and waits for the human Station holder`, () => {
|
|
const fixture = createSaphoStationPriorityFixture(advanced);
|
|
const initial = fixture.game;
|
|
assert.ok(stationSeat(initial, fixture.first).bot);
|
|
assert.ok(!stationSeat(initial, fixture.holder).bot);
|
|
const event = initial.saphoStationPriority!.event;
|
|
const game = runBots(initial, 12);
|
|
assert.deepEqual(game.saphoStationPriority, {event, passed:[fixture.first]});
|
|
assert.equal(game.botsPending, false);
|
|
assert.deepEqual(game.ecologicalStorm?.queue?.choices, []);
|
|
assert.deepEqual(game.ecologicalStorm?.queue?.order, fixture.order);
|
|
assert.equal(game.decision?.kind, 'ecologicalStorm');
|
|
assert.equal(game.decision?.player, fixture.first);
|
|
assert.equal(viewGame(game, fixture.holder).saphoStationPriority?.canPass, true);
|
|
for (const player of game.players) assert.equal(tableActionOwner(viewGame(game, player.id)), null);
|
|
const repeated = runBots(game, 12);
|
|
assert.deepEqual(repeated, game, 'The bot neither passes twice nor takes the protected choice');
|
|
});
|
|
|
|
for (const advanced of [false, true]) void test(
|
|
`${advanced ? 'Advanced' : 'Basic'} Station opens the same neutral pass window when no occupant holds Sapho`, () => {
|
|
const fixture = createSaphoStationPriorityFixture(advanced, false);
|
|
let game = fixture.game;
|
|
assert.equal(stationSeat(game, fixture.first).hand.some(card => card.id === SAPHO), false);
|
|
assert.equal(stationSeat(game, fixture.holder).hand.some(card => card.id === SAPHO), false);
|
|
const event = game.saphoStationPriority?.event; assert.ok(event);
|
|
for (const id of fixture.order) {
|
|
const view = viewGame(game, id);
|
|
assert.deepEqual(view.saphoStationPriority, {event, canPass:true, waiting:false});
|
|
game = applyAction(game, id, {type:'saphoStationPass', event});
|
|
}
|
|
assert.ok(!game.saphoStationPriority);
|
|
assert.deepEqual(game.ecologicalStorm?.queue?.order, fixture.order);
|
|
const choice = discoveryStormActions(viewGame(game, fixture.first)).find(action => action.delta === 0);
|
|
assert.ok(choice);
|
|
game = applyAction(game, fixture.first, choice);
|
|
assert.equal(game.ecologicalStorm?.queue?.choices.length, 1);
|
|
assert.deepEqual(stationCards(game).map(card => card.id).sort(), fixture.beforeCards);
|
|
});
|
|
|
|
void test('A previously opened unstamped Station continues without a new priority window', () => {
|
|
const fixture = createSaphoStationPriorityFixture(false, false);
|
|
const old = structuredClone(fixture.game);
|
|
delete old.saphoStationPriority;
|
|
const choice = discoveryStormActions(viewGame(old, fixture.first)).find(action => action.delta === 0);
|
|
assert.ok(choice);
|
|
const game = applyAction(old, fixture.first, choice);
|
|
assert.ok(!game.saphoStationPriority);
|
|
assert.equal(game.ecologicalStorm?.queue?.choices.length, 1);
|
|
});
|
|
|
|
void test('A Station table without Richese keeps its original unblocked choice', () => {
|
|
const game = createTestingStationContestFixture().game;
|
|
assert.ok(!game.players.some(player => player.faction === 'richese'));
|
|
assert.ok(!game.saphoStationPriority);
|
|
const owner = game.ecologicalStorm!.owner;
|
|
assert.equal(viewGame(game, owner).saphoStationPriority, null);
|
|
const choice = discoveryStormActions(viewGame(game, owner)).find(action => action.delta === 0);
|
|
assert.ok(choice);
|
|
const next = applyAction(game, owner, choice);
|
|
assert.equal(next.ecologicalStorm?.queue?.choices.length, 1);
|
|
});
|