167 lines
10 KiB
TypeScript
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);
|
|
});
|