Files
Dune/tests/sapho-truth-order-turn-promise.test.ts

105 lines
6.3 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { act, inventory, player, createSaphoTruthOrderFixture,
passFreshDiscard, truthOrderFrame, finishTruthOrder, finishOriginalTruthQuestions } from './fixture-sapho-truth-order';
/** Declare one original Truth, allowing every other player to pass the original
* neutral priority. The caller chooses the actual question afterwards. */
function declare(game: Game, actor: string, card: string) {
game = act(game,actor,{type:'card',card});
while (game.truthtrance?.stage === 'priority') {
const seat = game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(seat);
game = act(game,seat.id,{type:'truthPass'});
}
assert.equal(game.truthtrance?.stage,'ask');
return game;
}
void test('Sapho Truth-order disposal cannot consume the last card needed by an earlier Distrans Yes', () => {
const f = createSaphoTruthOrderFixture({advanced:true,mode:'first'});
let game = structuredClone(f.before);
const owner = player(game,f.owner), cache = game.richeseCache;
assert.ok(cache);
// Controlled, conserved custody supplement on top of the original public
// setup and earned Sapho: give the owner the physical cached Distrans, and
// put any other unneeded held cards in the original deck. The owner retains
// exactly their real Sapho, original dealt Truth and Distrans. No resources,
// phases, promises, auction records or extra cards are manufactured.
const at = cache.findIndex(card => card.effect === 'distrans'); assert.ok(at >= 0);
const [distrans] = cache.splice(at,1);
const retained = owner.hand.filter(card => card.id === f.card || card.id === f.truth);
const returned = owner.hand.filter(card => card.id !== f.card && card.id !== f.truth);
owner.hand = [...retained,distrans]; game.deck.push(...returned);
assert.equal(owner.hand.length,3);
assert.deepEqual(inventory(game),f.originalCards);
// Ask the earlier obligation through the real Truthtrance engine. At this
// moment the owner's other physical Truth card can legally be transferred;
// Sapho is another real alternative, so a Yes has a native completion.
game = declare(game,f.other,f.otherTruth);
game = act(game,f.other,{type:'truthAsk',question:{kind:'turnActions',target:f.owner,turn:game.turn,
mixed:{kind:'action',action:{kind:'cardPlay',effect:'distrans'}}}});
assert.ok(viewGame(game,f.owner).truthTurnActionAnswers?.includes('yes'));
game = act(game,f.owner,{type:'truthAnswer',answer:'yes'});
game = passFreshDiscard(game);
assert.ok(game.turnActionPromises?.some(row => row.player === f.owner && row.answer && !row.fulfilled && !row.released));
assert.deepEqual(inventory(game),f.originalCards);
const quote = viewGame(game,f.owner).distrans;
assert.ok(quote && !quote.blocked);
const recipient = quote.choices.find(row => !row.blocked && row.cards.some(card => card.id === f.card));
assert.ok(recipient,'The original physical Sapho is a legal Distrans transfer');
let completed = act(structuredClone(game),f.owner,{type:'card',card:distrans.id,
target:recipient.recipient,give:f.card});
completed = passFreshDiscard(completed);
assert.ok(completed.turnActionPromises?.some(row => row.player === f.owner && row.fulfilled),
'The earlier Yes has an actual legal physical transfer route before reordering');
assert.deepEqual(inventory(completed),f.originalCards);
// The previously played original Truth is moved from the discard back to
// its original holder solely to construct two simultaneous questions.
// This is a labeled controlled card-custody setup, not a claimed native
// recovery; physical inventory and the already-bound answer are preserved.
const atDiscard = game.discard.findIndex(card => card.id === f.otherTruth); assert.ok(atDiscard >= 0);
const [priorTruth] = game.discard.splice(atDiscard,1);
player(game,f.other).hand.push(priorTruth);
assert.deepEqual(inventory(game),f.originalCards);
// Both original Truth cards now become committed physical questions. The
// owner's Truth is reserved, leaving Sapho as the only card Distrans can
// transfer after this window. Reordering by discarding Sapho must not erase
// the earlier Yes or strand the question queue.
game = act(game,f.owner,{type:'card',card:f.truth});
game = act(game,f.other,{type:'card',card:f.otherTruth});
while (game.truthtrance?.stage === 'priority') {
const seat = game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(seat);
game = act(game,seat.id,{type:'truthPass'});
}
const frame = truthOrderFrame(game), prior = structuredClone(game.turnActionPromises);
const choices = viewGame(game,f.owner).saphoTruthOrderChoices;
assert.ok(!choices.some(row => row.card === f.card && row.mode === 'first'),
'A displayed choice must retain a completion of the earlier Yes');
const original = structuredClone(game);
assert.throws(() => act(game,f.owner,{type:'saphoTruthOrder',event:frame.event,revision:frame.revision,
card:f.card,mode:'first'}));
assert.deepEqual(game,original,'Rejected action must not mutate the original game');
assert.deepEqual(game.turnActionPromises,prior);
assert.ok(player(game,f.owner).hand.some(card => card.id === f.card));
assert.deepEqual(inventory(game),f.originalCards);
// Both holders can decline to reorder; resolve both actual queued questions
// and then complete the preserved earlier promise by transferring Sapho.
game = finishOriginalTruthQuestions(finishTruthOrder(game));
assert.equal(game.truthtrance,null);
assert.ok(game.turnActionPromises?.some(row => row.player === f.owner && row.answer && !row.fulfilled && !row.released));
const laterQuote = viewGame(game,f.owner).distrans;
assert.ok(laterQuote && !laterQuote.blocked);
const laterRecipient = laterQuote.choices.find(row => !row.blocked && row.cards.some(card => card.id === f.card));
assert.ok(laterRecipient,'The preserved Sapho remains a real legal Distrans transfer');
game = act(game,f.owner,{type:'card',card:distrans.id,target:laterRecipient.recipient,give:f.card});
game = passFreshDiscard(game);
assert.ok(game.turnActionPromises?.some(row => row.player === f.owner && row.fulfilled && !row.released));
assert.ok(player(game,laterRecipient.recipient).hand.some(card => card.id === f.card));
assert.deepEqual(inventory(game),f.originalCards);
});