Preflight worm-ride arrivals for bot candidates

This commit is contained in:
Quintine committed 2026-09-29 23:12:23 +10:00
1 parent 3d95db88cc
commit 583cd97a25
6 files changed
+108 -2

No files matched your search

+18
View File
@@ -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.
+8
View File
@@ -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)
+18
View File
@@ -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)
+34
View File
@@ -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;
}
}
+3 -2
View File
@@ -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(
@@ -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);
});