Support typed Advanced Truthtrance shipment commitments

Extend the existing reserve-shipment promise pipeline to base Advanced
games without Guild or optional modules. Share scope between parser, save
validation and UI. Reuse canonical Ghola normal/elite revival eligibility
and retain live BG conversion responses while proving conditional private
feasibility. Hide future-price guidance until pending responses finish.
Show exact Sardaukar/Fedaykin allocation in private executable steps.

Validation: types, lint and all 4,192 offline tests pass; production build
and all 40 HTTP tests pass. 41 focused shipment tests include 15 new cases
covering genuine Advanced setup, all five non-Guild factions and four bot
profiles, typed Ghola quota, BG allowance/cancellation/alternatives,
private views, saved continuations and production SQLite duplicate races.
Independent review found and resolved optional-module scope and pending
price-guidance issues. Browser rooms EUZZ8D4M and GWNCVCAP verified an AI No
route and human Yes with four-force/one-Sardaukar Ghola then six-force
shipment. Phone settlement controls and refresh passed. Both existing
browser room hashes/versions preserved through HTTP tests; 30 rooms added.

Full source-bound report: 20260913-advanced-shipment-final; fingerprint
6c2f29d9b3e61bf6b9544ee3a95c47538abfb05e1c3b2ae9d0a603ef0cef38dd
Broad checks ran once after focused fixes/review. No full-game AI study
or complete Advanced mode acceptance is claimed; all existing release
gates and pending Guild settlement questions remain intact.
This commit is contained in:
Quintine committed 2026-09-13 11:08:29 +10:00
1 parent 6e26ba5daa
commit 32791ed5e2
14 files changed
+902 -38

No files matched your search

+24 -9
View File
@@ -273,6 +273,7 @@ import {
import {
matchesShipment,
liveShipmentPromises,
shipmentPromiseModeSupported,
type ShipmentClaim,
type ShipmentPromise,
} from './shipment-promises';
@@ -12486,16 +12487,26 @@ function shipmentPromiseIntegrity(g: Game) {
);
if (promise.turn === g.turn && !promise.released && !promise.fulfilled)
requireRule(
!g.advanced &&
!g.expansions.length &&
shipmentPromiseModeSupported(g) &&
g.status === 'playing' &&
g.phase === 5 &&
g.active === promise.player &&
shipmentAvailable(g,getPlayer(g,promise.player)),
'The saved shipment promise does not belong to this unused Basic shipment opportunity.',
'The saved shipment promise does not belong to a supported unused shipment opportunity.',
);
}
}
/** A pending conversion may succeed. Resolve only its shipment effect in the
* private reachability copy; the live table retains every opponent response. */
function projectPendingShipmentKarama(g: Game, player: string) {
const pending = g.pendingKarama;
if (g.response?.kind !== 'worthlessKarama' || !pending ||
g.response.owner !== pending.owner || pending.use.kind !== 'shipment' ||
pending.use.recipient !== player) return;
g.response = null;
g.pendingKarama = null;
completeKarama(g, getPlayer(g, pending.owner), pending.use);
}
/** Search owned preparation and canonical shipment quotes, never the AI's scored shortlist. */
function findShipmentCompletion(
state: Game,
@@ -12517,6 +12528,7 @@ function findShipmentCompletion(
return { actions: [{ type: 'endMovement' }] };
const initial = structuredClone(state);
initial.truthtrance = null;
projectPendingShipmentKarama(initial, owner.id);
// Test choices after the interrupt, while leaving the real continuation intact.
// Every actual action still revalidates its timing, quote and physical custody.
initial.response = null;
@@ -12628,8 +12640,8 @@ function findShipmentCompletion(
}
}
}
// These Basic preparations are deterministic and use only the respondent's
// own cards and recoverable escrow. More reserves/lower cost cannot remove
// Preparations use only owned cards, recoverable escrow and already-pending
// conversion effects. More reserves/lower cost cannot remove
// a previously available choice of physical shipment count.
const preparations: Action[] = [];
if (g.aid[p.id]?.amount > 0)
@@ -12642,16 +12654,18 @@ function findShipmentCompletion(
mode: 'shipment',
target: p.id,
});
const ghola = p.hand.find((c) => c.effect === 'ghola');
if (ghola && p.tanks > 0)
const ghola = gholaOptions(g, p);
if (ghola.available && ghola.maxForces > 0)
preparations.push({
type: 'card',
card: ghola.id,
amount: Math.min(5, p.tanks),
card: ghola.cards[0],
amount: ghola.maxForces,
elite: Math.max(0, ghola.maxForces - (p.tanks - (p.elites?.tanks ?? 0))),
});
for (const action of preparations) {
try {
const next = applyActionInner(g, p.id, action, 'shipmentPreparation');
projectPendingShipmentKarama(next, p.id);
queue.push({ game: next, actions: [...actions, action] });
} catch (error) {
if (!(error instanceof RuleError)) throw error;
@@ -21194,6 +21208,7 @@ export function viewGame(state: Game, id: string) {
shipmentPromises: g.shipmentPromises ?? [],
shipmentCompletion:
!g.pendingTreacheryDiscard &&
!g.response && !g.pendingKarama &&
liveShipmentPromises(g.shipmentPromises ?? [], id, g.turn).some(
(p) => p.answer,
)
+5 -5
View File
@@ -2120,14 +2120,14 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Cards',
coverage: 'Partial',
summary:
'Bind a shipment from reserves to a named territory with a minimum physical force count during the active Basic shipment opportunity.',
'Bind a shipment from reserves to a named territory with a minimum physical force count during the active supported shipment opportunity.',
steps: [
'Declare Truthtrance, select the active player and choose Bind a shipment from reserves. Choose a printed destination and minimum of one to twenty forces. Automatic enforcement currently supports the Basic game without expansions, before that player ships and after pending decisions finish.',
'Declare Truthtrance, select the active player and choose Bind a shipment from reserves. Choose a printed destination and minimum of one to twenty forces. Automatic enforcement supports base Basic games and base Advanced games without Guild or optional modules, before that player ships and after pending decisions finish.',
'Yes requires a matching shipment while possible. No prohibits only that matching shipment: a smaller count, different destination, transport of forces already on the board or no shipment remains available. 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 answers are public and constrain the actual shipment, including any earlier shipment promises. Yes leaves count, sector and legal funding choices open. The shipper cannot move first, finish movement or voluntarily spend away their last way to comply. A completed opposing effect that makes the answer impossible releases it with a public notice.',
'Your shipment promises remain visible during Movement. Open Suggested next step for a private executable preparation or funded shipment example. It is optional guidance; any legal action sequence honoring the answer is allowed. Shipment fulfillment survives later movement or losses because it records what was shipped.',
'All four AI profiles answer supported questions and retain a complete legal preparation route when their normal strategic shortlist misses the required count or destination. Earlier-phase questions, Advanced and expansion shipment promises, combined shipment/fact logic and arbitrary freeform promises remain unfinished.',
'All four AI profiles answer supported questions and retain a complete legal preparation route when their normal strategic shortlist misses the required count or destination. Earlier-phase questions, Advanced games with Guild or optional modules, expansion shipment promises, combined shipment/fact logic and arbitrary freeform promises 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.',
@@ -2137,7 +2137,7 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Implementation',
status: 'Partial',
detail:
'Authoritative Basic reserve-origin commitments, joint feasibility with owned preparation, actual shipment completion, voluntary-spend guards and opposing release. Broader timing and expansion routes remain unfinished.',
'Authoritative base Basic and no-Guild Advanced reserve-origin commitments, joint feasibility with owned preparation, actual shipment completion, voluntary-spend guards and opposing release. Broader timing and expansion routes remain unfinished.',
},
{
area: 'Player controls',
@@ -2184,7 +2184,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 Basic shipment without expansions; 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 are enforced for the active unused shipment in base Basic and no-Guild Advanced games; 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.',
'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.',
'Holding both copies lets you declare one or both. Each question resolves separately, and each card is discarded only after its definite answer. Full timing and card-face audits remain pending.',
+20
View File
@@ -1,5 +1,25 @@
/** A current-turn promise about the player's ordinary physical shipment from reserves. */
export type ShipmentClaim = { territory: string; minimum: number };
/** Software readiness boundary; this is not a printed restriction on Truthtrance. */
export function shipmentPromiseModeSupported(game: {
advanced: boolean;
expansions: readonly unknown[];
players: readonly { faction: string }[];
techTokens?: unknown;
strongholdCards?: unknown;
nexusCards?: unknown;
homeworlds?: unknown;
}): boolean {
return (
!game.expansions.length &&
(!game.advanced ||
(!game.players.some((player) => player.faction === 'guild') &&
!game.techTokens &&
!game.strongholdCards &&
!game.nexusCards &&
!game.homeworlds))
);
}
export type ShipmentPromise = ShipmentClaim & {
turn: number;
player: string;
+3 -3
View File
@@ -1,5 +1,6 @@
import { shipmentAvailable } from './shipment-opportunity';
import type { ShipmentClaim } from './shipment-promises';
import { shipmentPromiseModeSupported } from './shipment-promises';
import { TERRITORIES } from './board';
import {
parseCardCountFact,
@@ -141,8 +142,7 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
);
if (v.kind === 'shipment') {
check(
!g.advanced &&
!g.expansions.length &&
shipmentPromiseModeSupported(g) &&
g.status === 'playing' &&
g.phase === 5 &&
g.active === v.target &&
@@ -151,7 +151,7 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
!g.decision &&
!g.phaseOpening &&
!g.pendingShipment,
'Automatic shipment promises currently support the active unused shipment in the Basic game without expansions. Finish any pending decision first.',
'Structured shipment promises currently support base Basic games and base Advanced games without Guild or optional modules, during the active unused shipment. Finish any pending decision first.',
);
check(
typeof v.territory === 'string' &&