94 lines
5.4 KiB
TypeScript
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);
|
|
},
|
|
);
|