Prevent saved CHOAM movement arrival deadlocks

Preflight ordinary movement through the existing arrival quote before creating new Ornithopter escrow or opening Ixian, Fremen and CHOAM prevention windows. An explicit legacy CHOAM decline can return an uncommitted unsupported arrival for another choice without consuming movement or touching forces, spice, cards or tokens. Preserve an existing Ornithopter and retire it exactly once on a legal replacement move. Keep dedicated Ambassador relocation handling and Karama cancellation validation before cost; unrelated exceptions remain atomic. Simultaneous Ambassador/Terror priority still awaits the recorded material interpretation, with no invented ordering.

Add reusable offline faction-game samples with genuine Basic/Advanced setup, all four AI profiles, physical custody checks, JSON restorations, private traces and failed snapshots. Source-bound reports and input snapshot hashes make fresh and resumed samples repeatable; document that a resumed continuation starts a new seeded stream and does not prove input setup provenance.

Validation: independently reviewed fresh declaration, native cancellation, supported single arrivals, legacy retry and card custody tests. Two production SQLite tests cover authenticated private restoration, stale/duplicate/wrong-owner rejection, resource conservation and concurrent version races. Typecheck, lint, 4583 offline tests, production build and 43 HTTP tests passed. Retain the initial failed check log for the corrected numeric-sort lint issue.

All six fresh samples complete. CHOAM Basic 1054 actions/28 JSON restores, CHOAM Advanced 225/6, Ecaz Basic 91/2, Ecaz Advanced 523/14, all-three Basic 476/12, all-three Advanced 1303/35 with a turn-six CHOAM win. Combined Advanced has 46 guarded arrival candidates with legal alternatives. The exact prior 614-action failed snapshot separately finishes after 691 additional actions/18 restores and 39 guarded candidates. These are bounded paths, not complete faction or combination certification. Gameplay source hashes match the final checkpoint; later evidence-only documentation and recovery-test lint edits are explicit.

After the reported outage, all 632 opening games and original seat/recovery records were preserved (665 after HTTP tests); integrity and foreign keys passed. Existing Ecaz and Ixian browser seats restored their same private hands, resources and pending choices after reload. This is browser restoration evidence, not a staged arrival-return claim. The healthy server was reused; no restart, database reset, recurring automation or publication.

Verified source tree: 550221850f4802c41155f7bbc35d623965bc7db3330825636d50f26bb42868f8
This commit is contained in:
Quintine committed 2026-09-13 23:15:00 +10:00
1 parent cab2f1c42d
commit 903d861170
13 files changed
+1390 -11

No files matched your search

+30 -6
View File
@@ -8657,6 +8657,8 @@ function balisetPrevents(g: Game, player: string, origin: string, to: string) {
);
}
function offerChoamMovement(g: Game, move: MovementOrder) {
if (move.source === 'ambassador') validateAmbassadorRelocationArrival(g, move);
else validateMovementArrival(g, move);
const choam = byFaction(g, 'choam');
if (
g.phase === 5 &&
@@ -8785,7 +8787,7 @@ function pendingFremenMovement(g: Game, response: ResponseWindow) {
validateMovementOrder(g, p, pending.order);
return pending;
}
function resumeChoamMovement(g: Game) {
function resumeChoamMovement(g: Game, retryUnsupportedArrival = false) {
const move = g.pendingChoamMove!;
if (move.source === 'ambassador')
currentFremenAmbassador(g, move.ambassadorEvent);
@@ -8802,16 +8804,28 @@ function resumeChoamMovement(g: Game) {
if (move.source === 'ambassador') offerAmbassadorRelocation(g);
return;
}
if (retryUnsupportedArrival) {
try {
if (move.source === 'ambassador') {
const blocked = ambassadorRelocationArrivalBlock(g, move.player, move.origin, move.to, move.total);
if (blocked) throw new MovementArrivalError(blocked);
} else quoteMovementArrival(g, move);
} catch (error) {
if (!(error instanceof MovementArrivalError)) throw error;
log(g, `${p.name}'s uncommitted movement returned for another choice. ${error.message} No additional forces, spice, cards or movement allowance were spent.`);
if (move.source === 'ambassador') offerAmbassadorRelocation(g);
return;
}
}
if (move.source === 'ambassador') commitAmbassadorRelocation(g, move);
else completeMove(g, move);
}
function validateMovementArrival(
function quoteMovementArrival(
g: Game,
move: MovementOrder,
cancelingChoam = false,
) {
try {
return quoteCompletedMovementArrival({
return quoteCompletedMovementArrival({
advanced: g.advanced,
players: g.players,
order: move,
@@ -8826,7 +8840,11 @@ function validateMovementArrival(
pendingAmbassador: !!g.pendingAmbassador,
paidBox: !!g.pendingNullentropy,
},
});
});
}
function validateMovementArrival(g: Game, move: MovementOrder, cancelingChoam = false) {
try {
return quoteMovementArrival(g, move, cancelingChoam);
} catch (error) {
if (error instanceof MovementArrivalError)
throw new RuleError(error.message);
@@ -20185,7 +20203,7 @@ function applyActionInner(
action.decline === true,
'Allow the movement or declare Baliset.',
);
resumeChoamMovement(g);
resumeChoamMovement(g, true);
return g;
}
if (
@@ -22419,6 +22437,12 @@ function applyActionInner(
sourceKeys.every((key) => key !== location(to, s)),
'Choose a different destination.',
);
// Reject known unsupported arrivals before any movement-card event or
// native movement-prevention window can commit this declaration.
validateMovementArrival(g, {
player: id, group, eliteGroup, elite, origin, origins, total: n,
to, sector: s, advisors, wantsFighters, lockedTurn,
});
let flight = g.ornithopter?.player === id ? g.ornithopter : null;
if (action.movementCard !== undefined) {
const mode = action.ornithopter;
+2 -1
View File
@@ -3604,6 +3604,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Ixians keep their private starting-card selection and physical cyborg/suboid forces. Tleilaxu receives three private Face Dancers. Advanced CHOAM receives its Auditor. Richese has a separate ten-card cache and three private No-Fields. Ecaz and Moritani receive their Ambassador and Terror inventories and share one Duke Vidal disc.',
'Without Ix, CHOAM adds Poison Tooth and Artillery Strike to the 33-card ordinary deck. With Ix, its 47-card deck contains those physical cards once. Ecaz faction selection adds no Treachery Cards: Recruits, Reinforcements and Harass & Withdraw form a separate unfinished optional variant.',
'Advanced Ixians may explicitly decline Technology for one Richese special lot. Their once-per-round use remains available later. The special-lot exchange itself is unfinished pending replacement-card custody interpretation.',
'Fresh unsupported movement arrivals reject before opening prevention choices or reserving a new movement card. A saved CHOAM allow choice can return an uncommitted unsupported move for another choice, preserving its existing reserved Ornithopter. Simultaneous Ambassador/Terror order remains unresolved.',
'Remaining abilities and combined timing can still encounter explicit development guards. Setup and saved-continuation evidence does not certify complete expansion play.',
],
related: ['setup', 'ix-modules', 'choam-modules', 'ecaz-modules', 'implementation-checklist'],
@@ -3612,7 +3613,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'Existing faction choices and private hands are reused. Ecaz has an owned sector-allocation control and setup waiting messages; Ixians have an explicit per-lot decline control. Public starts and full acceptance remain gated.', evidence: ['components/ecaz-setup.tsx', 'components/ix-richese-technology.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four profiles use genuine setup choices and the shared Ecaz allocation quote, and can decline this Richese lot without using Technology. Combined gameplay strategy, guarded interactions and full-game reliability remain unfinished.', evidence: ['game/bots.ts', 'game/ecaz-setup.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'The faction profile records ordinary deck composition, independent variants, Ecaz territory allocation and deterministic setup-order interpretations, with unfinished play explicit.', evidence: ['docs/EXPANSION_FACTIONS_PROTOTYPE.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Setup, inventory, private views, JSON/SQLite continuation, concurrent start and dirty-lobby rejection have focused checks. Ixian per-lot decline has ownership, stale/corrupt decision, private card custody and concurrent recovery checks. Sample gameplay and browser evidence are recorded separately, with remaining guards retained.', evidence: ['tests/expansion-factions-prototype.test.ts', 'tests/expansion-factions-recovery.test.ts', 'tests/ecaz-setup-controls.test.tsx', 'tests/ecaz-cards.test.ts', 'tests/ix-richese-technology.test.ts', 'tests/ix-richese-technology-controls.test.tsx', 'tests/ix-richese-technology-recovery.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Setup, inventory, private views, JSON/SQLite continuation, concurrent start and dirty-lobby rejection have focused checks. Ixian per-lot decline has ownership, stale/corrupt decision, private card custody and concurrent recovery checks. Sample gameplay and browser evidence are recorded separately, with remaining guards retained. Deferred movement checks cover preflight, reserved cards and saved concurrent recovery.', evidence: ['tests/deferred-movement-arrival.test.ts', 'tests/deferred-movement-arrival-recovery.test.ts', 'tools/faction-games.ts', 'tests/expansion-factions-prototype.test.ts', 'tests/expansion-factions-recovery.test.ts', 'tests/ecaz-setup-controls.test.tsx', 'tests/ecaz-cards.test.ts', 'tests/ix-richese-technology.test.ts', 'tests/ix-richese-technology-controls.test.tsx', 'tests/ix-richese-technology-recovery.test.ts'] },
],
},
...[