84 lines
4.8 KiB
TypeScript
84 lines
4.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, createGame, joinGame, newPlayer, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
|
|
import { acquireDuke, captureDuke } from '../game/duke-vidal';
|
|
import { actDukeAmbassador, advanceDukeAmbassador, dukeAmbassadorClean } from './fixture-duke-ambassador';
|
|
|
|
/** Actual Advanced setup and Harkonnen arrival, with only the unopened Spice
|
|
* cards and the existing fixture's scalar lotteries controlled. */
|
|
function initialArrival(): Game {
|
|
let game = createGame('LEGACYINITIALDUKE', newPlayer('ecaz', 'Ecaz', 'ecaz'), true, ['ecaz']);
|
|
joinGame(game, newPlayer('harkonnen', 'Harkonnen', 'harkonnen'));
|
|
joinGame(game, newPlayer('guild', 'Guild', 'guild'));
|
|
const act = (actor: string, action: Action) => { game = actDukeAmbassador(game, actor, action); };
|
|
const reach = (until: (state: Game) => boolean) => { game = advanceDukeAmbassador(game, until); };
|
|
for (const player of game.players) act(player.id, { type: 'ready' });
|
|
act(game.host, { type: 'start', advancedPreview: true });
|
|
reach(state => state.status === 'playing');
|
|
const token = game.ecazAmbassadors!.tokens.find(item => item.effect === 'ecaz');
|
|
assert.ok(token && token.zone === 'supply');
|
|
for (const position of [0, 1]) {
|
|
const index = game.spiceDeck.findIndex((card, i) => i >= position && 'territory' in card && !card.discovery);
|
|
assert.ok(index >= position);
|
|
[game.spiceDeck[position], game.spiceDeck[index]] = [game.spiceDeck[index], game.spiceDeck[position]];
|
|
}
|
|
reach(state => state.phase === 4 && !state.response && state.decision?.kind === 'ecazPlacement');
|
|
act('ecaz', { type: 'decision', token: token.id, territory: 'arrakeen' });
|
|
reach(state => state.phase === 4 && !state.response && state.decision?.kind === 'ecazPlacement');
|
|
act('ecaz', { type: 'decision', decline: true });
|
|
reach(state => state.phase === 5 && state.active === 'harkonnen' && dukeAmbassadorClean(state));
|
|
act('harkonnen', { type: 'ship', amount: 1, territory: 'arrakeen', sector: 10 });
|
|
reach(state => !state.response && state.pendingAmbassador?.stage === 'offer');
|
|
assert.equal(game.turn, 1);
|
|
assert.equal(game.pendingAmbassador!.token, token.id);
|
|
assert.equal(game.dukeCaptureUncertain, false);
|
|
delete game.dukeCaptureUncertain; // The only change simulates an older saved game.
|
|
return game;
|
|
}
|
|
|
|
void test('an unstamped first-turn Advanced arrival allows the Duke and stamps only an accepted action', () => {
|
|
const game = initialArrival();
|
|
const original = structuredClone(game);
|
|
assert.equal(viewGame(game, 'ecaz').ambassadorEntry?.dukeAcquisition?.blocked, null);
|
|
assert.deepEqual(game, original, 'Viewing a legacy save does not mutate it');
|
|
assert.throws(() => applyAction(game, 'guild', { type: 'decision', event: game.pendingAmbassador!.event,
|
|
trigger: true, beneficiary: 'ecaz', choice: 'duke' }));
|
|
assert.deepEqual(game, original, 'A rejected action leaves both state and marker unchanged');
|
|
const after = applyAction(game, 'ecaz', { type: 'decision', event: game.pendingAmbassador!.event,
|
|
trigger: true, beneficiary: 'ecaz', choice: 'duke' });
|
|
assert.equal(after.dukeCaptureUncertain, false);
|
|
assert.deepEqual([after.dukeVidal?.controller, after.dukeVidal?.source], ['ecaz', 'ecaz']);
|
|
assert.deepEqual(game, original, 'Applying an action leaves the input snapshot intact');
|
|
});
|
|
|
|
void test('automatic continuation records first-turn public proof without mutating the source', () => {
|
|
const game = initialArrival();
|
|
const original = structuredClone(game);
|
|
const after = normalizeAutomaticGame(game);
|
|
assert.equal(after.dukeCaptureUncertain, false);
|
|
assert.deepEqual(game, original);
|
|
});
|
|
|
|
void test('unstamped later games uniformly guard publicly equivalent Duke custody', () => {
|
|
const first = initialArrival();
|
|
for (const boundary of [{ turn: 1, phase: 6 }, { turn: 2, phase: 5 }]) {
|
|
const free = structuredClone(first);
|
|
Object.assign(free, boundary);
|
|
const prior = acquireDuke(free.dukeVidal!, 'guild', free.turn, 'ecaz');
|
|
free.dukeVidal = prior;
|
|
const captured = structuredClone(free);
|
|
captured.dukeVidal = captureDuke(prior, 'harkonnen', 'guild', free.turn, 'carthag');
|
|
const freeEntry = viewGame(free, 'ecaz').ambassadorEntry;
|
|
const capturedEntry = viewGame(captured, 'ecaz').ambassadorEntry;
|
|
assert.ok(freeEntry && capturedEntry);
|
|
assert.deepEqual(capturedEntry, freeEntry);
|
|
assert.ok(freeEntry.dukeAcquisition?.blocked, 'Unknown past capture stays uniformly guarded');
|
|
for (const game of [free, captured]) {
|
|
const original = structuredClone(game);
|
|
assert.throws(() => applyAction(game, 'ecaz', { type: 'decision', event: game.pendingAmbassador!.event,
|
|
trigger: true, beneficiary: 'ecaz', choice: 'duke' }));
|
|
assert.deepEqual(game, original);
|
|
}
|
|
}
|
|
});
|