diff --git a/docs/BOT_ARRIVAL_PREFLIGHT.md b/docs/BOT_ARRIVAL_PREFLIGHT.md index 2c1c8959..b373ed77 100644 --- a/docs/BOT_ARRIVAL_PREFLIGHT.md +++ b/docs/BOT_ARRIVAL_PREFLIGHT.md @@ -36,10 +36,14 @@ and Face Dancers are unnecessary. It does not mutate the view or sample randomness. Only the known typed arrival error excludes a candidate; unexpected faults propagate. The engine remains authoritative for every actual action. -Homeworlds, explicit Nexus shipments, special movement and unrecognized action -shapes retain their existing adapters and legal fallback search. They are not -classified as illegal by this narrower preview. This is not a complete action -validator or certification that every AI candidate in every module is legal. +Homeworld-sourced ordinary shipments now quote the same public arrival triggers, +including a colocated Ambassador and Terror, after the bot selects its own +physical source. Their off-planet origin is sufficient for the arrival quote; +Homeworld source eligibility, tariffs and any payment-dependent Guild response +remain authoritative engine checks, not duplicated AI rules. Other Homeworld +routes, explicit Nexus shipments, special movement and unrecognized actions +retain their own adapters and legal fallback search. This is not a complete +action validator or certification that every AI candidate is legal. ## Evidence @@ -53,3 +57,18 @@ stance settlement does not turn a lone advisor group into fighters. Final checks and same-seed integrated results are recorded with the source-bound private checkpoint and Git message. Existing saved games remain protected; no server restart or database change is required. + +## 29 September: Homeworld-sourced shipment overlap + +A genuine five-seat Advanced combined Homeworld/Nexus/expansion game on +`41fd6fe` completed in 786 actions but proposed two Tleilaxu shipments from +its own Homeworld into unsupported Ambassador/Terror arrival combinations. +Both were rejected without writes before a legal alternative was chosen. The +owner-view preflight formerly skipped every Homeworld shipment. It now passes +off-planet Homeworld-source arrivals through the same public quote without +assuming a price or hidden Terror face. All four AI profiles exclude the +reproduced action while retaining an executable alternative. A same-seed +genuine-setup replay completes in 786 accepted attempts, zero rejections, +21 JSON continuations and the same fourth-turn shared victory. This repairs +candidate legality, not the unsupported arrival-rule composition or public +expansion start gate. diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index a7a3a962..5cb28ea9 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -6,6 +6,14 @@ Expansion starts and full rules/publication acceptance remain gated.** ## Current checkpoint and work +A five-seat Advanced combined Homeworld/Nexus/expansion sample exposed two +known-unsupported Ecaz Ambassador/Moritani Terror shipment candidates from +Tleilaxu's own Homeworld. [AI arrival preflight](BOT_ARRIVAL_PREFLIGHT.md#29-september-homeworld-sourced-shipment-overlap) +now excludes them using only public entry reactions. The same genuine game +completed with 786/786 legal attempts and 21 JSON continuations; this is a +candidate-legality repair, not support for the combined arrival or a public +expansion start. The source is not in the last verified live image. + A bounded [Ecaz Ambassador/Moritani Terror same-entry continuation](MORITANI_ENTRY_TIMING.md#selected-bounded-overlap-order--29-september-2026) now snapshots both original optional triggers and offers their owners sequential choices in current storm order on classic plus Ecaz/Moritani Basic/Advanced diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index f8763d3e..7f47d704 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,26 @@ # Dune implementation status +## 29 September 2026 — Homeworld-sourced AI arrival preflight + +A genuine five-seat Advanced combined Homeworld/Nexus/expansion game on +`41fd6fe` exposed two rejected Tleilaxu shipments from its own Homeworld +into unsupported Ambassador/Terror entry combinations. The public AI +[preflight](BOT_ARRIVAL_PREFLIGHT.md#29-september-homeworld-sourced-shipment-overlap) +had skipped Homeworld sources. It now quotes their off-planet arrival through +the same authoritative public reaction helper, without sampling private +Terror faces or recreating source pricing. A failing-before/passing-after +real-engine regression exercises all four AI profiles and executable +fallbacks. The identical genuine game completes after the fix with +786 accepted attempts, no rejected candidates, 21 JSON continuations and +a fourth-turn shared victory. Other Homeworld payment-dependent reactions +and unsupported combined arrivals remain gated; no public start was opened. + +The initial full check exposed an existing Guild shipment test that asserted +payment before a legitimately opened Karama rate response. That test now +resolves the response before checking the rounded cost. The final +`npm run check` passes typecheck, lint and 5,757/5,757 offline cases. No HTTP +contract, database schema or player-facing control changed. + ## 29 September 2026 — Sapho finite-bid boundary and pending source questions The existing Once Around Juice of Sapho adapter permits an unacted holder to diff --git a/game/bot-arrival.ts b/game/bot-arrival.ts index c44a7163..72d523b7 100644 --- a/game/bot-arrival.ts +++ b/game/bot-arrival.ts @@ -14,10 +14,11 @@ import { import { quoteSmugglerShipment } from './smuggler-shipment'; /** - * Known arrival support for ordinary owner-view candidates, not a complete - * action validator. Homeworld, explicit Nexus and special movement routes keep - * their own adapters and fallbacks. No hidden Terror face or rival hand enters - * this quote, and unexpected failures are never treated as rejected candidates. + * Known arrival support for ordinary owner-view candidates, plus the public + * overlap guard for Homeworld-sourced shipments. This is not a complete action + * validator. Explicit Nexus and special movement routes retain their adapters. + * No hidden Terror face or rival hand enters this quote; unexpected failures + * are never treated as rejected candidates. */ export function botArrivalBlock(g: GameView, action: Action): string | null { if ( @@ -25,10 +26,8 @@ export function botArrivalBlock(g: GameView, action: Action): string | null { g.phase !== 5 || g.active !== g.me || !['ship', 'move'].includes(action.type) || - g.homeworlds || action.nexus !== undefined || action.source !== undefined || - action.homeworldSources !== undefined || typeof action.territory !== 'string' || typeof action.sector !== 'number' || !validLocation(action.territory, action.sector) @@ -45,9 +44,21 @@ export function botArrivalBlock(g: GameView, action: Action): string | null { paidBox: g.decision?.kind === 'nullentropy', }; let order: CompletedMovementArrivalInput['order']; - if (action.type === 'ship') { - if (g.moritaniAtomics?.territory === to) - return 'Atomics Aftermath permanently blocks shipments into this territory.'; + if (action.type === 'ship' && g.moritaniAtomics?.territory === to) + return 'Atomics Aftermath permanently blocks shipments into this territory.'; + if (g.homeworlds || action.homeworldSources !== undefined) { + if (action.type !== 'ship' || !g.homeworlds || action.homeworldSources === undefined) + return null; + // Off-planet Homeworld pools have the same arrival triggers. Only the + // public reaction overlap is quoted here; the engine owns source and price. + order = { + player: me.id, + origin: 'homeworld', + to, + advisors: arrivalAsAdvisor(g, me, to), + wantsFighters: false, + }; + } else if (action.type === 'ship') { const amount = action.noField === undefined ? action.amount : 1; if ( typeof amount !== 'number' || diff --git a/tests/bot-arrival.test.ts b/tests/bot-arrival.test.ts index 1df378c2..c95bae8e 100644 --- a/tests/bot-arrival.test.ts +++ b/tests/bot-arrival.test.ts @@ -3,6 +3,9 @@ import test from 'node:test'; import { botArrivalBlock } from '../game/bot-arrival'; import { botActions } from '../game/bots'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; +import { createHomeworldCustody } from '../game/homeworld-custody'; +import { homeworldContext } from '../game/homeworld-game'; +import { createTerrorState } from '../game/moritani-terror'; import { arrivalMove, arrivalPlayer, @@ -52,6 +55,35 @@ void test('all four profiles avoid the reproduced Ambassador/Terror entries and } }); +void test('Homeworld-sourced AI shipments avoid unsupported paired entry reactions', () => { + const game = movementArrivalGame(); + game.homeworlds = { custody: createHomeworldCustody(homeworldContext(game)) }; + arrivalPlayer(game, 'tleilaxu').shipped = false; + const action: Action = { + type: 'ship', + territory: 'carthag', + sector: 11, + amount: 1, + elite: 0, + homeworldSources: { 'homeworld:tleilaxu': { normal: 1, elite: 0 } }, + }; + assert.throws(() => applyAction(game, 'tleilaxu', action), overlap); + const before = JSON.stringify(game); + for (const profile of profiles) { + const view = ownView(game, profile); + assert.match(botArrivalBlock(view, action) ?? '', overlap); + const choices = botActions(view); + assert.ok(choices.length, profile); + assert.equal(choices.some(candidate => candidate.type === 'ship' && + candidate.territory === 'carthag'), false, profile); + assert.ok(applyAction(game, view.me, choices[0])); + } + assert.equal(JSON.stringify(game), before); + game.moritaniTerror = createTerrorState(() => 0.2); + assert.equal(botArrivalBlock(ownView(game), action), null); + assert.ok(applyAction(game, 'tleilaxu', action)); +}); + void test('single reactions and matching-faction Ambassador immunity remain playable', () => { for (const tokens of ['ambassador', 'terror'] as const) { const game = movementArrivalGame('tleilaxu', tokens); diff --git a/tests/engine.test.ts b/tests/engine.test.ts index eeed8214..2dbb755e 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -882,10 +882,10 @@ void test('Guild groups from several sectors pay one rounded shipment cost', () type: 'guildShip', forces: { 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 }, }; - const returned = applyAction(g, 'p0', { ...action, territory: 'reserves' }); + const returned = passResponses(applyAction(g, 'p0', { ...action, territory: 'reserves' })); assert.equal(returned.players[0].spice, before - 2); assert.equal(returned.players[0].reserves, 18); - g = applyAction(g, 'p0', { ...action, territory: 'sietch_tabr', sector: 14 }); + g = passResponses(applyAction(g, 'p0', { ...action, territory: 'sietch_tabr', sector: 14 })); assert.equal(g.players[0].spice, before - 2); assert.equal(g.players[0].forces['sietch_tabr:14'], 3); conservation(g);