From e41abdaf2e461d1a77eb0dff1c22f2f265ef8f8a Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:11:45 +1100 Subject: [PATCH] Complete concealed No-Field allied separation endings --- README.md | 5 + components/advanced-ally-separation.tsx | 19 +- components/game-table.tsx | 5 +- docs/ALLIED_TRANSIT_RULES.md | 79 +++++++- docs/CURRENT_STATUS.md | 38 ++++ docs/IMPLEMENTATION_STATUS.md | 39 ++++ docs/RICHESE_NO_FIELD_RULES.md | 12 ++ docs/RULE_DECISIONS.md | 13 ++ game/bots.ts | 4 +- game/engine.ts | 27 ++- game/reference.ts | 7 +- game/richese-no-field.ts | 4 +- tests/fixture-advanced-source.ts | 2 + tests/fixture-no-field-allied-separation.ts | 173 ++++++++++++++++++ ...-field-allied-separation-controls.test.tsx | 46 +++++ .../no-field-allied-separation-engine.test.ts | 124 +++++++++++++ tests/richese-no-field.test.ts | 2 + 17 files changed, 569 insertions(+), 30 deletions(-) create mode 100644 tests/fixture-no-field-allied-separation.ts create mode 100644 tests/no-field-allied-separation-controls.test.tsx create mode 100644 tests/no-field-allied-separation-engine.test.ts diff --git a/README.md b/README.md index df628ba4..082120bb 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,11 @@ advisors, Polar Sink and native Ecaz coexistence retain their exceptions. Basic and an already recorded current Storm keep their existing protocol. See [Advanced source boundaries](docs/BASE_ADVANCED_READINESS_20260907.md#authorized-advanced-source-follow-up--1-october-2026) and [allied separation](docs/ALLIED_TRANSIT_RULES.md). +At an allied separation deadline, a concealed No-Field provisionally reveals +before the ending player's actual shared forces go to Tanks. The persistent +warning identifies the affected territory; finishing needs no extra confirmation. +Basic retains its later deadline and new-alliance grace. +See [separation source and limits](docs/ALLIED_TRANSIT_RULES.md). Advanced Hajr is played immediately after a completed Force Movement and its required arrival decisions. Basic keeps its broader own-turn timing. The [Ornithopter combination](docs/RICHESE_ORNITHOPTER_ENGINE_AUDIT.md#hajr-and-unused-movement-entitlements--18-october-2026) diff --git a/components/advanced-ally-separation.tsx b/components/advanced-ally-separation.tsx index 316074a7..e1ba6533 100644 --- a/components/advanced-ally-separation.tsx +++ b/components/advanced-ally-separation.tsx @@ -1,12 +1,23 @@ import type { GameView } from '@/game/engine'; /** The engine quote carries public locations only, never concealed force values. */ -export function AdvancedAllySeparation({ quote }: { +export function AdvancedAllySeparation({ quote, basic = false }: { quote: GameView['advancedAllySeparation']; + basic?: boolean; }) { if (!quote?.territories.length) return null; - return
)} -Guild Cunning ends your ordinary shipment and movement, then offers one second shipment at normal Guild prices. Only an unused Hajr move can follow it. Provisional card combination: Ornithopter can replace that unused Hajr movement; declaring Cunning abandons any unfinished current card group.
@@ -5215,7 +5215,8 @@ export function GameTable({ !nexusGuildSkipShipmentAction(g, guildCunning.event))} {actionButton(guildCunning?.stage === 'secondShipment' ? 'Decline second shipment and finish' : guildCunning ? 'Finish Guild turn' : 'Finish shipment & movement', {type: 'endMovement'}, false, - g.advancedAllySeparation?.territories.length ? 'advanced-ally-separation' : undefined)} + g.advancedAllySeparation?.territories.length ? 'advanced-ally-separation' : + g.basicAllySeparation?.territories.length ? 'basic-ally-separation' : undefined)} > ) : (
diff --git a/docs/ALLIED_TRANSIT_RULES.md b/docs/ALLIED_TRANSIT_RULES.md
index e0c90e86..f9bec3df 100644
--- a/docs/ALLIED_TRANSIT_RULES.md
+++ b/docs/ALLIED_TRANSIT_RULES.md
@@ -2,6 +2,81 @@
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.
+## Concealed No-Field at the separation deadline — 27 October 2026
+
+A deployed marker is one force for all effects, including0, under publisher E2
+FAQ printed10 and supplied physical36 B. The supplied Advanced physical15
+constraint sends the ending player's shared forces to Tanks. Basic retains
+its existing later-player deadline and new-alliance grace. Neither source gives
+a dedicated No-Field/alliance-loss worked example.
+
+Under the 14 October recommended-option authority, the selected **provisional
+destructive composition** reveals a due marker using the distinct native
+`alliedSeparation` cause. Materialize only its original available reserves,
+then send the ending player's actual forces in that territory to Tanks. The
+native token history/event rotates once;0 adds no counter and no casualty.
+The ally's pieces stay. A marker elsewhere, a not-yet-due Basic turn, Polar Sink
+and the existing Advanced advisor/native Ecaz exemptions do not reveal or lose
+that marker. No optional loss confirmation is introduced.
+
+The old custody refusal could prevent the current player from finishing; native
+ending now completes the due consequence and advances the original movement
+queue. Printed movement-card continuations use the same final ending function;
+this does not certify their broader combinations. Other rejected actions keep
+their native guards and no completed turn is reevaluated.
+
+Both modes retain an own clean-turn warning based on the original canonical
+quote. Advanced keeps `advancedAllySeparation`; Basic now projects
+`basicAllySeparation`. Each carries only public territory names and optional
+`noField: true`, never denomination, quantity or token identity. If a marker is
+due, the persistent warning explains provisional revelation and loss. The normal
+Finish control refers to that warning and submits the same `endMovement`.
+Minimal policies use that shared deadline when choosing a physical departure;
+full AI strategy and calibration remain deferred.
+
+This connects ending custody, not allied No-Field shipment, special Guild return,
+Homeworld or any previously unsupported arrival. Saved games and recorded
+alliance timing are not rewritten. Wider interactions and full acceptance remain.
+
+### No-Field focused evidence
+
+A due allied-separation end now reveals the concealed marker through the
+selected provisional destructive composition, materializes only available
+reserves, then sends the ending player's actual shared group to Tanks.0 adds
+no counter. The ally stays unchanged. Basic keeps its later-player deadline
+and formation grace; Advanced keeps per-player timing and advisor/Polar/Ecaz
+exceptions. Normal Finish advances instead of rejecting concealed custody.
+Both modes have a persistent public territory/marker-presence warning, with no
+denomination or quantity and no extra confirmation. Minimal policies reuse the
+same deadline; full AI refinement remains deferred.
+[Source and contract](ALLIED_TRANSIT_RULES.md#concealed-no-field-at-the-separation-deadline--27-october-2026).
+
+Frozen **10-file union, 75/75**, types/lint/build and six original-game programmes
+pass. Actual public Richese/Emperor/Atreides +CHOAM-deck starts, dials4+5,
+marker shipment2 and physical shipment4 create protected Basin9/11 groups.
+An original worm Nexus and two actual alliance consents create co-occupation.
+Advanced0/3/5 and Basic0/5 reach due endings; Basic3 uses the recorded earlier
+turn2 formation prefix. Runtime records 19 GameTable SSR, 6 warning renders
+(5 populated) and 7 actual GameTable Finish callbacks. Source/further recorded
+action arrays total549/19. Other controls use native packets directly. No played
+hand, purse, army, phase, alliance, marker or event is installed. Browser
+hydration/navigation and depleted-reserve native journeys were not operated.
+
+Nine new native/control cases and the expanded pure cause/cap case pass.
+All four existing profiles first choose legal voluntary revelation; the test
+follows actual legal continuation through ending rather than demanding an
+immediate Finish proposal. Quiet source drivers now pass a separate optional
+allied shipment and accept the empty Battle phase's native Collection7. The
+older source fixture also now follows actual Charity order. Failed v1 evidence
+is preserved; v2 reruns the frozen related checks after those source repairs.
+
+Proof: `/home/quintine/.local/share/dune/checkpoints/20261027-no-field-separation-proof-v2/`;
+journeys/HTML: adjacent `20261027-no-field-separation-runtime/run-v1/`.
+All checks use one frozen source, followed only by a recorded three-document
+evidence overlay. Existing server HTTP200, no restart or saved-game/database
+operation. Extra-card endings, broader combinations, full modes and final
+rules/recovery/privacy/browser/deployed acceptance remain due.
+
## Advanced ordinary allied shipment follow-up — 8 October 2026
Ordinary physical reserve shipment may now enter an ally's territory on the
@@ -101,8 +176,8 @@ 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.
+ending player's group once, not its ally's group. The later No-Field follow-up
+above supplies provisional destructive revelation at the same deadline.
The own clean-turn warning uses the canonical engine quote via
`GameView.advancedAllySeparation: { territories: string[] } | null`.
diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md
index ed5e7be8..cc856047 100644
--- a/docs/CURRENT_STATUS.md
+++ b/docs/CURRENT_STATUS.md
@@ -20,6 +20,44 @@ open mode gates.
Keep identified Basic-function gaps ahead of Advanced and wider interactions.
Prototype labels and final acceptance gates remain.
+**No-Field / allied separation — 27 October. Prototyped / Partial.**
+A due allied-separation end now reveals the concealed marker through the
+selected provisional destructive composition, materializes only available
+reserves, then sends the ending player's actual shared group to Tanks.0 adds
+no counter. The ally stays unchanged. Basic keeps its later-player deadline
+and formation grace; Advanced keeps per-player timing and advisor/Polar/Ecaz
+exceptions. Normal Finish advances instead of rejecting concealed custody.
+Both modes have a persistent public territory/marker-presence warning, with no
+denomination or quantity and no extra confirmation. Minimal policies reuse the
+same deadline; full AI refinement remains deferred.
+[Source and contract](ALLIED_TRANSIT_RULES.md#concealed-no-field-at-the-separation-deadline--27-october-2026).
+
+Frozen **10-file union, 75/75**, types/lint/build and six original-game programmes
+pass. Actual public Richese/Emperor/Atreides +CHOAM-deck starts, dials4+5,
+marker shipment2 and physical shipment4 create protected Basin9/11 groups.
+An original worm Nexus and two actual alliance consents create co-occupation.
+Advanced0/3/5 and Basic0/5 reach due endings; Basic3 uses the recorded earlier
+turn2 formation prefix. Runtime records 19 GameTable SSR, 6 warning renders
+(5 populated) and 7 actual GameTable Finish callbacks. Source/further recorded
+action arrays total549/19. Other controls use native packets directly. No played
+hand, purse, army, phase, alliance, marker or event is installed. Browser
+hydration/navigation and depleted-reserve native journeys were not operated.
+
+Nine new native/control cases and the expanded pure cause/cap case pass.
+All four existing profiles first choose legal voluntary revelation; the test
+follows actual legal continuation through ending rather than demanding an
+immediate Finish proposal. Quiet source drivers now pass a separate optional
+allied shipment and accept the empty Battle phase's native Collection7. The
+older source fixture also now follows actual Charity order. Failed v1 evidence
+is preserved; v2 reruns the frozen related checks after those source repairs.
+
+Proof: `/home/quintine/.local/share/dune/checkpoints/20261027-no-field-separation-proof-v2/`;
+journeys/HTML: adjacent `20261027-no-field-separation-runtime/run-v1/`.
+All checks use one frozen source, followed only by a recorded three-document
+evidence overlay. Existing server HTTP200, no restart or saved-game/database
+operation. Extra-card endings, broader combinations, full modes and final
+rules/recovery/privacy/browser/deployed acceptance remain due.
+
**Family Atomics / No-Field — 27 October. Prototyped / Partial.**
The public controlled marker now supplies one qualifying force, including0,
through shared native/view/card legality and the unchanged Basic territory or
diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md
index 596902e5..ce514861 100644
--- a/docs/IMPLEMENTATION_STATUS.md
+++ b/docs/IMPLEMENTATION_STATUS.md
@@ -1,5 +1,44 @@
# Dune implementation status
+## 27 October 2026 — No-Field at allied separation
+
+Prototyped / Partial. A due allied-separation end now reveals the concealed marker through the
+selected provisional destructive composition, materializes only available
+reserves, then sends the ending player's actual shared group to Tanks.0 adds
+no counter. The ally stays unchanged. Basic keeps its later-player deadline
+and formation grace; Advanced keeps per-player timing and advisor/Polar/Ecaz
+exceptions. Normal Finish advances instead of rejecting concealed custody.
+Both modes have a persistent public territory/marker-presence warning, with no
+denomination or quantity and no extra confirmation. Minimal policies reuse the
+same deadline; full AI refinement remains deferred.
+[Source and contract](ALLIED_TRANSIT_RULES.md#concealed-no-field-at-the-separation-deadline--27-october-2026).
+
+Frozen **10-file union, 75/75**, types/lint/build and six original-game programmes
+pass. Actual public Richese/Emperor/Atreides +CHOAM-deck starts, dials4+5,
+marker shipment2 and physical shipment4 create protected Basin9/11 groups.
+An original worm Nexus and two actual alliance consents create co-occupation.
+Advanced0/3/5 and Basic0/5 reach due endings; Basic3 uses the recorded earlier
+turn2 formation prefix. Runtime records 19 GameTable SSR, 6 warning renders
+(5 populated) and 7 actual GameTable Finish callbacks. Source/further recorded
+action arrays total549/19. Other controls use native packets directly. No played
+hand, purse, army, phase, alliance, marker or event is installed. Browser
+hydration/navigation and depleted-reserve native journeys were not operated.
+
+Nine new native/control cases and the expanded pure cause/cap case pass.
+All four existing profiles first choose legal voluntary revelation; the test
+follows actual legal continuation through ending rather than demanding an
+immediate Finish proposal. Quiet source drivers now pass a separate optional
+allied shipment and accept the empty Battle phase's native Collection7. The
+older source fixture also now follows actual Charity order. Failed v1 evidence
+is preserved; v2 reruns the frozen related checks after those source repairs.
+
+Proof: `/home/quintine/.local/share/dune/checkpoints/20261027-no-field-separation-proof-v2/`;
+journeys/HTML: adjacent `20261027-no-field-separation-runtime/run-v1/`.
+All checks use one frozen source, followed only by a recorded three-document
+evidence overlay. Existing server HTTP200, no restart or saved-game/database
+operation. Extra-card endings, broader combinations, full modes and final
+rules/recovery/privacy/browser/deployed acceptance remain due.
+
## 27 October 2026 — Family Atomics and concealed No-Field
Prototyped / Partial. The public controlled marker now supplies one qualifying force, including0,
diff --git a/docs/RICHESE_NO_FIELD_RULES.md b/docs/RICHESE_NO_FIELD_RULES.md
index 280ce521..96e58cf3 100644
--- a/docs/RICHESE_NO_FIELD_RULES.md
+++ b/docs/RICHESE_NO_FIELD_RULES.md
@@ -2,6 +2,18 @@
Audit date: 2026-09-06. This document distinguishes the printed token lifecycle from engine integration and unresolved combinations. It does not certify Richese, Homeworlds, or any other gated game mode.
+## Allied separation at a due ending — 27 October 2026
+
+A due Basic/Advanced alliance constraint now accepts normal Finish with an
+original concealed marker. Under the standing recommended-option authority,
+provisionally reveal at the actual deadline, materialize only available reserves,
+then remove the ending player's whole shared group to Tanks.0 creates no force.
+The ally, a marker elsewhere and existing deadline/exemption rules remain.
+The public persistent warning names only the affected territories and the
+presence of a marker; it conveys no hidden denomination or count. This is
+source composition, not a dedicated publisher ruling.
+[Source, native ending and scope](ALLIED_TRANSIT_RULES.md#concealed-no-field-at-the-separation-deadline--27-october-2026).
+
## Family Atomics qualification and Wall loss — 27 October 2026
The printed one-force-for-all-effects rule, including concealed0, now supplies
diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md
index 755d8610..52e8348c 100644
--- a/docs/RULE_DECISIONS.md
+++ b/docs/RULE_DECISIONS.md
@@ -1,5 +1,18 @@
# Rule decision index
+## No-Field at allied separation — 27 October 2026
+
+Compose E2 FAQ10/supplied physical36 B's one-force presence with the actual
+Basic/Advanced alliance-loss deadline. Under the standing recommendation
+policy, a due marker follows a **provisional destructive reveal**: native
+`alliedSeparation` cause, original reserve-capped materialization, then the
+ending player's whole shared group to Tanks.0 creates no casualty. The ally
+and existing Basic grace/later deadline, Advanced advisors, Polar and Ecaz
+exceptions stay. This is not a specific publisher combined-effect ruling.
+Normal Finish advances instead of rejecting concealed custody; its public
+warning reveals no denomination/quantity. Other arrivals/transports and broader
+combinations remain separate. [Contract](ALLIED_TRANSIT_RULES.md#concealed-no-field-at-the-separation-deadline--27-october-2026).
+
## Family Atomics and concealed No-Field — 27 October 2026
Publisher E2 FAQ printed10 and supplied physical36 B count the concealed marker
diff --git a/game/bots.ts b/game/bots.ts
index 3ad22bcd..b5422806 100644
--- a/game/bots.ts
+++ b/game/bots.ts
@@ -3738,7 +3738,7 @@ function policyActions(g: GameView): Action[] {
const retained =
level > 0 &&
g.basicAlliedShipment?.territory !== territory(origin).name &&
- !g.advancedAllySeparation?.territories.includes(territory(origin).name) &&
+ !(g.advancedAllySeparation ?? g.basicAllySeparation)?.territories.includes(territory(origin).name) &&
territory(origin).type === 'stronghold' &&
!isAdvisor(me, origin)
? 1
@@ -4454,7 +4454,7 @@ export function botActions(g: GameView): Action[] {
(sourced.type!=='guildShip'||!g.advisorTransportMatchingDenied?.length||!guildTransportQuote(g,sourced).unavailableReasons.length) ? [sourced] : [];
});
const me = g.players.find((p) => p.id === g.me)!;
- const advancedDepartures = g.advancedAllySeparation?.territories;
+ const advancedDepartures = (g.advancedAllySeparation ?? g.basicAllySeparation)?.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
diff --git a/game/engine.ts b/game/engine.ts
index 693aa2f9..821c3fcf 100644
--- a/game/engine.ts
+++ b/game/engine.ts
@@ -4622,7 +4622,7 @@ function revealPlayerNoField(g: Game, p: Player, cause: NoFieldRevealCause) {
observeOccupation(g);
log(
g,
- `${p.name} revealed the ${result.value} No-Field in ${territory(result.location.territory).name} (${cause}) and placed ${result.forces} physical forces from reserves.${result.forces < result.value ? ' Only the remaining reserves could be placed.' : ''}`,
+ `${p.name} revealed the ${result.value} No-Field in ${territory(result.location.territory).name} (${cause === 'alliedSeparation' ? 'allied separation' : cause}) and placed ${result.forces} physical forces from reserves.${result.forces < result.value ? ' Only the remaining reserves could be placed.' : ''}`,
{ faction: 'richese', name: 'No-Field revealed' },
);
return result;
@@ -21506,13 +21506,8 @@ function alliedSeparationTerritories(g: Game, p: Player) {
})),
});
}
-function requireAlliedSeparationCustody(g: Game, p: Player) {
- const deployed = p.noField?.deployed;
- requireRule(!deployed || !alliedSeparationTerritories(g, p).includes(deployed.location.territory),
- 'This removal of a concealed No-Field awaits its specific reveal and casualty rules.');
-}
-function advancedAllySeparationView(g: Game, p: Player): {territories: string[]} | null {
- if (!g.advanced || g.status !== 'playing' || g.phase !== 5 || g.active !== p.id ||
+function alliedSeparationView(g: Game, p: Player): {territories: string[]; noField?: true} | null {
+ if (g.status !== 'playing' || g.phase !== 5 || g.active !== p.id ||
g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingKarama ||
g.pendingTreacheryDiscard || g.pendingNullentropy || g.pendingExchange ||
g.pendingRicheseGift || g.pendingRichesePurchaseIncome || g.pendingShipment ||
@@ -21523,8 +21518,11 @@ function advancedAllySeparationView(g: Game, p: Player): {territories: string[]}
pendingNexusTraitors(g) || g.nexusCards?.phase?.stage === 'drawing' ||
currentGuildCunning(g, p.id)?.stage === 'pending' ||
currentGuildCunning(g, p.id)?.stage === 'secondShipment') return null;
- const territories = alliedSeparationTerritories(g, p).map(id => territory(id).name);
- return territories.length ? {territories} : null;
+ const places = alliedSeparationTerritories(g, p);
+ const territories = places.map(id => territory(id).name);
+ const marker = p.noField?.deployed;
+ return territories.length ? {territories,
+ ...(marker?.controller === p.id && places.includes(marker.location.territory) ? {noField:true as const} : {})} : null;
}
function basicAlliedShipmentView(g: Game, p: Player) {
@@ -21580,7 +21578,6 @@ function finishMovementTurn(g: Game, id: string) {
if (error instanceof MovementClaimError) throw new RuleError(error.message);
throw error;
}
- requireAlliedSeparationCustody(g, p);
const cunning = currentGuildCunning(g,id);
if (cunning) { requireRule(cunning.stage !== 'pending' && cunning.shipment?.stage !== 'pending', 'Finish the Guild Cunning response or shipment first.'); cunning.stage = 'completed'; saveGuildCunning(g,cunning); }
finishBasicAlliedShipment(g, p);
@@ -21602,7 +21599,7 @@ function finishMovementTurn(g: Game, id: string) {
{ faction: 'ecaz', name: 'Occupy' },
);
for (const terr of alliedSeparationTerritories(g, p))
- killTerritory(g, p, terr);
+ killTerritory(g, p, terr, Infinity, false, 'alliedSeparation');
g.movementRemaining = g.movementRemaining.filter((other) => other !== id);
if (g.saphoMovementLast?.player === id) g.saphoMovementLast = null;
movementTurn(g);
@@ -42492,9 +42489,6 @@ function applyActionInner(
if (g.ornithopter?.player === id) finishOrnithopter(g, 'cunning');
return g;
}
- // Reject unsupported concealed custody before retiring a played movement
- // card and parking its native ending continuation.
- requireAlliedSeparationCustody(g, p);
if (g.ornithopter?.player === id) finishOrnithopter(g, 'end');
else finishMovementTurn(g, id);
return g;
@@ -43761,7 +43755,8 @@ export function viewGame(state: Game, id: string, now?: number) {
g.guildRateBlocked.player === id,
...(guildTransportCanceled(g, id) ? { guildTransportCanceled: true as const } : {}),
stormPending: g.stormPending ?? null,
- advancedAllySeparation: advancedAllySeparationView(g, me),
+ advancedAllySeparation: g.advanced ? alliedSeparationView(g, me) : null,
+ basicAllySeparation: !g.advanced ? alliedSeparationView(g, me) : null,
basicAlliedShipment: basicAlliedShipmentView(g, me),
stormForecast:
me.faction === 'fremen' && g.stormCardKnown ? g.stormCard : null,
diff --git a/game/reference.ts b/game/reference.ts
index 8984dd91..f68ba7e7 100644
--- a/game/reference.ts
+++ b/game/reference.ts
@@ -4371,16 +4371,17 @@ export const RULE_TOPICS: RuleTopic[] = [
'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.',
+ 'A concealed No-Field counts as one presence, including0. If its actual separation deadline is due, provisional destructive timing reveals it, places only available reserves, then sends your actual shared forces to Tanks. Your ally stays. A marker elsewhere, a not-yet-due Basic turn and existing exemptions retain concealment. The warning states that a marker is affected without exposing its value or quantity.',
'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.',
'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: '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: '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. Due concealed markers use provisional reserve-capped revelation instead of blocking Finish.', 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 warnings in both modes contain public loss territories and optional marker presence only. Mandatory loss adds no confirmation and pending-control locks retain priority.', evidence: ['components/game-table.tsx', 'components/advanced-ally-separation.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'] },
+ { 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/fixture-no-field-allied-separation.ts', 'tests/no-field-allied-separation-engine.test.ts', 'tests/no-field-allied-separation-controls.test.tsx', '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'] },
],
related: ['movement', 'spice-blow', 'advanced-advisors', 'ecaz-ambassadors', 'homeworlds'],
},
diff --git a/game/richese-no-field.ts b/game/richese-no-field.ts
index 6e6cc814..dc2fc5e0 100644
--- a/game/richese-no-field.ts
+++ b/game/richese-no-field.ts
@@ -25,7 +25,8 @@ export type NoFieldRevealCause =
| 'gamont'
| 'beforeAlly'
| 'allyShipment'
- | 'atomics';
+ | 'atomics'
+ | 'alliedSeparation';
export type NoFieldReveal = {
state: RicheseNoField;
tokenId: string;
@@ -261,6 +262,7 @@ export function revealRicheseNoField(
'beforeAlly',
'allyShipment',
'atomics',
+ 'alliedSeparation',
].includes(input.cause),
'Choose a valid No-Field reveal cause.',
);
diff --git a/tests/fixture-advanced-source.ts b/tests/fixture-advanced-source.ts
index 0c1c38de..d0abd002 100644
--- a/tests/fixture-advanced-source.ts
+++ b/tests/fixture-advanced-source.ts
@@ -72,6 +72,8 @@ function nativeStep(game: Game): Game {
}
if (game.phase === 3 && game.auction) return applyAction(game, game.auction.active, {type: 'passBid'});
if (game.phase === 5) return applyAction(game, game.active!, {type: 'endMovement'});
+ if (game.phase === 2 && game.charityOrder?.remaining[0])
+ return applyAction(game,game.charityOrder.remaining[0],{type:'ready'});
if (game.phase === 4 && game.revivalOrder?.remaining[0])
return applyAction(game,game.revivalOrder.remaining[0],{type:'ready'});
// A native Richese lot can own Bidding without an ordinary auction row.
diff --git a/tests/fixture-no-field-allied-separation.ts b/tests/fixture-no-field-allied-separation.ts
new file mode 100644
index 00000000..a69ad8d0
--- /dev/null
+++ b/tests/fixture-no-field-allied-separation.ts
@@ -0,0 +1,173 @@
+import assert from 'node:assert/strict';
+import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
+import type { NoFieldValue } from '../game/richese-no-field';
+import { actFamilyAtomicsNoField, familyAtomicsNoFieldPlayer, familyAtomicsNoFieldReload,
+ familyAtomicsNoFieldInventory, familyAtomicsNoFieldClean, nextFamilyAtomicsNoFieldStep,
+ type FamilyAtomicsNoFieldStep } from './fixture-family-atomics-no-field';
+
+export type NoFieldAlliedSeparationStep = FamilyAtomicsNoFieldStep;
+export const actNoFieldAlliedSeparation = actFamilyAtomicsNoField;
+export const noFieldAlliedSeparationPlayer = familyAtomicsNoFieldPlayer;
+export const noFieldAlliedSeparationReload = familyAtomicsNoFieldReload;
+export const noFieldAlliedSeparationInventory = familyAtomicsNoFieldInventory;
+export const noFieldAlliedSeparationClean = familyAtomicsNoFieldClean;
+/** Consume the native optional ally-shipment offer by passing. The source
+ * never starts a new token shipment after the measured separation ending. */
+export function nextNoFieldAlliedSeparation(game: Game): NoFieldAlliedSeparationStep | null {
+ if (!game.response && !game.phaseOpening && game.decision?.kind === 'richeseAllyOpportunity')
+ return { actor: game.decision.player, action: { type: 'decision', decline: true } };
+ return nextFamilyAtomicsNoFieldStep(game);
+}
+export function advanceNoFieldAlliedSeparation(game: Game, until: (state: Game) => boolean,
+ actions: NoFieldAlliedSeparationStep[] = []): Game {
+ for (let count = 0; count < 1800; count++) {
+ if (until(game)) return game;
+ assert.notEqual(game.status, 'finished', 'The original allied-marker chronology ended before its requested boundary');
+ const next = nextNoFieldAlliedSeparation(game); assert.ok(next, 'Complete the actual native pending control');
+ game = actNoFieldAlliedSeparation(game, next.actor, next.action, actions);
+ }
+ throw Error('The original allied-marker chronology did not reach its finite boundary');
+}
+export function finishNoFieldAlliedSeparationMovement(game: Game, actions: NoFieldAlliedSeparationStep[] = []): Game {
+ return advanceNoFieldAlliedSeparation(game, state => state.phase !== 5 && noFieldAlliedSeparationClean(state), actions);
+}
+
+type SpiceSelection = { position: number; from: number; card: Game['spiceDeck'][number] };
+function selectOriginalSpice(game: Game, position: number, worm: boolean): SpiceSelection {
+ const from = game.spiceDeck.findIndex((card, index) => index >= position && (worm
+ ? 'worm' in card && !card.greatMaker && !card.suppressed
+ : 'territory' in card && !card.discovery && card.territory !== 'imperial_basin' && card.sector !== game.storm &&
+ game.players.every(player => !Object.entries(player.forces).some(([key, count]) => count > 0 && key.startsWith(`${card.territory}:`)))));
+ assert.ok(from >= position, 'Select an actual remaining original safe Spice face');
+ const [card] = game.spiceDeck.splice(from, 1); game.spiceDeck.splice(position, 0, card);
+ return { position, from, card: structuredClone(card) };
+}
+
+/** Real unallied Basin9 marker and Basin11 two-counter army are separated by
+ * actual Storm10 in turn1. A real next-turn worm supplies the Nexus where both
+ * owners consent. Advanced Richese is first; Basic Emperor is first and Richese
+ * is later, retaining both the formation grace and actual later-player rule.
+ */
+export function createNoFieldAlliedSeparationFixture(options: { advanced?: boolean; value?: NoFieldValue } = {}) {
+ const advanced = options.advanced ?? true, value = options.value ?? 0;
+ const actor = 'richese', ally = 'emperor', other = 'atreides';
+ let game = createGame('NOFIELDALLIEDSEPARATION', newPlayer(actor, 'Richese', 'richese'), advanced, ['choam']);
+ joinGame(game, newPlayer(ally, 'Emperor', 'emperor')); joinGame(game, newPlayer(other, 'Atreides', 'atreides'));
+ const initial = structuredClone(game), actions: NoFieldAlliedSeparationStep[] = [];
+ const act = (id: string, action: Action) => { game = actNoFieldAlliedSeparation(game, id, action, actions); };
+ const reach = (until: (state: Game) => boolean) => { game = advanceNoFieldAlliedSeparation(game, until, actions); };
+ const transition = (run: () => void) => {
+ const before = structuredClone(game), from = actions.length; run();
+ return { before, steps: structuredClone(actions.slice(from)), after: structuredClone(game) };
+ };
+ const circlesFrom = actions.length;
+ if (!advanced) {
+ for (const [index, player] of game.players.entries()) act(player.id, { type: 'seatPosition', position: index + 4 });
+ act(ally, { type: 'seatPosition', position: 1 }); act(actor, { type: 'seatPosition', position: 2 });
+ act(other, { type: 'seatPosition', position: 3 });
+ }
+ const circles = structuredClone(actions.slice(circlesFrom));
+ for (const player of game.players) act(player.id, { type: 'ready' });
+ const lobby = structuredClone(game);
+ act(game.host, advanced ? { type: 'start', advancedPreview: true } : { type: 'start' });
+ const setup = structuredClone(game);
+ assert.equal(game.status, 'setup'); assert.ok(game.players.every(player => !player.hand.length));
+ const ordinary = ['projectile', 'shield', 'snooper'].map(kind => game.deck.find(card => card.kind === kind));
+ assert.ok(ordinary.every(card => card));
+ const dealOrder = ordinary.map((card, position) => {
+ assert.ok(card); const from = game.deck.findIndex(candidate => candidate.id === card.id); assert.ok(from >= position);
+ [game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]];
+ return { position, from, card: card.id };
+ });
+ reach(state => state.status === 'playing');
+ const afterSetup = structuredClone(game), setupActions = structuredClone(actions);
+ assert.deepEqual(noFieldAlliedSeparationPlayer(game, actor).forces, {});
+ const original = noFieldAlliedSeparationInventory(game);
+ const firstSpiceOrder = [selectOriginalSpice(game, 0, false),
+ ...(advanced ? [selectOriginalSpice(game, 1, false)] : [])];
+ const firstDials = transition(() => {
+ for (const [index, dialer] of game.stormDialers.entries()) act(dialer, { type: 'stormDial', amount: index === 0 ? 4 : 5 });
+ });
+ reach(state => state.turn === 1 && state.phase === 5 && noFieldAlliedSeparationClean(state));
+ assert.equal(game.storm, 10);
+ let markerShipment: ReturnType