Preflight Guild transport arrival candidates before bot play

This commit is contained in:
Quintine committed 2026-09-29 22:58:24 +10:00
1 parent 882d006b3c
commit 3d95db88cc
5 files changed
+123 -6

No files matched your search

+24
View File
@@ -72,3 +72,27 @@ 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.
## 29 September: Guild cross-planet transport and Terror
An unmodified six-seat Advanced Moritani sample at base seed `20260930`
completed 1,262 actions and 34 JSON restores but proposed six illegal
cross-planet Guild transports into a placed Terror while Guild income was
pending. The existing bot preflight covered ordinary shipments, not the
single-source `guildShip` action.
The owner-view adapter now quotes those transports with the submitted source,
physical count, Guild tariff and selected ally payment. It excludes only
publicly unsupported arrival combinations; payment entirely funded by the
Guild ally has no Guild income response and remains eligible. Multi-source,
Nexus and special routes still use their separate adapters. No Terror face or
rival private field is read. The authoritative engine still decides every
actual transport.
The focused engine regression first reproduced the rejected Guild action,
then exercised all four profile filters and a Guild-funded accepted action.
Same-seed genuine setup replay completed the same 1,262 accepted actions with
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.
+10
View File
@@ -6,6 +6,16 @@ Expansion starts and full rules/publication acceptance remain gated.**
## Current checkpoint and work
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)
now quotes the submitted single-source transport and payment using public
arrival triggers. The same genuine game completes 1,262 accepted actions,
zero rejected candidates and 34 JSON restores; both assassination choices
were declined. This repairs AI candidate legality, not the unsupported
arrival composition, natural assassination reveal coverage, normal
expansion starts or deployed acceptance.
The owner-only [administrator accounts](ADMIN_ACCOUNTS.md) prototype now
lists accounts, provisions a browser-generated one-time personal key, changes
another account’s role and disables access. Server actions repeat live owner
+21
View File
@@ -1,5 +1,26 @@
# Dune implementation status
## 29 September 2026 — Guild transport arrival preflight
The [arrival preflight](BOT_ARRIVAL_PREFLIGHT.md#29-september-guild-cross-planet-transport-and-terror)
now filters single-source Guild cross-planet transport candidates before
they enter a publicly unsupported Terror/Guild-income reaction combination.
A six-seat Advanced Moritani game at base seed `20260930` first completed
1,262 accepted actions, six rejected Guild candidates and 34 JSON
continuations. Same-seed replay after the fix completed 1,262 accepted
actions, no rejected candidates and 34 JSON continuations. Two real
assassination offers were declined; neither sample shows a natural reveal.
A focused engine test reproduced the illegal action before the fix, then
covered all four bot filters and accepted Guild-funded transport with no
income response. No card face, hidden rival data or unsupported arrival
ordering enters the quote. Existing expansion start and publication gates
stay closed.
`npm run check` passes typecheck, lint and 5,781/5,781 offline cases.
No app route or persisted database changed; the production build and HTTP
suite were not repeated for this bot-only correction.
## 29 September 2026 — owner account management prototype
The [account page](ADMIN_ACCOUNTS.md) lists only safe metadata to a live
+34 -4
View File
@@ -7,6 +7,7 @@ import {
type CompletedMovementArrivalInput,
} from './karama-movement-preflight';
import {
guildShipmentCost,
guildShipmentIncome,
reserveShipmentCost,
shipmentPaymentBounds,
@@ -15,8 +16,9 @@ import { quoteSmugglerShipment } from './smuggler-shipment';
/**
* 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.
* overlap guard for Homeworld-sourced shipments and single-source Guild
* transport. 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.
*/
@@ -25,7 +27,7 @@ export function botArrivalBlock(g: GameView, action: Action): string | null {
g.status !== 'playing' ||
g.phase !== 5 ||
g.active !== g.me ||
!['ship', 'move'].includes(action.type) ||
!['ship', 'guildShip', 'move'].includes(action.type) ||
action.nexus !== undefined ||
action.source !== undefined ||
typeof action.territory !== 'string' ||
@@ -46,7 +48,35 @@ export function botArrivalBlock(g: GameView, action: Action): string | null {
let order: CompletedMovementArrivalInput['order'];
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 === 'guildShip') {
if (
action.forces !== undefined ||
typeof action.from !== 'string' ||
typeof action.amount !== 'number' ||
!Number.isSafeInteger(action.amount) ||
action.amount < 1
) return null;
const origin = action.from === 'reserves' ? 'reserves' : splitLocation(action.from);
if (origin !== 'reserves' && !validLocation(origin.territory, origin.sector))
return null;
const guild = g.players.find((p) => p.faction === 'guild');
const cost = guildShipmentCost(territory(to).type, action.amount,
g.karamaShipping?.player === me.id || !g.guildRateCanceled);
const bounds = shipmentPaymentBounds(cost, me.spice ?? 0, g.aid.available);
const allyPayment = action.allyPayment ?? bounds.minimum;
if (typeof allyPayment !== 'number' || !Number.isSafeInteger(allyPayment) ||
allyPayment < bounds.minimum || allyPayment > bounds.maximum)
return null;
controls.response ||= guildShipmentIncome({
guild: guild?.id, shipper: me.id, ally: me.ally, cost, allyPayment,
bankOnly: g.karamaShipping?.player === me.id,
}) > 0;
order = {
player: me.id,
origin: origin === 'reserves' ? origin : origin.territory,
to, advisors: arrivalAsAdvisor(g, me, to), wantsFighters: false,
};
} else 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
+34 -2
View File
@@ -5,7 +5,7 @@ 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 { createTerrorState, placeTerror } from '../game/moritani-terror';
import {
arrivalMove,
arrivalPlayer,
@@ -81,7 +81,39 @@ void test('Homeworld-sourced AI shipments avoid unsupported paired entry reactio
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('Guild cross-planet transport avoids a Terror arrival blocked by its Guild income response', () => {
const game = shipmentArrivalGame();
const bg = arrivalPlayer(game, 'bg');
const guild = arrivalPlayer(game, 'guild');
game.active = bg.id;
bg.shipped = false;
bg.ally = guild.id;
guild.ally = bg.id;
bg.forces = { 'sietch_tabr:14': 2 };
bg.reserves = 18;
const terror = createTerrorState(() => 0.2);
game.moritaniTerror = placeTerror(terror, terror.tokens[0].id, 'carthag', 2);
const action: Action = { type: 'guildShip', from: 'sietch_tabr:14', amount: 2,
territory: 'carthag', sector: 11 };
assert.throws(() => applyAction(game, bg.id, 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 === 'guildShip' &&
candidate.territory === 'carthag'), false, profile);
assert.ok(applyAction(game, bg.id, choices[0]));
}
assert.equal(JSON.stringify(game), before);
const funded = JSON.parse(before) as Game;
funded.aid[guild.id] = { recipient: bg.id, amount: 1 };
const guildPaid = { ...action, allyPayment: 1 };
assert.equal(botArrivalBlock(ownView(funded), guildPaid), null);
assert.ok(applyAction(funded, bg.id, guildPaid));
});
void test('single reactions and matching-faction Ambassador immunity remain playable', () => {