Files
Dune/tests/public-deals-future-travel-runtime.test.ts
T
Quintine 1e81ce8ad8
Verify and publish checkpoint container / container (push) Successful in 11m8s
Support scheduled travel agreements
2026-10-29 08:19:52 +11:00

205 lines
11 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
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; dueTurn:number};
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => act(game, actor, action));
assert.deepEqual(game, before, 'Rejected consent and travel actions leave the game unchanged.');
}
function offer(game: Game, proposer: string, target: string, travel: Term) {
const next = act(game, proposer, {type:'dealOffer', target, terms:'Agreed travel on a later turn.', travel});
const deal = next.deals?.at(-1); assert.ok(deal);
return {game:next, id:deal.id};
}
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];
}
function receipt(game: Game, id: string) {
const row = game.travelPromises?.find(item => item.sourceDeal === id); assert.ok(row);
return row;
}
function endCurrent(game: Game, target: string) {
assert.equal(game.phase, 5); assert.equal(game.active, target);
return settle(act(game, target, {type:'endMovement'}));
}
function dueShipment(game: Game, target: string, dueTurn: number): Game {
return advance(game, state => state.turn === dueTurn && state.phase === 5 &&
state.active === target && clean(state) && !state.pendingShipment);
}
/** The shared Basic two-human fixture reaches the actual first-turn shipment
* opportunity through setup and phase actions. All agreements, transfers,
* Charity and due-turn actions below are real player actions; no funds, clocks,
* forces, choices or receipts are staged. */
void test('future consent does not reserve an empty purse; actual next-turn Charity funds the promised shipment', () => {
const f = fixture(), dueTurn = f.game.turn + 1, originalCards = inventory(f.game);
const purse = player(f.game, f.target).spice; assert.ok(purse > 0);
let game = act(f.game, f.target, {type:'bribe', target:f.asker, amount:purse});
assert.equal(player(game, f.target).spice, 0);
const offered = offer(game, f.asker, f.target,
{kind:'ship', territory:f.shipmentTerritory, minimum:1, answer:true, dueTurn});
game = accept(offered.game, f.target, offered.id);
assert.equal(player(game, f.target).spice, 0);
assert.equal(receipt(game, offered.id).turn, dueTurn);
assert.equal(receipt(game, offered.id).player, f.target);
assert.equal(status(game, f.asker, offered.id), 'scheduled');
assert.equal(status(game, f.target, offered.id), 'scheduled');
assert.equal(viewGame(game, f.target).travelPromiseGuide ?? null, null);
game = endCurrent(game, f.target);
assert.equal(status(game, f.target, offered.id), 'scheduled');
game = advance(game, state => state.turn === dueTurn && state.phase === 2 && clean(state) &&
!state.ready.includes(f.target));
assert.equal(status(game, f.target, offered.id), 'pending');
assert.equal(player(game, f.target).spice, 0);
game = act(game, f.target, {type:'charity'});
assert.equal(player(game, f.target).spice, 2);
game = dueShipment(game, f.target, dueTurn);
reject(game, f.target, f.currentEnd);
game = settle(act(game, f.target, f.shipmentAction));
assert.equal(status(game, f.asker, offered.id), 'fulfilled');
assert.equal(player(game, f.target).forces['polar_sink:0'], 1);
assert.deepEqual(inventory(game), originalCards);
});
for (const answer of [true, false]) void test(`future movement ${answer ? 'requires' : 'forbids'} the real matching action in its due turn`, () => {
const f = fixture(), dueTurn = f.game.turn + 1;
const offered = offer(f.game, f.asker, f.target,
{kind:'move', territory:f.movementTerritory, minimum:1, answer, dueTurn});
let game = accept(offered.game, f.target, offered.id);
assert.equal(status(game, f.target, offered.id), 'scheduled');
game = endCurrent(game, f.target);
game = dueShipment(game, f.target, dueTurn);
game = settle(act(game, f.target, f.shipmentAction));
if (answer) {
reject(game, f.target, f.currentEnd);
game = settle(act(game, f.target, f.firstMove));
} else {
reject(game, f.target, f.firstMove);
game = settle(act(game, f.target, f.currentEnd));
}
assert.equal(status(game, f.target, offered.id), 'fulfilled');
assert.deepEqual(inventory(game), f.originalCards);
});
void test('an unaccepted offer remains available through its due turn and expires only afterwards', () => {
const f = fixture(), dueTurn = f.game.turn + 1;
const offered = offer(f.game, f.asker, f.target,
{kind:'ship', territory:f.shipmentTerritory, minimum:1, answer:true, dueTurn});
let game = endCurrent(offered.game, f.target);
game = dueShipment(game, f.target, dueTurn);
assert.equal(game.deals!.find(row => row.id === offered.id)!.status, 'offered');
const accepted = accept(game, f.target, offered.id);
assert.equal(status(accepted, f.target, offered.id), 'pending');
assert.equal(receipt(accepted, offered.id).turn, dueTurn);
game = endCurrent(game, f.target);
game = advance(game, state => state.turn > dueTurn);
reject(game, f.target, {type:'dealRespond', deal:offered.id, accept:true});
assert.equal(game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
});
void test('mutual release before the due turn cancels the scheduled receipt without blocking current travel', () => {
const f = fixture(), dueTurn = f.game.turn + 1;
const offered = offer(f.game, f.asker, f.target,
{kind:'ship', territory:f.shipmentTerritory, minimum:1, answer:true, dueTurn});
let game = accept(offered.game, f.target, offered.id);
game = act(game, f.asker, {type:'dealRelease', deal:offered.id, agree:true});
assert.equal(status(game, f.target, offered.id), 'scheduled');
game = act(game, f.target, {type:'dealRelease', deal:offered.id, agree:true});
assert.equal(status(game, f.target, offered.id), 'released');
assert.equal(receipt(game, offered.id).releasedByDeal, true);
game = endCurrent(game, f.target);
game = dueShipment(game, f.target, dueTurn);
assert.equal(status(game, f.asker, offered.id), 'released');
game = settle(act(game, f.target, f.currentEnd));
assert.equal(status(game, f.target, offered.id), 'released');
});
void test('an unfunded future promise remains pending until its actual travel turn can close', () => {
const f = fixture(), dueTurn = f.game.turn + 1;
const offered = offer(f.game, f.asker, f.target,
{kind:'ship', territory:f.shipmentTerritory, minimum:20, answer:true, dueTurn});
let game = accept(offered.game, f.target, offered.id);
game = endCurrent(game, f.target);
game = dueShipment(game, f.target, dueTurn);
assert.equal(status(game, f.target, offered.id), 'pending');
assert.ok(player(game, f.target).spice < 40);
game = settle(act(game, f.target, f.currentEnd));
assert.equal(status(game, f.target, offered.id), 'void');
});
void test('a witnessed due shipment cannot be voluntarily made unaffordable', () => {
const f = fixture(), dueTurn = f.game.turn + 1;
const offered = offer(f.game, f.asker, f.target,
{kind:'ship', territory:f.shipmentTerritory, minimum:1, answer:true, dueTurn});
let game = endCurrent(accept(offered.game, f.target, offered.id), f.target);
game = dueShipment(game, f.target, dueTurn);
const purse = player(game, f.target).spice; assert.ok(purse > 2);
reject(game, f.target, {type:'bribe', target:f.asker, amount:purse - 1});
game = act(game, f.target, {type:'bribe', target:f.asker, amount:purse - 2});
game = settle(act(game, f.target, f.shipmentAction));
assert.equal(status(game, f.target, offered.id), 'fulfilled');
});
void test('public contradictions cannot be accepted and do not disturb the earlier future agreement', () => {
const f = fixture(), dueTurn = f.game.turn + 1;
const term:Term = {kind:'ship', territory:f.shipmentTerritory, minimum:1, answer:true, dueTurn};
const first = offer(f.game, f.asker, f.target, term);
let game = accept(first.game, f.target, first.id);
const second = offer(game, f.asker, f.target, {...term, answer:false});
game = second.game;
reject(game, f.target, {type:'dealRespond', deal:second.id, accept:true});
const third = offer(game, f.asker, f.target, {...term, territory:'carthag'});
game = third.game;
reject(game, f.target, {type:'dealRespond', deal:third.id, accept:true});
assert.equal(status(game, f.target, first.id), 'scheduled');
assert.equal(game.deals!.find(deal => deal.id === second.id)!.status, 'offered');
assert.equal(game.deals!.find(deal => deal.id === third.id)!.status, 'offered');
game = endCurrent(game, f.target);
game = dueShipment(game, f.target, dueTurn);
assert.equal(status(game, f.target, first.id), 'pending');
const purse = player(game, f.target).spice; assert.ok(purse > 2);
reject(game, f.target, {type:'bribe', target:f.asker, amount:purse - 1});
game = settle(act(game, f.target, f.shipmentAction));
assert.equal(status(game, f.target, first.id), 'fulfilled');
});
void test('all minimal autopilot profiles complete an accepted future shipment on the due turn', () => {
const f = fixture(), dueTurn = f.game.turn + 1;
const offered = offer(f.game, f.asker, f.target,
{kind:'ship', territory:f.shipmentTerritory, minimum:1, answer:true, dueTurn});
let game = accept(offered.game, f.target, offered.id);
game = endCurrent(game, f.target);
const due = dueShipment(game, f.target, dueTurn);
for (const difficulty of DIFFICULTIES) {
let controlled = act(due, f.target, {type:'setAutopilot', difficulty});
for (let step = 0; step < 8 && status(controlled, f.target, offered.id) === 'pending'; step++) {
const actions = botActions(viewGame(controlled, f.target));
assert.ok(actions.length, `${difficulty} needs a legal native continuation`);
controlled = settle(act(controlled, f.target, actions[0]));
}
assert.equal(status(controlled, f.target, offered.id), 'fulfilled', difficulty);
}
});
void test('a game that naturally ends before the scheduled turn voids the unfulfilled term', () => {
const f = fixture(), dueTurn = 11;
const offered = offer(f.game, f.asker, f.target,
{kind:'ship', territory:f.shipmentTerritory, minimum:1, answer:true, dueTurn});
let game = accept(offered.game, f.target, offered.id);
game = endCurrent(game, f.target);
game = advance(game, state => state.status === 'finished');
assert.ok(game.turn < dueTurn);
assert.equal(status(game, f.asker, offered.id), 'void');
assert.equal(receipt(game, offered.id).fulfilled, false);
});