From 583cd97a250bce4b2a9f570bc1e2893e9d49db41 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:12:23 +1000 Subject: [PATCH] Preflight worm-ride arrivals for bot candidates --- docs/BOT_ARRIVAL_PREFLIGHT.md | 18 +++++++++++ docs/CURRENT_STATUS.md | 8 +++++ docs/IMPLEMENTATION_STATUS.md | 18 +++++++++++ game/bot-arrival.ts | 34 ++++++++++++++++++++ game/bots.ts | 5 +-- tests/ambassador-terror-overlap-worm.test.ts | 27 ++++++++++++++++ 6 files changed, 108 insertions(+), 2 deletions(-) diff --git a/docs/BOT_ARRIVAL_PREFLIGHT.md b/docs/BOT_ARRIVAL_PREFLIGHT.md index 5f84d265..e3c77c16 100644 --- a/docs/BOT_ARRIVAL_PREFLIGHT.md +++ b/docs/BOT_ARRIVAL_PREFLIGHT.md @@ -96,3 +96,21 @@ zero rejected candidates and 34 JSON continuations. Both actual assassination opportunities were declined; this is not a natural reveal or replacement sample. The unsupported reaction combination and public expansion gate remain unchanged. + +## 29 September: worm ride into Terror with BG intrusion + +Another unmodified six-seat Advanced Moritani game at base seed `20261007` +finished in 1,541 accepted actions, two rejected Fremen worm-ride candidates +and 41 JSON continuations. Each rejected ride targeted a placed Terror while +a Bene Gesserit fighter intrusion would open another arrival decision. + +The owned bot candidate now quotes the same public destination triggers +before choosing a worm ride; its current worm decision is the parent, not a +second reaction. It leaves legal single reactions and supported Ambassador/ +Terror overlap available. The focused regression reproduced the engine +rejection, then showed all four bot profiles skip the conflicting destination +and execute an alternative. The same-seed genuine game completes 1,541 +accepted attempts, zero rejects and 41 JSON continuations. No hidden Terror +face or rival hand enters the quote. Homeworld rides and other special +arrivals retain their own gates; this does not implement the conflicting +reaction or open public expansion starts. diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 372cd3a2..14ac74c0 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 +Another natural six-seat Moritani game exposed two illegal Fremen worm +rides into a placed Terror with a pending BG fighter intrusion. The +[worm-arrival bot preflight](BOT_ARRIVAL_PREFLIGHT.md#29-september-worm-ride-into-terror-with-bg-intrusion) +now omits only that public reaction conflict. Same-seed replay completes +1,541 accepted actions, zero rejects and 41 JSON restores. This does not +implement the simultaneous reaction or demonstrate a natural assassination +reveal; expansion starts and deployment remain gated. + A six-seat Advanced Moritani sample exposed six illegal bot Guild cross-planet transport candidates into a placed Terror while Guild income was pending. [Arrival preflight](BOT_ARRIVAL_PREFLIGHT.md#29-september-guild-cross-planet-transport-and-terror) diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index fb37ed0e..388b0ffd 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,23 @@ # Dune implementation status +## 29 September 2026 — worm-ride arrival candidate correction + +A natural six-seat Moritani Advanced sample at base seed `20261007` +completed 1,541 actions with two rejected Fremen worm rides into a placed +Terror while a BG intrusion would open. Both were rejected without mutating +the game. The [owner-view preflight](BOT_ARRIVAL_PREFLIGHT.md#29-september-worm-ride-into-terror-with-bg-intrusion) +now quotes that public worm destination and excludes the unsupported +combined reaction before the bot selects an action. A focused engine +regression reproduced the rejection first and covers four legal AI +fallbacks. The same genuine game finishes 1,541 accepted attempts, zero +rejects and 41 JSON continuations. Two assassination opportunities were +declined; no natural reveal/replacement was proved. The authoritative +reaction-order gap and public expansion gate remain. + +`npm run check` passes typecheck, lint and 5,782/5,782 offline cases. +No app route or persistence schema changed; build and HTTP checks were not +repeated for this bot candidate filter. + ## 29 September 2026 — Guild transport arrival preflight The [arrival preflight](BOT_ARRIVAL_PREFLIGHT.md#29-september-guild-cross-planet-transport-and-terror) diff --git a/game/bot-arrival.ts b/game/bot-arrival.ts index 60f86cf8..5015d21a 100644 --- a/game/bot-arrival.ts +++ b/game/bot-arrival.ts @@ -224,3 +224,37 @@ export function botArrivalBlock(g: GameView, action: Action): string | null { throw error; } } + +/** The worm decision itself is replaced before entry reactions; only a new + * intrusion or another pending control conflicts with the destination. + * Homeworld riders retain their separate unsupported-combination policy. + */ +export function botWormRideArrivalBlock(g: GameView, to: string, sector: number): string | null { + const ride = g.decision; + if (g.status !== 'playing' || g.phase !== 1 || ride?.kind !== 'wormRide' || + ride.player !== g.me || g.homeworlds || !validLocation(to, sector)) + return null; + try { + quoteCompletedMovementArrival({ + advanced: g.advanced, + overlapSupported: g.arrivalOverlapMode === true, + players: g.players, + order: { player: g.me, origin: ride.territory, to, + advisors: false, wantsFighters: false }, + ambassadors: (g.ambassadors?.tokens ?? []).map( + ({ zone, location, effect }) => ({ zone, location, effect })), + terror: (g.moritaniTerror?.tokens ?? []).map( + ({ status, location }) => ({ status, location })), + controls: { + response: !!g.response, decision: false, + pendingTerror: !!g.terrorEntry, pendingAmbassador: !!g.ambassadorEntry, + paidBox: false, + }, + flight: null, + }); + return null; + } catch (error) { + if (error instanceof MovementArrivalError) return error.message; + throw error; + } +} diff --git a/game/bots.ts b/game/bots.ts index 107e9c8c..28f735e3 100644 --- a/game/bots.ts +++ b/game/bots.ts @@ -100,7 +100,7 @@ import { sandmasterDefaultChoice } from './sandmaster-movement'; import { sandmasterWormCollection } from './sandmaster-worm'; import { richeseCardDefinition } from './richese-cards'; import { presenceAt } from './force-presence'; -import { botArrivalBlock } from './bot-arrival'; +import { botArrivalBlock, botWormRideArrivalBlock } from './bot-arrival'; import { validateCohortSelection, type OrnithopterMode } from './ornithopter'; import { fighterCount, isAdvisor } from './advisors'; import { casualtyOptions, maxCombatDial, maxCombatSupport, type CombatForces } from './combat'; @@ -2667,7 +2667,8 @@ function policyActions(g: GameView): Action[] { ...destinations(g) .filter( (to) => - to.t !== d.territory && botEntryAllowed(g, me, to.t, to.s, 'move'), + to.t !== d.territory && botEntryAllowed(g, me, to.t, to.s, 'move') && + !botWormRideArrivalBlock(g, to.t, to.s), ) .slice(0, 16) .map( diff --git a/tests/ambassador-terror-overlap-worm.test.ts b/tests/ambassador-terror-overlap-worm.test.ts index 22f2ef53..076a2cec 100644 --- a/tests/ambassador-terror-overlap-worm.test.ts +++ b/tests/ambassador-terror-overlap-worm.test.ts @@ -12,6 +12,7 @@ import { baseDeck } from '../game/cards'; import { createAmbassadors, placeAmbassador } from '../game/ecaz-ambassadors'; import { createTerrorState, placeTerror, type TerrorKind } from '../game/moritani-terror'; import { botArrivalBlock } from '../game/bot-arrival'; +import { botActions } from '../game/bots'; const seat = (g: Game, id: string) => g.players.find((p) => p.id === id)!; const reload = (g: Game): Game => JSON.parse(JSON.stringify(g)); @@ -192,3 +193,29 @@ void test('bots treat the supported overlapping stronghold as a legal ordinary a assert.equal(entered.pendingTerrorEntry?.resume, 'none'); assert.equal(seat(entered, 'in').forces['arrakeen:10'], 2); }); + +void test('worm-riding bots avoid Terror arrival while BG fighter intrusion is pending', () => { + const { g } = table('robbery'); + g.ecazAmbassadors = createAmbassadors(() => 0.2); + const bg = newPlayer('bg', 'Bene Gesserit', 'beneGesserit'); + bg.forces = { 'arrakeen:10': 1 }; + bg.reserves = 19; + g.players.push(bg); + g.order.push(bg.id); + g.movementRemaining!.push(bg.id); + g.decision = { kind: 'wormRide', player: 'in', territory: 'imperial_basin' }; + g.wormRides = ['hagga_basin']; + const before = JSON.stringify(g); + assert.throws(() => action(g, 'in', ride), + /Terror combined with another arrival reaction/); + for (const bot of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) { + const view = viewGame(g, 'in'); + view.players.find(p => p.id === 'in')!.bot = bot; + const choices = botActions(view); + assert.ok(choices.length, bot); + assert.equal(choices.some(choice => choice.type === 'decision' && + choice.accept === true && choice.territory === 'arrakeen'), false, bot); + assert.ok(applyAction(g, 'in', choices[0])); + } + assert.equal(JSON.stringify(g), before); +});