Support scheduled travel agreements
Verify and publish checkpoint container / container (push) Successful in 11m8s
Verify and publish checkpoint container / container (push) Successful in 11m8s
This commit is contained in:
1 parent
e1e02fc1a1
commit
1e81ce8ad8
13 files changed
+767
-56
No files matched your search
+157
-32
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user