Support native early Basic movement promises

Price native Once Around and Silent funding in early movement forecasts, retaining full sealed donor locks and exact paid-sale boundaries. Add current zero/pass guidance, fixed non-CHOAM preopening Charity entitlement and shared owned normal-revival projection while actual counters remain pending. Preserve the mandatory Ix phase-opening continuation.

Validation: affected rules/control tests, types/lint, production build and six gameplay/control/table-rendering programmes with eight existing-profile native runs. Wider preparations, CHOAM own unresolved opening, modules/Advanced, full rules and deployed acceptance remain open. No saved-room, database, restart or deployment operation.
This commit is contained in:
Quintine committed 2026-10-24 16:05:41 +11:00
1 parent f995b6f4cd
commit c311ed4251
16 files changed
+787 -54

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
import type { RicheseAuction } from './richese-auction';
import { richeseOwnCommitment, richeseAllyCommitment, type RicheseFunding } from './richese-funding';
export type EarlyAuctionFunding = {
phase: number;
owner: string;
ally: string | null;
paid: boolean;
normal: { bidder: string | null; bid: number; allyPayment?: number } | null;
native?: RicheseAuction | null;
funding?: RicheseFunding;
pledge: number;
};
/** Private owned-stage funding, not settlement or a prediction of who wins.
* Silent donor locks use submission status, never the ally's private split.
* A current sale already paid its invoice and consumed any donated escrow. */
export function earlyAuctionFunding(input: EarlyAuctionFunding) {
const { owner, ally, pledge } = input;
const normal = input.phase === 3 && !input.paid ? input.normal : null;
const native = input.phase === 3 && !input.paid ? input.native : null;
const funding = input.funding ?? {};
const normalCash = normal?.bidder === owner ? normal.bid - (normal.allyPayment ?? 0) : 0;
const nativeCash = richeseOwnCommitment(native, funding, owner);
const normalDonor = ally && normal?.bidder === ally ? normal.allyPayment ?? 0 : 0;
const sealedLock = native?.method === 'silent' && !native.outcome && ally &&
Object.hasOwn(native.sealed, ally);
const nativeDonor = sealedLock ? pledge : richeseAllyCommitment(native, funding, owner);
const refund = pledge - normalDonor - nativeDonor;
for (const amount of [normalCash, nativeCash, refund])
if (!Number.isSafeInteger(amount) || amount < 0)
throw new RangeError('The current auction funding exceeds its owned cash or pledged escrow.');
return { normalCash, nativeCash, refund };
}
+83 -29
View File
@@ -1,3 +1,4 @@
import { earlyAuctionFunding } from './early-auction-funding';
import { recordMovementDiscardKnowledge, knownMovementDiscards, recordMovementBoxKnowledge, recordMovementClaimKnowledge, clearMovementDiscardKnowledge, type MovementDiscardKnowledge } from './movement-discard-knowledge';
import { movementShipmentPreparations } from './movement-shipment-preparation';
import { MovementClaimError, liveMovementPromises, movementEventAllowed, movementPromiseModeSupported, movementPromiseTargetAvailable, settleMovementEvent, finishMovementPromises, validMovementClaim, type MovementClaim, type MovementPromise } from './movement-promises';
@@ -16434,6 +16435,10 @@ function recoverableUnsupportedShipment(shipment: PendingShipment) {
function groundMovementProbe(source: Game, actor: string): Game {
const game = structuredClone(source);
projectPendingShipmentKarama(game, actor);
if (game.phase < 5) {
if (game.pendingRevival?.player === actor) projectOwnedOrdinaryRevival(game, actor);
else game.pendingRevival = null;
}
if (game.active !== actor) {
// Forecast only this owner's unused turn on the current board. Do not
// complete a foreign declaration, price, flight or private search, and do
@@ -16506,6 +16511,10 @@ function groundMovementCompletion(source: Game, actor: string,
promises = liveMovementPromises(source.movementPromises ?? [], actor, source.turn)): MovementCompletion | null {
if (!movementPromiseModeSupported(source) || source.phase < 0 || source.phase > 5) return null;
const game = groundMovementProbe(source, actor);
if (source.phase === 5 && source.phaseOpening?.initialize) {
resetGroundMovementForecast(game, actor);
return groundMovementCompletionAtMovement(game, actor, promises);
}
if (source.phase < 5) {
const paid = source.pendingNullentropy;
if (paid?.player === actor) {
@@ -16519,10 +16528,7 @@ function groundMovementCompletion(source: Game, actor: string,
return null;
}
for (const seed of earlyShipmentCompletionSeeds(game, source, actor)) {
resetPlayerMovement(getPlayer(seed.game, actor));
seed.game.hajr = seed.game.hajr.filter(player => player !== actor);
seed.game.ornithopter = null;
seed.game.movementRemaining = [actor];
resetGroundMovementForecast(seed.game, actor);
const completion = groundMovementCompletionAtMovement(seed.game, actor, promises);
if (completion) return { actions: [...seed.actions, ...completion.actions], early: seed.early };
}
@@ -16530,6 +16536,12 @@ function groundMovementCompletion(source: Game, actor: string,
}
return groundMovementCompletionAtMovement(game, actor, promises);
}
function resetGroundMovementForecast(game: Game, actor: string) {
resetPlayerMovement(getPlayer(game, actor));
game.hajr = game.hajr.filter(player => player !== actor);
game.ornithopter = null;
game.movementRemaining = [actor];
}
function groundMovementCompletionAtMovement(game: Game, actor: string, promises: readonly MovementPromise[]): MovementCompletion | null {
if (!movementPromiseTargetAvailable(game.pendingNullentropy?.player === actor ? { ...game, decision: null } : game, actor)) return null;
return findMovementCompletion(game, actor, promises, {
@@ -16617,12 +16629,21 @@ function earlyGroundMovementPolicy(g: Game, player: Player): Action[] | null {
if (g.status !== 'playing' || !movementPromiseModeSupported(g) || !live.some(promise => promise.answer) ||
g.truthtrance || g.response || g.phaseOpening || g.pendingTreacheryDiscard ||
(g.decision && !(g.decision.kind === 'auctionPayment' && g.decision.player === player.id))) return null;
// Passing cannot spend the reserved route. In particular, a foreign Silent
// invoice may still refund escrow: do not replace a legal zero with a bid
// merely because that unresolved reservation prevents a current witness.
if (g.phase === 3 && !g.decision) {
const lot = g.richeseAuction;
if (lot) return !lot.outcome && lot.eligible.includes(player.id) &&
(lot.method === 'silent' ? !Object.hasOwn(lot.sealed, player.id) : lot.active === player.id)
? [{ type: 'richeseBid', event: lot.event, amount: lot.method === 'silent' ? 0 : null, allyPayment: 0 }] : [];
return g.auction?.active === player.id ? [{ type: 'passBid' }] : [];
}
const completion = groundMovementCompletion(g, player.id);
if (!completion) return null;
const current = completion.early?.find(step => step.phase === g.phase)?.action;
if (g.decision?.kind === 'auctionPayment')
return [current?.type === 'decision' ? current : { type: 'decision', karama: false }];
if (g.phase === 3) return g.auction?.active === player.id ? [{ type: 'passBid' }] : [];
if (g.phase === 2 || g.phase === 4) return current ? [current] : g.ready.includes(player.id) ? [] : [{ type: 'ready' }];
return null;
}
@@ -16673,11 +16694,14 @@ function reconcileGroundMovementPromises(g: Game, actor?: string, action?: Actio
g.pendingChoamMove?.player === p.id) continue;
const early = g.phase < 5 || sourcePhase < 5;
const voluntary = actor === p.id && (!early || !!action &&
(['card', 'bribe', 'pledgeAid', 'emperorGift', 'bid', 'revive', 'reviveLeader', 'reviveKwisatz', 'emperorRevival', 'decision'].includes(action.type) ||
(['card', 'bribe', 'pledgeAid', 'emperorGift', 'bid', 'richeseGift', 'revive', 'reviveLeader', 'reviveKwisatz', 'emperorRevival', 'decision'].includes(action.type) ||
action.type === 'richeseBid' && typeof action.amount === 'number' && action.amount > 0 ||
action.type === 'ready' && [2, 4].includes(sourcePhase)));
const busy = !!(g.response || g.decision || g.phaseOpening || g.pendingTreacheryDiscard ||
g.pendingAmbassador || g.pendingTerrorEntry || g.pendingArrivalOverlap || g.pendingArrivalReaction?.length ||
g.phase === 3 && !g.currentAuctionSale && g.auction?.bidder === p.ally && (g.auction?.allyPayment ?? 0) > 0);
g.phase === 3 && !g.currentAuctionSale &&
(g.auction?.bidder === p.ally && (g.auction?.allyPayment ?? 0) > 0 ||
g.richeseAuction && !g.richeseAuction.outcome));
if (busy && !voluntary) continue;
if (groundMovementCompletion(g, p.id, live)) continue;
requireRule(!voluntary, 'Your action would remove the last legal completion of an accepted ground-movement promise.');
@@ -23341,7 +23365,14 @@ function earlyShipmentCompletionSeeds(initial: Game, source: Game, owner: string
if (p.charityTurn === g.turn || charityQuote(g, p).total <= 0) continue;
try {
const action: Action = { type: 'charity' };
const next = applyActionInner(g, owner, action, 'shipmentPreparation');
// Before CHOAM's opening response, another faction's fixed ordinary
// entitlement is unchanged by its outcome. Quote only that entitlement:
// no CHOAM income, payment or invented receipt enters this forecast.
let next: Game;
if (source.phase < 2 && byFaction(g, 'choam') && g.choamCharity?.turn !== g.turn) {
if (p.faction === 'choam') continue;
p.spice += charityQuote(g, p).total; p.charityTurn = g.turn; next = g;
} else next = applyActionInner(g, owner, action, 'shipmentPreparation');
charitySeeds.push({ game: next, actions: [action], early: [{ phase: 2, action }] });
} catch (error) { if (!(error instanceof RuleError)) throw error; }
}
@@ -23352,15 +23383,17 @@ function earlyShipmentCompletionSeeds(initial: Game, source: Game, owner: string
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 funding = earlyAuctionFunding({ phase: source.phase, owner, ally: p.ally,
paid: !!source.currentAuctionSale, normal: source.auction,
native: source.richeseAuction, funding: source.richeseFunding,
pledge: source.aid[owner]?.amount ?? 0 });
const cash = funding.normalCash;
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 (beneficiary.spice < funding.nativeCash + (card ? 0 : cash)) continue;
beneficiary.spice += funding.refund - (card ? 0 : cash) - funding.nativeCash;
if (!Number.isSafeInteger(beneficiary.spice) || beneficiary.spice < 0) continue;
const actions = [...seed.actions], early = [...seed.early];
if (card) {
@@ -23372,6 +23405,7 @@ function earlyShipmentCompletionSeeds(initial: Game, source: Game, owner: string
// 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;
g.richeseAuction = null; g.richeseFunding = {}; g.richeseBidding = null;
fundingSeeds.push({ game: g, actions, early });
}
}
@@ -23391,6 +23425,7 @@ function earlyShipmentCompletionSeeds(initial: Game, source: Game, owner: string
try {
const action: Action = { type: 'revive', amount };
const next = applyActionInner(g, owner, action, 'shipmentPreparation');
projectOwnedOrdinaryRevival(next, owner);
revivalSeeds.push({ game: next, actions: [...seed.actions, action], early: [...seed.early, { phase: 4, action }] });
} catch (error) { if (!(error instanceof RuleError)) throw error; }
}
@@ -23701,9 +23736,10 @@ function reconcileShipmentPromises(
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 === 'richeseBid' && typeof voluntary.action.amount === 'number' && voluntary.action.amount > 0 ||
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) ||
(['card', 'bribe', 'pledgeAid', 'emperorGift', 'richeseGift'].includes(voluntary.action.type) ||
earlyVoluntary ||
future && voluntary.action.type === 'decision');
// A foreign pending parent may spend or refund its approved funding. Wait
@@ -23711,7 +23747,9 @@ function reconcileShipmentPromises(
if (future && !ownVoluntary && (g.truthtrance || g.response || g.decision ||
g.phaseOpening || g.pendingShipment || g.pendingGuildTransport ||
g.pendingHomeworldShipment || g.pendingTreacheryDiscard || g.ornithopter ||
g.phase === 3 && !g.currentAuctionSale && g.auction?.bidder === p.ally && (g.auction?.allyPayment ?? 0) > 0)) {
g.phase === 3 && !g.currentAuctionSale &&
(g.auction?.bidder === p.ally && (g.auction?.allyPayment ?? 0) > 0 ||
g.richeseAuction && !g.richeseAuction.outcome))) {
kept.push(promise);
continue;
}
@@ -26490,6 +26528,35 @@ function currentRevivalResumeQuote(g: Game, stage: 'stop' | 'finish') {
throw error;
}
}
/** Shared physical accounting after the authoritative revival quote. */
function commitRevivalForcePieces(g: Game, p: Player, revival: PendingRevival) {
const n = revival.amount!, elite = revival.elite ?? 0;
addNativeReserves(g, p, n, elite);
p.tanks -= n;
if (revival.emperorExtra)
g.emperorExtra[p.id] = (g.emperorExtra[p.id] ?? 0) + n;
else if (!revival.nexusEmperorEvent) {
// Keep unknown historical free usage unknown in both real and quoted paths.
if (p.freeForcesRevived !== undefined || p.revived === 0)
p.freeForcesRevived = (p.freeForcesRevived ?? 0) + revival.free;
p.revived += n;
}
if (p.elites) { p.elites.tanks -= elite; p.elites.revived += elite; }
}
/** Private allowed-outcome projection for the owner's declared ordinary force
* request. Native CHOAM/Tleilaxu controls remain real unresolved decisions.
* Do not execute their choices, income, technology or continuation effects. */
function projectOwnedOrdinaryRevival(g: Game, owner: string) {
const pending = g.pendingRevival;
if (!pending || pending.player !== owner || pending.kind !== 'forces' ||
pending.emperorExtra || pending.nexusEmperorEvent || pending.payer && pending.payer !== owner || g.homeworlds) return;
const quote = currentRevivalResumeQuote({ ...g, pendingRevival: { ...pending, checks: [] } }, 'finish');
if (quote.outcome !== 'revive') return;
const p = getPlayer(g, owner);
p.spice -= quote.pending.cost;
commitRevivalForcePieces(g, p, quote.pending);
g.pendingRevival = null; g.decision = null; g.response = null;
}
function finishRevival(
g: Game,
quote: RevivalResumeQuote = currentRevivalResumeQuote(g, 'finish'),
@@ -26538,20 +26605,7 @@ function finishRevival(
const source = revival.nexusEmperorEvent ? 'nexusEmperor' :
revival.emperorExtra ? 'emperorExtra' : 'normal';
const group = { amount: n, elite, free: revival.free };
addNativeReserves(g, p, n, elite);
p.tanks -= n;
if (revival.emperorExtra)
g.emperorExtra[p.id] = (g.emperorExtra[p.id] ?? 0) + n;
else if (!revival.nexusEmperorEvent) {
// Preserve unknown legacy usage instead of guessing how earlier paid returns were priced.
if (p.freeForcesRevived !== undefined || p.revived === 0)
p.freeForcesRevived = (p.freeForcesRevived ?? 0) + revival.free;
p.revived += n;
}
if (p.elites) {
p.elites.tanks -= elite;
p.elites.revived += elite;
}
commitRevivalForcePieces(g, p, revival);
finishNexusEmperorRevival(g, revival, 'returned');
if (quote.techIncome) {
const income = quote.techIncome;
-4
View File
@@ -85,10 +85,6 @@ export function finishMovementPromises<T extends MovementPromise>(
* in the engine. Truthtrance and an ordinary active Ornithopter may remain. */
export function movementPromiseModeSupported(g: Game): boolean {
return g.advanced === false &&
// Early owned-stage forecasts currently price ordinary classic invoices.
// Native phase5 coverage does not establish Richese/CHOAM bidding prices.
(g.phase >= 5 || (!g.expansions.length && g.players.every(player =>
FACTIONS.find(entry => entry.id === player.faction)?.expansion === 'base'))) &&
g.players.length >= 2 && g.players.length <= 6 &&
g.expansions.every(id => ['ix', 'choam', 'ecaz'].includes(id)) &&
new Set(g.expansions).size === g.expansions.length &&
+7 -7
View File
@@ -2930,12 +2930,12 @@ export const RULE_TOPICS: RuleTopic[] = [
developmentStage: 'Prototyped',
summary: 'Bind one future qualifying physical ground movement before a Basic respondent finishes their current-round turn.',
steps: [
'Choose Bind ground movement this turn, a printed territory and a minimum of one to twenty physical forces. Classic Basic supports clean phases0–4 before Movement. During Movement ask an active player before or after shipment, or a still-unused waiting respondent. Selected native families currently begin in phase5; native early invoices, optional modules, Advanced and later rounds remain unfinished.',
'Choose Bind ground movement this turn, a printed territory and a minimum of one to twenty physical forces. Basic with declared native families supports clean phases0–4 before Movement. During Movement ask an active player before or after shipment, or a still-unused waiting respondent. Finish pending decisions first. Optional modules, Advanced, later rounds and wider owned preparations remain unfinished.',
'Yes requires one qualifying ground movement from now until that player’s Shipment and Movement turn ends, while possible. Smaller groups do not add together and earlier moves do not count. Shipment, HMS relocation and Ambassador relocation do not fulfill this statement; a concealed No-Field marker itself is not a physical on-board force counter.',
'A nonmatching move leaves Yes pending when an owned later route remains. One matching physical commitment fulfills it before card disposal and later Intrusion, Ambassador or Terror effects. You are promising the move, not survival afterward. No forbids every later matching move, including Hajr and both Ornithopter groups, until the actual turn closes.',
'Ordinary reserve shipment may prepare or reinforce a later ground group using its actual cost, native allocation and pledged payment split. Shipment itself neither fulfills movement Yes nor violates movement No. Joint guidance honors a required shipment before End; an already declared reserve or native Guild transport keeps its exact source, destination and funding while responses finish.',
'Waiting answers use current board/resources without assuming foreign income, movement, cooperation or discarded cards. Another player cannot fulfill or close your promise by moving or ending. Ordinary guidance appears on your actual turn; your already-paid Box choice can finish now. Deferred gifts/accompaniment cannot voluntarily spend the last route before another player responds.',
'Earlier classic answers include owned Charity and ordinary Revival, reopen readiness when needed, and reserve a current winning auction invoice using cash or a held Karama. Only current preparation, Ready, bid-pass or owned payment appears before Movement. Unknown purchases and future income are not assumed. Forced Storm losses can release impossibility; old movement allowances reset at actual phase entry. A separate held Karama may prepare Guild shipment rates.',
'Earlier classic answers include owned Charity and ordinary Revival, reopen readiness when needed, and reserve a current winning auction invoice using cash or a held Karama. Only current preparation, Ready, bid-pass or owned payment appears before Movement. Unknown purchases and future income are not assumed. Forced Storm losses can release impossibility; old movement allowances reset at actual phase entry. A separate held Karama may prepare Guild shipment rates. Native Once Around and owned Silent bids reserve exact funding; a submitted ally locks its full pledge until reveal. Current native guidance passes or submits zero with the original event. A paid sale is not charged again, and foreign invoice outcomes precede release.',
'Private feasible answers and the next owned step use actual movement legality, typed groups, held Hajr, both Ornithopter modes and original cohorts, current city access, held native Kulon and owned Box recovery. Before paying the two-spice Box fee, only a known card still in discard supplies a route; unknown future draws/gifts are not invented. Native relocation and wider preparation remain unfinished.',
'A paid Box search retains its full private choice and original event. A guided choice preserves Yes; an incompatible selection rejects before shuffle and keeps that paid search available without another fee. No-only promises retain ordinary selection before movement can end. The existing provisional atomic full-hand exchange remains.',
'Voluntary actions cannot erase the last required route. Native counter responses finish before impossibility is decided. An opposing loss releases only incompatible answers, retaining No constraints and still-possible Yes answers in accepted order. This owned-route and release priority is a provisional software composition of the general current-turn rule, not a publisher movement-specific FAQ.',
@@ -2943,11 +2943,11 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['card-truthtrance', 'truthtrance-shipment', 'movement', 'implementation-checklist'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Classic Basic phases0–4 forecast owned Charity, ordinary auction payment and Revival, reopen readiness, preserve joint shipment predicates and reset old allowances. Current cash/held Karama and a distinct later shipment-rate card retain costs. Forced Storm losses release impossibility; foreign invoices finish before release. Native phase5 waiting/card support remains. Native early invoices, later rounds, remaining preparations and modules remain unfinished.', evidence: ['game/movement-promises.ts', 'game/movement-promise-search.ts', 'game/movement-shipment-preparation.ts', 'game/movement-discard-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Dedicated public questions/private answers and persistent busy-safe guidance now include only current Charity, Revival, Ready, bid-pass or owned auction payment before Movement. No future shipment/move control appears early; combined guidance avoids conflicting shipment preparation. Existing waiting/paid-search controls remain. Browser/visual acceptance stays open.', evidence: ['components/movement-claim-fields.tsx', 'components/movement-promises.tsx', 'components/shipment-promises.tsx', 'components/truthtrance.tsx', 'components/nullentropy-box.tsx', 'components/game-table.tsx', 'tests/truth-movement-waiting-controls.test.tsx', 'tests/truth-movement-early-controls.test.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Four existing profiles make24 accepted early-preparation/later-movement proposals plus76 separate native continuations. Combined movement guidance precedes independent shipment suggestions; all-No leaves native auction payment intact. Minimal legal participation only; no strategy tuning or calibration.', evidence: ['game/bots.ts', 'tests/truth-movement-waiting-engine.test.ts', 'tests/truth-movement-early-engine.test.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Basic declared native families now join classic phases0–4. Exact Richese own/donor invoices, full submitted-ally Silent locks and already-paid boundaries prepare the owned turn without speculative bought cards/income. Fixed non-CHOAM preopening Charity and shared ordinary force-revival accounting preserve real native counter decisions. Mandatory Ix phase5 opening retains the future turn/reset. CHOAM own unresolved opening, later rounds, wider preparations/modules remain unfinished.', evidence: ['game/early-auction-funding.ts', 'game/movement-promises.ts', 'game/movement-promise-search.ts', 'game/movement-shipment-preparation.ts', 'game/movement-discard-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Persistent busy-safe guidance includes current Charity, Revival, Ready, ordinary bid-pass/payment and event-bearing native Once Around pass/Silent zero. Foreign pending funding cannot force a speculative positive bid or expose future movement. Actual native pass/zero handlers preserve unread obligations. Existing waiting/card controls remain; browser/visual acceptance stays open.', evidence: ['components/movement-claim-fields.tsx', 'components/movement-promises.tsx', 'components/shipment-promises.tsx', 'components/truthtrance.tsx', 'components/nullentropy-box.tsx', 'components/game-table.tsx', 'tests/truth-movement-waiting-controls.test.tsx', 'tests/truth-movement-early-controls.test.tsx', 'tests/truth-movement-native-early-controls.test.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Eight existing-profile runs across native Once Around/Silent make56 accepted early-preparation/later-movement proposals plus148 separate native continuations. Zero/pass preserves the route while foreign funding resolves; all-No retains ordinary payment. Minimal legal participation only; no strategy tuning or calibration.', evidence: ['game/bots.ts', 'tests/truth-movement-waiting-engine.test.ts', 'tests/truth-movement-early-engine.test.ts', 'tests/truth-movement-native-early-engine.test.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'General FAQ authority, existential single-event meaning, explicit software composition, source selection and unfinished scope are documented.', evidence: ['docs/TRUTHTRANCE_MOVEMENT_PROMISES.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Current21-file155/155, types/lint/build and four gameplay/control/table-rendering programmes pass. Original Storm8/Charity22 sources retain Emperor10 spice/20 reserves/zero board, then perform purchase2/shipment8/ground8. Controlled prior losses, conserved cards, ally escrow and stale allowance cases disclose staging. Prior125/125,165/165,231/231,218/218 remain historical. Native early invoices, complete modes, browser/deployed and comprehensive assurance remain open.', evidence: ['tests/truth-movement-waiting-engine.test.ts', 'tests/truth-movement-waiting-controls.test.tsx', 'tests/fixture-truth-movement-waiting.ts', 'tests/fixture-truth-movement-waiting-box.ts', 'tests/fixture-hajr-replay.ts', 'tests/truth-movement-early-engine.test.ts', 'tests/truth-movement-early-controls.test.tsx', 'tests/fixture-truth-movement-early.ts', 'docs/TRUTHTRANCE_MOVEMENT_PROMISES.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Current29-file244/244, types/lint/build and six gameplay/control/table-rendering programmes pass. E2/E1/E3 original Charity prefixes25/36/25 retain native setup/cards and Emperor10 spice/20 reserves, then actual invoice2/shipment8/ground8. Controlled escrow, original-card/prior-loss and allowed/canceled Charity/revival cases disclose staging. Initial Ix opening failure is repaired and final frozen union passes. Earlier counts remain historical. Full modes, wider forecast scope, browser/deployed and comprehensive assurance remain open.', evidence: ['tests/truth-movement-waiting-engine.test.ts', 'tests/truth-movement-waiting-controls.test.tsx', 'tests/fixture-truth-movement-waiting.ts', 'tests/fixture-truth-movement-waiting-box.ts', 'tests/fixture-hajr-replay.ts', 'tests/truth-movement-early-engine.test.ts', 'tests/truth-movement-early-controls.test.tsx', 'tests/fixture-truth-movement-early.ts', 'tests/early-auction-funding.test.ts', 'tests/truth-movement-native-early-engine.test.ts', 'tests/truth-movement-native-early-controls.test.tsx', 'tests/fixture-truth-movement-native-early.ts', 'docs/TRUTHTRANCE_MOVEMENT_PROMISES.md'] },
],
},
{
@@ -3044,7 +3044,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 include earlier bare Basic phases and unused active or waiting phase5 turns in base Basic/Advanced, including Guild; see the shipment promise guide. Ground-movement minima include earlier classic Basic phases with owned Charity/ordinary invoice/Revival preparation, and active or unused waiting native Basic phase5 turns before or after shipment. Yes binds a qualifying event while possible, and No remains binding through later Hajr/Ornithopter until actual turn end. See the separate ground-movement guide and its remaining preparation/timing boundaries.',
'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. Ground-movement minima include earlier Basic phases with declared native families, owned Charity/ordinary revival preparation and exact normal/native auction funding, plus active or unused waiting phase5 turns before or after shipment. Yes binds a qualifying event while possible, and No remains binding through later Hajr/Ornithopter until actual turn end. See the separate ground-movement guide and its remaining preparation/timing boundaries.',
'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.',
+1 -1
View File
@@ -197,7 +197,7 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
check(Object.keys(v).sort().join(',') === 'kind,minimum,target,territory',
'Supply one ground-movement destination, minimum and respondent.');
check(movementPromiseTargetAvailable(g, v.target as string),
'Ground-movement promises need a supported Basic opportunity this round with pending decisions finished. Earlier phases currently use the classic roster; optional modules and Advanced remain unfinished.');
'Ground-movement promises need a supported Basic opportunity this round with pending decisions finished. Optional modules and Advanced remain unfinished.');
try { return { kind: 'movement', target: v.target as string, ...parseMovementClaim(v) }; }
catch (error) { if (error instanceof MovementClaimError) throw new TruthError(error.message); throw error; }
}