Connect Advanced allied reserve transit and legal departure

This commit is contained in:
Quintine committed 2026-10-08 16:58:21 +11:00
1 parent 869412bebd
commit 7cef1a3412
10 files changed
+118 -44

No files matched your search

+6 -6
View File
@@ -177,7 +177,7 @@ import { nexusGuildSecretAllyAction, nexusGuildSecretAllyQuote } from '@/game/ne
import { nexusRicheseAction, nexusRicheseQuote } from '@/game/nexus-richese-options';
import { guildTransportQuote } from '@/game/transport-quote';
import { fremenReserveEntry, botGroundMoveAllowed } from '@/game/bot-mobility';
import { isBasicAlliedShipmentVisit } from '@/game/allied-separation';
import { isAlliedShipmentVisit } from '@/game/allied-separation';
import { location as boardLocation } from '@/game/board';
import { fighterCount, isAdvisor } from '@/game/advisors';
import { useState } from 'react';
@@ -4396,12 +4396,12 @@ export function GameTable({
</div>
)}
{destination}
{isBasicAlliedShipmentVisit(g, me, selected) && (
{isAlliedShipmentVisit(g, me, selected) && (
<p className="notice">
Basic allied transit: ship ordinary forces here, then move the visiting group out
before finishing your turn. Storm and stronghold capacity still apply.
Provisional fallback: new visitors left behind go to Tanks, even if departure is prevented.
This does not waive the older allied co-occupation deadline.
{g.advanced
? 'Advanced allied transit: ship ordinary forces here, then depart before finishing. Supplied p15 sends all your forces left sharing with an ally to Tanks, including old forces; advisor, Polar Sink and native Ecaz coexistence exemptions remain.'
: 'Basic allied transit: ship ordinary forces here, then move the visiting group out before finishing your turn. Provisional fallback: new visitors left behind go to Tanks, even if departure is prevented. This does not waive the older allied co-occupation deadline.'}
{' '}Storm and stronghold capacity still apply.
</p>
)}
{amountInput}
+38 -11
View File
@@ -2,6 +2,33 @@
Audit: 7 September 2026. Read-only source and code audit, retained by the coordinating agent. Scope is classic GF9 base rules and the applicable November 2020 FAQ, not tournament or movie-game rules.
## Advanced ordinary allied shipment follow-up — 8 October 2026
Ordinary physical reserve shipment may now enter an ally's territory on the
shipper's own unused turn before movement, under the same scoped entry
permission as Basic. This composes the November FAQ's shipment/departure
permission with the authorized supplied **physical p15** ending constraint.
Stronghold capacity, storm, price, counters and movement range still apply.
No-Fields, allied No-Field offers, Homeworld and mobile transports keep their
separate guards; normal ground-movement endpoints are not broadened.
Advanced creates no Basic visitor receipt. At ending, the existing native
Advanced quote removes **all the ending player's shared forces**, including
old counters, not just newcomers. Advisor, Polar Sink and native Ecaz
coexistence exemptions remain. Basic keeps its visibly provisional excess-
visitor policy and older co-occupation deadline. No extra move or refund
is granted when departure is unavailable.
The pre-shipment notice explains the mode-specific consequence; the
existing ending warning names the shared territories. Minimal legal AI
prioritizes a native departure and does not leave a city sentinel behind.
Three direct smokes cover paid normal1/special1 arrival and Medium departure,
ending with old5 plus new2 (only the actor's7 go to Tanks), and third-faction
capacity rejection. Chromium observed native GameTable SSR pre-arrival and
ending warnings with enabled finish. No broad suite, recovery campaign or
deployed acceptance was run.
## Basic ordinary allied shipment prototype — 8 October 2026
The content-first Basic pass connects the FAQ's ordinary physical
@@ -29,10 +56,10 @@ co-occupation deadlines still resolve separately. AI prioritizes available
native departures and does not retain a stronghold sentinel from a
visiting group.
Concealed and allied No-Field arrivals, mobile/homeworld transports,
optional arrival compositions and Advanced entry permission are not
expanded by this prototype. Refinement of interrupted and mixed-cohort
interactions remains later work.
Concealed and allied No-Field arrivals, mobile/homeworld transports and
optional arrival compositions are not expanded by the Basic prototype.
Advanced ordinary physical entry is connected in the follow-up above.
Refinement of interrupted and mixed-cohort interactions remains later work.
**Brief smoke evidence:** paid Basic allied arrival followed by Medium's
native departure and clean ending; ending without departure removes two
@@ -69,13 +96,13 @@ the existing later-player and formation-turn conditions, including its
non-exempt advisor treatment.
This resolves the **Advanced** timing/advisor and end-turn consequence
questions below. It does not remove the current allied-entry guard, grant
new shipments or extra moves, alter alliance formation/breaking windows,
or settle every optional arrival composition. A forced end consequence
does not require a new loss confirmation; the existing `endMovement`
action remains legal even when no escape is available. The native force,
elite and No-Field custody paths remove the ending player's group once,
not its ally's group.
questions below. The 8 October follow-up also connects scoped ordinary
allied reserve entry, without granting new shipments or extra moves,
altering alliance formation/breaking windows or settling optional arrivals.
A forced end consequence needs no new loss confirmation; `endMovement`
remains legal even when no escape is available. Native custody removes the
ending player's group once, not its ally's group; concealed No-Field removal
retains its separate custody guard.
The own clean-turn warning uses the canonical engine quote via
`GameView.advancedAllySeparation: { territories: string[] } | null`.
+16 -4
View File
@@ -20,10 +20,14 @@ open mode gates.
requests usable first versions for every faction before refinement. Administration
is deferred. Broad suites and capture-resumption campaigns are deferred; rule
delivery uses brief changed-path smoke only, with batched compiler feedback.
**Active pass — Advanced first versions.** Basic standalone core-function
inventory is closed at prototype level, with its stated provisional policies.
This does not certify Basic timing variants, combinations or public expansion
starts. Those remain after the Advanced function pass.
**Active pass — rule interactions and remaining optional paths.** Basic and
Advanced standalone core-function inventories are closed at prototype level.
Bounded classic/expansion mapping plus the connected follow-ups below found
no further absent standalone faction primitive in the examined paths.
This is not complete-mode acceptance: exceptional custody, timing variants,
optional skill bands, combinations and public expansion starts remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Richese cache cancellation connected.** The printed Karama cancellation
@@ -120,6 +124,14 @@ Two native Easy smokes, two cycle regression cases, owned GameTable SSR
Chromium choices and types/lint pass. No broad suite or deployed acceptance
was run; prevention, price, death history and one-leader allowance remain.
**Advanced ordinary allied arrival connected.** Scoped physical reserve
shipment can enter an ally's territory before movement. Native supplied
p15 ending removes all shared actor forces, including old counters; Basic
keeps its separate provisional visitor policy. Medium departs typed arrivals
without a city sentinel. Three direct scenarios, one cross-mode arrival/ending
regression, types/lint and native GameTable SSR pre-arrival/ending controls
pass; no broad suite or deployed proof was run.
**7 October 2026 — five pending rulings resolved by the user.** The user
answered five recorded questions; each is now recorded in
[the decision index](RULE_DECISIONS.md#user-rulings--7-october-2026) and
+10 -3
View File
@@ -25,9 +25,10 @@ rulings below remain distinct from these prototype choices.
## Advanced prototype pass — 8 October 2026
The Basic standalone core-function inventory is closed at first-version
level, not complete-mode acceptance; Advanced function delivery now precedes
timing/combination refinement. Ecaz's supplied physical PDF **p30, A**
Basic and Advanced standalone core-function inventories are closed at
first-version level, not complete-mode acceptance. Exceptional custody,
timing variants and optional combinations remain active rule work.
Ecaz's supplied physical PDF **p30, A**
requires an available Duke grant after accepted Ambassador alliance consent.
The accepting action now performs that same-disc transfer automatically,
without a new loan acknowledgement. Basic keeps its optional prototype.
@@ -57,6 +58,12 @@ The separate five-ordinary-disc cycle exclusion is a visibly provisional
implementation policy, not a numerical user ruling or publisher answer.
[Source, policy and runtime evidence](CHOAM_AUDITOR.md).
Advanced ordinary allied reserve shipment now uses the FAQ's scoped
shipment-then-departure permission and supplied **p15** end-turn constraint.
Its ending removes all shared actor forces, not the Basic prototype's
excess newcomers. Storm/capacity and advisor/Polar/Ecaz exemptions remain.
No-Field and optional transports are separate. [Allied transit](ALLIED_TRANSIT_RULES.md).
## User rulings — 7 October 2026
Five previously pending questions were answered directly by the user. These are
+3 -4
View File
@@ -12,10 +12,9 @@ export type BasicAlliedShipment = Readonly<{
baseline: Readonly<{ normal: number; elite: number }>;
}>;
/** Ordinary Basic reserve arrivals only; movement endpoints keep their guard. */
export function isBasicAlliedShipmentVisit(
/** Ordinary reserve arrivals only; movement endpoints keep their guard. */
export function isAlliedShipmentVisit(
context: Readonly<{
advanced: boolean;
phase: number;
active: string | null;
players: readonly OccupancySeat[];
@@ -23,7 +22,7 @@ export function isBasicAlliedShipmentVisit(
player: OccupancySeat & Readonly<{ shipped?: boolean; moved?: number }>,
to: string,
): boolean {
if (context.advanced || context.phase !== 5 || context.active !== player.id ||
if (context.phase !== 5 || context.active !== player.id ||
player.shipped !== false || player.moved !== 0 || to === 'polar_sink' ||
to === MOBILE_STRONGHOLD) return false;
const ally = context.players.find(seat => seat.id === player.ally);
+2 -2
View File
@@ -9,7 +9,7 @@ import {
type PlanetologistMoveMode,
} from './planetologist-movement';
import { territoryEntryBlock, strongholdPathBlocked } from './occupancy';
import { isBasicAlliedShipmentVisit } from './allied-separation';
import { isAlliedShipmentVisit } from './allied-separation';
import {
distance,
gameDistance,
@@ -67,7 +67,7 @@ export function botEntryAllowed(
return false;
try {
return !territoryEntryBlock(g.players, p.id, to, advisors,
kind === 'ship' && isBasicAlliedShipmentVisit(g, p, to));
kind === 'ship' && isAlliedShipmentVisit(g, p, to));
} catch (error) {
if (error instanceof EcazOccupancyError) return false;
throw error;
+8 -5
View File
@@ -3485,6 +3485,7 @@ function policyActions(g: GameView): Action[] {
const retained =
level > 0 &&
g.basicAlliedShipment?.territory !== territory(origin).name &&
!g.advancedAllySeparation?.territories.includes(territory(origin).name) &&
territory(origin).type === 'stronghold' &&
!isAdvisor(me, origin)
? 1
@@ -4128,13 +4129,15 @@ export function botActions(g: GameView): Action[] {
return sourced && botHomeworldShipmentPaymentAllowed(g, sourced) && !homeworldRevivalActionBlock(g, sourced) && !botArrivalBlock(g, sourced) ? [sourced] : [];
});
const me = g.players.find((p) => p.id === g.me)!;
if (g.basicAlliedShipment && g.phase === 5 && g.active === me.id &&
me.shipped && me.moved === 0) {
// Do not choose an unrelated move or retain a city sentinel while visitors
// owe departure. Native response/decision branches do not offer these moves.
const advancedDepartures = g.advancedAllySeparation?.territories;
if ((g.basicAlliedShipment || advancedDepartures?.length) &&
g.phase === 5 && g.active === me.id && me.shipped && me.moved === 0) {
// Depart from shared groups before unrelated moves; do not retain a
// sentinel that the ending alliance constraint would send to Tanks.
const departures = actions.filter(action => action.type === 'move' &&
typeof action.from === 'string' &&
territory(splitLocation(action.from).territory).name === g.basicAlliedShipment!.territory);
(territory(splitLocation(action.from).territory).name === g.basicAlliedShipment?.territory ||
advancedDepartures?.includes(territory(splitLocation(action.from).territory).name)));
if (departures.length) return departures;
}
const ghola = standaloneGholaAction(g, actions);
+4 -4
View File
@@ -567,7 +567,7 @@ import {
settleAdvisors,
arrivalAsAdvisor,
} from './advisors';
import { quoteAlliedSeparation, isBasicAlliedShipmentVisit, alliedShipmentPhysicalPool,
import { quoteAlliedSeparation, isAlliedShipmentVisit, alliedShipmentPhysicalPool,
type BasicAlliedShipment } from './allied-separation';
import { casualtyOptions, maxCombatDial, maxCombatSupport, validCombatForces, type Casualties, type CombatForces } from './combat';
import { FACTIONS, faction, type FactionId } from './catalog';
@@ -25095,7 +25095,7 @@ function validatePhysicalShipment(g: Game, shipment: PendingShipment) {
shipment.sector,
!shipment.guildSecretEvent && g.advanced && p.faction === 'fremen',
shipment.advisors,
!shipment.noField && !shipment.alliedNoField && isBasicAlliedShipmentVisit(g, p, shipment.territory),
!shipment.noField && !shipment.alliedNoField && isAlliedShipmentVisit(g, p, shipment.territory),
);
if (p.faction === 'fremen' && !shipment.guildSecretEvent)
requireRule(
@@ -25594,7 +25594,7 @@ function commitShipment(g: Game, shipment: PendingShipment, settlement?: {recipi
}
let homeworldOrigins = '';
if (!shipment.noField) {
if (!shipment.alliedNoField && n > 0 && isBasicAlliedShipmentVisit(g, p, to))
if (!g.advanced && !shipment.alliedNoField && n > 0 && isAlliedShipmentVisit(g, p, to))
g.basicAlliedShipment = {
player: p.id, ally: p.ally!, turn: g.turn, territory: to,
baseline: alliedShipmentPhysicalPool(p, to),
@@ -30940,7 +30940,7 @@ function applyActionInner(
'Only Ixians may ship directly into the mobile stronghold.',
);
allowedEntry(g, p, to, s, !guildSecretEvent && g.advanced && p.faction === 'fremen', advisors,
action.noField === undefined && isBasicAlliedShipmentVisit(g, p, to));
action.noField === undefined && isAlliedShipmentVisit(g, p, to));
let noField: PendingShipment['noField'];
if (action.noField !== undefined) {
requireRule(
+7 -5
View File
@@ -145,7 +145,7 @@ const phaseDetails = [
'For movement, select the source forces. Enable Combine sectors to include several groups from the same territory.',
'A Fremen move using the two-territory advantage has a Karama response. Cancellation leaves the forces in place and limits this move to one territory; city ornithopters and a later extra move keep their normal rules.',
'Guild transport can move one exact physical group across the planet or return it to reserves. In classic games its normal half-price rate can be canceled separately from its transport permission. Ordinary movement is checked against range, storm and stronghold occupancy.',
'You may pass through an ally’s territory on a legal route that ends elsewhere. Basic ordinary physical reserve shipments may also enter allied territory before movement, provided the visiting group departs during that turn. Storm and stronghold capacity remain binding. The table visibly labels its provisional no-exit policy: excess new visitors still present when the turn ends go to Tanks, without destroying the ally or granting an extra move. Concealed No-Fields, optional arrivals and Advanced entry remain separate.',
'You may pass through an ally’s territory on a legal route that ends elsewhere. Ordinary physical reserve shipments in both modes may enter allied territory before movement, then depart during that turn. Storm and stronghold capacity remain binding. Basic visibly labels its provisional no-exit policy: excess new visitors still present go to Tanks. Advanced follows supplied p15: all the ending player’s shared forces go to Tanks, including old forces, subject to advisor/Polar/Ecaz exemptions. Neither grants an extra move. Concealed No-Fields and optional arrivals remain separate.',
'Ecaz and its reciprocal ally may share territories and count as one faction toward stronghold capacity, without extra movement or storm/Homeworld exceptions. Their three-joint-stronghold victory and desert allocation are connected. Fremen/Ecaz cooccupation permits the Fremen special win in Sietch Tabr in Basic; supplied Advanced p32 permits both Tabr and Habbanya Sietch. Other victory conditions and exceptional combined interactions remain separate.',
'A restored pending physical shipment is checked again before forces or spice are committed. If its saved turn, force allocation, destination, price or authorized contribution no longer matches the table, it is rejected without changing the saved declaration.',
'Finish shipment and movement explicitly. Any Bene Gesserit free-shipment decision and its response must finish first.',
@@ -3144,18 +3144,20 @@ export const RULE_TOPICS: RuleTopic[] = [
title: 'Advanced alliances: ending shared occupation',
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Each ending player loses its own shared fighters; advisor coexistence, Polar Sink and native Ecaz coexistence are exempt.',
steps: [
'Under the adopted unofficial Advanced revision, finish each player’s Shipment and Movement separately. Your own fighters still sharing a territory with your ally go to Tanks when your turn ends. Your ally’s fighters stay; its later turn does not postpone your loss. An alliance formed this turn has no Advanced grace period.',
'Polar Sink is exempt. If either allied group is Bene Gesserit advisors, coexistence sends neither group to Tanks. Bene Gesserit fighters have no advisor exemption. Native Ecaz peaceful coexistence keeps its own conditions, including applicable Karama effects.',
'Before an ordinary finish, the table warns which public territories would lose your forces. It does not reveal force amounts or concealed No-Field values. Finish movement uses the existing action, without another loss confirmation. If no legal escape is available, you may still finish and accept the mandatory consequence.',
'Complete any pending shipment, movement, arrival or response before ordinary finish controls become available. Extra-move and deferred-turn endings use the same separation rule; they do not replay the arrival or charge shipment again.',
'Basic still uses its existing later-player deadline and formation-turn exemption, without this advisor exception. Homeworlds are outside this Dune-territory rule and keep their native allied-entry restrictions. The rule grants no new allied-entry permission or alliance formation window, and completed turns are not reevaluated.',
'An ordinary physical reserve shipment may enter an allied territory before your movement on your own unused turn. Ship, then move out; stronghold capacity and storm still apply. The ending consequence covers your entire shared group, not only new visitors. No-Fields and optional Homeworld/mobile transport keep their separate legality.',
'Basic still uses its older later-player deadline and formation-turn exemption, plus its visibly provisional new-visitor consequence. Homeworlds keep their native allied-entry restrictions. The rule grants no extra shipment, movement or alliance formation window; completed turns are not reevaluated.',
],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'One canonical separation quote distinguishes Basic from Advanced timing and exceptions; native ending-group custody remains responsible for physical losses.', evidence: ['game/allied-separation.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'The own clean-turn warning contains public loss territories only; forced loss adds no action or confirmation and shared pending-control locks retain priority.', evidence: ['components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four legal policies use native projected movement/ending availability and can finish with mandatory loss; no duplicate separation calculator or strategic alliance calibration was added.', evidence: ['game/bots.ts', 'tests/bot-advanced-source.test.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'A shared ordinary reserve-entry quote connects native preflight and commit. Canonical separation distinguishes Basic from Advanced timing and exceptions; native ending-group custody removes all Advanced shared forces.', evidence: ['game/allied-separation.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Pre-shipment notice explains the mode-specific consequence; own clean-turn warning contains public loss territories only. Mandatory loss adds no confirmation and pending-control locks retain priority.', evidence: ['components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four legal policies use projected movement/ending availability. Native departure takes priority, without retaining a doomed city sentinel; a Medium typed-arrival/departure smoke passes. No separate separation calculator or strategic calibration was added.', evidence: ['game/bots.ts', 'game/bot-mobility.ts', 'tests/bot-advanced-source.test.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'The unofficial per-player/advisor departures are distinguished from the earlier publisher/FAQ and unchanged Basic policy.', evidence: ['docs/ALLIED_TRANSIT_RULES.md', 'docs/RULE_DECISIONS.md', 'docs/BASE_ADVANCED_READINESS_20260907.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Genuine native overlaps cover first/later newly allied ends, exact normal/elite custody, Hajr, advisor/Ecaz/Polar exceptions and unchanged Basic policy. Authenticated SQLite covers restart, races, privacy and immutable rejects. Actual phone ending moved only the ending group to Tanks and refresh retained it; wider combinations, full modes and deployment remain open.', evidence: ['tests/allied-separation.test.ts', 'tests/advanced-source-engine.test.ts', 'tests/advanced-source-recovery.test.ts', 'tests/bot-advanced-source.test.ts', 'docs/ALLIED_TRANSIT_RULES.md'] },
],
+24
View File
@@ -4,6 +4,7 @@ import {
quoteAlliedSeparation,
type AlliedSeparationQuoteContext,
} from '../game/allied-separation';
import { applyAction, createGame, newPlayer } from '../game/engine';
type TerritoryFacts = AlliedSeparationQuoteContext['territories'][number];
@@ -196,3 +197,26 @@ void test('quoting frozen canonical facts does not change them or retain a mutab
assert.deepEqual(quoteAlliedSeparation(input), ['arrakeen']);
assert.deepEqual(input, before);
});
void test('ordinary allied reserve arrival retains distinct Basic visitor and Advanced whole-group ending losses', () => {
for (const advanced of [false, true]) {
let g = createGame('ALLIEDARRIVAL', newPlayer('e', 'Emperor', 'emperor'), advanced);
g.players.push(newPlayer('a', 'Atreides', 'atreides'));
g.status = 'playing'; g.phase = 5; g.turn = 2; g.storm = 18;
g.active = 'e'; g.order = ['e', 'a']; g.movementRemaining = ['e', 'a'];
for (const p of g.players) {
p.forces = {}; p.reserves = 20; p.tanks = 0; p.spice = 10;
p.hand = []; p.traitors = []; p.shipped = false; p.moved = 0;
}
g.players[0].ally = 'a'; g.players[1].ally = 'e';
g.players[0].forces = { 'arrakeen:10': 5 }; g.players[0].reserves = 15;
g.players[1].forces = { 'arrakeen:10': 3 }; g.players[1].reserves = 17;
g = applyAction(g, 'e', { type: 'ship', amount: 2, territory: 'arrakeen', sector: 10 });
assert.equal(g.players[0].spice, 8);
g = applyAction(g, 'e', { type: 'endMovement' });
assert.equal(g.players[0].tanks, advanced ? 7 : 2);
assert.equal(g.players[0].forces['arrakeen:10'] ?? 0, advanced ? 0 : 5);
assert.equal(g.players[1].forces['arrakeen:10'], 3);
assert.equal(g.players[1].tanks, 0);
}
});