Support earlier classic Basic movement promises
Allow current-round classic Basic questions before Movement, with owned Charity, ordinary auction payment and Revival preparation. Reopen readiness, retain combined shipment obligations, reset stale allowances and release forced storm impossibility. Current-phase guidance and minimal bots preserve the route; held Karama now prepares its actual Guild shipment rate. Validation: affected rules/control tests, types/lint, production build and four gameplay/control/table-rendering programmes. Native early invoices, wider preparations/modules, full rules and deployed acceptance remain open. No saved-game, database, restart or deployment operation.
This commit is contained in:
1 parent
49586efd2f
commit
f995b6f4cd
18 files changed
+708
-46
No files matched your search
+3
-3
@@ -4076,12 +4076,12 @@ 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.movementPromisePolicy && g.status === 'playing' &&
|
||||
!g.roomControl?.paused && !g.roomControl?.closed && !g.automaticContinuationPending)
|
||||
return g.movementPromisePolicy;
|
||||
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 [];
|
||||
|
||||
+74
-16
@@ -1,6 +1,6 @@
|
||||
import { recordMovementDiscardKnowledge, knownMovementDiscards, recordMovementBoxKnowledge, recordMovementClaimKnowledge, clearMovementDiscardKnowledge, type MovementDiscardKnowledge } from './movement-discard-knowledge';
|
||||
import { movementShipmentPreparations } from './movement-shipment-preparation';
|
||||
import { MovementClaimError, liveMovementPromises, movementEventAllowed, movementPromiseTargetAvailable, settleMovementEvent, finishMovementPromises, validMovementClaim, type MovementClaim, type MovementPromise } from './movement-promises';
|
||||
import { MovementClaimError, liveMovementPromises, movementEventAllowed, movementPromiseModeSupported, movementPromiseTargetAvailable, settleMovementEvent, finishMovementPromises, validMovementClaim, type MovementClaim, type MovementPromise } from './movement-promises';
|
||||
import { findMovementCompletion, type MovementCompletion } from './movement-promise-search';
|
||||
import { classicNexusModulesProfile, mixedE1E2NexusProfile, mixedE3NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
|
||||
import { independentDiscoveryProfile, independentNexusProfile, nexusCoreProfile, nativeFactionTechProfile } from './faction-module-profile';
|
||||
@@ -16433,6 +16433,7 @@ function recoverableUnsupportedShipment(shipment: PendingShipment) {
|
||||
* owned movement reachability; unresolved opposing outcomes are never replayed. */
|
||||
function groundMovementProbe(source: Game, actor: string): Game {
|
||||
const game = structuredClone(source);
|
||||
projectPendingShipmentKarama(game, actor);
|
||||
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
|
||||
@@ -16503,7 +16504,33 @@ function projectMovementBoxRecovery(source: Game, actor: string, box: string, se
|
||||
}
|
||||
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) {
|
||||
const paid = source.pendingNullentropy;
|
||||
if (paid?.player === actor) {
|
||||
if (nullentropyIntegrity(source)) return null;
|
||||
for (const card of eligibleNullentropyCards(source.discard)) {
|
||||
const projected = projectMovementBoxRecovery(source, actor, paid.box, card.id, true);
|
||||
const completion = projected && groundMovementCompletion(projected, actor, promises);
|
||||
if (completion) return { ...completion,
|
||||
actions: [{ type: 'decision', event: paid.event, card: card.id }, ...completion.actions] };
|
||||
}
|
||||
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];
|
||||
const completion = groundMovementCompletionAtMovement(seed.game, actor, promises);
|
||||
if (completion) return { actions: [...seed.actions, ...completion.actions], early: seed.early };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return groundMovementCompletionAtMovement(game, actor, promises);
|
||||
}
|
||||
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, {
|
||||
allowance: movesAllowed,
|
||||
@@ -16534,6 +16561,11 @@ function groundMovementCompletion(source: Game, actor: string,
|
||||
catch (error) { if (error instanceof RuleError) return null; throw error; }
|
||||
});
|
||||
for (const card of player.hand) {
|
||||
if (!player.shipped && !state.karamaShipping && canUseAsKaramaRole(state, player, card)) {
|
||||
const action: Action = { type: 'card', card: card.id, mode: 'shipment', target: owner };
|
||||
try { yield { game: groundMovementProbe(applyActionInner(state, owner, action, 'movementPreparation'), owner), actions: [action] }; }
|
||||
catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
if (card.effect === 'hajr' && hajrAvailable(state, player) && !transferCardBlock(state, player, card)) {
|
||||
try { yield { game: groundMovementProbe(applyActionInner(state, owner,
|
||||
{ type: 'card', card: card.id }, 'movementPreparation'), owner), actions: [{ type: 'card', card: card.id }] }; }
|
||||
@@ -16572,6 +16604,7 @@ function groundMovementCompletion(source: Game, actor: string,
|
||||
function projectedGroundMovementPolicy(g: Game, player: Player): Action[] | null {
|
||||
const live = liveMovementPromises(g.movementPromises ?? [], player.id, g.turn);
|
||||
const paid = g.pendingNullentropy?.player === player.id;
|
||||
if (!paid && g.phase < 5) return earlyGroundMovementPolicy(g, player);
|
||||
if (g.truthtrance || !live.length || (!paid && g.active !== player.id) ||
|
||||
(paid && !live.some(promise => promise.answer))) return null;
|
||||
const available = paid ? groundMovementProbe(g, player.id) : g;
|
||||
@@ -16579,16 +16612,35 @@ function projectedGroundMovementPolicy(g: Game, player: Player): Action[] | null
|
||||
const completion = groundMovementCompletion(g, player.id);
|
||||
return completion ? completion.actions.length ? completion.actions.slice(0, 1) : [{ type: 'endMovement' }] : null;
|
||||
}
|
||||
function earlyGroundMovementPolicy(g: Game, player: Player): Action[] | null {
|
||||
const live = liveMovementPromises(g.movementPromises ?? [], player.id, g.turn);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
function movementTruthAnswers(g: Game, p: Player, claim: MovementClaim): TruthAnswer[] {
|
||||
const live = liveMovementPromises(g.movementPromises ?? [], p.id, g.turn);
|
||||
const answers: TruthAnswer[] = [];
|
||||
for (const answer of [true, false]) {
|
||||
const promise = { ...claim, turn: g.turn, player: p.id, asker: g.players.find(other => other.id !== p.id)!.id, answer };
|
||||
if (groundMovementCompletion(g, p.id, [...live, promise])) answers.push(answer ? 'yes' : 'no');
|
||||
const answerSource = g.phase === 2 || g.phase === 4 ? { ...g, ready: g.ready.filter(id => id !== p.id) } : g;
|
||||
if (groundMovementCompletion(answerSource, p.id, [...live, promise])) answers.push(answer ? 'yes' : 'no');
|
||||
}
|
||||
return answers.length ? answers : ['unknown'];
|
||||
}
|
||||
function bindMovementTruth(g: Game, p: Player, claim: MovementClaim, answer: boolean, asker: string) {
|
||||
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 movement commitment and must confirm readiness again after any needed preparation.`);
|
||||
}
|
||||
(g.movementPromises ??= []).push({ ...claim, turn: g.turn, player: p.id, asker, answer });
|
||||
}
|
||||
function checkGroundMovementPromises(g: Game, p: Player, move: MovementOrder) {
|
||||
@@ -16611,7 +16663,7 @@ function recordGroundMovementPromises(g: Game, p: Player, move: MovementOrder) {
|
||||
log(g, `${p.name} fulfilled a Truthtrance ground-movement promise with this committed move.`);
|
||||
});
|
||||
}
|
||||
function reconcileGroundMovementPromises(g: Game, actor?: string) {
|
||||
function reconcileGroundMovementPromises(g: Game, actor?: string, action?: Action, sourcePhase = g.phase) {
|
||||
// A declared native movement is awaiting a real opponent choice. It is not
|
||||
// impossible and must not be fulfilled before physical commitment.
|
||||
for (const p of g.players) {
|
||||
@@ -16619,11 +16671,16 @@ function reconcileGroundMovementPromises(g: Game, actor?: string) {
|
||||
if (!live.some(promise => promise.answer)) continue;
|
||||
if (g.pendingFremenMove?.order.player === p.id || g.pendingIxMove?.player === p.id ||
|
||||
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) ||
|
||||
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);
|
||||
if (busy && actor !== p.id) continue;
|
||||
g.pendingAmbassador || g.pendingTerrorEntry || g.pendingArrivalOverlap || g.pendingArrivalReaction?.length ||
|
||||
g.phase === 3 && !g.currentAuctionSale && g.auction?.bidder === p.ally && (g.auction?.allyPayment ?? 0) > 0);
|
||||
if (busy && !voluntary) continue;
|
||||
if (groundMovementCompletion(g, p.id, live)) continue;
|
||||
requireRule(actor !== p.id, 'Your action would remove the last legal completion of an accepted ground-movement promise.');
|
||||
requireRule(!voluntary, 'Your action would remove the last legal completion of an accepted ground-movement promise.');
|
||||
const kept = live.filter(promise => !promise.answer);
|
||||
// Keep still-feasible earlier answers and every No constraint; an opposing
|
||||
// loss does not erase independent promises that remain possible.
|
||||
@@ -20495,6 +20552,13 @@ function advisorBattleOptions(g: Game) {
|
||||
applyAdvisorReleases(g, quote.released);
|
||||
return quote.territories;
|
||||
}
|
||||
function resetPlayerMovement(p: Player) {
|
||||
p.shipped = false;
|
||||
p.moved = 0;
|
||||
delete p.hajrPlays;
|
||||
delete p.ornithopterExtraMoves;
|
||||
delete p.hajrWindow;
|
||||
}
|
||||
function commitMovementPhaseStart(g: Game, quote: MovementPhaseQuote) {
|
||||
if (quote.kind === 'opening') {
|
||||
g.phaseOpening = quote.phaseOpening;
|
||||
@@ -20514,13 +20578,7 @@ function commitMovementPhaseStart(g: Game, quote: MovementPhaseQuote) {
|
||||
passed: [],
|
||||
};
|
||||
}
|
||||
for (const p of g.players) {
|
||||
p.shipped = false;
|
||||
p.moved = 0;
|
||||
delete p.hajrPlays;
|
||||
delete p.ornithopterExtraMoves;
|
||||
delete p.hajrWindow;
|
||||
}
|
||||
for (const p of g.players) resetPlayerMovement(p);
|
||||
applyAdvisorReleases(g, quote.released);
|
||||
g.decision = quote.decision;
|
||||
g.active = quote.active;
|
||||
@@ -31260,7 +31318,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
return normalizeAutomaticGame(state);
|
||||
}
|
||||
const g = applyActionInner(state, id, action);
|
||||
reconcileGroundMovementPromises(g, id);
|
||||
reconcileGroundMovementPromises(g, id, action, state.phase);
|
||||
harkonnenNexusBetrayalIntegrity(g);
|
||||
if (g.pendingNexusHarkonnenBetrayal) return g;
|
||||
ixianNexusBetrayalIntegrity(g);
|
||||
@@ -33468,7 +33526,7 @@ function applyActionInner(
|
||||
const afterAdvisor = structuredClone(g);
|
||||
finishResponse(afterAdvisor, false);
|
||||
reconcileShipmentPromises(afterAdvisor, { actor: p.id, action });
|
||||
reconcileGroundMovementPromises(afterAdvisor, p.id);
|
||||
reconcileGroundMovementPromises(afterAdvisor, p.id, action, g.phase);
|
||||
}
|
||||
} else log(g, `${p.name} declined the free shipment.`);
|
||||
} else if (decision.kind === 'wormPlacement') {
|
||||
@@ -34414,7 +34472,7 @@ function applyActionInner(
|
||||
getPlayer(afterPayment, p.id).spice -= t === 'emperorGift'
|
||||
? amount : paidForceRevivalCost(recipient, amount, elite);
|
||||
reconcileShipmentPromises(afterPayment, { actor: p.id, action });
|
||||
reconcileGroundMovementPromises(afterPayment, p.id);
|
||||
reconcileGroundMovementPromises(afterPayment, p.id, action, g.phase);
|
||||
}
|
||||
g.response = {
|
||||
kind: t,
|
||||
|
||||
@@ -9,7 +9,7 @@ export type MovementPromiseAdapter = {
|
||||
reachable: (game: Game, player: Player, origin: string, destination: string, sector: number) => boolean;
|
||||
project: (game: Game, player: string, action: Action) => { game: Game; event: MovementEvent } | null;
|
||||
};
|
||||
export type MovementCompletion = { actions: Action[] };
|
||||
export type MovementCompletion = { actions: Action[]; early?: { phase: number; action: Action }[] };
|
||||
|
||||
/** Enumerate actual physical subgroups from one territory, including native
|
||||
* elite allocations and several sectors. No strategic shortlist proves legality. */
|
||||
|
||||
@@ -83,11 +83,12 @@ export function finishMovementPromises<T extends MovementPromise>(
|
||||
|
||||
/** Prototype profile/timing only. Actual route feasibility stays authoritative
|
||||
* in the engine. Truthtrance and an ordinary active Ornithopter may remain. */
|
||||
export function movementPromiseTargetAvailable(g: Game, player: string): boolean {
|
||||
const target = g.players.find(candidate => candidate.id === player);
|
||||
return g.status === 'playing' && g.advanced === false && g.phase === 5 &&
|
||||
!!target && (g.active === player || (!target.shipped && target.moved === 0)) &&
|
||||
!!g.movementRemaining?.includes(player) &&
|
||||
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 &&
|
||||
@@ -97,7 +98,14 @@ export function movementPromiseTargetAvailable(g: Game, player: string): boolean
|
||||
}) &&
|
||||
!g.homeworlds && !g.nexusCards && !g.leaderSkills && !g.techTokens &&
|
||||
!g.strongholdCards && !g.discoveryEnabled && !g.discoveries &&
|
||||
!g.discoveryStash && !g.greatMaker && !g.ecazTreachery &&
|
||||
!g.discoveryStash && !g.greatMaker && !g.ecazTreachery;
|
||||
}
|
||||
export function movementPromiseTargetAvailable(g: Game, player: string): boolean {
|
||||
const target = g.players.find(candidate => candidate.id === player);
|
||||
return movementPromiseModeSupported(g) && g.status === 'playing' &&
|
||||
Number.isInteger(g.phase) && g.phase >= 0 && g.phase <= 5 && !!target &&
|
||||
(g.phase < 5 || ((g.active === player || (!target.shipped && target.moved === 0)) &&
|
||||
!!g.movementRemaining?.includes(player))) &&
|
||||
!g.response && !g.decision && !g.phaseOpening &&
|
||||
!g.pendingShipment && !g.pendingHomeworldShipment && !g.pendingGuildTransport &&
|
||||
!g.pendingAmbassador && !g.pendingTerrorEntry && !g.pendingArrivalOverlap &&
|
||||
|
||||
+8
-7
@@ -2928,13 +2928,14 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Cards',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Bind one future qualifying physical ground movement before an active Basic respondent finishes their current turn.',
|
||||
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. Ask an active Basic player before or after shipment, or a still-unused waiting respondent in the current Movement queue. Finish pending decisions first. Selected native families are supported; optional modules, Advanced, earlier phases and later rounds remain unfinished.',
|
||||
'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.',
|
||||
'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.',
|
||||
'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.',
|
||||
@@ -2942,11 +2943,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
],
|
||||
related: ['card-truthtrance', 'truthtrance-shipment', 'movement', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'The pure ledger and shared movement/shipment search now include unused waiting Basic respondents. Forecast current owned resources without foreign outcomes; ordinary policy waits until active, while the original paid Box can select any entitled card preserving the route. Deferred gift/advisor costs retain preflight; native moves defer only their owner. Earlier phases/later rounds, remaining preparation families, No-Field/modules and wider interactions 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 question/private answers and persistent busy-safe guidance include waiting obligations, actual-turn reinforcement and paid search selection while waiting. Both ordinary paid faces remain inspectable; selecting returns to waiting with no premature movement button. Prior active/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'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Eight runs across all four profiles make32 accepted waiting-answer or paid-selection/later-turn proposals, with12 separate neutral acknowledgements. Optional accompaniment may be declined under a positive promise. Minimal legal participation only; no strategy tuning or calibration.', evidence: ['game/bots.ts', 'tests/truth-movement-waiting-engine.test.ts'] },
|
||||
{ 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: '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: 'Current eighteen-file125/125, types/lint, retained passing app build and four original-action/control/table programmes pass. Classic source19 reverses its unopened deal; native source44 buys Box/Hajr and spends the first Truth; the actual later question supplies the second discard. Controlled pact, BG losses, foreign Ornithopter and native Fremen/Box cases disclose staging and prove rejected/affordable costs. Only the regression amended after build; app identity is verified and final tests/types/runtime rerun. Prior165/165,231/231,218/218 stay historical. No browser/visual/deployed, full-mode or comprehensive assurance claim.', 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', 'docs/TRUTHTRANCE_MOVEMENT_PROMISES.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'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -3043,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. Active post-shipment Basic native turns also support one future ground-movement minimum: 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 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.',
|
||||
'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
@@ -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 require an active or still-waiting Basic turn this round, with pending decisions finished and no optional modules.');
|
||||
'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.');
|
||||
try { return { kind: 'movement', target: v.target as string, ...parseMovementClaim(v) }; }
|
||||
catch (error) { if (error instanceof MovementClaimError) throw new TruthError(error.message); throw error; }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user