Files
Dune/tests/sapho-testing-station-public-start.test.ts
T
Quintine 98ead3a4b5
Verify and publish checkpoint container / container (push) Canceled after 0s
Enable Semuta in public Discovery games
2026-10-29 09:31:40 +11:00

94 lines
5.4 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
import { DISCOVERY_CARD_PLACEMENTS, validateDiscoveryState } from '../game/discoveries';
import { createDiscoveryStorm, createStormSource, discoveryStormDistance } from '../game/discovery-storm';
import { discoveryStormActions } from '../game/discovery-storm-options';
import { advanceTestingStationContest } from './fixture-testing-station-contest';
const CARD = 'richese-juice-of-sapho';
const STATION = 'ecological-testing-station';
function seat(game: Game, id: string) {
const player = game.players.find(candidate => candidate.id === id);
assert.ok(player); return player;
}
function physicalCards(game: Game) {
return [...game.deck, ...game.discard, ...(game.richeseCache ?? []),
...(game.richeseRemoved ?? []), ...game.players.flatMap(player => player.hand)];
}
/** A real lobby selects CHOAM expansion and Discovery through public actions,
* starts and completes the original faction setup. Only after setup, the
* revealed physical station, two ordinary reserve forces, Storm source and
* its unchosen queue are staged to reach the rare contested boundary; one
* existing physical Sapho moves from Richese's cache into its intended owner.
* This is not a natural full game, purchase, reveal, or collection proof. */
function fixture(advanced: boolean) {
let game = createGame('SAPHOSTATIONPUBLIC', newPlayer('richese', 'Richese', 'richese'), advanced, ['choam']);
for (const faction of ['atreides', 'emperor'] as const)
joinGame(game, newPlayer(faction, faction, faction));
game = applyAction(game, game.host, {type:'moduleOption', module:'discoveries', enabled:true});
for (const player of game.players) game = applyAction(game, player.id, {type:'ready'});
game = applyAction(game, game.host, {type:'start', ...(advanced ? {advancedPreview:true} : {})});
assert.equal(game.status, 'setup');
assert.equal(game.discoveryEnabled, true);
assert.deepEqual(game.expansions, ['choam']);
assert.ok(game.discoveries && game.richeseCache);
const startInventory = physicalCards(game).map(card => card.id).sort();
assert.equal(startInventory.filter(id => id === CARD).length, 1);
game = advanceTestingStationContest(game, state => state.status === 'playing');
assert.equal(game.phase, 0);
const token = game.discoveries!.tokens.find(candidate => candidate.face === STATION);
assert.ok(token && token.status === 'supply');
const position = Object.values(DISCOVERY_CARD_PLACEMENTS).find(candidate => candidate.type === token.type);
assert.ok(position);
Object.assign(token, {status:'placed', territory:position.territory, sector:position.sector, revealedTurn:game.turn});
validateDiscoveryState(game.discoveries!);
for (const id of ['atreides', 'emperor']) {
const player = seat(game, id); assert.ok(player.reserves > 0);
player.reserves--;
player.forces[`${STATION}:0`] = (player.forces[`${STATION}:0`] ?? 0) + 1;
}
const occupants = game.order.filter(id => id === 'atreides' || id === 'emperor');
assert.equal(occupants.length, 2);
const holder = occupants[1];
const index = game.richeseCache!.findIndex(card => card.id === CARD); assert.ok(index >= 0);
const [sapho] = game.richeseCache!.splice(index, 1);
seat(game, holder).hand.push(sapho);
assert.deepEqual(physicalCards(game).map(card => card.id).sort(), startInventory);
game.stormPending = 3;
game.stormMovementSource = createStormSource(game.turn, 'dials', 3);
game.ready = game.players.map(player => player.id);
game.phaseOpening = null;
game.stormResolution = null;
game.ecologicalStorm = createDiscoveryStorm(game, 'public-station-setup');
game.decision = {kind:'ecologicalStorm', player:game.ecologicalStorm.owner, event:game.ecologicalStorm.event};
return {game, holder, occupants, startInventory};
}
for (const advanced of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} public Richese/Discovery start reaches staged conserved Sapho station use`, () => {
const f = fixture(advanced), prior = structuredClone(f.game);
const option = viewGame(prior, f.holder).saphoOptions.find(row => row.scope === 'testingStation' && row.mode === 'first');
assert.ok(option, 'The existing physical Sapho can take the pending station choice first');
let game = applyAction(prior, f.holder, {type:'card', card:CARD, scope:option.scope, event:option.event, mode:option.mode});
assert.deepEqual(game.order, prior.order);
assert.deepEqual(game.ecologicalStorm?.queue?.order, [...f.occupants].reverse());
assert.equal(game.pendingTreacheryDiscard?.batch.cause, 'sapho:station');
const discardEvent = game.pendingTreacheryDiscard!.batch.event;
for (const player of game.players) game = applyAction(game, player.id, {type:'semutaPass', event:discardEvent});
assert.equal(game.decision?.player, f.holder);
assert.equal(game.discard.filter(card => card.id === CARD).length, 1);
for (const delta of [1, -1]) {
const actor = game.ecologicalStorm!.owner;
const action = discoveryStormActions(viewGame(game, actor)).find(row => row.delta === delta);
assert.ok(action);
game = applyAction(game, actor, action);
}
assert.equal(game.ecologicalStorm?.stage, 'complete');
assert.equal(discoveryStormDistance(game.ecologicalStorm!), 3);
assert.deepEqual(physicalCards(game).map(card => card.id).sort(), f.startInventory);
},
);