import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { createTruthtranceChoamPromisesFixture as fixture, actTruthtranceChoamPromises as act, advanceTruthtranceChoamPromises as advance, allowTruthtranceChoamPromisesIncome as allowIncome, finishTruthtranceChoamPromises as finish, truthtranceChoamPromisesPlayer as player, truthtranceChoamPromisesInventory as inventory, truthtranceChoamPromisesPhysical as physical, type TruthtranceChoamPromisesFixture, } from './fixture-truthtrance-choam-promises'; const variants = [ { advanced: false, kind: 'shipment' as const }, { advanced: true, kind: 'shipment' as const }, { advanced: false, kind: 'movement' as const }, { advanced: true, kind: 'movement' as const }, ]; const promise = (game: Game, f: TruthtranceChoamPromisesFixture) => f.kind === 'shipment' ? game.shipmentPromises?.[0] : game.movementPromises?.[0]; function reject(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before); } function nativeSource(f: TruthtranceChoamPromisesFixture) { assert.equal(f.setup.status, 'setup'); assert.equal(f.setup.advanced, f.advanced); assert.deepEqual(f.setup.expansions, ['choam']); assert.deepEqual(f.game.players.map(player => player.faction), ['atreides', 'choam', 'emperor', 'harkonnen']); assert.ok(!f.setup.homeworlds && !f.setup.nexusCards && !f.setup.leaderSkills && !f.setup.techTokens && !f.setup.strongholdCards && !f.setup.discoveryEnabled && !f.setup.ecazTreachery); assert.ok(f.actions.some(step => step.action.type === 'start' && (!f.advanced || step.action.advancedPreview === true))); assert.ok(f.beforeStartingDeal.players.every(player => player.hand.length === 0)); assert.deepEqual(f.afterSetup.players.flatMap(player => player.hand.map(card => card.id)), f.deal); assert.ok(player(f.afterSetup, f.asker).hand.some(card => card.id === f.card && card.effect === 'truthtrance')); assert.ok(player(f.afterSetup, f.counterHolder).hand.some(card => card.id === f.karama && card.effect === 'karama')); assert.equal(player(f.afterSetup, f.target).hand.length, 1); assert.ok(player(f.afterSetup, f.target).hand.every(card => card.kind === 'projectile' && !card.effect)); assert.equal(f.game.phase, 0); assert.equal(f.game.turn, 1); assert.equal(f.game.truthtrance ?? null, null); assert.equal(player(f.game, f.target).spice, 2); assert.equal(player(f.game, f.target).reserves, 20); assert.deepEqual(player(f.game, f.target).forces, {}); assert.equal(player(f.game, f.target).tanks, 0); assert.equal(f.game.choamCharity, undefined); assert.equal(f.incomeAmount, 8); assert.equal(f.allowedCash, 10); assert.equal(f.originalInventory.length, 35); assert.deepEqual(inventory(f.game), f.originalInventory); assert.deepEqual(physical(f.game), f.originalPhysical); assert.equal(f.sourceActionCount, f.actions.length); } function asked(f: TruthtranceChoamPromisesFixture) { let game = act(structuredClone(f.game), f.declarationStep.actor, f.declarationStep.action); game = advance(game, state => state.truthtrance?.stage === 'ask'); game = act(game, f.askStep.actor, f.askStep.action); const answers = f.kind === 'shipment' ? viewGame(game, f.target).truthShipmentAnswers : viewGame(game, f.target).truthMovementAnswers; assert.ok(answers?.includes('yes'), 'Native owned opening income supplies an allowed-outcome route'); assert.ok(answers?.includes('no')); return game; } function bound(f: TruthtranceChoamPromisesFixture, answer: 'yes' | 'no' = 'yes') { const game = act(asked(f), f.target, { type: 'truthAnswer', answer }); assert.equal(game.truthtrance ?? null, null); assert.equal(promise(game, f)?.answer, answer === 'yes'); assert.equal(promise(game, f)?.released, undefined); assert.equal(promise(game, f)?.fulfilled, undefined); assert.equal(player(game, f.target).spice, 2); assert.equal(game.choamCharity, undefined); assert.deepEqual(physical(game), f.originalPhysical); assert.deepEqual(inventory(game), f.originalInventory); const view = viewGame(game, f.target); assert.equal(view.shipmentCompletion, null); assert.ok(!(view.movementPromisePolicy ?? []).some(action => action.type === 'ship' || action.type === 'move')); reject(game, f.target, f.shipment); reject(game, f.target, f.move); return game; } function incomePending(game: Game, f: TruthtranceChoamPromisesFixture) { const actions: { actor: string; action: Action }[] = []; const before = structuredClone(game); game = advance(game, state => state.phase === 2 && state.response?.kind === 'choamCharity', actions); assert.equal(game.response?.owner, f.target); assert.equal(player(game, f.target).spice, 2); assert.equal(game.choamCharity, undefined); assert.equal(promise(game, f)?.released, undefined); assert.ok(viewGame(game, f.counterHolder).responseControls?.cancelCards.includes(f.karama)); reject(game, f.target, { type: 'ready' }); reject(game, f.target, { type: 'charity' }); reject(game, f.target, f.shipment); return { game, before, actions }; } for (const variant of variants) void test( `${variant.advanced ? 'Advanced' : 'Basic'} native CHOAM ${variant.kind} promise awaits real opening income then completes its funded native route`, () => { const f = fixture(variant); nativeSource(f); const pending = incomePending(bound(f), f), originalResponse = structuredClone(pending.game.response); assert.equal(promise(pending.game, f)?.fulfilled, undefined); const incomeActions: { actor: string; action: Action }[] = []; const allowed = allowIncome(pending.game, incomeActions); assert.equal(player(allowed, f.target).spice, 10); assert.deepEqual(allowed.choamCharity, { turn: 1, canceled: false }); assert.equal(originalResponse?.kind, 'choamCharity'); assert.ok(incomeActions.some(step => step.action.type === 'passResponse')); assert.equal(promise(allowed, f)?.released, undefined); reject(allowed, f.target, f.blockedBribe); const completed = finish(f, allowed); reject(completed.beforeBid, f.target, f.blockedBid); assert.equal(completed.afterShipment.choamCharity?.canceled, false); assert.equal(player(completed.delivered, f.target).forces[f.location], f.minimum); assert.equal(promise(completed.delivered, f)?.fulfilled, true); assert.equal(promise(completed.delivered, f)?.released, undefined); if (f.kind === 'shipment') { assert.equal(completed.purchase, null); assert.equal(completed.move, null); assert.equal(player(completed.delivered, f.target).spice, 0); assert.equal(player(completed.delivered, f.target).reserves, 10); assert.equal(player(completed.delivered, f.target).moved, 0); } else { assert.ok(completed.purchase && completed.move); assert.equal(player(completed.purchase.after, f.target).spice, 8); assert.ok(player(completed.purchase.after, f.target).hand.some(card => card.id === f.purchaseCard)); assert.equal(player(completed.afterShipment, f.target).forces['arrakeen:10'], 6); assert.equal(promise(completed.afterShipment, f)?.fulfilled, undefined, 'The reserve shipment is not the promised ground event'); assert.equal(player(completed.delivered, f.target).forces['arrakeen:10'] ?? 0, 0); assert.equal(player(completed.delivered, f.target).reserves, 14); assert.equal(player(completed.delivered, f.target).spice, 2); assert.equal(player(completed.delivered, f.target).moved, 1); } assert.equal(player(completed.delivered, f.target).shipped, true); assert.ok(player(completed.delivered, f.counterHolder).hand.some(card => card.id === f.karama)); assert.equal(completed.delivered.discard.filter(card => card.id === f.card).length, 1); assert.deepEqual(inventory(completed.game), f.originalInventory); }); for (const variant of variants) void test( `${variant.advanced ? 'Advanced' : 'Basic'} native CHOAM ${variant.kind} opening cancellation spends the real Karama and releases only the unfunded obligation`, () => { const f = fixture(variant); nativeSource(f); const pending = incomePending(bound(f), f); const game = act(pending.game, f.counterHolder, f.cancelIncome); assert.equal(game.response ?? null, null); assert.equal(game.pendingTreacheryDiscard ?? null, null); assert.deepEqual(game.choamCharity, { turn: 1, canceled: true }); assert.equal(player(game, f.target).spice, 2); assert.equal(player(game, f.target).reserves, 20); assert.deepEqual(player(game, f.target).forces, {}); assert.equal(promise(game, f)?.released, true); assert.equal(promise(game, f)?.fulfilled, undefined); assert.equal(player(game, f.counterHolder).hand.some(card => card.id === f.karama), false); assert.equal(game.discard.filter(card => card.id === f.karama).length, 1); reject(game, f.counterHolder, f.cancelIncome); reject(game, f.target, { type: 'passResponse' }); assert.deepEqual(inventory(game), f.originalInventory); }); for (const variant of variants) void test( `${variant.advanced ? 'Advanced' : 'Basic'} native CHOAM ${variant.kind} No retains a real smaller alternative without reserving Yes funds`, () => { const f = fixture(variant), pending = incomePending(bound(f, 'no'), f), allowed = allowIncome(pending.game); const spent = act(structuredClone(allowed), f.target, { type: 'bribe', target: f.asker, amount: 1 }); assert.equal(player(spent, f.target).spice, 9); assert.equal(promise(spent, f)?.released, undefined); const completed = finish(f, allowed, { answer: 'no' }); assert.equal(player(completed.delivered, f.target).forces[f.location], f.minimum - 1); assert.equal(promise(completed.delivered, f)?.released, undefined); if (f.kind === 'shipment') { assert.equal(promise(completed.delivered, f)?.fulfilled, true); assert.equal(player(completed.delivered, f.target).spice, 1); assert.equal(completed.turnEnd, null); } else { assert.equal(promise(completed.delivered, f)?.fulfilled, undefined, 'A later move could still match until this actual turn ends'); assert.equal(player(completed.delivered, f.target).forces['arrakeen:10'], 1); assert.ok(completed.turnEnd); assert.deepEqual(completed.turnEnd.steps, [{ actor: f.target, action: { type: 'endMovement' } }]); assert.equal(promise(completed.finished, f)?.fulfilled, true); assert.ok(!completed.finished.movementRemaining?.includes(f.target)); assert.deepEqual(player(completed.finished, f.target).forces, player(completed.delivered, f.target).forces); assert.equal(player(completed.finished, f.target).spice, player(completed.delivered, f.target).spice); assert.equal(player(completed.finished, f.target).reserves, player(completed.delivered, f.target).reserves); } assert.deepEqual(inventory(completed.game), f.originalInventory); }); void test('native CHOAM promise questions reject foreign and underlying replies while raw cash stays uncredited', () => { for (const kind of ['shipment', 'movement'] as const) { const f = fixture({ advanced: true, kind }), game = asked(f); reject(game, f.asker, { type: 'truthAnswer', answer: 'yes' }); reject(game, f.target, { type: 'ready' }); reject(game, f.target, f.shipment); reject(game, f.asker, f.askStep.action); assert.equal(player(game, f.target).spice, 2); assert.equal(game.choamCharity, undefined); const answered = act(game, f.target, { type: 'truthAnswer', answer: 'yes' }); reject(answered, f.target, { type: 'truthAnswer', answer: 'yes' }); const pending = incomePending(answered, f); assert.equal(player(pending.game, f.target).spice, 2); assert.equal(promise(pending.game, f)?.released, undefined); } }); void test('all four minimal policies preserve native CHOAM promise funding during actual Bidding', () => { for (const variant of variants) { const f = fixture(variant), pending = incomePending(bound(f), f), allowed = allowIncome(pending.game); const game = advance(allowed, state => state.phase === 3 && !state.response && !state.phaseOpening && !state.decision && state.auction?.active === f.target); for (const difficulty of DIFFICULTIES) { const view = viewGame(game, f.target); view.players.find(player => player.id === f.target)!.bot = difficulty; const before = structuredClone(view), action = botActions(view)[0]; assert.ok(action, difficulty); assert.deepEqual(view, before); const done = act(structuredClone(game), f.target, action); assert.equal(promise(done, f)?.released, undefined); assert.ok(player(done, f.target).spice >= (f.kind === 'shipment' ? 10 : 6)); assert.deepEqual(inventory(done), f.originalInventory); } } });