From 97c2b6765083226c535cb928c7cacebc3cfee3f7 Mon Sep 17 00:00:00 2001
From: Quintine <15965533+Quintine@users.noreply.github.com>
Date: Sun, 18 Oct 2026 10:20:08 +1100
Subject: [PATCH] Compose classic independent cards with World Nexus rules
Retain independent36, original Homeworlds and Nexus12 through classic fresh setup with optional Skills and phase modules. Resolve own-World Reinforcements fees before computing temporary Sardaukar roles, preserving original Cunning declarations, printed traits and physical counters. Record the provisional ordinary-nonrole-first policy and align human/AI quotes and captions.
Validation: types/lint/build, 128 affected tests across 15 files, bounded rule-source review and finite original-action smoke for Recruits, Harass, local-fee Cunning, all four legal policies and rendered source controls. Verified runtime c219ec587a54b44079539168d93835a5af904c8afb4de58bcec0a3d60c809f0c; documentation-only proof update 8ee21598e7be0c89a08fbad50786c346ebc8ae705492d8b31ea00fae0ffefa3c. Full modes, combinations, assurance and deployed acceptance remain open. No saved-room/provider changes.
---
components/game-table.tsx | 5 +-
components/nexus-sardaukar.tsx | 8 +-
components/reinforcements-sources.tsx | 2 +-
docs/CURRENT_STATUS.md | 30 ++-
docs/IMPLEMENTATION_STATUS.md | 16 ++
docs/NEXUS_CARD_RULES.md | 43 +++++
docs/REINFORCEMENTS_RUNTIME.md | 31 +++
docs/RULE_DECISIONS.md | 17 ++
game/engine.ts | 33 +++-
game/faction-module-profile.ts | 6 +-
game/reference.ts | 13 +-
game/reinforcements-homeworld.ts | 10 +-
tests/fixture-nexus-core-world-battles.ts | 15 +-
.../fixture-nexus-world-independent-cards.ts | 179 ++++++++++++++++++
tests/nexus-sardaukar-controls.test.ts | 3 +-
tests/nexus-world-independent-battles.test.ts | 154 +++++++++++++++
tests/nexus-world-independent-cards.test.ts | 164 ++++++++++++++++
tests/reinforcements-temporary-role.test.ts | 38 ++++
18 files changed, 737 insertions(+), 30 deletions(-)
create mode 100644 tests/fixture-nexus-world-independent-cards.ts
create mode 100644 tests/nexus-world-independent-battles.test.ts
create mode 100644 tests/nexus-world-independent-cards.test.ts
create mode 100644 tests/reinforcements-temporary-role.test.ts
diff --git a/components/game-table.tsx b/components/game-table.tsx
index cb027527..70f78839 100644
--- a/components/game-table.tsx
+++ b/components/game-table.tsx
@@ -1293,8 +1293,9 @@ export function GameTable({
{classicNexusPreview && (
Classic Nexus reactions preview · Basic/Advanced classic factions, original base33
- or selected independent36 Treachery, optionally all14 Leader Skills. Homeworld/Nexus
- core currently keeps base33; independent World cards remain a separate preview boundary.
+ or selected independent36 Treachery, optionally all14 Leader Skills and Homeworlds.
+ Own-World Reinforcements fees provisionally spend ordinary non-role counters first,
+ reducing temporary Sardaukar roles only when converted counters pay.
Original Tech (3+ seats) and Advanced Strongholds (2+ seats) may compose. Native reactions
retain original card, force and payment authority. Acknowledgement timing and
purchase/bonus ordering remain provisional. Other modules and complete Nexus
diff --git a/components/nexus-sardaukar.tsx b/components/nexus-sardaukar.tsx
index fe8cdf8c..5689c8ee 100644
--- a/components/nexus-sardaukar.tsx
+++ b/components/nexus-sardaukar.tsx
@@ -23,9 +23,11 @@ export function NexusSardaukar({ game, act, busy }: Props) {
return (
- Five ordinary Emperor counters count as Sardaukar for this battle.
- They remain ordinary physical counters; any losses go to the ordinary
- Tanks.
+ Cunning declared five ordinary Emperor counters as Sardaukar for this
+ battle. An own-Homeworld reserve fee can reduce that group;
+ the selected plan shows the remaining combat pool. These counters
+ retain their ordinary physical identity and go to the ordinary Tanks
+ when lost.
Provisional own-World composition: counters paying the fee cannot also fund your physical dial. The original printed face and support traits stay fixed; the bonus adds no physical counters.
+
Provisional own-World composition: counters paying the fee cannot also fund your physical dial. Ordinary counters without a temporary Sardaukar role pay first; if needed, paying converted counters reduces that role. The original printed face and support traits stay fixed; the bonus adds no physical counters.
;
}
diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md
index 4384fae8..884e62c4 100644
--- a/docs/CURRENT_STATUS.md
+++ b/docs/CURRENT_STATUS.md
@@ -16,6 +16,29 @@ open mode gates.
## Current checkpoint and work
+**Classic independent World/Nexus — 18 October. Prototyped / Partial.**
+Fresh classic2–6 independent36 + Homeworlds + Nexus12 now retains optional all14,
+Tech3+ and Advanced Strongholds2+ through original module selection/setup.
+Actual Recruits returns recheck Fremen population; foreign-World Harass returns
+typed visitors and own-World Harass stays forbidden. Local Reinforcements fees
+provisionally spend non-role normals before converted counters: Kaitain7 paying3
+leaves normal4/temporary4, while the original Cunning declaration remains5.
+Real Salusa stars, pre-fee face/support traits and physical fee/loss separation
+remain. Shared engine, human selected-source limits and legal bot pools agree;
+active/preview captions now explain declaration versus remaining roles.
+
+Frozen15-file **128/128**, types/lint/build pass. Direct original-action smoke
+finishes the local-fee battle (97 actions), Recruits (101), typed Harass (59),
+and four legal policies' card plans within 89-action programmes each. Rendered
+source/active controls retain the real choices and provisional explanation.
+The first typecheck exposed a nullable test-view field; two setup assertions
+were corrected to actual lobby selections/deal chronology. A smoke caption
+assertion was corrected to the actual text; diagnostic logs are retained.
+No saved room/database, server restart or deployment was involved. Browser,
+full combinations and comprehensive assurance remain deferred. Other native
+families, Discovery and unrelated preview envelopes remain separate.
+[Canonical source, implementation and limits](NEXUS_CARD_RULES.md#classic-independent-world-nexus--18-october-2026).
+
**Current priority — Basic → Advanced → interaction refinement.** The user
requests usable first versions for every faction before refinement. Administration
is deferred. Broad suites and capture-resumption campaigns are deferred; rule
@@ -31,8 +54,7 @@ remain deferred until all rules are implemented.
**Integration review — 18 October.** The earlier non-AI checklist closure
was unsupported: a topic marked Prototyped/Partial does not prove every
-rules-permitted combination works. Independent36 World/Nexus and other named
-interaction boundaries remain open. Rules delivery is not complete, and the
+rules-permitted combination works. Specific module envelopes and named interaction boundaries remain open. Rules delivery is not complete, and the
deferred comprehensive multiplayer/privacy/custody gate has not been opened.
The regression repair now admits original Ecaz CLI snapshots with their actual
@@ -54,8 +76,8 @@ Plain base33 World/Nexus lacked native reaction states; one public capability
now composes original Homeworlds with base33, optional14 and original
Tech3+/Advanced Strongholds2+. Actual host controls retain every selected
module through private training/Traitors into first Storm; salusa elite5 and
-unassigned tokens/cards prove no premature owners. Independent36 World/Nexus
-stays a named pending gate; other envelopes remain separate.
+unassigned tokens/cards prove no premature owners. Independent36 World/Nexus was left pending at that checkpoint; its current
+composition is the active delivery above. Other envelopes remain separate.
Real World fee sources pass their original gates: Guild Junction→Giedi
departure2, Cunning visitor return through native permission/rate/fee2;
diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md
index 10d119be..d1247615 100644
--- a/docs/IMPLEMENTATION_STATUS.md
+++ b/docs/IMPLEMENTATION_STATUS.md
@@ -1,5 +1,21 @@
# Dune implementation status
+## 18 October 2026 — Classic independent World/Nexus
+
+Fresh classic independent36/World/Nexus now retains optional14 and original
+phase modules through native setup. Actual Recruits, foreign Harass and
+own-Kaitain Reinforcements/Cunning use the original card/actions. Local fees
+provisionally spend non-role normals first; the shared physical quote reduces
+only paid temporary roles, retaining the original five-counter declaration and
+real Salusa custody. Human/bot source pools and active captions use that split.
+
+Two disjoint fixture lanes and bounded source review were integrated by one
+owner. Frozen15-file128/128, types/lint/build and finite original-action/
+rendered-control smoke pass. Source assertion/type corrections and limitations
+are recorded in the [canonical contract](NEXUS_CARD_RULES.md#classic-independent-world-nexus--18-october-2026).
+No stored room/reset/restart/deployment; complete modes and comprehensive
+assurance remain open.
+
## 18 October 2026 — Original World Nexus core
Plain base33 World/Nexus started without native reaction states, and
diff --git a/docs/NEXUS_CARD_RULES.md b/docs/NEXUS_CARD_RULES.md
index eed580f2..3bcdbb6d 100644
--- a/docs/NEXUS_CARD_RULES.md
+++ b/docs/NEXUS_CARD_RULES.md
@@ -2,6 +2,49 @@
Original source audit, 10 September 2026, with dated bounded runtime follow-ups below. This document establishes the printed component inventory, source protocol and implementation boundaries; none lifts expansion release gates. The ordinary worm-triggered alliance Nexus already in the engine is distinct from this optional card module.
+## Classic independent World Nexus — 18 October 2026
+
+**Prototyped / Partial.** Fresh classic2–6 Homeworlds + Nexus12 now also
+retains explicitly selected independent36, optional all14 Skills, Tech3+ and
+Advanced Strongholds2+. The public core capability and original setup own
+admission and actual Guild/Harkonnen/Ixian reaction initialization; no played
+game is reflagged and no card or power is silently added. This supersedes the
+base33-only admission boundary in the earlier checkpoint below. Other native
+families, Discovery/Semuta and unrelated reaction envelopes remain separate.
+
+Recruits uses actual current native population/rates and the original typed
+revival destination. Foreign-World Harass returns real normal/special visitors
+to their native Worlds; own-World Harass stays forbidden. These are the existing
+card effects through the newly composed selection, not new numerical rules.
+
+The substantive battle fix is local Reinforcements fees with Emperor Cunning.
+Ordinary counters without the temporary role provisionally pay first; converted
+fee counters then lose that role. Kaitain7 paying local3 leaves a legal combat
+pool normal4/temporary4, preserving the original count5 declaration, pre-fee face
+and support traits, and real Salusa stars. Off-World fees leave the original
+Kaitain role pool. This overlap is an explicit
+[recommended provisional composition](RULE_DECISIONS.md#provisional-own-world-temporary-role-reserve-fee--18-october-2026),
+not a retrieved publisher ruling.
+
+Implementation: [core capability](../game/faction-module-profile.ts),
+[shared fee pool](../game/reinforcements-homeworld.ts), existing engine/actions,
+selected-source controls and legal bot source pools. Original-action fixtures
+and cases are `tests/fixture-nexus-world-independent-cards.ts`,
+`tests/nexus-world-independent-cards.test.ts` and
+`tests/nexus-world-independent-battles.test.ts`. Comprehensive rules/combination,
+save/privacy/custody and final deployed acceptance remain open.
+
+Owner frozen15-file **128/128**, types/lint/build pass. A finite original-action
+programme completes the local fee/Cunning battle (97 actions), Recruits (101)
+and typed foreign Harass (59), plus four legal policies' source-bearing card
+plans within 89-action programmes each. Rendered selected-source and active
+Cunning controls retain the real choices and distinguish original declaration
+from the remaining pool. Initial nullable-view/lobby chronology assertions and
+one smoke caption matcher were corrected; no game numeric rule was changed to
+satisfy those assertions. Logs remain outside Git. No saved room/database,
+server restart or deployment was involved; browser/deployed and full
+combination/assurance acceptance remain deferred.
+
## Original World Nexus core — 18 October 2026
**Prototyped / Partial.** Fresh original base33 classic Homeworld+Nexus now
diff --git a/docs/REINFORCEMENTS_RUNTIME.md b/docs/REINFORCEMENTS_RUNTIME.md
index 4c788b06..839e65df 100644
--- a/docs/REINFORCEMENTS_RUNTIME.md
+++ b/docs/REINFORCEMENTS_RUNTIME.md
@@ -16,6 +16,37 @@ fixes the exact card. Harass & Withdraw may share the plan under the
Original or copied Stone Burner composes in clean selected Ix/CHOAM families
and the existing native Advanced Occupy envelope. Other profile boundaries remain.
+## Independent World/Nexus temporary roles — 18 October 2026
+
+Fresh classic independent36 + Homeworlds + Nexus12 now composes in Basic and
+Advanced, retaining optional all14, Tech3+ and Advanced Strongholds2+. Original
+host controls select these modules before readiness and native setup. Other
+native-family, Discovery and unrelated preview envelopes retain their limits.
+This is prototype integration, not complete-mode certification.
+
+The shared own-World fee pool now removes a temporary role only when its
+physical normal counter pays the fee. Provisionally, ordinary non-role counters
+pay first. Kaitain7 with five Cunning roles paying three here leaves normal4 /
+temporary4, a valid physical pool with maximum dial8. A fee on the other native
+World leaves all seven Kaitain counters and five roles. Pre-fee printed face and
+support traits remain; the reserve fee cannot enter dial casualties or Suk rescue.
+The original Cunning receipt retains count5. A reduced-role casualty record
+additionally identifies its local reserve fee; unchanged historical five-role
+records keep their existing form.
+
+This extends the existing provisional fee-exclusion policy, using the user's
+choose-recommended direction. The sources establish the original three-counter
+fee and five-counter declaration, not the ordering of this overlap. It is not a
+new publisher ruling. Engine, human selected-source maximum and minimal bot
+source pools all use [the same physical quote](../game/reinforcements-homeworld.ts).
+The source selector explains the policy and the table displays the remaining
+temporary count.
+
+Original-action cases: [native battles](../tests/nexus-world-independent-battles.test.ts),
+[Recruits/Harass and setup](../tests/nexus-world-independent-cards.test.ts), and
+[physical role quote](../tests/reinforcements-temporary-role.test.ts).
+Full combinations and comprehensive assurance remain deferred.
+
## Original Skills/phase composition — 17 October 2026
Supported original all14/card profiles retain Tech3–6 Basic/Advanced and
diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md
index 1983cdc9..e6081870 100644
--- a/docs/RULE_DECISIONS.md
+++ b/docs/RULE_DECISIONS.md
@@ -2,6 +2,23 @@
Updated 18 October 2026. This is the authoritative navigation index for existing decisions, not a replacement rulebook or a new ruling. Detailed linked contracts retain their source citations and exact supported boundaries. Runtime follow-ups supersede historical absence claims; passing tests establish software behavior, not publisher authority.
+## Provisional own-World temporary-role reserve fee — 18 October 2026
+
+Under the 14 October choose-recommended direction, an own-World Reinforcements
+fee spends physical ordinary counters without a temporary Sardaukar role first.
+If that is insufficient, paying converted counters removes their temporary role
+from the combat pool. The original Emperor Cunning declaration still records
+five converted ordinary counters. Seven Kaitain normals paying three here leave
+four normals with four temporary roles; paying on Salusa leaves the Kaitain
+group unchanged. Real starred-counter custody and pre-fee support/face traits
+remain separate.
+
+The printed five-counter Cunning group, three-reserve fee and independent
+modules are sourced in the existing contracts. Their overlap does not specify
+which converted counters pay. This is an explicit provisional composition,
+not a publisher FAQ or a newly supplied user numerical ruling.
+[Physical fee and scope](REINFORCEMENTS_RUNTIME.md#independent-worldnexus-temporary-roles--18-october-2026).
+
## Provisional paid Black Market retention — 18 October 2026
The supplied revision physical36/printed34, Richese A/C, gives the shared
diff --git a/game/engine.ts b/game/engine.ts
index a2c7caae..d77d72f6 100644
--- a/game/engine.ts
+++ b/game/engine.ts
@@ -2001,7 +2001,8 @@ export type Game = {
nexusAdvisorHistory?: { receipt: NexusAdvisorReceipt; stage: 'pending' | 'completed' | 'canceled'; frame: string; signature: string }[];
nexusAdvisorLast?: { event: string; stage: 'pending' | 'completed' | 'canceled' };
nexusSardaukarHistory?: {receipt: NexusSardaukarReceipt; stage: 'pending' | 'active' | 'canceled'; parent: string; signature: string;
- casualties?: {forces: CombatForces; dial: number; support: number; options: Casualties[]; outcome: 'pending' | 'complete'}}[];
+ casualties?: {forces: CombatForces; dial: number; support: number; options: Casualties[]; outcome: 'pending' | 'complete';
+ reserveFee?: {normal: number; elite: number}}}[];
nexusSardaukarLast?: {event: string; stage: 'pending' | 'active' | 'canceled'};
nexusChoamHistory?: {receipt: NexusChoamReceipt; stage: 'pending' | 'canceled' | 'complete' | 'fizzled'; frame: string; parent: string; signature: string}[];
nexusMoritaniHistory?: {receipt:NexusMoritaniReceipt;stage:'pending'|'complete'|'canceled';before:string;frame:string;signature:string}[];
@@ -3795,9 +3796,21 @@ function nexusSardaukarIntegrity(g: Game) {
nexusRule(() => validateNexusSardaukar(g, record.receipt));
const losses = record.casualties;
if (losses) {
+ const fee = losses.reserveFee;
+ if (fee) {
+ const home = g.homeworlds && homeworldBattleLocation(g,record.receipt.territory);
+ const original = {...losses.forces,normal:record.receipt.normal,temporaryElite:record.receipt.count};
+ requireRule(g.ecazTreachery && home && home.native === record.receipt.owner &&
+ Object.keys(fee).sort().join(',') === 'elite,normal' && fee.elite === 0 &&
+ Number.isSafeInteger(fee.normal) && fee.normal >= 1 && fee.normal <= 3 &&
+ losses.forces.normal === record.receipt.normal-fee.normal &&
+ validCombatForces(original) && Number(losses.forces.temporaryElite) < record.receipt.count &&
+ JSON.stringify(reinforcementsBattleForces(original,fee)) === JSON.stringify(losses.forces),
+ 'The reduced temporary Sardaukar need their original own-World reserve fee.');
+ }
requireRule(record.stage === 'active' && ['pending','complete'].includes(losses.outcome) &&
- Object.keys(losses).sort().join(',') === 'dial,forces,options,outcome,support' &&
- validCombatForces(losses.forces) && losses.forces.temporaryElite === 5 &&
+ Object.keys(losses).sort().join(',') === (fee ? 'dial,forces,options,outcome,reserveFee,support' : 'dial,forces,options,outcome,support') &&
+ validCombatForces(losses.forces) && (fee ? Number(losses.forces.temporaryElite) >= 2 : losses.forces.temporaryElite === 5) &&
Array.isArray(losses.options) && losses.options.length > 0 &&
JSON.stringify(losses.options) === JSON.stringify(casualtyOptions(losses.forces,losses.dial,losses.support)),
'The Nexus Sardaukar casualties have lost their committed force allocation.');
@@ -10742,7 +10755,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
const homeworlds = homeworldLeaderSkillsProfile(state);
const independent = independentTreacheryLeaderSkillsProfile(state);
if (state.ecazTreachery) {
- requireRule(independent, 'Independent Skills cards require supported selected decks with original World/phase modules, or classic Nexus with original phase modules without Homeworlds; independent World/Nexus and unrelated preview contexts remain gated.');
+ requireRule(independent, 'Independent Skills cards require supported selected decks with original World/phase modules, or classic Nexus with original World/phase modules; unrelated preview contexts remain gated.');
requireFreshBaseRuntime(state);
requireFreshFactionInventory(state);
}
@@ -10789,6 +10802,7 @@ function independentCardModuleRoster(g: Game): boolean {
(g.expansions.length === 1 && g.expansions[0] === 'ecaz'));
}
function ecazHomeworldProfile(g: Game): boolean {
+ if (g.homeworlds && classicIndependentNexusProfile(g)) return true;
if (g.homeworlds && g.leaderSkills && !g.sandtrout && independentTreacheryLeaderSkillsProfile(g)) return true;
return !!g.homeworlds && g.ecazTreachery === true && independentCardModuleRoster(g) &&
g.players.length >= 2 && g.players.length <= 6 &&
@@ -10828,7 +10842,7 @@ export function initializeEcazTreacheryGameForAudit(state: Game): Game {
g.ecazTreachery = true;
const homeworlds = !!g.homeworlds;
requireRule(!homeworlds || ecazHomeworldProfile(g),
- 'Independent World cards need a supported selected-deck roster and original phase modules without Nexus; independent World/Nexus remains a separate pending interaction.');
+ 'Independent World cards need a supported selected-deck roster and original phase modules, or classic Nexus with original phase modules.');
const initialized = initializeSetupGameForAudit(g, homeworlds, classicIndependentNexusProfile(g), false, false, false, false, true, true);
if (byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
if (moritaniAssassinateModeSupported(initialized)) initializeMoritaniAssassinateState(initialized);
@@ -23481,7 +23495,14 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
? quote.fixedLosses?.find(loss => loss.owner === winner.id) : undefined;
const sardaukar = currentNexusSardaukar(g);
if (sardaukar?.stage === 'active' && casualtyCommitment && winner?.id === sardaukar.receipt.owner) {
- sardaukar.casualties = {...structuredClone(casualtyCommitment),outcome:'pending'};
+ const reduced = casualtyCommitment.forces.temporaryElite !== sardaukar.receipt.count;
+ const plan = b.plans[winner.id];
+ const fee = reduced && usesNativeBattleSpecial(plan,REINFORCEMENTS_CARD)
+ ? currentReinforcementsCost(g,winner,plan.reinforcementsSources).sources?.[b.territory] : undefined;
+ requireRule(!reduced || (fee && g.homeworlds && homeworldBattleLocation(g,b.territory)?.native === winner.id),
+ 'Reduced temporary Sardaukar require the original native Reinforcements fee.');
+ sardaukar.casualties = {...structuredClone(casualtyCommitment),
+ ...(fee ? {reserveFee:structuredClone(fee)} : {}),outcome:'pending'};
sardaukar.signature = nexusSardaukarSignature(sardaukar);
}
for (const payment of quote.payments) {
diff --git a/game/faction-module-profile.ts b/game/faction-module-profile.ts
index 35220ea3..fbec6262 100644
--- a/game/faction-module-profile.ts
+++ b/game/faction-module-profile.ts
@@ -30,13 +30,13 @@ type ClassicNexusProfile = Partial & Pick 6 ||
- (game.homeworlds && game.ecazTreachery) || (game.strongholdCards && !game.advanced) || game.sandtrout ||
+ (game.strongholdCards && !game.advanced) || game.sandtrout ||
game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
game.richeseBetrayalPreview ||
diff --git a/game/reference.ts b/game/reference.ts
index 52c8b601..99d31141 100644
--- a/game/reference.ts
+++ b/game/reference.ts
@@ -4105,7 +4105,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'The separately marked fresh Tupile entry grants one hand slot to the proved occupier and reciprocal ally. Every server-derived hand limit includes it, so Harkonnen can hold nine without receiving a tenth bonus. After a proved loss, each old holder privately selects exactly its excess original eligible cards to return to the normal limit; committed cards remain protected. Original phase opening and remaining movement resume without another shipment or payment. Basic unknown lifecycle blocks rather than guesses cleanup. Advanced departure can restore CHOAM’s low intelligence without clearing its previous answers or used factions.',
'The fresh native Ecaz high-card preview shows a separate prospective conjunction: native seven-plus population, at least one actual jointly held stronghold and proved current alliance holdings of two other native factions’ Homeworlds. This does not change the ordinary four-stronghold or three-joint Ecaz targets. Original Mentat decides the win before prediction/fallback; source uncertainty remains blocked. Duke authority and the Ecaz-only revival ruling are separate.',
'The card collection below is available for reference. Homeworld starts are enabled in supported fresh preview envelopes; the remaining occupation variants and combinations stay under verification.',
- 'Original base33 World/Nexus now composes optional all14, Tech3+ and Advanced Strongholds2+ with real first-Storm token owners and end-Mentat card claims. Guild World shipments and Cunning visitor returns pass native permission/rate before the funded fee; borrowed Caladan fee1/fee5 settles to the distinct holder while typed custody waits. Temporary-Sardaukar own-Kaitain battles keep ordinary-only casualties and true Salusa custody; Giedi native-call cancellation follows original capture/execution/cleanup and one private replacement. Normal Smuggler companions may also supply the fee in core. Independent36 World/Nexus remains a separate pending own-World fee/conversion gate with public messaging; other envelopes stay separate.',
+ 'Original classic World/Nexus uses base33 or selected independent36 with optional all14, Tech3+ and Advanced Strongholds2+ with real first-Storm token owners and end-Mentat card claims. Guild World shipments and Cunning visitor returns pass native permission/rate before the funded fee; borrowed Caladan fee1/fee5 settles to the distinct holder while typed custody waits. Temporary-Sardaukar own-Kaitain battles keep ordinary-only casualties and true Salusa custody; Giedi native-call cancellation follows original capture/execution/cleanup and one private replacement. Normal Smuggler companions may also supply the fee in core. On an own World, Reinforcements provisionally spends ordinary non-role counters first, reducing temporary Sardaukar roles only when converted counters pay. Kaitain7 paying local3 leaves four temporary roles on four normals; the original five-counter declaration and pre-fee traits remain. Other envelopes and complete-mode acceptance stay separate.',
],
example:
'Atreides with six reserves on Caladan uses the high face. Dropping to five uses the low face and removes the next-Spice-Blow inspection advantage. Either face adds two to native battle strength on Caladan.',
@@ -4124,6 +4124,11 @@ export const RULE_TOPICS: RuleTopic[] = [
detail:
"All 13 cards, saved typed custody, supported-deck setup, Emperor movement, native Arrakis shipment sources, revival destinations, low charity/free-revival benefits, Tleilax’s phase-start income penalty and high-Salusa free Sardaukar support are connected. World-to-world invasion, Guild transport from Arrakis and Guild native returns use exact source pools, funding and interception. Homeworld battles connect native bonuses, limited explosion casualties, native-only Traitors and Face Dancers, and Ixian substitution. Junction offers connect sponsored board and Homeworld routes, including own returns, foreign departures and their distinct payments. Caladan foresight, Wallach advisor counts, Ix stronghold movement and Richese token movement now respect native population. High Tupile blocks Worthless sales and Ambassador payouts, including after an interrupted Ghola revival. Kaitain paid discards share the closing Bidding opportunity with CHOAM; Ecaz poison income uses actual physical disposal and current native population. Mandatory winning-card disposal follows winner casualties. Supported low Kaitain and Junction payment receipts preserve the payer’s cost and use current native population; divergent allied rounding remains guarded. Giedi grants its once-per-phase bonus on actual positive desert receipts, including settled Ecaz shares. Low Grumman uses original public entry counts across Terror and competing-reaction checks. Southern and Tleilax preserve the exact eligible revival group through one optional placement, with held income and explicit unresolved destination rules. Fresh occupation additionally connects bank and percentage receipts, completed Southern Collection, immediate sharing, Caladan inspection, displaced Ix pool, Richese card-only choice with native terms, real Giedi receiver choice and native protections. Fresh native CHOAM entries additionally connect original Tupile slots, actual normal-limit cleanup and restored low-query authority without resetting usage. Advanced retains sole-qualified ownership until last departure; Basic lifecycle, exceptional Duke, concealed transport and full/public acceptance remain unfinished. Already purchased ordinary Karama now composes with immune Junction through explicit unused-price acknowledgment; original tariff/payees stay unchanged and only delivered transport consumes its grant.",
evidence: [
+ 'game/faction-module-profile.ts',
+ 'game/reinforcements-homeworld.ts',
+ 'components/reinforcements-sources.tsx',
+ 'tests/nexus-world-independent-cards.test.ts',
+ 'tests/nexus-world-independent-battles.test.ts',
'game/homeworld-cards.ts',
'game/homeworld-game.ts',
'game/homeworld-native-reserves.ts',
@@ -4542,7 +4547,7 @@ export const RULE_TOPICS: RuleTopic[] = [
developmentStage: 'Verified',
summary: 'An unallied rival holding Guild Nexus may take one full original positive shipment payment, including their own pre-funded payment, instead of Guild, bank or occupied Junction income in fresh classic core or supported original standalone entries.',
steps: [
- 'Fresh classic2–6 Basic/Advanced Nexus core uses base33 or selected independent36, optionally all14 Skills, original Tech3+ and Advanced Strongholds2+. Actual Guild receives original fee state at fresh setup alongside supported Harkonnen/Ixian reactions; played games are not reflagged. Original explicit native Guild/Homeworld entry retains its seeded boundary. Other selected families, Worlds, Discovery/Semuta and reaction envelopes stay separate.',
+ 'Fresh classic2–6 Basic/Advanced Nexus core uses base33 or selected independent36, optionally all14 Skills, original Homeworlds, Tech3+ and Advanced Strongholds2+. Actual Guild receives original fee state at fresh setup alongside supported Harkonnen/Ixian reactions; played games are not reflagged. Original explicit native Guild/Homeworld entry retains its seeded boundary. Other selected families, Discovery/Semuta and reaction envelopes stay separate.',
'Remain unallied and hold the actual singleton Guild Nexus while another seated player is native Guild. Use spends that physical Nexus once, with no Treachery Card, hand slot or extra activation spice. Cunning and absent-Guild Secret Ally are separate modes. An empty Guild board or temporary leader control does not change the mode.',
'Within classic core, original Guild Cunning second shipments and Richese Secret Ally discounted shipments retain their actual signed grant, physical group, paid tariff and native continuation through the same funded fee gate. One Guild singleton cannot simultaneously fund native Cunning and rival Betrayal Use; other held faces retain neutral Pass. Outside core, original producer envelopes remain separate. Guild Secret Ally still requires Guild to be absent and is not another native-Guild producer.',
'Wait for a legal positive shipment fee after any applicable native stop and rate responses. The original source, tariff and physical forces must already be legal and fully funded. Stopped, declined, invalid or zero-cost shipments cannot award spice or spend this Nexus; ordinary free Fremen reinforcement and free Guild Ambassador transport stay distinct.',
@@ -4677,7 +4682,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'A separate twelve-card deck rewards remaining unallied. Private draws and selected Atreides, Harkonnen, Tleilaxu, Ixian, Bene Gesserit, Emperor, Fremen, CHOAM, Moritani, Richese and Guild effects are connected. Other effects and complete module play remain in development.',
steps: [
'This optional module can be combined with Basic or Advanced rules independently of the factions and other expansion modules selected. Include one Nexus Card for each of the twelve factions.',
- 'Fresh classic2–6 Basic/Advanced Nexus core admits base33 or selected independent36, optionally all14, original Tech3+ and Advanced Strongholds2+. Actual Treachery, twelve Nexus, fourteen Skills, three Tech and six held-card stocks remain distinct; no selection is silently added. Original fresh Guild/Harkonnen/Ixian states use actual role/card authority. Starting hands/training, real closing draws, first-Storm tokens and end-Mentat claims stay native. Other families, Worlds, Discovery/Semuta and reaction envelopes remain separate.',
+ 'Fresh classic2–6 Basic/Advanced Nexus core admits base33 or selected independent36, optionally all14, original Homeworlds, Tech3+ and Advanced Strongholds2+. Actual Treachery, twelve Nexus, fourteen Skills, three Tech and six held-card stocks remain distinct; no selection is silently added. Original fresh Guild/Harkonnen/Ixian states use actual role/card authority. Starting hands/training, real closing draws, first-Storm tokens and end-Mentat claims stay native. Other families, Discovery/Semuta and reaction envelopes remain separate.',
'Recruits changes native Revival rates and the ordinary seven ceiling, not the fixed Tleilaxu Secret Ally group or its one-spice price. Five paid card counters may leave two ordinary free returns, using the one leader slot once. Emperor Cunning still makes five ordinary board counters temporarily Sardaukar: independent reserve fees use separate physical reserves, Harass returns actual undialed counters, and native Karama changes only the original Nexus effect. Native winner/inspection and Collection continue without extra cleanup prompts.',
'At the end of the entire Spice Blow and Nexus phase, if a Nexus occurred and at least one alliance exists, each unallied player may draw one card or discard their held card and draw a replacement. You may decline. The two Advanced spice piles do not grant two closing draws.',
'Keep your card secret until it is used. You may hold at most one Nexus Card. It is separate from your Treachery hand and does not occupy a Treachery hand slot.',
@@ -4751,7 +4756,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{area:"Player controls",status:'Partial',detail:"Four current original-action390px journeys prove native Junction5/full2, poor1/full2 release plus ordinary one-force replacement, still-held source sand2 disabled then five-force stronghold1, and independent purchased1. The response explains the spent-card consequence. No overflow, browser error or application exception; two tool-only stale-node errors were resolved by fresh evidence without replay. Earlier Junction/loan/refund surfaces remain scoped evidence.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx", "components/junction-transport.tsx"]},
{area:"AI",status:'Partial',detail:"All four original policies execute a legal still-held Richese full-rate one-counter alternative after native cancellation, with actual physical delivery and card cost. Shared human/AI quotes no longer promise a canceled discount. Native permission alternatives, recipient-specific lender prices and immune Junction source choice remain. Optional non-reoffer is not forced. No strategic first-candidate pin, tuning or calibration.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']},
{area:"Documentation",status:'Partial',detail:"Richese printed one-counter pricing and native Guild eligibility stay distinct from absent-faction cancellation, physical force count and independent purchased price. Unfunded spent-card/no-refund release is selected existing no-debt composition, not a new publisher worked example. Immune Junction and earlier permission/refund/loan interpretations remain explicit. Full rules and public gates stay open.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/GUILD_RATE_KARAMA_RULES.md', 'docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md', 'docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md", "docs/JUNCTION_TRANSPORT_RULES.md", "docs/JUNCTION_TRANSPORT_RUNTIME.md"]},
- {area:"Verification",status:'Partial',detail:"Classic core now composes original Tech3+ and Advanced Strongholds2+ with base33/selected36 and optional14. Real first-Storm owners, phase-end income and first-Mentat held cards precede actual card grants, funded receipts, support and casualties. Four390px paths and four legal policies complete earned income/combat; new regressions cover fixed/free returns, spendability, one-pile accrual, native winner transfer and held custody. Other native/World/Discovery/Semuta envelopes and full assurance remain open.",evidence:['tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', 'tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.ts", "tests/nexus-emperor-bank-runtime.test.ts", "tests/duke-captured-custody.test.ts", "tests/nexus-ecaz-duke-captured.test.ts", "tests/fixture-nexus-duke-capture.ts", "tests/nexus-duke-capture-runtime.test.ts", "tests/nexus-duke-timing-runtime.test.ts", "tests/junction-purchased-price.test.ts", "tests/richese-native-guild-rate.test.ts"]},
+ {area:"Verification",status:'Partial',detail:"Classic core retains original World/Tech/Stronghold modules with base33/selected36 and optional14. Independent World cases use native setup, population-sensitive Recruits, typed foreign Harass and actual own-Kaitain Reinforcements/Cunning. The local fee reduces only its paid temporary roles; original count5 and real stars remain. Earlier phase/card proofs are dated in the canonical contract. Native-family/Discovery/Semuta envelopes, full-mode and comprehensive assurance remain open.",evidence:['tests/nexus-world-independent-cards.test.ts','tests/nexus-world-independent-battles.test.ts','tests/reinforcements-temporary-role.test.ts','tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', 'tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.ts", "tests/nexus-emperor-bank-runtime.test.ts", "tests/duke-captured-custody.test.ts", "tests/nexus-ecaz-duke-captured.test.ts", "tests/fixture-nexus-duke-capture.ts", "tests/nexus-duke-capture-runtime.test.ts", "tests/nexus-duke-timing-runtime.test.ts", "tests/junction-purchased-price.test.ts", "tests/richese-native-guild-rate.test.ts"]},
],
},
{
diff --git a/game/reinforcements-homeworld.ts b/game/reinforcements-homeworld.ts
index c87ecc11..23362682 100644
--- a/game/reinforcements-homeworld.ts
+++ b/game/reinforcements-homeworld.ts
@@ -1,4 +1,4 @@
-import type { CombatForces } from './combat';
+import { validCombatForces, type CombatForces } from './combat';
import {
homeworldForceGroups, type HomeworldCustody, type HomeworldCustodyContext,
type HomeworldForces,
@@ -63,14 +63,18 @@ export function quoteReinforcementsHomeworldCost(context: HomeworldCustodyContex
/** On an own World the reserve fee and combat army share physical counters.
* Reserve the fee first, but retain the original sealed support/strength traits.
+ * Provisionally spend ordinary counters without a temporary role first, then
+ * remove that role only from converted counters actually used to pay the fee.
* Foreign-World and Arrakis callers pass no on-location cost. */
export function reinforcementsBattleForces(forces: CombatForces,
onLocationCost?: HomeworldForces): CombatForces {
if (!onLocationCost) return forces;
const { normal, elite } = onLocationCost;
- if (!Number.isSafeInteger(normal) || normal < 0 || !Number.isSafeInteger(elite) || elite < 0 ||
+ if (!validCombatForces(forces) || !Number.isSafeInteger(normal) || normal < 0 || !Number.isSafeInteger(elite) || elite < 0 ||
normal > forces.normal || elite > forces.elite)
throw new ReinforcementsError('The reserve fee cannot also fund the physical Homeworld dial.');
if (normal + elite === 0) return forces;
- return { ...forces, normal: forces.normal - normal, elite: forces.elite - elite };
+ const remainingNormal = forces.normal - normal;
+ return { ...forces, normal: remainingNormal, elite: forces.elite - elite,
+ ...(forces.temporaryElite === undefined ? {} : {temporaryElite: Math.min(forces.temporaryElite, remainingNormal)}) };
}
diff --git a/tests/fixture-nexus-core-world-battles.ts b/tests/fixture-nexus-core-world-battles.ts
index 44b29d13..de017bc3 100644
--- a/tests/fixture-nexus-core-world-battles.ts
+++ b/tests/fixture-nexus-core-world-battles.ts
@@ -5,12 +5,20 @@ import { combatArmy } from '../game/combat-location';
import { nativeShipmentSources } from '../game/homeworld-options';
import { homeworldShipmentChoice } from '../game/homeworld-shipment-options';
import { homeworldGameIntegrity } from '../game/homeworld-game';
+import { REINFORCEMENTS_CARD } from '../game/reinforcements';
import { drawClassicNexusReaction, reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
import { advanceClassicIndependentNexus, type ClassicIndependentNexusStep } from './fixture-classic-independent-nexus';
import { classicDiscoveryNexusClean, nextClassicDiscoveryNexusStep } from './fixture-discovery-classic-nexus';
export type WorldNexusBattleStep = ClassicIndependentNexusStep;
-export type WorldNexusBattleOptions = { advanced?: boolean; skills?: boolean; tech?: boolean; strongholds?: boolean };
+export type WorldNexusBattleOptions = {
+ advanced?: boolean;
+ skills?: boolean;
+ tech?: boolean;
+ strongholds?: boolean;
+ /** Opt in before the original deal; default fixtures retain base33. */
+ independent?: boolean;
+};
type WorldNexusInvasion = { before: Game; action: Action; cost: number; after: Game };
export const worldNexusClean = (game: Game) => classicDiscoveryNexusClean(game) &&
!game.pendingGuildBetrayal && !game.pendingNexusHarkonnenBetrayal && !game.pendingNexusIxianBetrayal;
@@ -59,7 +67,8 @@ function invade(game: Game, actor: string, location: string, amount: number, act
* route exports four actual stars to an empty desert, never converts them. */
export function createWorldNexusEmperorBattle(options: WorldNexusBattleOptions & { salusa?: 'high' | 'low' } = {}) {
const fixture = drawClassicNexusReaction('emperor', 'emperor', { ...options, advanced: true,
- independent: false, homeworlds: true, roster: ['atreides', 'guild', 'emperor'],
+ independent: options.independent ?? false, homeworlds: true, roster: ['atreides', 'guild', 'emperor'],
+ startingCards: options.independent ? { emperor: [REINFORCEMENTS_CARD] } : undefined,
training: options.skills ? { emperor: 'planetologist', atreides: 'warmaster', guild: 'sandmaster' } : undefined });
const actions = fixture.actions, location = 'homeworld:emperor', trained = options.skills ? ['emperor'] : [];
let game = advanceWorldNexusBattle(fixture.game, state => state.phase === 5 && worldNexusClean(state), actions);
@@ -114,7 +123,7 @@ export function createWorldNexusEmperorBattle(options: WorldNexusBattleOptions &
* ordinary call, while the invader owns the independently drawn cancel card. */
export function createWorldNexusHarkonnenBattle(options: WorldNexusBattleOptions = {}) {
const fixture = drawClassicNexusReaction('harkonnen', 'atreides', { ...options,
- independent: false, homeworlds: true, roster: ['atreides', 'harkonnen', 'guild'],
+ independent: options.independent ?? false, homeworlds: true, roster: ['atreides', 'harkonnen', 'guild'],
training: options.skills ? { harkonnen: 'planetologist', atreides: 'warmaster', guild: 'sandmaster' } : undefined });
const actions = fixture.actions, location = 'homeworld:harkonnen';
let game = advanceWorldNexusBattle(fixture.game, state => state.phase === 5 && state.active === 'atreides' &&
diff --git a/tests/fixture-nexus-world-independent-cards.ts b/tests/fixture-nexus-world-independent-cards.ts
new file mode 100644
index 00000000..640c62a2
--- /dev/null
+++ b/tests/fixture-nexus-world-independent-cards.ts
@@ -0,0 +1,179 @@
+import assert from 'node:assert/strict';
+import { viewGame, type Action, type Game } from '../game/engine';
+import { combatArmy } from '../game/combat-location';
+import { nativeShipmentSources } from '../game/homeworld-options';
+import { homeworldShipmentChoice } from '../game/homeworld-shipment-options';
+import { RECRUITS_CARD_ID } from '../game/recruits';
+import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
+import {
+ createClassicIndependentNexusFixture, advanceClassicIndependentNexus,
+ type ClassicIndependentNexusOptions, type ClassicIndependentNexusStep,
+} from './fixture-classic-independent-nexus';
+import { applyClassicDiscoveryNexusStep, nextClassicDiscoveryNexusStep } from './fixture-discovery-classic-nexus';
+
+export type WorldIndependentCardStep = ClassicIndependentNexusStep;
+export type WorldIndependentCardOptions = Pick;
+export const worldIndependentCardPlayer = (game: Game, actor: string) => {
+ const player = game.players.find(player => player.id === actor); assert.ok(player); return player;
+};
+export const worldIndependentCardsClean = (game: Game) => !game.phaseOpening && !game.response &&
+ !game.decision && !game.truthtrance && !game.pendingTreacheryDiscard && !game.pendingGuildBetrayal &&
+ !game.pendingNexusHarkonnenBetrayal && !game.pendingNexusIxianBetrayal;
+
+/** Only still-unopened original Spice Cards are ordered. Neither a clock nor
+ * any army, payment, card hand or battle outcome is assigned. */
+function orderOriginalLandBlows(game: Game, turns: number): void {
+ for (let index = 0; index < turns * (game.advanced ? 2 : 1); index++) {
+ const source = game.spiceDeck.findIndex((card, position) => position >= index && 'territory' in card && !card.discovery);
+ assert.ok(source >= index);
+ game.spiceDeck.splice(index, 0, game.spiceDeck.splice(source, 1)[0]);
+ }
+}
+function nextWorldIndependentCardStep(game: Game): WorldIndependentCardStep | null {
+ if (!game.phaseOpening && !game.response) {
+ const decision = game.decision;
+ if (decision?.kind === 'harassWithdraw') return null;
+ if (decision?.kind === 'homeworldDefense')
+ return { actor: decision.player, action: { type: 'decision', event: decision.event, use: false } };
+ if (decision?.kind === 'homeworldExplosion')
+ return { actor: decision.player, action: { type: 'decision', event: decision.event, choice: 0 } };
+ if (decision?.kind === 'homeworldRevivalDeployment' || decision?.kind === 'caladanReinforcement')
+ return { actor: decision.player, action: { type: 'decision', event: decision.event, decline: true } };
+ }
+ return nextClassicDiscoveryNexusStep(game);
+}
+export function actWorldIndependentCard(game: Game, actor: string, action: Action,
+ actions?: WorldIndependentCardStep[]): Game {
+ return applyClassicDiscoveryNexusStep(game, { actor, action }, actions);
+}
+/** Reuse public native controls and legal Nexus acknowledgement. Sealed plans
+ * and typed Harass allocations stay explicit human-owned actions. */
+export function advanceWorldIndependentCards(game: Game, until: (state: Game) => boolean,
+ actions: WorldIndependentCardStep[] = []): Game {
+ for (let count = 0; count < 2000; count++) {
+ if (until(game)) return game;
+ assert.notEqual(game.status, 'finished', 'The original game ended before the requested card action');
+ if (game.pendingGuildBetrayal) {
+ game = advanceClassicIndependentNexus(game, state => !state.pendingGuildBetrayal, actions);
+ continue;
+ }
+ const step = nextWorldIndependentCardStep(game);
+ assert.ok(step, `A real human plan/allocation is required at ${game.turn}/${game.phase}/${game.decision?.kind}`);
+ game = actWorldIndependentCard(game, step.actor, step.action, actions);
+ }
+ throw Error('Original Nexus/World card programme did not reach its requested boundary');
+}
+
+/** Fresh selected classic independent36/Nexus12/Homeworlds only. The physical
+ * card is dealt from the original shuffle, not moved out of a played hand. */
+export function createWorldIndependentRecruitsFixture(options: WorldIndependentCardOptions = {}) {
+ const source = createClassicIndependentNexusFixture({ ...options, independent: true, homeworlds: true,
+ roster: ['emperor', 'fremen', 'atreides', 'guild'], startingCards: { emperor: [RECRUITS_CARD_ID] } });
+ let game = structuredClone(source.game);
+ const afterSetup = structuredClone(game), actions = source.actions;
+ orderOriginalLandBlows(game, 2);
+ const staging = [...source.staging, 'Order two original non-worm Spice Blows before they are drawn; hold the genuinely dealt Recruits through real native battles.'];
+ const act = (actor: string, action: Action) => { game = actWorldIndependentCard(game, actor, action, actions); };
+ const reach = (until: (state: Game) => boolean) => { game = advanceWorldIndependentCards(game, until, actions); };
+ const shipments = new Map([
+ ['emperor', { amount: 3, elite: 0, territory: 'sietch_tabr', sector: 14 }],
+ ['fremen', { amount: 8, elite: 3, territory: 'sietch_tabr', sector: 14 }],
+ ['guild', { amount: 3, elite: 0, territory: 'arrakeen', sector: 10 }],
+ ]);
+ reach(state => state.phase === 5 && worldIndependentCardsClean(state));
+ while (game.phase === 5) {
+ const actor = game.active!, request = worldIndependentCardsClean(game) ? shipments.get(actor) : undefined;
+ if (request) {
+ const sources = nativeShipmentSources(viewGame(game, actor), request.amount, request.elite); assert.ok(sources);
+ act(actor, { type: 'ship', ...request, homeworldSources: sources });
+ shipments.delete(actor);
+ } else {
+ const step = nextWorldIndependentCardStep(game); assert.ok(step); act(step.actor, step.action);
+ }
+ }
+ assert.equal(shipments.size, 0);
+ reach(state => state.phase === 6 && worldIndependentCardsClean(state));
+ while (game.phase === 6) {
+ if (worldIndependentCardsClean(game) && !game.battle && game.active) {
+ const choice = viewGame(game, game.active).battleChoices[0]; assert.ok(choice);
+ act(choice.chooser, { type: 'chooseBattle', territory: choice.territory,
+ target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
+ }
+ if (game.battle && !game.battle.revealed) {
+ reach(state => worldIndependentCardsClean(state) && !state.battle?.preparation && state.battle?.preLeader?.closed !== false);
+ const battle = game.battle!;
+ for (const actor of [battle.attacker, battle.defender]) {
+ if (game.battle!.plans[actor]) continue;
+ // Keep optional assigned skill discs out of this focused printed-rate
+ // case; they remain genuinely assigned in their original physical pool.
+ const leaders = worldIndependentCardPlayer(game, actor).leaders.filter(leader => !leader.dead && !leader.usedAt &&
+ !game.leaderSkills?.assignments.some(assignment => assignment.owner === actor && assignment.leader === leader.id))
+ .sort((a, b) => a.strength - b.strength);
+ const leader = actor === 'emperor' || actor === 'guild' ? leaders.at(-1) : leaders[0]; assert.ok(leader);
+ act(actor, { type: 'battlePlan', leader: leader.id, dial: 0, support: 0 });
+ reach(state => worldIndependentCardsClean(state) && !state.battle?.preparation && state.battle?.preLeader?.closed !== false);
+ }
+ } else {
+ const step = nextWorldIndependentCardStep(game); assert.ok(step); act(step.actor, step.action);
+ }
+ }
+ reach(state => state.turn === 2 && state.phase === 4 && worldIndependentCardsClean(state));
+ assert.equal(worldIndependentCardPlayer(game, 'fremen').reserves, 2);
+ assert.equal(worldIndependentCardPlayer(game, 'fremen').tanks, 18);
+ assert.ok(worldIndependentCardPlayer(game, 'emperor').hand.some(card => card.id === RECRUITS_CARD_ID));
+ return { ...source, afterSetup, game, actions, staging };
+}
+
+/** Original typed invasion of a foreign World, or a native defender's legal
+ * own-World rejection. The Harass card is held from its native starting deal. */
+export function createWorldIndependentHarassFixture(options: WorldIndependentCardOptions & { native?: boolean } = {}) {
+ const source = createClassicIndependentNexusFixture({ ...options, independent: true, homeworlds: true,
+ roster: ['emperor', 'guild', 'atreides'], startingCards: { emperor: [HARASS_WITHDRAW_CARD] } });
+ let game = structuredClone(source.game);
+ const afterSetup = structuredClone(game), actions = source.actions;
+ orderOriginalLandBlows(game, 1);
+ const staging = [...source.staging, 'Order the unopened original first non-worm Spice Blow; use real typed native invasion and sealed plans.'];
+ const owner = 'emperor', native = options.native ? owner : 'guild', visitor = options.native ? 'guild' : owner;
+ const location = options.native ? 'homeworld:emperor' : 'homeworld:guild';
+ game = advanceWorldIndependentCards(game, state => state.phase === 5 && state.active === visitor && worldIndependentCardsClean(state), actions);
+ const beforeShipment = structuredClone(game);
+ const sources = nativeShipmentSources(viewGame(game, visitor), options.native ? 5 : 7, options.native ? 0 : 4); assert.ok(sources);
+ const quote = homeworldShipmentChoice(viewGame(game, visitor), location,
+ Object.fromEntries(Object.entries(sources).filter(([, force]) => force.normal + force.elite > 0)));
+ assert.ok(quote.action, quote.blocked ?? 'The original native invasion must have a paid typed route');
+ const shipmentStep: WorldIndependentCardStep = { actor: visitor, action: quote.action };
+ game = actWorldIndependentCard(game, visitor, quote.action, actions);
+ game = advanceWorldIndependentCards(game, state => !state.pendingHomeworldShipment && !state.pendingShipment && worldIndependentCardsClean(state), actions);
+ const afterShipment = structuredClone(game);
+ assert.deepEqual(combatArmy(game, visitor, location), options.native ? { normal: 5, elite: 0 } : { normal: 3, elite: 4 });
+ game = advanceWorldIndependentCards(game, state => state.phase === 6 && !!state.active && worldIndependentCardsClean(state), actions);
+ const beforeBattle = structuredClone(game), choice = viewGame(game, game.active!).battleChoices.find(battle => battle.territory === location);
+ assert.ok(choice);
+ const battleStep: WorldIndependentCardStep = { actor: choice.chooser, action: { type: 'chooseBattle', territory: location,
+ target: choice.attacker === choice.chooser ? choice.defender : choice.attacker } };
+ game = actWorldIndependentCard(game, battleStep.actor, battleStep.action, actions);
+ game = advanceWorldIndependentCards(game, state => !!state.battle && !state.battle.preparation &&
+ state.battle.preLeader?.closed !== false && worldIndependentCardsClean(state), actions);
+ const leaders = (actor: string) => worldIndependentCardPlayer(game, actor).leaders.filter(leader => !leader.dead && !leader.usedAt &&
+ !game.leaderSkills?.assignments.some(assignment => assignment.owner === actor && assignment.leader === leader.id))
+ .sort((a, b) => a.strength - b.strength);
+ const ownLeader = leaders(owner)[0], otherLeader = leaders('guild').at(-1); assert.ok(ownLeader && otherLeader);
+ const plans: WorldIndependentCardStep[] = [
+ { actor: owner, action: { type: 'battlePlan', dial: options.native ? 0 : 2, support: options.advanced && !options.native ? 1 : 0,
+ leader: ownLeader.id, defense: HARASS_WITHDRAW_CARD } },
+ { actor: 'guild', action: { type: 'battlePlan', dial: options.native ? 0 : 3, support: options.advanced && !options.native ? 3 : 0,
+ leader: otherLeader.id } },
+ ];
+ return { ...source, afterSetup, game, actions, staging, owner, native, visitor, location,
+ beforeShipment, shipmentStep, afterShipment, beforeBattle, battleStep, plans,
+ returns: { [location]: { normal: 2, elite: 3 } } };
+}
+export function revealWorldIndependentHarass(game: Game, plans: readonly WorldIndependentCardStep[], actions: WorldIndependentCardStep[] = []) {
+ for (const plan of plans) {
+ game = actWorldIndependentCard(game, plan.actor, plan.action, actions);
+ game = advanceWorldIndependentCards(game, state => !!state.battle && !state.battle.preparation &&
+ state.battle.preLeader?.closed !== false && (!state.decision || state.decision.kind === 'harassWithdraw') &&
+ !state.phaseOpening && !state.response, actions);
+ }
+ assert.ok(game.battle?.revealed); return game;
+}
diff --git a/tests/nexus-sardaukar-controls.test.ts b/tests/nexus-sardaukar-controls.test.ts
index 5edc0e81..eb437066 100644
--- a/tests/nexus-sardaukar-controls.test.ts
+++ b/tests/nexus-sardaukar-controls.test.ts
@@ -137,7 +137,8 @@ void test('public pending and active notices avoid private hands and identify or
const allowed = allowNexusSardaukar(g);
const observer = viewGame(allowed, 'q');
assert.equal(observer.battle!.opponentForces?.temporaryElite, 5);
- assert.match(html(observer), /Five ordinary Emperor counters/);
+ assert.match(html(observer), /Cunning declared five ordinary Emperor counters/);
+ assert.match(html(observer), /own-Homeworld reserve fee can reduce that group/);
assert.match(html(observer), /ordinary Tanks/);
assert.doesNotMatch(html(observer), /