Files
Dune/tests/public-deals-paid-travel-runtime.test.ts
T
Quintine 3987c5c16b
Verify and publish checkpoint container / container (push) Canceled after 0s
Allow travelers to pay in two-party travel deals
2026-10-29 17:39:06 +11:00

167 lines
10 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import { actTruthTravel as act, advanceTruthTravel as advance, createTruthTravelFixture as fixture,
settleTruthTravel as settle, truthTravelClean as clean, truthTravelInventory as inventory,
truthTravelPlayer as player } from './fixture-truth-travel';
type Term = { kind: 'ship' | 'move'; territory: string; minimum: number; answer: boolean };
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => act(game, actor, action));
assert.deepEqual(game, before, 'Failed consent and action packets preserve the source table.');
}
function offer(game: Game, proposer: string, target: string, travel: Term, amount: number) {
const payment = { kind: 'immediateBribe', payer: proposer, recipient: target, amount };
const next = act(game, proposer, { type: 'dealOffer', target, terms: 'Payment for agreed travel.', payment, travel });
const deal = next.deals?.at(-1); assert.ok(deal);
return { game: next, id: deal.id, payment, travel };
}
function accept(game: Game, target: string, id: string) {
return act(game, target, { type: 'dealRespond', deal: id, accept: true });
}
function status(game: Game, viewer: string, id: string) {
return viewGame(game, viewer).dealTravelStatus[id];
}
/** Native Basic Atreides/Emperor game at the real pre-shipment or shipped
* phase-five boundary. Only original unopened deals/Spice and scalar lotteries
* are selected by the shared TruthTravel fixture; all agreements and payments
* below are real game actions. */
void test('final consent atomically pays the proposer bribe and creates the travel obligation', () => {
const f = fixture(), initial = f.game;
const beforePayer = player(initial, f.asker).spice, beforeCash = player(initial, f.target).spice,
beforeEscrow = player(initial, f.target).bribes;
assert.ok(beforePayer >= 3);
const offered = offer(initial, f.asker, f.target,
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, 3);
assert.equal(player(offered.game, f.asker).spice, beforePayer);
assert.equal(player(offered.game, f.target).bribes, beforeEscrow);
const game = accept(offered.game, f.target, offered.id);
assert.equal(game.deals!.find(deal => deal.id === offered.id)!.status, 'active');
assert.equal(player(game, f.asker).spice, beforePayer - 3);
assert.equal(player(game, f.target).spice, beforeCash);
assert.equal(player(game, f.target).bribes, beforeEscrow + 3);
assert.equal(game.travelPromises!.filter(row => row.sourceDeal === offered.id).length, 1);
assert.equal(status(game, f.target, offered.id), 'pending');
reject(game, f.target, { type: 'dealRespond', deal: offered.id, accept: true });
const next = settle(act(game, f.target, f.shipmentAction));
assert.equal(status(next, f.target, offered.id), 'fulfilled');
assert.equal(player(next, f.target).spice, beforeCash - f.shipmentCost);
assert.equal(player(next, f.target).bribes, beforeEscrow + 3);
assert.deepEqual(inventory(next), f.originalCards);
const collection = advance(next, state => state.turn === initial.turn && state.phase === 7 && clean(state));
const cashBeforeMentat = player(collection, f.target).spice;
assert.equal(player(collection, f.target).bribes, beforeEscrow + 3);
const mentat = advance(collection, state => state.turn === initial.turn && state.phase === 8 && clean(state));
assert.equal(player(mentat, f.target).spice, cashBeforeMentat + beforeEscrow + 3);
assert.equal(player(mentat, f.target).bribes, 0);
assert.deepEqual(inventory(mentat), f.originalCards);
});
void test('a payer who spends after offering cannot partly activate or pay the deal', () => {
const f = fixture(), payerCash = player(f.game, f.asker).spice; assert.ok(payerCash >= 1);
const offered = offer(f.game, f.asker, f.target,
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, payerCash);
const spent = act(offered.game, f.asker, { type: 'bribe', target: f.target, amount: 1 });
assert.equal(player(spent, f.asker).spice, payerCash - 1);
const before = structuredClone(spent);
reject(spent, f.target, { type: 'dealRespond', deal: offered.id, accept: true });
assert.deepEqual(spent, before);
assert.equal(spent.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
assert.equal(spent.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
assert.deepEqual(inventory(spent), f.originalCards);
});
void test('an infeasible promised shipment rolls back payment and final acceptance', () => {
const f = fixture({ window: 'shipped' }), before = f.game;
const offered = offer(before, f.asker, f.target,
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, 1);
const payer = player(offered.game, f.asker).spice, cash = player(offered.game, f.target).spice,
escrow = player(offered.game, f.target).bribes;
reject(offered.game, f.target, { type: 'dealRespond', deal: offered.id, accept: true });
assert.equal(offered.game.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
assert.equal(player(offered.game, f.asker).spice, payer);
assert.equal(player(offered.game, f.target).spice, cash);
assert.equal(player(offered.game, f.target).bribes, escrow);
assert.equal(offered.game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
});
void test('the recipient cannot use incoming Mentat escrow to fund the promised shipment', () => {
const f = fixture(), cash = player(f.game, f.target).spice; assert.ok(cash >= 2);
const spent = act(f.game, f.target, { type: 'bribe', target: f.asker, amount: cash - 1 });
assert.equal(player(spent, f.target).spice, 1);
assert.ok(player(spent, f.asker).spice >= 3);
const offered = offer(spent, f.asker, f.target,
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, 3);
reject(offered.game, f.target, { type: 'dealRespond', deal: offered.id, accept: true });
assert.equal(player(offered.game, f.target).spice, 1);
assert.equal(offered.game.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
assert.equal(offered.game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
});
void test('unanimous release ends the pending travel obligation without refunding payment', () => {
const f = fixture(), offered = offer(f.game, f.asker, f.target,
{ kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true }, 1);
let game = accept(offered.game, f.target, offered.id);
const paid = player(game, f.asker).spice, escrow = player(game, f.target).bribes;
game = act(game, f.asker, { type: 'dealRelease', deal: offered.id, agree: true });
assert.equal(status(game, f.target, offered.id), 'pending');
game = act(game, f.target, { type: 'dealRelease', deal: offered.id, agree: true });
assert.equal(status(game, f.target, offered.id), 'released');
assert.equal(player(game, f.asker).spice, paid);
assert.equal(player(game, f.target).bribes, escrow);
game = settle(act(game, f.target, f.currentEnd));
assert.equal(player(game, f.asker).spice, paid);
assert.equal(player(game, f.target).bribes, escrow);
});
void test('paid travel excludes deferred and multiparty payment variants', () => {
const f = fixture(), travel = { kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true };
const base = { type: 'dealOffer', target: f.target, terms: 'Travel.', travel } satisfies Action;
reject(f.game, f.asker, { ...base, payment: { kind: 'deferredBribe', payer: f.asker, recipient: f.target, amount: 1, dueTurn: f.game.turn } });
reject(f.game, f.asker, { type: 'dealOffer', targets: [f.target, f.asker], terms: 'Travel.', travel,
payment: { kind: 'immediateBribe', payer: f.asker, recipient: f.target, amount: 1 } });
});
void test('the travelling recipient can pay the proposer at final consent and then complete shipment', () => {
const f = fixture(), payerBefore = player(f.game, f.target).spice,
recipientBefore = player(f.game, f.asker).bribes;
assert.ok(payerBefore >= f.shipmentCost + 1);
const payment = { kind: 'immediateBribe', payer: f.target, recipient: f.asker, amount: 1 };
const travel = { kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true };
const offered = act(f.game, f.asker, { type: 'dealOffer', target: f.target,
terms: 'Payment for agreed travel.', payment, travel });
const deal = offered.deals!.at(-1)!;
assert.equal(player(offered, f.target).spice, payerBefore);
const accepted = accept(offered, f.target, deal.id);
assert.equal(accepted.deals!.find(row => row.id === deal.id)!.status, 'active');
assert.equal(player(accepted, f.target).spice, payerBefore - 1);
assert.equal(player(accepted, f.asker).bribes, recipientBefore + 1);
assert.equal(accepted.travelPromises!.filter(row => row.sourceDeal === deal.id).length, 1);
assert.equal(status(accepted, f.target, deal.id), 'pending');
const shipped = settle(act(accepted, f.target, f.shipmentAction));
assert.equal(status(shipped, f.target, deal.id), 'fulfilled');
assert.equal(player(shipped, f.target).spice, payerBefore - 1 - f.shipmentCost);
assert.equal(player(shipped, f.asker).bribes, recipientBefore + 1);
assert.deepEqual(inventory(shipped), f.originalCards);
});
void test('reverse payment that consumes the travel funds rolls back the final consent atomically', () => {
const f = fixture(), cash = player(f.game, f.target).spice,
recipientEscrow = player(f.game, f.asker).bribes;
assert.ok(cash >= f.shipmentCost);
const payment = { kind: 'immediateBribe', payer: f.target, recipient: f.asker, amount: cash };
const travel = { kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true };
const offered = act(f.game, f.asker, { type: 'dealOffer', target: f.target,
terms: 'Payment for agreed travel.', payment, travel });
const deal = offered.deals!.at(-1)!;
reject(offered, f.target, { type: 'dealRespond', deal: deal.id, accept: true });
assert.equal(offered.deals!.find(row => row.id === deal.id)!.status, 'offered');
assert.equal(player(offered, f.target).spice, cash);
assert.equal(player(offered, f.asker).bribes, recipientEscrow);
assert.equal(offered.travelPromises?.some(row => row.sourceDeal === deal.id) ?? false, false);
assert.deepEqual(inventory(offered), f.originalCards);
});