import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine'; import { actChoamCharityFunding, advanceChoamCharityFunding, choamCharityFundingInventory, createChoamCharityFundingFixture, passChoamCharityFundingResponses } from './fixture-choam-charity-funding'; import { actTruthtranceChoamPromises, advanceTruthtranceChoamPromises, allowTruthtranceChoamPromisesIncome, createTruthtranceChoamPromisesFixture, finishTruthtranceChoamPromises, truthtranceChoamPromisesInventory } from './fixture-truthtrance-choam-promises'; function reject(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before); } /** Genuine public Basic setup and first Storm closing. */ function firstMarket() { const source = createChoamCharityFundingFixture(); return advanceChoamCharityFunding(structuredClone(source.afterSetup), state => state.turn === 1 && state.phase === 0 && state.decision?.kind === 'choamMarket'); } void test('a pending sale and its Karama response finish exactly once before a standing waiver can close the market', () => { const game = firstMarket(); const owner = game.players.find(player => player.id === 'choam'); assert.ok(owner); // Preserve all 35 physical cards while staging a saleable original card in // place of CHOAM's original deal. Atreides already genuinely holds Karama. const worthless = game.deck.findIndex(card => card.kind === 'worthless'); assert.ok(worthless >= 0); [owner.hand[0], game.deck[worthless]] = [game.deck[worthless], owner.hand[0]]; const card = owner.hand[0], beforeCards = choamCharityFundingInventory(game), beforeSpice = owner.spice; const pending = actChoamCharityFunding(game, owner.id, {type:'decision', mode:'sell', card:card.id}); assert.equal(pending.response?.kind, 'choamSale'); assert.equal(pending.choamMarket?.sale?.card, card.id); // The sole later-state staging is a previously accepted same-turn preference. pending.choamMarketSkip = {owner:owner.id, turn:pending.turn}; const normalized = normalizeAutomaticGame(structuredClone(pending)); assert.equal(normalized.response?.kind, 'choamSale'); assert.equal(normalized.choamMarket?.sale?.card, card.id); assert.equal(normalized.players.find(player => player.id === owner.id)!.spice, beforeSpice); assert.equal(viewGame(normalized, owner.id).choamMarketSkipAvailable, false); reject(normalized, owner.id, {type:'decision', done:true, skipMarketsThisTurn:true}); reject(normalized, owner.id, {type:'cancelChoamMarketSkip'}); const settled = passChoamCharityFundingResponses(normalized); assert.equal(settled.response, null); assert.equal(settled.choamMarket, null); assert.equal(settled.phase, 1); assert.equal(settled.players.find(player => player.id === owner.id)!.spice, beforeSpice + 2); assert.equal(settled.discard.filter(item => item.id === card.id).length, 1); assert.deepEqual(choamCharityFundingInventory(settled), beforeCards); }); void test('a real binding Truthtrance shipment prevents market skipping until the commitment can proceed', () => { const f = createTruthtranceChoamPromisesFixture({advanced:false, kind:'shipment'}); let game = actTruthtranceChoamPromises(structuredClone(f.game), f.asker, f.declarationStep.action); game = advanceTruthtranceChoamPromises(game, state => state.truthtrance?.stage === 'ask'); game = actTruthtranceChoamPromises(game, f.asker, f.askStep.action); game = actTruthtranceChoamPromises(game, f.target, {type:'truthAnswer', answer:'yes'}); assert.equal(game.shipmentPromises?.[0]?.answer, true); assert.equal(game.shipmentPromises?.[0]?.fulfilled, undefined); // Only the previously accepted same-turn preference is staged; the card, // question and binding answer all came from actual native actions above. game.choamMarketSkip = {owner:f.target, turn:game.turn}; game = advanceTruthtranceChoamPromises(game, state => state.phase === 0 && state.decision?.kind === 'choamMarket'); const commitment = structuredClone(game.shipmentPromises); const cards = truthtranceChoamPromisesInventory(game); assert.equal(viewGame(game, f.target).choamMarketSkipActive, true); assert.equal(viewGame(game, f.target).choamMarketSkipAvailable, false); const normalized = normalizeAutomaticGame(structuredClone(game)); assert.equal(normalized.decision?.kind, 'choamMarket'); assert.deepEqual(normalized.shipmentPromises, commitment); reject(normalized, f.target, {type:'decision', done:true, skipMarketsThisTurn:true}); game = actTruthtranceChoamPromises(normalized, f.target, {type:'decision', done:true}); assert.deepEqual(game.shipmentPromises, commitment); game = advanceTruthtranceChoamPromises(game, state => state.phase === 2 && state.response?.kind === 'choamCharity'); game = allowTruthtranceChoamPromisesIncome(game); const completed = finishTruthtranceChoamPromises(f, game); assert.equal(completed.delivered.shipmentPromises?.[0]?.fulfilled, true); assert.equal(completed.delivered.shipmentPromises?.[0]?.released, undefined); assert.deepEqual(truthtranceChoamPromisesInventory(completed.delivered), cards); }); void test('malformed opt-in packets cannot waive a market; switching to autopilot cancels an accepted waiver', () => { const market = firstMarket(); for (const action of [ {type:'decision', done:true, skipMarketsThisTurn:false}, {type:'decision', done:true, skipMarketsThisTurn:'true'}, {type:'decision', done:true, skipMarketsThisTurn:true, extra:true}, ]) reject(market, 'choam', action); let game = actChoamCharityFunding(market, 'choam', {type:'decision', done:true, skipMarketsThisTurn:true}); assert.equal(viewGame(game, 'choam').choamMarketSkipActive, true); game = actChoamCharityFunding(game, 'choam', {type:'setAutopilot', difficulty:'Easy'}); assert.equal(game.choamMarketSkip, undefined); game = actChoamCharityFunding(game, 'choam', {type:'setAutopilot', difficulty:null}); assert.equal(viewGame(game, 'choam').choamMarketSkipActive, false); });