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
@@ -4070,6 +4070,9 @@ export function botActions(g: GameView): Action[] {
|
||||
if (g.homeworldTupileBlocked) return [];
|
||||
if (g.decision?.kind === 'homeworldTupileCleanup' || g.homeworldTupileCleanup)
|
||||
return tupileCleanupActions(g);
|
||||
if (g.shipmentPromisePolicy && g.status === 'playing' &&
|
||||
!g.roomControl?.paused && !g.roomControl?.closed && !g.automaticContinuationPending)
|
||||
return g.shipmentPromisePolicy;
|
||||
if (g.nexusHarkonnenBetrayalReaction) {
|
||||
if (g.status !== 'playing' || g.roomControl?.paused || g.roomControl?.closed ||
|
||||
g.automaticContinuationPending) return [];
|
||||
|
||||
+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 &&
|
||||
|
||||
+17
-8
@@ -2921,16 +2921,17 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
title: 'Truthtrance: shipment promises',
|
||||
category: 'Cards',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary:
|
||||
'Bind an unused reserve shipment this phase, including a player whose turn is still ahead, to a named destination and minimum physical count.',
|
||||
'Bind this turn’s reserve shipment to a destination and physical minimum, including earlier Basic phases and players whose shipment turn is still ahead.',
|
||||
steps: [
|
||||
'Declare Truthtrance, select an eligible player whose unused shipment is active or still ahead this phase, and choose Bind a shipment from reserves. Choose a printed destination and minimum of one to twenty forces, or combine two shipment conditions using AND or OR. Each describes the same shipment. Automatic enforcement supports base Basic and base Advanced games, including Guild, before that player ships and after pending decisions finish. Earlier players keep their original turns; answering never grants an early shipment. Other optional configurations keep their existing boundaries.',
|
||||
'Declare Truthtrance, select an eligible player and choose Bind a shipment from reserves. Bare Basic rules allow the question during Storm through Revival as well as before that player’s shipment. During Shipment and Movement, base Basic and Advanced, including Guild, also support an unused active or waiting turn. Finish pending decisions first. Choose a printed destination and minimum of one to twenty forces, or combine conditions using AND or OR; each describes the same shipment. Answering never grants an early shipment. Earlier Advanced, expansion and optional configurations keep their existing boundaries.',
|
||||
'Yes requires the complete statement to match while possible: all AND conditions or at least one OR condition. No requires the whole statement to be false: at least one AND condition false, or every OR condition false. A smaller count, different destination, transport of forces already on the board or no shipment may satisfy No. Forces already in the destination and ground movement do not satisfy the question. Fremen reinforcements and Guild transport from southern reserves do count.',
|
||||
'The target privately sees feasible answers based on their own resources and earlier promises. Available Ghola revival, Karama discounts, incoming pledged aid and reclaiming their own unused pledge are included. Other players’ unplayed cards and future voluntary gifts are not assumed.',
|
||||
'Accepted whole answers are public and constrain the event together with earlier promises. When an answer requires a shipment, honor it before ground movement or finishing and do not voluntarily spend away the last legal completion. A completed opposing effect that makes compliance impossible releases it publicly.',
|
||||
'The target privately sees feasible answers based on the current board, owned resources and all earlier promises. Before Movement, include remaining owned Charity and normal Revival, the current winning invoice and unused outgoing Bidding refunds; incoming Bidding aid expires. Auction payment and shipping discounts need distinct physical Karamas. At shipment time, available Ghola, Karama and current pledged aid retain their normal rules. Unknown purchases, unresolved Storm outcomes, rival gifts and future income are not assumed.',
|
||||
'Accepted whole answers are public and constrain the event together. A new answer reopens your Charity or Revival readiness when needed. Claim or revive before marking Ready if that is your last legal route; bids, gifts and paid revivals cannot spend it away. Honor a required shipment before ground movement or finishing. An unresolved foreign bid and its income responses finish before release. A completed opposing effect that makes compliance impossible releases it publicly.',
|
||||
'Guild interception and rate or transport-permission responses finish before delivery. An opposing stop consumes the shipment without payment or arrival: answers matching no shipment fulfill and impossible answers release. You cannot stop your own shipment to evade your answer. An unaffordable canceled rate leaves shipment available; a retained owned discount or ordinary Fremen reinforcement may still provide a legal route.',
|
||||
'Choose Mixed current fact and shipment for nested AND/OR groups of supported current facts and this-turn reserve shipments. Current facts freeze privately when answered: Yes OR a true current fact can allow skipping even after later losing that holding; No AND a false current fact can allow shipping even after that fact changes. Unknown is not false. Only the whole question and answer are public.',
|
||||
'Your own promises show the original grouped question, not private compiled clause results. While waiting, the promise remains visible without an executable action. Optional private next-step guidance includes preparation, funding or shipment only on your actual turn after pending controls finish. All four legal policies use server-feasible choices and the same complete route. Earlier phases, other modules, expansion shipment modes, other future actions and arbitrary prose enforcement remain unfinished.',
|
||||
'Your promises retain the original grouped question without exposing private compiled facts. Earlier phases show later-this-turn guidance and only an available current Charity or Revival preparation. Shipment actions wait for your actual phase5 turn and finished controls. The owned-route/readiness protocol is a provisional interpretation. All four profiles retain a minimal legal path; answers allowing a skip retain ordinary legal payment choices. Earlier Advanced/modules, expansion shipment modes, other future actions and arbitrary prose enforcement remain unfinished.',
|
||||
],
|
||||
example:
|
||||
'Yes to at least six forces to Carthag and No to at least eight there leaves a shipment of six or seven. Moving existing forces into Carthag does not fulfill either shipment event.',
|
||||
@@ -2940,7 +2941,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Authoritative reserve commitments, mixed current-fact AND/OR, frozen clauses, owned preparation, actual shipment/skip and opposing release include base Advanced Guild interception, permission and rates. Same-phase waiting turns retain joint promises through native Guild WAIT/TAKE and paid intervening turns. Own gifts/accompaniment cannot spend the last completion; broader phases/modules remain unfinished.',
|
||||
'Bare Basic phases0–4 use explicit provisional current-board/owned-stage feasibility, auction liabilities, distinct card costs, expiring aid and readiness reopening. Voluntary bids/payments/closure preserve a route; foreign invoices await their actual outcome. Existing phase5 waiting/native Guild interception, permission and rates remain. Earlier Advanced/modules and wider future actions remain unfinished.',
|
||||
},
|
||||
{
|
||||
area: 'Player controls',
|
||||
@@ -2964,7 +2965,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Verification',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Current waiting-turn batch passes146 rule cases plus one updated existing in-memory scope case, types/lint/build, three original-action/rendered-table programmes and four finite legal policy continuations. Earlier persistence/privacy evidence is historical; full Truthtrance, modes, final assurance and deployed acceptance remain open.',
|
||||
'Current early-phase batch passes18 files131/131, types/lint/build, two original Storm/Charity question programmes with actual purchase2 and shipment8, rendered table guidance and four finite legal profiles. Controlled preparation, readiness, physical cards, invoice allow/cancel and No overbids remain separate. Earlier persistence/privacy evidence is historical; full Truthtrance, modes, assurance and deployed acceptance remain open.',
|
||||
evidence: [
|
||||
'tests/shipment-promises.test.ts',
|
||||
'tests/shipment-promises-bots.test.ts',
|
||||
@@ -2975,6 +2976,14 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'tests/compound-shipment-recovery.test.ts',
|
||||
'tests/mixed-shipment-question.test.ts',
|
||||
'tests/mixed-shipment-engine.test.ts',
|
||||
'tests/truthtrance-early-shipment-engine.test.ts',
|
||||
'tests/truthtrance-early-shipment-controls.test.tsx',
|
||||
'tests/fixture-truthtrance-early-shipment.ts',
|
||||
'components/shipment-promises.tsx',
|
||||
'components/truthtrance.tsx',
|
||||
'components/game-table.tsx',
|
||||
'game/shipment-promises.ts',
|
||||
'game/truthtrance.ts',
|
||||
'tests/truthtrance-future-shipment.test.ts',
|
||||
'tests/truthtrance-future-shipment-engine.test.ts',
|
||||
'tests/fixture-truthtrance-future-shipment.ts',
|
||||
@@ -3001,7 +3010,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'The questioned player answers publicly. A definite answer discards the card. If the answer cannot be known, the holder may ask a different question or save Truthtrance for later.',
|
||||
'A saved definite answer resumes automatically: its public answer, binding promise and consumed card are recorded once, then the next queued question or interrupted action resumes. Private answer and preparation guidance is unavailable during this automatic recovery.',
|
||||
'Freeform questions also accept public Yes, No, or I don’t know answers. Their truthfulness relies on the players. AI answers structured facts and battle-plan questions; it cannot yet interpret freeform questions.',
|
||||
'An answer may bind actions or decisions during the current turn only. The answering player must do everything in their power to comply; if compliance later becomes impossible, the answer is no longer binding. Freeform promises are recorded in the table’s turn history. Arbitrary freeform enforcement and broader action commitments remain unfinished. Structured reserve-shipment promises are enforced for the active unused shipment in base Basic and no-Guild Advanced games; see the shipment promise guide.',
|
||||
'An answer may bind actions or decisions during the current turn only. The answering player must do everything in their power to comply; if compliance later becomes impossible, the answer is no longer binding. Freeform promises are recorded in the table’s turn history. Arbitrary freeform enforcement and broader action commitments remain unfinished. Structured reserve-shipment promises include earlier bare Basic phases and unused active or waiting phase5 turns in base Basic/Advanced, including Guild; see the shipment promise guide.',
|
||||
'Battle-plan questions can bind dial values or ranges, named weapon/defense slots, a leader or either Cheap Hero, spice support and Kwisatz use. AND/OR combinations preserve legal alternatives. Only answers with a legal completion now or through available Ghola preparation are offered; a sealed plan fixes its answer. Previous battle commitments, Voice and prescience also apply. Private preparation guidance can play a required Ghola, then a compliant-plan example can fill the form for review before sealing.',
|
||||
'Named conditions include Reinforcements and Harass slot occupancy without making either a weapon/defense category. In clean classic Homeworlds, a source-aware legal completion can choose the native fee needed for a promised dial; review and filling retain that exact allocation. Active preview, answers/history and server records use the actual combat location name, including native Worlds, rather than treating every battle as an Arrakis territory.',
|
||||
'If a completed opposing power makes a battle answer impossible, it is publicly released. A pending Voice response resolves before that release. A player cannot voluntarily spend a promised battle card or give away necessary support spice to evade the answer. Feasibility includes available Ghola cards, eligible leader/Kwisatz or force revivals, and possible revival income. Each actual card can be spent only once. Other future card/power sequences and the full timing audit remain unfinished.',
|
||||
|
||||
@@ -149,6 +149,7 @@ export function shipmentClaimText(
|
||||
/** Software readiness boundary; this is not a printed restriction on Truthtrance. */
|
||||
export function shipmentPromiseModeSupported(game: {
|
||||
advanced: boolean;
|
||||
phase?: number;
|
||||
expansions: readonly unknown[];
|
||||
players: readonly { id?: string; faction: string }[];
|
||||
techTokens?: unknown;
|
||||
@@ -159,7 +160,11 @@ export function shipmentPromiseModeSupported(game: {
|
||||
semutaPreview?: unknown;
|
||||
leaderSkills?: unknown;
|
||||
discoveryEnabled?: unknown;
|
||||
ecazTreachery?: unknown;
|
||||
}, respondent = game.active): boolean {
|
||||
if (game.phase !== undefined && game.phase < 5 &&
|
||||
(game.advanced || game.expansions.length || game.techTokens || game.strongholdCards ||
|
||||
game.nexusCards || game.homeworlds || game.leaderSkills || game.discoveryEnabled || game.ecazTreachery)) return false;
|
||||
if (game.semutaPreview === true && game.expansions.length === 1 &&
|
||||
game.expansions[0] === 'choam' && !game.techTokens &&
|
||||
!game.strongholdCards && !game.nexusCards && !game.homeworlds &&
|
||||
@@ -188,8 +193,9 @@ export function shipmentPromiseTurnAvailable(game: {
|
||||
active?: string | null;
|
||||
movementRemaining?: readonly string[] | null;
|
||||
}, player: string, unused: boolean): boolean {
|
||||
return game.status === 'playing' && game.phase === 5 && unused &&
|
||||
(game.active === player || !!game.movementRemaining?.includes(player));
|
||||
return game.status === 'playing' && Number.isInteger(game.phase) && game.phase >= 0 &&
|
||||
(game.phase < 5 || game.phase === 5 && unused &&
|
||||
(game.active === player || !!game.movementRemaining?.includes(player)));
|
||||
}
|
||||
export type ShipmentPromise = ShipmentClaim & {
|
||||
turn: number;
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
|
||||
!g.decision &&
|
||||
!g.phaseOpening &&
|
||||
!g.pendingShipment,
|
||||
'Structured shipment promises require a supported unused shipment this phase: the respondent must be active or still waiting for their turn. Finish pending decisions first.',
|
||||
'Structured shipment promises require a supported unused shipment this turn. During Shipment and Movement the respondent must be active or still waiting. Finish pending decisions first.',
|
||||
);
|
||||
try {
|
||||
if (v.kind === 'mixedShipment') {
|
||||
|
||||
Reference in new issue
Block a user