Files
Dune/tests/sapho-movement-replay.test.ts
T

87 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { reorderSaphoMovementOrder, saphoMovementOrderEvent, SaphoMovementOrderError,
type SaphoMovementOrderContext } from '../game/sapho-movement-order';
import { advanceSaphoStartedMovement, createSaphoStartedMovementFixture, saphoStartedMovementClean,
saphoStartedMovementInventory, saphoStartedMovementPlayer, STARTED_MOVEMENT_SAPHO } from './fixture-sapho-started-movement';
function order(started = false): SaphoMovementOrderContext {
return { queue: { event: 'movement:3', eligible: ['done', 'active', 'later', 'owner'],
completed: ['done'], remaining: ['active', 'later', 'owner'], current: 'active',
currentStarted: started, protectedLast: 'owner' },
started: started ? ['done', 'active'] : ['done'], guild: null };
}
function reorder(context: SaphoMovementOrderContext, player: string, mode: 'first' | 'last') {
return reorderSaphoMovementOrder(context, { event: saphoMovementOrderEvent(context), player, mode });
}
void test('a fresh Sapho First relinquishes the same holder’s prior Last protection', () => {
for (const started of [false, true]) {
const context = order(started), before = structuredClone(context);
const quote = reorder(context, 'owner', 'first');
assert.deepEqual(quote.next.remaining, started ? ['active', 'owner', 'later'] : ['owner', 'active', 'later']);
assert.equal(quote.next.protectedLast, null);
assert.deepEqual(quote.next.completed, ['done']);
assert.equal(quote.afterCurrent, started ? 'active' : null);
assert.deepEqual(context, before);
}
});
void test('another seat cannot replace an existing Last, and replay cannot duplicate it', () => {
const context = order(), before = structuredClone(context), event = saphoMovementOrderEvent(context);
for (const [player, mode] of [['later', 'first'], ['later', 'last'], ['owner', 'last']] as const)
assert.throws(() => reorderSaphoMovementOrder(context, { event, player, mode }), SaphoMovementOrderError);
assert.throws(() => reorderSaphoMovementOrder(context, { event: 'movement:4', player: 'owner', mode: 'first' }), SaphoMovementOrderError);
assert.deepEqual(context, before);
const guild = order(); guild.guild = { player: 'active', granted: true, locked: false };
assert.throws(() => reorder(guild, 'owner', 'first'), SaphoMovementOrderError);
const suspended = order(); suspended.started = ['done', 'owner'];
const resumed = reorder(suspended, 'owner', 'first');
assert.equal(resumed.next.current, 'owner');
assert.equal(resumed.next.currentStarted, true);
});
function movementAction(game: Game, actor: string, mode: 'first' | 'last'): Action {
const option = viewGame(game, actor).saphoOptions.find(item => item.scope === 'movement' && item.mode === mode);
assert.ok(option, `Sapho ${mode} must be available to its holder`);
return { type: 'card', card: STARTED_MOVEMENT_SAPHO, scope: 'movement', event: option.event, mode };
}
void test('paid Box recovers the physical Sapho for First after its own Last in a Basic movement phase', () => {
const f = createSaphoStartedMovementFixture();
let game = structuredClone(f.beforeShipment);
const owner = saphoStartedMovementPlayer(game, f.owner);
// Supplemental staging exchanges one unopened cache card for an existing held
// card. Setup, the Sapho purchase and the original movement entry are real.
const index = game.richeseCache!.findIndex(card => card.effect === 'nullentropyBox');
assert.ok(index >= 0 && owner.hand.length > 1);
const [box] = game.richeseCache!.splice(index, 1);
const exchange = owner.hand.findIndex(card => card.id !== STARTED_MOVEMENT_SAPHO);
assert.ok(exchange >= 0); game.richeseCache!.push(owner.hand.splice(exchange, 1)[0]); owner.hand.push(box);
assert.ok(owner.spice >= 2); assert.deepEqual(saphoStartedMovementInventory(game), f.originalInventory);
const beforeLast = structuredClone(game), last = movementAction(game, f.owner, 'last');
game = applyAction(game, f.owner, last);
game = advanceSaphoStartedMovement(game, saphoStartedMovementClean);
assert.equal(game.saphoMovementLast?.player, f.owner);
assert.equal(game.movementRemaining!.at(-1), f.owner);
assert.ok(game.discard.some(card => card.id === STARTED_MOVEMENT_SAPHO));
assert.equal(saphoStartedMovementPlayer(game, f.owner).spice, owner.spice);
const savedQueue = [...game.movementRemaining!], cash = saphoStartedMovementPlayer(game, f.owner).spice;
game = applyAction(game, f.owner, { type: 'card', card: box.id });
if (game.pendingNullentropy) game = applyAction(game, f.owner, { type: 'decision',
event: game.pendingNullentropy.event, card: STARTED_MOVEMENT_SAPHO });
game = advanceSaphoStartedMovement(game, saphoStartedMovementClean);
assert.equal(saphoStartedMovementPlayer(game, f.owner).spice, cash - 2);
assert.ok(saphoStartedMovementPlayer(game, f.owner).hand.some(card => card.id === STARTED_MOVEMENT_SAPHO));
assert.deepEqual(game.movementRemaining, savedQueue);
const first = movementAction(game, f.owner, 'first');
game = applyAction(game, f.owner, first);
game = advanceSaphoStartedMovement(game, saphoStartedMovementClean);
assert.equal(game.movementRemaining![0], f.owner);
assert.equal(game.saphoMovementLast, null);
assert.equal(game.discard.filter(card => card.id === STARTED_MOVEMENT_SAPHO).length, 1);
assert.deepEqual(game.order, beforeLast.order);
assert.deepEqual(saphoStartedMovementInventory(game), f.originalInventory);
});