Support scheduled travel agreements
Verify and publish checkpoint container / container (push) Successful in 11m8s

This commit is contained in:
Quintine committed 2026-10-29 08:19:52 +11:00
1 parent e1e02fc1a1
commit 1e81ce8ad8
13 files changed
+767 -56

No files matched your search

+157 -32
View File
@@ -8,7 +8,7 @@ import { createFactionDraft, validateFactionDraft, currentFactionDrafter, choose
import { createFactionSwap, validateFactionSwap, answerFactionSwap, cancelFactionSwap,
FactionSwapError, type FactionSwap } from './faction-swap';
import { createPublicDeal, createMultipartyPublicDeal, validatePublicDeals, respondPublicDeal, withdrawPublicDeal,
releasePublicDeal, markPublicDealPaid, voidPublicDealPayment, setPublicDealTravel, publicDealTravelActor, PublicDealError, type PublicDeal, type PublicDealPayment } from './public-deals';
releasePublicDeal, markPublicDealPaid, voidPublicDealPayment, setPublicDealTravel, publicDealTravelActor, publicDealTravelDueTurn, PublicDealError, type PublicDeal, type PublicDealPayment } from './public-deals';
import { outstandingPublicDealPayments, pendingPublicDealPayments, publicDealPaymentTotal, PublicDealObligationError, type PendingPublicDealPayment } from './public-deal-obligations';
import { CharityPromiseError, liveCharityPromises, charityPromiseAnswers, bindCharityPromise, guardCharityClaim, guardCharityReady, reconcileCharityPromises, type CharityPromise } from './charity-promises';
import { victoryFinalTurn, continueVictoryCalendar, VictoryCalendarError } from './victory-calendar';
@@ -5968,7 +5968,7 @@ function publicDealIntegrity(g: Game): void {
const deal = g.deals?.find(entry => entry.id === row.sourceDeal);
const term = deal?.travel;
requireRule(deal && term && ['active', 'released'].includes(deal.status) &&
row.player === publicDealTravelActor(deal) && row.asker === deal.from && row.turn === deal.turn &&
row.player === publicDealTravelActor(deal) && row.asker === deal.from && row.turn === publicDealTravelDueTurn(deal) &&
row.answer === term.answer && row.mixed.kind === 'action' &&
row.mixed.action.kind === term.kind && row.mixed.action.territory === term.territory &&
row.mixed.action.minimum === term.minimum && row.mixed.action.sector === undefined &&
@@ -18125,7 +18125,7 @@ function bidPromiseCompletion(g: Game, p: Player, promises = g.bidPromises ?? []
// The detached accepted declaration satisfies its own Bid Yes before
// another promise checks the remaining purse; it is not a second bid.
trial.bidPromises = recordPromisedBid(promises, p.id, opportunity, action.amount as number);
reconcileTravelPromises(trial, p.id, action, g.phase);
reconcileTravelPromises(trial, p.id, action, g.phase, g);
reconcileShipmentPromises(trial, { actor: p.id, action, phase: g.phase });
reconcileGroundMovementPromises(trial, p.id, action, g.phase);
reconcileRevivalPromises(trial, { actor: p.id, action });
@@ -22333,7 +22333,7 @@ function guardThumperKnownEncounter(g: Game, actor: string) {
const trial = structuredClone(g);
projectAfterKnownThumper(trial);
const action: Action = { type: 'card' };
reconcileTravelPromises(trial, actor, action, 1);
reconcileTravelPromises(trial, actor, action, 1, g);
reconcileBidPromises(trial, { actor, action });
reconcileRevivalPromises(trial, { actor, action });
reconcileBribePromises(trial, { actor, action });
@@ -22713,7 +22713,7 @@ function guardTurnActionOpening(g: Game, actor: string, pass: boolean) {
if (pass && trial.turnActionPromises) trial.turnActionPromises = ownTurnActionWindowClosure(trial, actor, ['amal']);
projectTurnActionOpeningEnd(trial);
const action: Action = { type: 'card' };
reconcileTravelPromises(trial, actor, action, g.phase);
reconcileTravelPromises(trial, actor, action, g.phase, g);
reconcileBidPromises(trial, { actor, action });
reconcileRevivalPromises(trial, { actor, action });
reconcileBribePromises(trial, { actor, action });
@@ -23174,7 +23174,7 @@ function bindTurnActionTruth(g: Game, p: Player, claim: TurnActionClaim, answer:
g.turnActionPromises.push(promise);
appendTravelOrder(g, 'turnActions', g.turnActionPromises.length - 1);
}
function reconcileTurnActionPromises(g: Game, actor?: string, action?: Action, sourcePhase = g.phase) {
function reconcileTurnActionPromises(g: Game, actor?: string, action?: Action, sourcePhase = g.phase, before?: Game) {
const suspendedKull = g.pendingTreacheryDiscard?.continuation.kind === 'kullSuccessDiscard' ||
g.pendingTreacheryDiscard?.continuation.kind === 'kullCounterDiscard';
if (g.pendingKull || suspendedKull) return;
@@ -23217,7 +23217,7 @@ function reconcileTurnActionPromises(g: Game, actor?: string, action?: Action, s
closeTurnActionTransport(g.turnActionPromises!, p.id, g.turn));
}
for (const promise of g.travelPromises ?? []) if (!promise.fulfilled && !promise.released &&
(promise.turn !== g.turn || g.phase > 5 || g.status !== 'playing')) promise.released = true;
(promise.turn < g.turn || promise.turn === g.turn && g.phase > 5 || g.status !== 'playing')) promise.released = true;
for (const promise of [...(g.bribePromises ?? []), ...(g.bribeExpressionPromises ?? [])])
if (!promise.fulfilled && !promise.released && (promise.turn !== g.turn || !bribePromiseTurn(g))) {
requireRule(actor !== promise.player || !promise.answer, 'You cannot voluntarily evade your Truthtrance bribe answer.');
@@ -23246,15 +23246,42 @@ function reconcileTurnActionPromises(g: Game, actor?: string, action?: Action, s
movementExpressionPromises: [] as MovementExpressionPromise[], travelPromises: [] as TravelPromise[],
bribePromises: [] as BribePromise[], bribeExpressionPromises: [] as BribeExpressionPromise[],
turnActionPromises: [] as TurnActionPromise[] };
const priorTrial = before?.turn === g.turn ? { ...before,
shipmentPromises: [] as ShipmentPromise[], movementPromises: [] as MovementPromise[],
movementExpressionPromises: [] as MovementExpressionPromise[], travelPromises: [] as TravelPromise[],
bribePromises: [] as BribePromise[], bribeExpressionPromises: [] as BribeExpressionPromise[],
turnActionPromises: [] as TurnActionPromise[] } : undefined;
for (const entry of g.travelPromiseOrder!) {
const key = ({ shipment: 'shipmentPromises', movement: 'movementPromises', movementExpression: 'movementExpressionPromises',
travel: 'travelPromises', bribe: 'bribePromises', bribeExpression: 'bribeExpressionPromises',
turnActions: 'turnActionPromises' } as const)[entry.kind];
const promise = g[key]?.[entry.index];
if (!promise || promise.player !== p.id || promise.turn !== g.turn || promise.fulfilled || promise.released) continue;
if (!promise || promise.player !== p.id || promise.turn !== g.turn) continue;
const previous = before?.[key]?.[entry.index];
const hasPrevious = !!priorTrial && !!previous && previous.player === p.id && previous.turn === g.turn &&
!previous.fulfilled && !previous.released;
if (promise.fulfilled || promise.released) {
if (hasPrevious) (priorTrial[key] as (typeof previous)[]).push(previous);
continue;
}
// Each key and row came from the same discriminated accepted-order entry.
(trial[key] as (typeof promise)[]).push(promise);
if (hasPrevious) (priorTrial[key] as (typeof previous)[]).push(previous);
if (entry.kind === 'travel' && isFutureDealTravel(g, promise as TravelPromise) && !voluntary && g.phase < 5) {
trial.travelPromises.pop();
if (hasPrevious) priorTrial!.travelPromises.pop();
continue;
}
if (turnActionCompletion(trial, p.id)) continue;
if (entry.kind === 'travel' && isFutureDealTravel(g, promise as TravelPromise)) {
// A missing early witness is not proof that later income or another
// player's action cannot make the agreed journey possible.
const witnessed = voluntary && hasPrevious && turnActionCompletion(priorTrial!, p.id);
requireRule(!witnessed, 'Your action would remove a known legal completion of an accepted public travel agreement.');
trial.travelPromises.pop();
if (hasPrevious) priorTrial!.travelPromises.pop();
continue;
}
requireRule(!voluntary, 'Your action would remove the last legal completion of an accepted whole turn-action commitment.');
promise.released = true;
log(g, 'sourceDeal' in promise && promise.sourceDeal
@@ -23423,6 +23450,11 @@ function travelRule<T>(operation: () => T): T {
function travelTargetAvailable(g: Game, actor: string): boolean {
return movementPromiseTargetAvailable(g, actor);
}
/** A later-turn deal has no resource witness when it is first accepted. */
function isFutureDealTravel(g: Game, promise: TravelPromise): boolean {
const deal = promise.sourceDeal ? g.deals?.find(entry => entry.id === promise.sourceDeal) : undefined;
return !!deal?.travel && publicDealTravelDueTurn(deal) > deal.turn;
}
function appendTravelOrder(g: Game, kind: NonNullable<Game['travelPromiseOrder']>[number]['kind'], index: number) {
g.travelPromiseOrder?.push({ kind, index });
}
@@ -23513,29 +23545,51 @@ function bindTravelTruth(g: Game, p: Player, claim: TravelClaim, answer: boolean
* travel constraint, not a Truthtrance question or a fabricated card play. */
function bindPublicDealTravel(g: Game, deal: PublicDeal) {
const term = deal.travel, player = getPlayer(g, publicDealTravelActor(deal));
requireRule(term && deal.turn === g.turn && !g.advanced && g.expansions.length === 0 &&
nativePromiseScopeSupported(g) && travelTargetAvailable(g, player.id) && !g.truthtrance &&
!hasBribePromises(g, player.id) &&
!g.deals?.some(other => other.payment?.kind === 'deferredBribe' && other.paymentStatus === 'pending'),
'This travel agreement needs a supported quiet Basic opportunity in its offer turn.');
const due = publicDealTravelDueTurn(deal);
requireRule(term && due >= g.turn && !g.advanced && g.expansions.length === 0 && nativePromiseScopeSupported(g),
'This travel agreement needs a supported Basic table and an unexpired turn.');
const action = { kind: term.kind, territory: term.territory, minimum: term.minimum };
const claim: TravelClaim = { turn: g.turn, mixed: { kind: 'action', action } };
requireRule(travelTruthAnswers(g, player, claim).includes(term.answer ? 'yes' : 'no'),
'This travel agreement cannot be accepted at the current opportunity.');
reopenMovementPreparation(g, player, 'deal');
const claim: TravelClaim = { turn: due, mixed: { kind: 'action', action } };
if (due > g.turn) {
requireRule(!g.deals?.some(other => other.payment?.kind === 'deferredBribe' &&
other.paymentStatus === 'pending' && other.payment.payer === player.id && other.payment.dueTurn === due),
'This traveler already has a payment due in the same turn.');
// These are public, physical contradictions, not a forecast of future
// funds: there is one shipment, and a required action cannot also be
// forbidden at an equal or lower threshold in that same territory.
const conflicts = liveTravelPromises(g.travelPromises ?? [], player.id, due).some(row => {
if (!row.sourceDeal || row.mixed.kind !== 'action') return false;
const prior = row.mixed.action;
if (prior.kind !== action.kind) return false;
if (action.kind === 'ship' && row.answer && term.answer && prior.territory !== action.territory) return true;
if (prior.territory !== action.territory || row.answer === term.answer) return false;
const required = row.answer ? prior.minimum : action.minimum;
const forbidden = row.answer ? action.minimum : prior.minimum;
return required >= forbidden;
});
requireRule(!conflicts, 'This future travel term conflicts with an already accepted public travel agreement.');
}
if (due === g.turn) {
requireRule(travelTargetAvailable(g, player.id) && !g.truthtrance && !hasBribePromises(g, player.id) &&
!g.deals?.some(other => other.payment?.kind === 'deferredBribe' && other.paymentStatus === 'pending'),
'This travel agreement needs a supported quiet Basic opportunity in its due turn.');
requireRule(travelTruthAnswers(g, player, claim).includes(term.answer ? 'yes' : 'no'),
'This travel agreement cannot be accepted at the current opportunity.');
reopenMovementPreparation(g, player, 'deal');
}
initializeTravelOrder(g);
(g.travelPromises ??= []).push({ ...claim, event: crypto.randomUUID(), sourceDeal: deal.id,
player: player.id, asker: deal.from, answer: term.answer, compiled: { kind: 'action', action },
shipment: { pending: g.phase < 5 || shipmentAvailable(g, player), event: null },
shipment: { pending: due > g.turn || g.phase < 5 || shipmentAvailable(g, player), event: null },
progress: [], movementClosed: false, fulfilled: false, released: false });
appendTravelOrder(g, 'travel', g.travelPromises.length - 1);
}
function reconcileTravelPromises(g: Game, actor?: string, action?: Action, sourcePhase = g.phase) {
if (g.turnActionPromises) { reconcileTurnActionPromises(g, actor, action, sourcePhase); return; }
function reconcileTravelPromises(g: Game, actor?: string, action?: Action, sourcePhase = g.phase, before?: Game) {
if (g.turnActionPromises) { reconcileTurnActionPromises(g, actor, action, sourcePhase, before); return; }
if (!g.travelPromises?.length) return;
travelRule(() => validateTravelPromises(g.travelPromises!, g.players.map(player => player.id)));
for (const promise of g.travelPromises) if (!promise.fulfilled && !promise.released &&
(promise.turn !== g.turn || g.phase > 5 || g.status !== 'playing')) promise.released = true;
(promise.turn < g.turn || promise.turn === g.turn && g.phase > 5 || g.status !== 'playing')) promise.released = true;
if (g.phase < 0 || g.phase > 5) return;
initializeTravelOrder(g);
for (const p of g.players) {
@@ -23558,18 +23612,54 @@ function reconcileTravelPromises(g: Game, actor?: string, action?: Action, sourc
(g.auction?.bidder === p.ally && (g.auction?.allyPayment ?? 0) > 0 ||
g.richeseAuction && !g.richeseAuction.outcome))) continue;
const shipments: ShipmentPromise[] = [], movements: MovementPromise[] = [], expressions: MovementExpressionPromise[] = [], travel: TravelPromise[] = [];
const priorShipments: ShipmentPromise[] = [], priorMovements: MovementPromise[] = [],
priorExpressions: MovementExpressionPromise[] = [], priorTravel: TravelPromise[] = [];
for (const entry of g.travelPromiseOrder!) {
const ledger = entry.kind === 'shipment' ? g.shipmentPromises : entry.kind === 'movement' ? g.movementPromises
: entry.kind === 'movementExpression' ? g.movementExpressionPromises : g.travelPromises;
const promise = ledger?.[entry.index];
if (!promise || promise.player !== p.id || promise.turn !== g.turn || promise.fulfilled || promise.released) continue;
if (!promise || promise.player !== p.id || promise.turn !== g.turn) continue;
const priorLedger = entry.kind === 'shipment' ? before?.shipmentPromises : entry.kind === 'movement' ? before?.movementPromises
: entry.kind === 'movementExpression' ? before?.movementExpressionPromises : before?.travelPromises;
const previous = priorLedger?.[entry.index];
const hasPrevious = before?.turn === g.turn && !!previous && previous.player === p.id &&
previous.turn === g.turn && !previous.fulfilled && !previous.released;
if (promise.fulfilled || promise.released) {
if (hasPrevious) {
if (entry.kind === 'shipment') priorShipments.push(previous as ShipmentPromise);
else if (entry.kind === 'movement') priorMovements.push(previous as MovementPromise);
else if (entry.kind === 'movementExpression') priorExpressions.push(previous as MovementExpressionPromise);
else priorTravel.push(previous as TravelPromise);
}
continue;
}
if (entry.kind === 'shipment') shipments.push(promise as ShipmentPromise);
else if (entry.kind === 'movement') movements.push(promise as MovementPromise);
else if (entry.kind === 'movementExpression') expressions.push(promise as MovementExpressionPromise);
else travel.push(promise as TravelPromise);
if (hasPrevious) {
if (entry.kind === 'shipment') priorShipments.push(previous as ShipmentPromise);
else if (entry.kind === 'movement') priorMovements.push(previous as MovementPromise);
else if (entry.kind === 'movementExpression') priorExpressions.push(previous as MovementExpressionPromise);
else priorTravel.push(previous as TravelPromise);
}
if (entry.kind === 'travel' && isFutureDealTravel(g, promise as TravelPromise) && !voluntary && g.phase < 5) {
travel.pop();
if (hasPrevious) priorTravel.pop();
continue;
}
const trial = { ...g, shipmentPromises: shipments, movementPromises: movements,
movementExpressionPromises: expressions, travelPromises: travel };
if (travelCompletion(trial, p.id)) continue;
if (entry.kind === 'travel' && isFutureDealTravel(g, promise as TravelPromise)) {
const prior = before && { ...before, shipmentPromises: priorShipments, movementPromises: priorMovements,
movementExpressionPromises: priorExpressions, travelPromises: priorTravel };
const witnessed = voluntary && hasPrevious && prior && travelCompletion(prior, p.id);
requireRule(!witnessed, 'Your action would remove a known legal completion of an accepted public travel agreement.');
travel.pop();
if (hasPrevious) priorTravel.pop();
continue;
}
requireRule(!voluntary, 'Your action would remove the last legal completion of an accepted travel commitment.');
promise.released = true;
log(g, 'sourceDeal' in promise && promise.sourceDeal
@@ -23579,20 +23669,20 @@ function reconcileTravelPromises(g: Game, actor?: string, action?: Action, sourc
}
}
function projectedTravelPromises(g: Game, actor: string) {
return (g.travelPromises ?? []).filter(promise => promise.player === actor).map(promise => ({
return (g.travelPromises ?? []).filter(promise => promise.player === actor && promise.turn <= g.turn).map(promise => ({
event: promise.event, player: promise.player, asker: promise.asker, turn: promise.turn, mixed: structuredClone(promise.mixed),
answer: promise.answer, fulfilled: promise.fulfilled, released: promise.released,
...(promise.sourceDeal ? {sourceDeal: promise.sourceDeal} : {}),
}));
}
function projectedDealTravelStatus(g: Game): Record<string, 'pending' | 'fulfilled' | 'void' | 'released'> {
const status: Record<string, 'pending' | 'fulfilled' | 'void' | 'released'> = {};
function projectedDealTravelStatus(g: Game): Record<string, 'scheduled' | 'pending' | 'fulfilled' | 'void' | 'released'> {
const status: Record<string, 'scheduled' | 'pending' | 'fulfilled' | 'void' | 'released'> = {};
for (const deal of g.deals ?? []) {
if (!deal.travel || !['active', 'released'].includes(deal.status)) continue;
const receipt = g.travelPromises?.find(row => row.sourceDeal === deal.id);
if (!receipt) continue; // Integrity validation reports malformed saved records.
status[deal.id] = receipt.fulfilled ? 'fulfilled' : receipt.releasedByDeal ? 'released'
: receipt.released ? 'void' : 'pending';
: receipt.released ? 'void' : receipt.turn > g.turn ? 'scheduled' : 'pending';
}
return status;
}
@@ -24435,6 +24525,37 @@ function finishBasicAlliedShipment(g: Game, p: Player) {
log(g, `${p.name}'s ${quote.normal + quote.elite} new allied-shipment visitors left in ${quote.territory} went to Tanks. This is the provisional Basic no-departure policy, not a publisher ruling.`);
}
/** A later-turn agreement can be accepted before its funding exists. If no
* native completion is available when its owner's actual turn is being closed,
* release only the unsupported future terms in accepted order. Other existing
* obligations keep their normal checks. A planner witness is not a prediction
* of future income, so this is deliberately delayed until the actual close. */
function releaseUnwitnessedFutureTravelAtClose(g: Game, player: Player) {
const due = liveTravelPromises(g.travelPromises ?? [], player.id, g.turn)
.filter(row => isFutureDealTravel(g, row));
if (!due.length || travelCompletion(g, player.id)) return;
initializeTravelOrder(g);
const trial = { ...g, shipmentPromises: [] as ShipmentPromise[], movementPromises: [] as MovementPromise[],
movementExpressionPromises: [] as MovementExpressionPromise[], travelPromises: [] as TravelPromise[],
bribePromises: [] as BribePromise[], bribeExpressionPromises: [] as BribeExpressionPromise[],
turnActionPromises: [] as TurnActionPromise[], travelPromiseOrder: undefined };
for (const entry of g.travelPromiseOrder!) {
const key = ({ shipment: 'shipmentPromises', movement: 'movementPromises', movementExpression: 'movementExpressionPromises',
travel: 'travelPromises', bribe: 'bribePromises', bribeExpression: 'bribeExpressionPromises',
turnActions: 'turnActionPromises' } as const)[entry.kind];
const promise = g[key]?.[entry.index];
if (!promise || promise.player !== player.id || promise.turn !== g.turn || promise.fulfilled || promise.released) continue;
const future = entry.kind === 'travel' && isFutureDealTravel(g, promise as TravelPromise);
const prior = future && travelCompletion(trial, player.id);
(trial[key] as (typeof promise)[]).push(promise);
if (future && prior && !travelCompletion(trial, player.id)) {
trial.travelPromises.pop();
promise.released = true;
log(g, `${player.name} has no witnessed legal completion for a due public travel agreement as their travel turn closes; that term is no longer binding.`);
}
}
}
/** A played movement card is already retired before queue advancement. */
function finishMovementTurn(g: Game, id: string) {
const p = getPlayer(g, id);
@@ -30202,7 +30323,7 @@ function guardCharityOrderFinish(g: Game, player: string, frame: CharityOrder, a
closed.ready.push(player);
closed.active = frame.remaining[0] ?? null;
const voluntary = { actor: player, action };
reconcileTravelPromises(closed, player, action, g.phase);
reconcileTravelPromises(closed, player, action, g.phase, g);
reconcileRevivalPromises(closed, voluntary);
reconcileBribePromises(closed, voluntary);
reconcileShipmentPromises(closed, voluntary);
@@ -36806,7 +36927,7 @@ function revivalKeepsLaterPromises(g: Game, p: Player, revival?: PendingRevival)
if (!trial.ready.includes(p.id)) trial.ready.push(p.id);
try {
const action = { type: 'ready' };
reconcileTravelPromises(trial, p.id, action, g.phase);
reconcileTravelPromises(trial, p.id, action, g.phase, g);
reconcileShipmentPromises(trial, { actor: p.id, action, phase: g.phase });
reconcileGroundMovementPromises(trial, p.id, action, g.phase);
return true;
@@ -42431,8 +42552,8 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
let next: PublicDeal;
if (action.type === 'dealRespond') {
requireRule(typeof action.accept === 'boolean', 'Choose whether to accept or decline.');
requireRule(!action.accept || !current.travel || current.turn === g.turn,
'The travel agreement was offered for an earlier turn; decline or withdraw it.');
requireRule(!action.accept || !current.travel || publicDealTravelDueTurn(current) >= g.turn,
'The travel agreement’s due turn has passed; decline or withdraw it.');
requireRule(!action.accept || current.payment?.kind !== 'deferredBribe' || current.payment.dueTurn >= g.turn,
'The deferred payment deadline has passed; decline or withdraw this offer.');
next = publicDealRule(() => respondPublicDeal(current, publicDealSeats(g), id, action.accept as boolean));
@@ -42454,7 +42575,10 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
}
g.deals = deals.map((deal, position) => position === index ? next : deal);
if (current.status === 'offered' && next.status === 'active' && next.payment?.kind === 'deferredBribe') {
const payer = getPlayer(g,next.payment.payer);
const payer = getPlayer(g,next.payment.payer), dueTurn = next.payment.dueTurn;
requireRule(!(g.travelPromises ?? []).some(row => row.sourceDeal && row.player === payer.id &&
row.turn === dueTurn && !row.fulfilled && !row.released),
'The payer already has a public travel commitment in the payment’s due turn.');
requireRule(next.payment.dueTurn !== g.turn ||
!liveTravelPromises(g.travelPromises ?? [], payer.id, g.turn).some(row => !!row.sourceDeal),
'Finish the current public travel commitment before accepting a payment due this turn.');
@@ -42669,7 +42793,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
const voluntary = forcedThumperDraw || action.type === 'passResponse' ? undefined : { actor: id, action };
const pledgeVoluntary = initiator === id ? voluntary : undefined;
reconcilePledgeCommitments(g, pledgeVoluntary);
reconcileTravelPromises(g, promiseActor, action, state.phase);
reconcileTravelPromises(g, promiseActor, action, state.phase, state);
reconcileBidPromises(g, voluntary);
reconcilePhaseBids(g, voluntary);
reconcileEarlyRicheseTruth(g, voluntary);
@@ -47120,6 +47244,7 @@ function applyActionInner(
return g;
}
if (t === 'endMovement') {
if (g.phase === 5 && g.active === id) releaseUnwitnessedFutureTravelAtClose(g, p);
requireRule(!hasMovementPromises(g, id, true),
'Complete your accepted ground-movement promises before ending movement.');
finishShipmentPromises(g, p, null);