Support earlier Basic Truthtrance shipment promises
Accept current-turn shipment and mixed questions before Movement in bare classic Basic games. Project remaining owned Charity/normal Revival, net current auction invoices and unused outgoing refunds; do not invent future purchases, incoming Bidding aid or rival income. Physical Karama payment and shipping discounts require distinct cards. Reopen owned Charity/Revival readiness when binding and prevent voluntary spending or closure from evading an answer. Preserve unresolved foreign invoices until their actual outcome; preflight Emperor gifts and extra ally revival costs before responses. Connect persistent earlier-phase guidance and current preparation to the actual table and retain phase5 shipping timing. Minimal required-delivery policies preserve a legal route; nonrestrictive answers retain ordinary legal payment. Document the explicit provisional witness/readiness interpretation and inherited profile boundaries. Verify focused rules/controls, types/lint/build, two original public Basic Storm/Charity question programmes with actual purchase2 and shipment8, and four finite minimal legal policy continuations. Full rules, wider earlier-phase configurations, comprehensive assurance and deployed acceptance remain open. No strategic calibration, saved-room operation, reset, restart or deployment.
This commit is contained in:
1 parent
6d30e7c5e1
commit
16f654071a
17 files changed
+799
-48
No files matched your search
+134
-22
@@ -22758,7 +22758,7 @@ function shipmentPromiseIntegrity(g: Game) {
|
||||
requireRule(
|
||||
shipmentPromiseModeSupported(g, promise.player) &&
|
||||
shipmentPromiseTurnAvailable(g, promise.player, shipmentAvailable(g,getPlayer(g,promise.player))),
|
||||
'The saved shipment promise does not belong to a supported unused shipment opportunity this phase.',
|
||||
'The saved shipment promise does not belong to a supported unused shipment opportunity this turn.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22773,6 +22773,81 @@ function projectPendingShipmentKarama(g: Game, player: string) {
|
||||
g.pendingKarama = null;
|
||||
completeKarama(g, getPlayer(g, pending.owner), pending.use);
|
||||
}
|
||||
type ShipmentCompletion = { actions: Action[]; early?: { phase: number; action: Action }[] };
|
||||
type ShipmentCompletionSeed = { game: Game; actions: Action[]; early: { phase: number; action: Action }[] };
|
||||
/** Private owned-stage witnesses, not an early action or a simulation of rival
|
||||
* choices, unknown draws, future gifts or the unresolved Storm outcome. */
|
||||
function earlyShipmentCompletionSeeds(initial: Game, source: Game, owner: string): ShipmentCompletionSeed[] {
|
||||
let seeds: ShipmentCompletionSeed[] = [{ game: initial, actions: [], early: [] }];
|
||||
if (source.phase > 4) return seeds;
|
||||
if (source.phase <= 2 && !(source.phase === 2 && source.ready.includes(owner))) {
|
||||
const charitySeeds: ShipmentCompletionSeed[] = [];
|
||||
for (const seed of seeds) {
|
||||
const g = structuredClone(seed.game); g.phase = 2;
|
||||
const p = getPlayer(g, owner);
|
||||
if (p.charityTurn === g.turn || charityQuote(g, p).total <= 0) continue;
|
||||
try {
|
||||
const action: Action = { type: 'charity' };
|
||||
const next = applyActionInner(g, owner, action, 'shipmentPreparation');
|
||||
charitySeeds.push({ game: next, actions: [action], early: [{ phase: 2, action }] });
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
seeds.push(...charitySeeds);
|
||||
}
|
||||
if (source.phase <= 3) {
|
||||
const fundingSeeds: ShipmentCompletionSeed[] = [];
|
||||
for (const seed of seeds) {
|
||||
const p = getPlayer(seed.game, owner), a = source.phase === 3 && !source.currentAuctionSale ? source.auction : null;
|
||||
const ownBid = a?.bidder === owner && a.bid > 0;
|
||||
const cash = ownBid ? a.bid - (a.allyPayment ?? 0) : 0;
|
||||
const committed = source.phase === 3 && source.currentAuctionSale ? 0 : committedOutgoingAid(source, getPlayer(source, owner));
|
||||
const refund = (source.aid[owner]?.amount ?? 0) - committed;
|
||||
const alternatives: (string | null)[] = [null, ...(ownBid ? p.hand
|
||||
.filter(card => canUseAsKaramaRole(seed.game, p, card)).map(card => card.id) : [])];
|
||||
for (const card of alternatives) {
|
||||
const g = structuredClone(seed.game), beneficiary = getPlayer(g, owner);
|
||||
if (!card && beneficiary.spice < cash) continue;
|
||||
beneficiary.spice += refund - (card ? 0 : cash);
|
||||
if (!Number.isSafeInteger(beneficiary.spice) || beneficiary.spice < 0) continue;
|
||||
const actions = [...seed.actions], early = [...seed.early];
|
||||
if (card) {
|
||||
const index = beneficiary.hand.findIndex(c => c.id === card);
|
||||
g.discard.push(beneficiary.hand.splice(index, 1)[0]);
|
||||
const action: Action = { type: 'decision', karama: true, card };
|
||||
actions.push(action); early.push({ phase: 3, action });
|
||||
}
|
||||
// Native Bidding refunds its unused pledges. An ally's incoming
|
||||
// Bidding escrow is never a promised future Shipment contribution.
|
||||
g.aid = {}; g.auction = null; g.currentAuctionSale = null;
|
||||
fundingSeeds.push({ game: g, actions, early });
|
||||
}
|
||||
}
|
||||
seeds = fundingSeeds;
|
||||
}
|
||||
const revivalSeeds: ShipmentCompletionSeed[] = [];
|
||||
for (const seed of seeds) {
|
||||
const g = structuredClone(seed.game); g.phase = 4;
|
||||
if (source.phase < 4) {
|
||||
g.revivalRules = newRevivalRules(); g.revivalRequests = {}; g.freeRevival = []; g.emperorExtra = {};
|
||||
const p = getPlayer(g, owner); p.revived = 0; p.freeForcesRevived = 0; p.leaderRevived = false;
|
||||
}
|
||||
revivalSeeds.push({ ...seed, game: g });
|
||||
if (source.phase === 4 && source.ready.includes(owner)) continue;
|
||||
const p = getPlayer(g, owner);
|
||||
for (let amount = 1; amount <= forceRevivalRemaining(g, p); amount++) {
|
||||
try {
|
||||
const action: Action = { type: 'revive', amount };
|
||||
const next = applyActionInner(g, owner, action, 'shipmentPreparation');
|
||||
revivalSeeds.push({ game: next, actions: [...seed.actions, action], early: [...seed.early, { phase: 4, action }] });
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
}
|
||||
return revivalSeeds.map(seed => {
|
||||
seed.game.phase = 5; seed.game.ready = []; seed.game.karamaShipping = null;
|
||||
const p = getPlayer(seed.game, owner); p.shipped = false; p.moved = 0;
|
||||
return seed;
|
||||
});
|
||||
}
|
||||
/** Search owned preparation and canonical shipment quotes, never the AI's scored shortlist. */
|
||||
function findShipmentCompletion(
|
||||
state: Game,
|
||||
@@ -22782,7 +22857,7 @@ function findShipmentCompletion(
|
||||
owner.id,
|
||||
state.turn,
|
||||
),
|
||||
): { actions: Action[] } | null {
|
||||
): ShipmentCompletion | null {
|
||||
if (
|
||||
!shipmentPromiseTurnAvailable(state, owner.id, shipmentAvailable(state,owner))
|
||||
)
|
||||
@@ -22792,7 +22867,7 @@ function findShipmentCompletion(
|
||||
const initial = structuredClone(state);
|
||||
initial.truthtrance = null;
|
||||
projectPendingShipmentKarama(initial, owner.id);
|
||||
const waiting = state.active !== owner.id;
|
||||
const waiting = state.phase < 5 || state.active !== owner.id;
|
||||
const outgoingFloor = committedOutgoingAid(state, owner);
|
||||
if (waiting) {
|
||||
// Quote this owner's later turn without taking it or resolving another
|
||||
@@ -22810,13 +22885,11 @@ function findShipmentCompletion(
|
||||
initial.decision = null;
|
||||
initial.phaseOpening = null;
|
||||
initial.shipmentPromises = [];
|
||||
const queue: { game: Game; actions: Action[] }[] = [
|
||||
{ game: initial, actions: [] },
|
||||
];
|
||||
const queue = earlyShipmentCompletionSeeds(initial, state, owner.id);
|
||||
const visited = new Set<string>();
|
||||
const destinations = [...new Set(promises.flatMap(shipmentClaimDestinations))];
|
||||
for (let index = 0; index < queue.length; index++) {
|
||||
const { game: g, actions } = queue[index];
|
||||
const { game: g, actions, early } = queue[index];
|
||||
const p = getPlayer(g, owner.id);
|
||||
const signature = JSON.stringify([
|
||||
p.spice,
|
||||
@@ -22882,6 +22955,7 @@ function findShipmentCompletion(
|
||||
if (nexus && !guildSecretEvent) recordNexusRicheseShipment(trial,shipment);
|
||||
validatePhysicalShipment(trial,shipment);
|
||||
return {
|
||||
early,
|
||||
actions: [
|
||||
...actions,
|
||||
{
|
||||
@@ -22911,7 +22985,7 @@ function findShipmentCompletion(
|
||||
};
|
||||
try {
|
||||
applyActionInner(g, p.id, action);
|
||||
return { actions: [...actions, action] };
|
||||
return { actions: [...actions, action], early };
|
||||
} catch (error) {
|
||||
if (!(error instanceof RuleError)) throw error;
|
||||
}
|
||||
@@ -22944,7 +23018,7 @@ function findShipmentCompletion(
|
||||
try {
|
||||
const next = applyActionInner(g, p.id, action, 'shipmentPreparation');
|
||||
projectPendingShipmentKarama(next, p.id);
|
||||
queue.push({ game: next, actions: [...actions, action] });
|
||||
queue.push({ game: next, actions: [...actions, action], early });
|
||||
} catch (error) {
|
||||
if (!(error instanceof RuleError)) throw error;
|
||||
}
|
||||
@@ -22952,6 +23026,28 @@ function findShipmentCompletion(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/** Minimal current-phase controls for an early commitment. Strategic bidding
|
||||
* remains deferred; preserve a legal owned route instead of exposing a future
|
||||
* shipment or asking a bot to spend the resources reserved for it. */
|
||||
function earlyShipmentGuidance(g: Game, p: Player): { preparation: { actions: Action[] } | null; policy?: Action[] } {
|
||||
const none = { preparation: null };
|
||||
const promises = liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn);
|
||||
if (g.status !== 'playing' || g.phase >= 5 || !shipmentPromiseModeSupported(g, p.id) ||
|
||||
!promises.length || promises.every(promise => matchesShipment(promise, null) === promise.answer) ||
|
||||
g.truthtrance || g.response || g.phaseOpening || g.pendingTreacheryDiscard ||
|
||||
(g.decision && !(g.decision.kind === 'auctionPayment' && g.decision.player === p.id))) return none;
|
||||
const completion = findShipmentCompletion(g, p);
|
||||
if (!completion) return none;
|
||||
const current = completion.early?.find(step => step.phase === g.phase)?.action;
|
||||
if (g.decision?.kind === 'auctionPayment')
|
||||
return { preparation: null, policy: [current?.type === 'decision' ? current : { type: 'decision', karama: false }] };
|
||||
if (g.phase === 3)
|
||||
return { preparation: null, policy: g.auction?.active === p.id ? [{ type: 'passBid' }] : [] };
|
||||
if (g.phase === 2 || g.phase === 4)
|
||||
return { preparation: current ? { actions: [current] } : null,
|
||||
policy: current ? [current] : g.ready.includes(p.id) ? [] : [{ type: 'ready' }] };
|
||||
return none;
|
||||
}
|
||||
function shipmentTruthAnswers(
|
||||
g: Game,
|
||||
p: Player,
|
||||
@@ -22964,7 +23060,10 @@ function shipmentTruthAnswers(
|
||||
...prior,
|
||||
{ ...claim, turn: g.turn, player: p.id, asker: '', answer },
|
||||
];
|
||||
if (findShipmentCompletion(g, p, promises))
|
||||
// A new commitment reopens its respondent's owned Charity/Revival Ready.
|
||||
const answerSource = g.phase === 2 || g.phase === 4
|
||||
? { ...g, ready: g.ready.filter(id => id !== p.id) } : g;
|
||||
if (findShipmentCompletion(answerSource, p, promises))
|
||||
answers.push(answer ? 'yes' : 'no');
|
||||
}
|
||||
return answers.length ? answers : ['unknown'];
|
||||
@@ -22977,6 +23076,10 @@ function bindShipmentTruth(
|
||||
asker: string,
|
||||
mixed?: MixedShipmentExpression,
|
||||
) {
|
||||
if ((g.phase === 2 || g.phase === 4) && g.ready.includes(p.id)) {
|
||||
g.ready = g.ready.filter(id => id !== p.id);
|
||||
log(g, `${p.name} has a new Truthtrance shipment commitment and must confirm readiness again after completing any needed preparation.`);
|
||||
}
|
||||
(g.shipmentPromises ??= []).push({
|
||||
turn: g.turn,
|
||||
player: p.id,
|
||||
@@ -23034,7 +23137,7 @@ function settleStoppedShipmentPromises(g: Game, p: Player, actor: string) {
|
||||
}
|
||||
function reconcileShipmentPromises(
|
||||
g: Game,
|
||||
voluntary?: { actor: string; action: Action },
|
||||
voluntary?: { actor: string; action: Action; phase?: number },
|
||||
) {
|
||||
if (g.pendingKull || g.pendingRicheseBetrayal) return;
|
||||
const kept: ShipmentPromise[] = [];
|
||||
@@ -23042,15 +23145,20 @@ function reconcileShipmentPromises(
|
||||
if (promise.turn !== g.turn || promise.released || promise.fulfilled)
|
||||
continue;
|
||||
const p = getPlayer(g, promise.player);
|
||||
const future = g.active !== p.id;
|
||||
const future = g.phase < 5 || g.active !== p.id;
|
||||
const earlyVoluntary = (voluntary?.phase ?? g.phase) < 5 && voluntary &&
|
||||
(['bid', 'revive', 'reviveLeader', 'reviveKwisatz', 'emperorRevival'].includes(voluntary.action.type) ||
|
||||
voluntary.action.type === 'ready' && [2, 4].includes(voluntary.phase ?? g.phase));
|
||||
const ownVoluntary = voluntary?.actor === p.id &&
|
||||
(['card', 'bribe', 'pledgeAid', 'emperorGift'].includes(voluntary.action.type) ||
|
||||
earlyVoluntary ||
|
||||
future && voluntary.action.type === 'decision');
|
||||
// A foreign pending parent may spend or refund its approved funding. Wait
|
||||
// for that outcome; never release a future promise just for waiting.
|
||||
if (future && !ownVoluntary && (g.truthtrance || g.response || g.decision ||
|
||||
g.phaseOpening || g.pendingShipment || g.pendingGuildTransport ||
|
||||
g.pendingHomeworldShipment || g.pendingTreacheryDiscard || g.ornithopter)) {
|
||||
g.pendingHomeworldShipment || g.pendingTreacheryDiscard || g.ornithopter ||
|
||||
g.phase === 3 && !g.currentAuctionSale && g.auction?.bidder === p.ally && (g.auction?.allyPayment ?? 0) > 0)) {
|
||||
kept.push(promise);
|
||||
continue;
|
||||
}
|
||||
@@ -30755,7 +30863,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
finishLeaderSkillCustody(g, state);
|
||||
leaderSkillsIntegrity(g);
|
||||
reconcileBattlePromises(g, { actor: id, action });
|
||||
reconcileShipmentPromises(g, { actor: id, action });
|
||||
reconcileShipmentPromises(g, { actor: id, action, phase: state.phase });
|
||||
settleAutomaticContinuations(g);
|
||||
observeOccupation(g);
|
||||
discoveryIntegrity(g);
|
||||
@@ -33758,13 +33866,6 @@ function applyActionInner(
|
||||
maximum,
|
||||
t === 'emperorGift' ? 'Spice' : 'Forces',
|
||||
);
|
||||
if (t === 'emperorGift' && liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length) {
|
||||
// Permission to cancel the gift is not funding for an already promised
|
||||
// shipment. Validate the voluntary outflow before opening its response.
|
||||
const afterGift = structuredClone(g);
|
||||
getPlayer(afterGift, p.id).spice -= amount;
|
||||
reconcileShipmentPromises(afterGift, { actor: p.id, action });
|
||||
}
|
||||
const elite =
|
||||
t === 'emperorRevival'
|
||||
? eliteChoice(
|
||||
@@ -33783,6 +33884,14 @@ function applyActionInner(
|
||||
p.spice >= paidForceRevivalCost(recipient, amount, elite),
|
||||
'Not enough spice for these extra revivals.',
|
||||
);
|
||||
if (liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length) {
|
||||
// A later responder cannot erase the owner's voluntary gift/revival
|
||||
// invoice. Check its allowed outcome before opening the response.
|
||||
const afterPayment = structuredClone(g);
|
||||
getPlayer(afterPayment, p.id).spice -= t === 'emperorGift'
|
||||
? amount : paidForceRevivalCost(recipient, amount, elite);
|
||||
reconcileShipmentPromises(afterPayment, { actor: p.id, action });
|
||||
}
|
||||
g.response = {
|
||||
kind: t,
|
||||
elite,
|
||||
@@ -35212,6 +35321,7 @@ export function viewGame(state: Game, id: string) {
|
||||
settleAdvisors(g);
|
||||
normalizeBattle(g);
|
||||
const me = getPlayer(g, id);
|
||||
const earlyShipment = earlyShipmentGuidance(g, me);
|
||||
const sharedDuke = projectedSharedDuke(g, id);
|
||||
const choamPlayer = byFaction(g, 'choam');
|
||||
let victoryProgress: StrongholdProgress[] = [];
|
||||
@@ -35285,8 +35395,10 @@ export function viewGame(state: Game, id: string) {
|
||||
// Mixed compiled claims contain private past-clause answers. The complete
|
||||
// original question and whole answer remain public through truthHistory.
|
||||
shipmentPromises: (g.shipmentPromises ?? []).filter(p => !p.mixed || p.player === id),
|
||||
shipmentPreparation: earlyShipment.preparation,
|
||||
shipmentPromisePolicy: earlyShipment.policy,
|
||||
shipmentCompletion:
|
||||
g.active === id &&
|
||||
g.phase === 5 && g.active === id &&
|
||||
!g.pendingTreacheryDiscard &&
|
||||
!g.response && !g.pendingKarama && !g.truthtrance &&
|
||||
!g.decision && !g.phaseOpening && !g.pendingShipment &&
|
||||
|
||||
Reference in new issue
Block a user