105 lines
6.3 KiB
TypeScript
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);
|
|
});
|