Files
Dune/tests/public-deals-group-paid-travel-runtime.test.ts
T
Quintine 6c1530382d
Verify and publish checkpoint container / container (push) Successful in 10m49s
Support immediate payments in group travel deals
2026-10-29 17:41:58 +11:00

100 lines
5.9 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { actTruthMovement as act, truthMovementClean as clean, truthMovementInventory as inventory,
truthMovementPlayer as player } from './fixture-truth-movement';
import { nextStrongholdFactionsNativeStep as next } from './fixture-stronghold-factions';
const proposer = 'atreides', traveler = 'emperor', first = 'harkonnen', outsider = 'fremen';
const shipment: Action = { type:'ship', territory:'polar_sink', sector:0, amount:1, elite:0, allyPayment:0 };
const travel = { actor:traveler, kind:'ship', territory:'polar_sink', minimum:1, answer:true } as const;
function reach(game: Game, until: (game: Game) => boolean): Game {
for (let i = 0; i < 1200; i++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished');
const step = next(game); assert.ok(step);
game = act(game, step.actor, step.action);
}
throw Error('The native shipment opportunity was not reached.');
}
function nativeSource(): Game {
let game = createGame('GROUPPAIDTRAVEL', newPlayer(proposer, 'Atreides', 'atreides'), false, []);
for (const [id, faction] of [[traveler, 'emperor'], [first, 'harkonnen'], [outsider, 'fremen']] as const)
joinGame(game, newPlayer(id, id, faction));
for (const seat of game.players) game = act(game, seat.id, { type:'ready' });
game = act(game, proposer, { type:'start' });
const cards = game.deck.filter(card => ['projectile', 'poison', 'shield', 'snooper'].includes(card.kind)).slice(0, 4);
assert.equal(cards.length, 4);
for (const [position, card] of cards.entries()) {
const from = game.deck.findIndex(row => row.id === card.id); assert.ok(from >= position);
[game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]];
}
game = reach(game, state => state.status === 'playing');
const land = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery); assert.ok(land >= 0);
game.spiceDeck.unshift(game.spiceDeck.splice(land, 1)[0]);
game = reach(game, state => state.turn === 1 && state.phase === 5 && state.active === traveler && clean(state));
assert.equal(player(game, traveler).shipped, false);
assert.ok(player(game, traveler).spice >= 2);
return game;
}
let source: Game | undefined;
const fixture = () => structuredClone(source ??= nativeSource());
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => act(game, actor, action));
assert.deepEqual(game, before);
}
function offer(game: Game, payer: string, recipient: string, amount: number) {
const result = act(game, proposer, { type:'dealOffer', targets:[first, traveler], terms:'Travel and payment.',
payment:{ kind:'immediateBribe', payer, recipient, amount }, travel });
const deal = result.deals?.at(-1); assert.ok(deal);
return { game:result, id:deal.id };
}
const accept = (game: Game, actor: string, id: string) => act(game, actor, { type:'dealRespond', deal:id, accept:true });
for (const payer of [traveler, first]) void test(`group paid travel pays ${payer} only after the traveler gives final consent`, () => {
const initial = fixture(), cash = player(initial, payer).spice, escrow = player(initial, proposer).bribes;
assert.ok(cash >= 1);
const cards = inventory(initial), offered = offer(initial, payer, proposer, 1);
let game = offered.game;
reject(game, traveler, { type:'dealRespond', deal:offered.id, accept:true });
assert.equal(player(game, payer).spice, cash);
game = accept(game, first, offered.id);
assert.equal(game.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
assert.equal(player(game, payer).spice, cash);
assert.equal(player(game, proposer).bribes, escrow);
assert.equal(game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
game = accept(game, traveler, offered.id);
assert.equal(game.deals!.find(deal => deal.id === offered.id)!.status, 'active');
assert.equal(player(game, payer).spice, cash - 1);
assert.equal(player(game, proposer).bribes, escrow + 1);
assert.equal(viewGame(game, traveler).dealTravelStatus[offered.id], 'pending');
reject(game, traveler, { type:'dealRespond', deal:offered.id, accept:true });
game = reach(act(game, traveler, shipment), state => clean(state) && !state.pendingShipment);
assert.equal(viewGame(game, traveler).dealTravelStatus[offered.id], 'fulfilled');
assert.deepEqual(inventory(game), cards);
});
void test('traveler payment that consumes shipment funds rolls back only final consent', () => {
const initial = fixture(), cash = player(initial, traveler).spice, escrow = player(initial, proposer).bribes;
const offered = offer(initial, traveler, proposer, cash);
const waiting = accept(offered.game, first, offered.id);
reject(waiting, traveler, { type:'dealRespond', deal:offered.id, accept:true });
assert.equal(waiting.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
assert.equal(player(waiting, traveler).spice, cash);
assert.equal(player(waiting, proposer).bribes, escrow);
assert.equal(waiting.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
});
void test('a prior consenting payer with insufficient funds cannot make the traveler partly activate the group deal', () => {
const initial = fixture(), cash = player(initial, first).spice; assert.ok(cash >= 1);
const offered = offer(initial, first, proposer, cash);
let game = accept(offered.game, first, offered.id);
game = act(game, first, { type:'bribe', target:proposer, amount:1 });
const before = structuredClone(game);
reject(game, traveler, { type:'dealRespond', deal:offered.id, accept:true });
assert.deepEqual(game, before);
assert.equal(game.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
assert.equal(game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
});