import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Game } from '../game/engine'; import { SEMUTA_DRUG_ID } from '../game/semuta-drug'; import { createSemutaHomeworldSaphoFixture } from './fixture-semuta-homeworld-sapho'; import { createSemutaHomeworldBattleSaphoFixture, prepareSemutaHomeworldBattleSapho } from './fixture-semuta-homeworld-battle-sapho'; import { reactSemutaHomeworldChoam } from './fixture-semuta-homeworld-choam'; import { moveSemutaEmperorCard } from './fixture-semuta-emperor-share'; import { biddingPhysicalIds } from './fixture-homeworld-occupied-bidding'; import { finishOriginalTruthQuestions, finishTruthOrder, playSaphoTruthOrder, truthOrderFrame } from './fixture-sapho-truth-order'; const SAPHO = 'richese-juice-of-sapho'; const copy = (game: Game): Game => JSON.parse(JSON.stringify(game)); /** Existing Homeworld fixtures create the real finite auction or native * pre-plan battle. Card hands alone are staged by moving original physical * components between their existing zones; no invoice, battle or phase is * fabricated. Both declarations and every later answer are real actions. */ function source(parent: 'auction' | 'battle', mode: 'first' | 'last') { const original = parent === 'auction' ? createSemutaHomeworldSaphoFixture('onceAround', mode, false) : createSemutaHomeworldBattleSaphoFixture('aggressor', 'first', false); let game = copy(original.game); if (parent === 'battle') game = prepareSemutaHomeworldBattleSapho(game); const inventory = biddingPhysicalIds(game); for (const seat of game.players) game.deck.push(...seat.hand.splice(0)); const holders = game.order.filter(id => id !== original.holder); assert.ok(holders.length >= 2); const owner = mode === 'first' ? holders.at(-1)! : holders[0]; const other = mode === 'first' ? holders[0] : holders.at(-1)!; moveSemutaEmperorCard(game, SAPHO, owner); moveSemutaEmperorCard(game, SEMUTA_DRUG_ID, original.holder); const truths = [...game.deck, ...game.discard, ...(game.richeseCache ?? []), ...(game.richeseRemoved ?? []), ...game.players.flatMap(p => p.hand)] .filter(card => card.effect === 'truthtrance').map(card => card.id); assert.ok(truths.length >= 2); const first = moveSemutaEmperorCard(game, truths[0], owner); const second = moveSemutaEmperorCard(game, truths[1], other); assert.deepEqual(biddingPhysicalIds(game), inventory); const originalParent = {turn:game.turn, phase:game.phase, active:game.active, auction:copyValue(game.auction), richeseAuction:copyValue(game.richeseAuction), battle:copyValue(game.battle), order:[...game.order]}; for (const [actor, card] of [[owner,first],[other,second]]) game = applyAction(game, actor, {type:'card', card}); while (game.truthtrance?.stage === 'priority') { const seat = game.players.find(p => !game.truthtrance!.passed.includes(p.id)); assert.ok(seat); game = applyAction(game, seat.id, {type:'truthPass'}); } const frame = truthOrderFrame(game); assert.deepEqual([...frame.holders].sort(), [owner,other].sort()); assert.equal(mode === 'first' ? game.truthtrance!.queue.at(-1)?.player : game.truthtrance!.queue[0].player, owner); return {game, owner, other, holder:original.holder, inventory, originalParent}; } function copyValue(value: T): T { return value === undefined ? value : JSON.parse(JSON.stringify(value)); } for (const parent of ['auction', 'battle'] as const) for (const mode of ['first', 'last'] as const) void test(`Basic Homeworld ${parent} preserves its original parent through Sapho ${mode} and Semuta`, () => { const f = source(parent, mode), prior = copy(f.game), frame = truthOrderFrame(prior); assert.ok(viewGame(prior, f.owner).saphoTruthOrderChoices.some(row => row.card === SAPHO && row.mode === mode)); const pending = playSaphoTruthOrder(copy(prior), f.owner, SAPHO, mode); assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'saphoTruthOrderDiscard'); assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer'); assert.equal(pending.truthtrance, null); for (const claim of [false, true]) { let next = reactSemutaHomeworldChoam(copy(pending), f.holder, claim); assert.equal(next.pendingTreacheryDiscard, null); assert.equal(truthOrderFrame(next).event, frame.event); assert.deepEqual(copyValue(next.auction), f.originalParent.auction); assert.deepEqual(copyValue(next.richeseAuction), f.originalParent.richeseAuction); assert.deepEqual(copyValue(next.battle), f.originalParent.battle); next = finishTruthOrder(next); assert.equal(mode === 'first' ? next.truthtrance!.queue[0].player : next.truthtrance!.queue.at(-1)?.player, f.owner); next = finishOriginalTruthQuestions(next); assert.equal(next.truthtrance, null); assert.equal(next.turn, f.originalParent.turn); assert.equal(next.phase, f.originalParent.phase); assert.equal(next.active, f.originalParent.active); assert.deepEqual(next.order, f.originalParent.order); assert.deepEqual(copyValue(next.auction), f.originalParent.auction); assert.deepEqual(copyValue(next.richeseAuction), f.originalParent.richeseAuction); assert.deepEqual(copyValue(next.battle), f.originalParent.battle); assert.deepEqual(biddingPhysicalIds(next), f.inventory); } });