import assert from 'node:assert/strict'; import test from 'node:test'; import { viewGame, type Action, type Game } from '../game/engine'; import { actTruthTravel as act, createTruthTravelFixture as fixture, truthTravelInventory as inventory, truthTravelPlayer as player } from './fixture-truth-travel'; function reject(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before, 'Conflicting consent must leave the source game unchanged.'); } function travelOffer(game: Game, proposer: string, traveler: string, dueTurn: number) { const next = act(game, proposer, {type:'dealOffer', target:traveler, terms:'Travel in a future turn.', travel:{kind:'ship', territory:'polar_sink', minimum:1, answer:true, dueTurn}}); const deal = next.deals?.at(-1); assert.ok(deal); return {game:next, id:deal.id}; } function paymentOffer(game: Game, proposer: string, target: string, payer: string, recipient: string, dueTurn: number) { const next = act(game, proposer, {type:'dealOffer', target, terms:'Pay a future bribe.', payment:{kind:'deferredBribe', payer, recipient, amount:2, dueTurn}}); const deal = next.deals?.at(-1); assert.ok(deal); return {game:next, id:deal.id}; } function accept(game: Game, recipient: string, id: string) { return act(game, recipient, {type:'dealRespond', deal:id, accept:true}); } function deal(game: Game, id: string) { const found = game.deals?.find(row => row.id === id); assert.ok(found); return found; } /** Each case uses the genuine Basic Atreides/Emperor setup and actual native * first-turn pre-shipment window. These are independent two-party offers and * acceptances; no wallet, force, pending debt or travel receipt is staged. */ void test('a same-turn deferred debt cannot be accepted after the payer schedules travel', () => { const f = fixture(), dueTurn = f.game.turn + 1, originalCards = inventory(f.game); const future = travelOffer(f.game, f.asker, f.target, dueTurn); let game = accept(future.game, f.target, future.id); assert.equal(viewGame(game, f.target).dealTravelStatus[future.id], 'scheduled'); const payment = paymentOffer(game, f.asker, f.target, f.target, f.asker, dueTurn); game = payment.game; const purse = player(game, f.target).spice; reject(game, f.target, {type:'dealRespond', deal:payment.id, accept:true}); assert.equal(deal(game, future.id).status, 'active'); assert.equal(deal(game, payment.id).status, 'offered'); assert.equal(game.travelPromises?.filter(row => row.sourceDeal === future.id).length, 1); assert.equal(player(game, f.target).spice, purse); assert.deepEqual(inventory(game), originalCards); }); void test('a same-turn scheduled journey cannot be accepted after the traveler owes a deferred debt', () => { const f = fixture(), dueTurn = f.game.turn + 1, originalCards = inventory(f.game); const payment = paymentOffer(f.game, f.asker, f.target, f.target, f.asker, dueTurn); let game = accept(payment.game, f.target, payment.id); assert.equal(deal(game, payment.id).paymentStatus, 'pending'); const future = travelOffer(game, f.asker, f.target, dueTurn); game = future.game; const purse = player(game, f.target).spice; reject(game, f.target, {type:'dealRespond', deal:future.id, accept:true}); assert.equal(deal(game, payment.id).status, 'active'); assert.equal(deal(game, payment.id).paymentStatus, 'pending'); assert.equal(deal(game, future.id).status, 'offered'); assert.equal(game.travelPromises?.some(row => row.sourceDeal === future.id) ?? false, false); assert.equal(player(game, f.target).spice, purse); assert.deepEqual(inventory(game), originalCards); }); for (const order of ['travel-first', 'payment-first'] as const) void test( `different future due turns coexist when accepted ${order}`, () => { const f = fixture(), travelDue = f.game.turn + 1, paymentDue = travelDue + 1; let game = f.game, travelId: string, paymentId: string; if (order === 'travel-first') { const future = travelOffer(game, f.asker, f.target, travelDue); travelId = future.id; game = accept(future.game, f.target, travelId); const payment = paymentOffer(game, f.asker, f.target, f.target, f.asker, paymentDue); paymentId = payment.id; game = accept(payment.game, f.target, paymentId); } else { const payment = paymentOffer(game, f.asker, f.target, f.target, f.asker, paymentDue); paymentId = payment.id; game = accept(payment.game, f.target, paymentId); const future = travelOffer(game, f.asker, f.target, travelDue); travelId = future.id; game = accept(future.game, f.target, travelId); } assert.equal(deal(game, travelId).status, 'active'); assert.equal(viewGame(game, f.target).dealTravelStatus[travelId], 'scheduled'); assert.equal(game.travelPromises?.find(row => row.sourceDeal === travelId)?.turn, travelDue); assert.equal(deal(game, paymentId).paymentStatus, 'pending'); assert.equal(player(game, f.target).spice, player(f.game, f.target).spice); assert.deepEqual(inventory(game), f.originalCards); }, ); for (const order of ['travel-first', 'payment-first'] as const) void test( `the same future due turn can involve a different payment payer when accepted ${order}`, () => { const f = fixture(), dueTurn = f.game.turn + 1; let game = f.game, travelId: string, paymentId: string; if (order === 'travel-first') { const future = travelOffer(game, f.asker, f.target, dueTurn); travelId = future.id; game = accept(future.game, f.target, travelId); const payment = paymentOffer(game, f.asker, f.target, f.asker, f.target, dueTurn); paymentId = payment.id; game = accept(payment.game, f.target, paymentId); } else { const payment = paymentOffer(game, f.asker, f.target, f.asker, f.target, dueTurn); paymentId = payment.id; game = accept(payment.game, f.target, paymentId); const future = travelOffer(game, f.asker, f.target, dueTurn); travelId = future.id; game = accept(future.game, f.target, travelId); } assert.equal(deal(game, travelId).status, 'active'); assert.equal(viewGame(game, f.target).dealTravelStatus[travelId], 'scheduled'); assert.equal(deal(game, paymentId).paymentStatus, 'pending'); assert.equal(player(game, f.asker).spice, player(f.game, f.asker).spice); assert.equal(player(game, f.target).spice, player(f.game, f.target).spice); assert.deepEqual(inventory(game), f.originalCards); }, );