From a7cc25ae29d255c08f38d9767f49dc75bd1eb8d4 Mon Sep 17 00:00:00 2001
From: Quintine <15965533+Quintine@users.noreply.github.com>
Date: Sun, 18 Oct 2026 05:34:36 +1100
Subject: [PATCH] Unify classic Nexus core and funded native shipments
---
components/game-table.tsx | 27 +++-
docs/CURRENT_STATUS.md | 22 +++
docs/IMPLEMENTATION_STATUS.md | 39 +++++
docs/LEADER_SKILLS_RUNTIME.md | 12 ++
docs/NEXUS_CARD_RULES.md | 53 +++++++
docs/NEXUS_GUILD_CUNNING_RUNTIME.md | 17 +++
docs/NEXUS_GUILD_RULES.md | 18 +++
docs/NEXUS_HARKONNEN_RULES.md | 11 ++
docs/NEXUS_IXIAN_REPLACEMENT_RULES.md | 11 ++
docs/NEXUS_RICHESE_RUNTIME.md | 16 +++
docs/RULE_DECISIONS.md | 1 +
game/engine.ts | 95 +++++++------
game/faction-module-profile.ts | 23 +--
game/leader-skill-profile.ts | 6 +-
game/nexus-emperor-purchase.ts | 4 +-
game/nexus-emperor-secret-ally.ts | 4 +-
game/nexus-fremen-betrayal.ts | 4 +-
game/nexus-module-profile.ts | 4 +-
game/reference.ts | 34 ++---
tests/classic-nexus-core.test.ts | 133 ++++++++++++++++++
...ure-classic-independent-nexus-reactions.ts | 26 ++--
tests/fixture-classic-independent-nexus.ts | 18 ++-
tests/fixture-classic-nexus-core.ts | 94 +++++++++++++
tests/nexus-guild-betrayal-engine.test.ts | 53 -------
24 files changed, 575 insertions(+), 150 deletions(-)
create mode 100644 tests/classic-nexus-core.test.ts
create mode 100644 tests/fixture-classic-nexus-core.ts
diff --git a/components/game-table.tsx b/components/game-table.tsx
index 20b76c62..ab88fba3 100644
--- a/components/game-table.tsx
+++ b/components/game-table.tsx
@@ -14,6 +14,7 @@ import { AdvancedPreviewNotice, RulesetControls } from './ruleset-controls';
import { EcazOccupyLeadChoice, EcazOccupyBattleSummary, EcazOccupyDialExplanation } from './ecaz-occupy';
import { ecazOccupyOwnProfile, ecazOccupyPlanControl } from '@/game/ecaz-occupy-options';
import { nexusGuildCunningAction, nexusGuildCunningActive, nexusGuildMovementAvailable, nexusGuildShipmentAvailable, nexusGuildSkipShipmentAction } from '@/game/nexus-guild-cunning-options';
+import { classicNexusCoreProfile } from '@/game/faction-module-profile';
import {
matchesShipment,
liveShipmentPromises,
@@ -261,6 +262,8 @@ export function GameTable({
const movementAvailable = nexusGuildMovementAvailable(g);
const guildCunning = nexusGuildCunningActive(g);
const guildCunningAction = g.nexusGuildCunning?.offer ? nexusGuildCunningAction(g, g.nexusGuildCunning.offer.event) : null;
+ const classicNexusPreview = classicNexusCoreProfile(g) &&
+ (g.guildBetrayalPreview || g.nexusIxianReplacementPreview || g.nexusHarkonnenBetrayalPreview);
const combatName = (id: string) =>
g.combatLocations?.find((place) => place.id === id)?.name ??
gameTerritories(g).find((place) => place.id === id)?.name ??
@@ -1287,7 +1290,17 @@ export function GameTable({
Preview rules and limits
)}
- {g.guildBetrayalPreview && (
+ {classicNexusPreview && (
+
+ Classic Nexus reactions preview · Basic/Advanced classic factions, original base33
+ or selected independent36 Treachery, optionally all14 Leader Skills. Enabled native
+ reactions retain original card, force and payment authority. Acknowledgement timing
+ and purchase/bonus ordering remain provisional. Other modules and complete Nexus
+ acceptance remain separate.{' '}
+ Preview rules and limits
+
+ )}
+ {g.guildBetrayalPreview && !classicNexusPreview && (
Guild Nexus Betrayal development preview · whole funded shipment
payment replacement, including your own payment and occupied Junction.
@@ -1296,12 +1309,12 @@ export function GameTable({
Preview rules and limits
)}
- {g.nexusIxianReplacementPreview && (
+ {g.nexusIxianReplacementPreview && !classicNexusPreview && (
- Ixian Nexus purchased-card replacement development preview · classic Basic/Advanced
- with base Treachery, or Basic Tleilaxu/classic with Ixian Treachery; Nexus alone.
- Ixians are absent. Normal paid and printed Karama purchases are supported;
- Harkonnen buyers and special auctions are guarded. Not complete Ixian or certified Nexus rules.{' '}
+ Ixian Nexus purchased-card replacement development preview · original supported
+ standalone Tleilaxu or Richese entries. Normal paid/Karama and supported native
+ Richese purchases retain their exact source; replacement-before-bonus and cache
+ interpretations remain provisional. Not complete Ixian or certified Nexus rules.{' '}
Preview rules and limits
)}
@@ -1314,7 +1327,7 @@ export function GameTable({
Preview rules and limits
)}
- {g.nexusHarkonnenBetrayalPreview && (
+ {g.nexusHarkonnenBetrayalPreview && !classicNexusPreview && (
Harkonnen Nexus Betrayal development preview · cancel an actually declared native Harkonnen
traitor call, return that physical card to the Traitor Deck, and draw one private replacement
diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md
index 6e51f3ec..10012188 100644
--- a/docs/CURRENT_STATUS.md
+++ b/docs/CURRENT_STATUS.md
@@ -29,6 +29,28 @@ optional skill bands and combinations remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
+**Classic Nexus core — 18 October. Prototyped / Partial.**
+Ordinary base33 all14 native calls/purchases lacked reaction prompts. One
+public capability now admits original33 or selected36 with Nexus12 and
+optional14, initializing actual Guild/Harkonnen/Ixian reaction states at fresh
+setup. Independent cards are neither required nor silently added; no played
+game is reflagged or supplied invented history. Other envelopes stay separate.
+
+Original signed Cunning reserve/cross/return and Richese five-for-one groups
+retain funded fee and physical continuation through the same acknowledgement.
+Native counters and actual third-party Bureaucrat remain; one spent Guild
+singleton cannot also authorize rival Use. Timing remains inherited/provisional.
+
+Frozen31-file **304/304**, focused10/10, types/lint/build pass. Three390px
+original-control journeys reach whole1 claim, Cunning return/turn finish and
+third-party5→recipient3/Bank2, with no errors/overflow. Four legal policies
+execute28 actual choices, not calibration or full games. One accurate classic
+notice replaces contradictory standalone stacks. Three old exclusion cases
+are deleted, not re-pinned; the driver honors actual owned fee acknowledgement.
+Temporary memory/routes removed/released204, no stored room/reset/restart/
+deployment. Other modes, universal timing and deferred assurance remain open.
+[Exact source and current proof](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026).
+
**Selected-card native reactions — 18 October. Prototyped / Partial.**
Real Harkonnen call/Nexus and paid Reinforcements/Ixian purchase exposed absent
original effect prompts. Fresh classic2–6 independent36/Nexus12 optional14 now
diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md
index 1def1043..d1da5414 100644
--- a/docs/IMPLEMENTATION_STATUS.md
+++ b/docs/IMPLEMENTATION_STATUS.md
@@ -1,5 +1,44 @@
# Dune implementation status
+## 18 October 2026 — Classic Nexus core and funded native card shipments
+
+Ordinary base33 all14 genuine Harkonnen call and Ixian purchase still lacked
+native prompts. One public core capability now admits original33 or selected36
+with Nexus12, optionally14, initializing native Guild/Harkonnen/Ixian states
+only at fresh setup. The independent predicate still requires its actual
+selection; no three cards, faction role or played history are invented.
+
+Guild Cunning and Richese Secret Ally retain their real signed producer and
+funded original invoice through Guild fee acknowledgement. Actual pending
+reserve/cross frames remain their original grant's frame; conditional source
+bindings leave ordinary/old receipt shape unchanged. Native interception,
+Karama and return/rate authority remain separate. A Cunning-spent Guild
+singleton cannot simultaneously be a rival's usable card.
+
+Ten meaningful consumers cover ordinary all14 Mentat replacement/purchase,
+discounted5 at stronghold1 with Use/Pass, own-payer Bureaucrat exclusion,
+actual third-party5→recipient3/Bank2, Cunning's three native routes and native
+cancellation preserving the paid first shipment. Shared original driver now
+uses the actual pending holder's canonical policy instead of bypassing the
+new fee acknowledgement. Three obsolete producer-exclusion cases are deleted,
+not re-pinned; other original native source/permission checks remain.
+
+Frozen31-file **304/304**, focused10/10, types/lint/build pass. Four legal
+policies perform28 actual choices, including private real Mentat replacement,
+purchase, discounted claim, Bureaucrat diversion and Cunning return/turn
+closure. No calibration or full-combination claim.
+
+Three390px original-control journeys pass: five-for-one→Guild interception/
+whole1 claim; paid first shipment→Cunning/native allowance→separate return
+permission→wrong-face Pass→spice4/reserves15/turn finish; ordinary5→whole
+claim→actual third-party Bank2 with payer7/recipient13/Guild6. One accurate
+classic notice replaces three contradictory standalone warning stacks.
+Completed notices are explicitly dismissed, never used to replay a rule
+action; no browser/application errors or overflow. Temporary routes/memory
+removed/released204, no stored room/reset/server restart/deployment. Existing
+build warnings, wider modes, universal timing and assurance remain open.
+[Canonical source and boundary](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026).
+
## 18 October 2026 — Native classic independent-card Nexus reactions
Real Harkonnen call with an actually drawn Harkonnen Nexus resolved without
diff --git a/docs/LEADER_SKILLS_RUNTIME.md b/docs/LEADER_SKILLS_RUNTIME.md
index cd9d02ef..2ceaf467 100644
--- a/docs/LEADER_SKILLS_RUNTIME.md
+++ b/docs/LEADER_SKILLS_RUNTIME.md
@@ -7,6 +7,18 @@ and fixture opt-ins are removed; existing games are not retrofitted.
Earlier dated references to a separate entry describe historical checkpoints.
See [current activation and limits](SPICE_BANKER_RUNTIME.md#mandatory-ordinary-skills-default--17-october-2026).
+## Classic Nexus core without card dependency — 18 October 2026
+
+Original base33 or explicitly selected independent36 Nexus core admits
+optional all14 in classic2–6 Basic/Advanced starts. Native offers/deal order,
+mandatory normal Banker and actual role/card sources remain. Fresh reaction
+states do not require the three-card variant or reflag a played game.
+Real paid5 redirected by Guild Nexus names the actual recipient; a different
+living Bureaucrat diverts2, while the paying trainer remains ineligible.
+Native Cunning and Richese grants retain their signed physical/payment
+continuations through the same fee gate. No new numerical or universal rule.
+[304/304, native phone controls and four legal policies](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026).
+
## Classic selected-card native reactions — 18 October 2026
The fresh independent36/Nexus12 optional14 entry now initializes original
diff --git a/docs/NEXUS_CARD_RULES.md b/docs/NEXUS_CARD_RULES.md
index 8ac60c92..46e6317d 100644
--- a/docs/NEXUS_CARD_RULES.md
+++ b/docs/NEXUS_CARD_RULES.md
@@ -2,6 +2,59 @@
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 Nexus core and funded card shipments — 18 October 2026
+
+**Prototyped / Partial.** Original E3 pp4/9/11 permits Nexus independently of
+other variants. Fresh classic2–6 Basic/Advanced Nexus core now keeps base33
+or explicitly selected independent36, optionally all14 Skills. The three-card
+variant is not required for native reactions and is never silently added.
+`classicNexusCoreProfile` owns this public envelope; the narrower independent
+predicate still requires the actual card selection. Other native families,
+World/phase/Discovery/Semuta and reaction envelopes retain their own bounds.
+
+Fresh setup initializes original actual-Guild fee replacement, actual-Harkonnen
+call cancellation and absent-Ixian purchase replacement through their existing
+protocols. Original offers/deals, canonical census and mandatory normal Banker
+remain. No played game is reflagged or given invented history. Original
+standalone admission APIs keep their narrower roles and reuse fresh state.
+
+The funded Guild gate now binds the actual signed Cunning or Richese shipment
+grant as well as its original typed continuation. Suspended reserve/cross
+frames remain the original producer's current pending frame; no extra pending
+alias, card spend, fee or movement is created. Ordinary parents/old v1 receipts
+gain no unconditional new fields. Native stop, route and rate controls precede
+the fee. Existing bounded acknowledgement/order conventions remain provisional.
+
+- Distinct real Richese/Guild draws compose five physical counters at a
+ stronghold's one-counter price1. The Richese card is spent at declaration;
+ Guild Use redirects only that actual1 and spends the distinct Guild card.
+ Pass instead pays original Guild1 and retains Guild Nexus.
+- Cunning preserves its first paid1 shipment. Native allowance then reserve,
+ cross or return second shipment retains its original fee1, group and queue.
+ A rival cannot hold a second Guild singleton after Cunning spent it; the
+ actual irrelevant face has only neutral Pass. Native Karama cancellation
+ keeps the first shipment/payment and spent Nexus, without a second grant.
+- Original paid5 can go to the real Nexus recipient, then a third-party living
+ Bureaucrat diverts2: payer12→7, recipient10→13, Guild6 unchanged, physical5
+ delivered once. The paying Bureaucrat cannot redirect its own transaction.
+
+New `tests/classic-nexus-core.test.ts` and its named original-action producer
+cover ten consumer cases. Frozen31-file **304/304**, focused10/10, types/lint
+and build pass. Three obsolete producer-exclusion cases are removed, not
+re-pinned; other native permission/source regressions remain.
+Four legal policies execute28 actual choices through private Mentat, purchase,
+discounted claim, third-party redirection and Cunning return/turn closure.
+These are playability paths, not calibration or full-combination games.
+
+Three actual390px journeys exercise the original five-for-one form and Guild
+interception/claim, Cunning declaration/native Karama/return permission/
+wrong-face Pass/turn finish, and ordinary5→Nexus claim→Bureaucrat2. The updated
+single classic notice replaces contradictory standalone-only warning stacks.
+Completed notices are explicitly dismissed; no rule action is replayed.
+No browser/application errors or horizontal overflow. Memory/routes removed/
+released204; no stored room, reset, restart or deployment. Existing build
+warnings and complete-mode, universal-timing and assurance gates remain.
+
## Classic selected-card reactions — 18 October 2026
Frozen16-file **156/156**, focused new6/6, final types/lint and build pass.
diff --git a/docs/NEXUS_GUILD_CUNNING_RUNTIME.md b/docs/NEXUS_GUILD_CUNNING_RUNTIME.md
index 6dadeb26..42db08a8 100644
--- a/docs/NEXUS_GUILD_CUNNING_RUNTIME.md
+++ b/docs/NEXUS_GUILD_CUNNING_RUNTIME.md
@@ -2,6 +2,23 @@
This checkpoint follows the [Guild source contract](NEXUS_GUILD_RULES.md): native Guild can spend Cunning when finishing its ordinary Shipment and Movement turn to obtain one second shipment. It preserves one combined turn and the actual history of the first shipment and movement. It does not implement Guild Secret Ally, Betrayal or complete Nexus play.
+## Classic core payment acknowledgement — 18 October 2026
+
+Original classic base33 or selected independent36 Nexus, optionally all14,
+now preserves Cunning through the same original Guild fee gate. A pending
+second reserve/cross frame remains the actual signed Cunning shipment while
+the fee waits; first payment, movement and queue never reopen. The card is
+already spent, so an actual rival holds another face and has only neutral Pass,
+not a fabricated second Guild card. Native cancellation still preserves the
+first shipment and consumes its attempted Cunning card.
+
+Genuine reserve/cross/return groups each pay1 once in the new core producer.
+Actual390px declaration, original Karama allowance, separate native return
+permission, wrong-face Pass and Finish Guild turn end at spice4/reserves15
+without ordinary movement. Other native/module and physical Ornithopter
+boundaries remain separate.
+[Source-bound combined proof](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026).
+
## Native permission opportunity scope — 15 October 2026
Cross/return permission is now a separate prepayment counter before native
diff --git a/docs/NEXUS_GUILD_RULES.md b/docs/NEXUS_GUILD_RULES.md
index c738f7e5..9ead65d7 100644
--- a/docs/NEXUS_GUILD_RULES.md
+++ b/docs/NEXUS_GUILD_RULES.md
@@ -4,6 +4,24 @@
Source audit, 10 September 2026. This documents the three printed panels and their integration boundaries. It does not implement them or certify complete Guild, Homeworld, Nexus or expansion compliance. [Common Nexus lifecycle](NEXUS_CARD_RULES.md) and existing release gates continue to apply. No user question was sent during this audit.
+## Classic core funded card shipments — 18 October 2026
+
+Fresh classic2–6 Basic/Advanced Nexus uses original base33 or selected
+independent36, optionally all14 Skills, with actual native Guild payment
+replacement initialized through the shared public core capability. No card
+variant or separate public reaction selector is required. Original standalone
+Homeworld and other native envelopes remain separate; played games are not
+reflagged or supplied fabricated history.
+
+Within this core entry, the earlier Cunning/Richese producer exclusions below
+are superseded. Original signed reserve/cross/return Cunning and one-counter
+Richese shipments retain their actual funded invoice, native counters and
+physical continuation through the existing fee acknowledgement. The actual
+Guild singleton spent for Cunning cannot also be held for rival Betrayal Use.
+Existing neutral wrong-face Pass and provisional timing remain unchanged.
+Actual5/price1 and third-party5→recipient3/Bank2 retain their original rules.
+[Current source, 304/304, controls and legal policies](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026).
+
## Primary evidence
The Guild face was freshly inspected in the [original photograph of GF9's twelve Nexus cards](https://boardgamegeek.com/image/7767032/dune-ecaz-and-moritani), local inspection copy `/tmp/dune-nexus-cards.jpg`:
diff --git a/docs/NEXUS_HARKONNEN_RULES.md b/docs/NEXUS_HARKONNEN_RULES.md
index 750a1e88..c348dfd9 100644
--- a/docs/NEXUS_HARKONNEN_RULES.md
+++ b/docs/NEXUS_HARKONNEN_RULES.md
@@ -2,6 +2,17 @@
Source audit, 10 September 2026. This is a rules contract, not a runtime completion claim. The [common Nexus lifecycle](NEXUS_CARD_RULES.md) and existing expansion release gates still apply. The pending reaction-privacy question remains unresolved; this audit does not authorize a Betrayal reaction policy.
+## Original base33 core without card dependency — 18 October 2026
+
+Fresh classic2–6 Basic/Advanced Nexus now initializes the original native
+call protocol with base33 or selected independent36, optionally all14. The
+independent three-card variant is not required. A genuine base33 all14 call
+can be canceled and reaches one actual private Mentat replacement; without
+Harass, its ordinary losing board counter still goes to Tanks. Public
+selection preserves the original base33 census/three-element history grammar.
+Original counter/acknowledgement policy and other module envelopes remain.
+[Shared current core proof](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026).
+
## Classic independent-card native reactions — 18 October 2026
**Prototyped / Partial.** Fresh normal classic2–6 Basic/Advanced
diff --git a/docs/NEXUS_IXIAN_REPLACEMENT_RULES.md b/docs/NEXUS_IXIAN_REPLACEMENT_RULES.md
index bb17551a..17c02365 100644
--- a/docs/NEXUS_IXIAN_REPLACEMENT_RULES.md
+++ b/docs/NEXUS_IXIAN_REPLACEMENT_RULES.md
@@ -2,6 +2,17 @@
Source audit: 2026-09-10. Bounded local prototype integration and verification: 2026-10-01. **Partial development verified**, not complete Ixian/Nexus family or expansion acceptance. This document records the source and runtime boundary of the fresh `ixian-replacement` profile. Existing [Nexus release gates](NEXUS_CARD_RUNTIME.md) remain in force.
+## Original base33 core without card dependency — 18 October 2026
+
+Fresh classic2–6 Basic/Advanced Nexus initializes original purchase replacement
+with base33 or selected independent36, optionally all14. Actual base-card
+paid2 now reaches the same own event-only choice without requiring the
+three-card variant: one original invoice, exact purchased discard and real
+deck-top draw. Original normal Banker, native bonus and existing standalone
+family/special-purchase contracts remain. No played game gains a new stamp
+and no independent Treachery Card is silently inserted.
+[Current core source and actual consumer proof](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026).
+
## Classic independent-card native reactions — 18 October 2026
Fresh normal classic2–6 Basic/Advanced independent36/Nexus12 now initializes
diff --git a/docs/NEXUS_RICHESE_RUNTIME.md b/docs/NEXUS_RICHESE_RUNTIME.md
index 66644ee2..aeffa3cc 100644
--- a/docs/NEXUS_RICHESE_RUNTIME.md
+++ b/docs/NEXUS_RICHESE_RUNTIME.md
@@ -2,6 +2,22 @@
This checkpoint adds the [source-audited Secret Ally](NEXUS_RICHESE_RULES.md) to an unallied holder's unused ordinary reserve shipment when Richese is absent. Choose one to five actual forces and price the group as one force. The physical force count, starred identity, destination and arrival restrictions remain ordinary shipment rules. It creates no concealed No-Field and no second shipment.
+## Classic core Guild fee composition — 18 October 2026
+
+Classic base33 or selected independent36 Nexus, optionally all14, now retains
+the original signed Secret Ally declaration through actual native Guild fee
+acknowledgement. Original Guild interception/rate rules precede that fee.
+The declared five-for-one group remains pending, without moving counters or
+charging spice early, while its actual Richese card stays spent.
+
+Real distinct Richese/Guild draws permit a rival's full fee claim: five
+physical counters to Carthag cost the ordinary one-counter stronghold price1.
+Actual mobile controls complete native Guild Allow then rival Use: shipper
+12→11/reserves10→5, recipient10→11, both distinct Nexus cards discarded once.
+Pass instead retains Guild Nexus and credits native Guild1. No concealed
+marker, extra shipment or change to original rate/affordability policy.
+[Exact core source, tests and controls](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026).
+
## Native Guild rate followup — 15 October 2026
Original Guild/Richese Secret Ally declarations now admit the distinct unpaid
diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md
index 7231a4a5..356a8200 100644
--- a/docs/RULE_DECISIONS.md
+++ b/docs/RULE_DECISIONS.md
@@ -704,6 +704,7 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP
| Independent Treachery / all14 Skills | Printed variant independence composes registered actual selected-family all14 Basic/Advanced2–6, including mixed E3/E1/E2, unused decks, original Worlds and Tech3+/Advanced Strongholds2+. No Nexus/exact E3 family/card dependency is invented. Original36/38/50, separate cache and native setup/custody remain. Bare mixed E3 World/phase cards, original Nexus/Discovery/Semuta, Advanced role, shared Duke and effect-specific limits stay separate. Fees/H/committed-only Suk, own-World bans and separate Banker/Tech remain. No new numeric rule/save retrofit. | [Current registered native scope](LEADER_SKILLS_RUNTIME.md#registered-native-e3-selected-decks--18-october-2026), [physical sources](ECAZ_TREACHERY_RULES.md) |
| Classic independent Treachery / Nexus | Printed independent-variant authority composes fresh classic2–6 Basic/Advanced independent36/Nexus12, optionally all14, through one public configuration predicate and original setup. Recruits changes ordinary rates/total7, not fixed Tleilaxu group price/free classification. Cunning keeps temporary ordinary counters, separate Reinforcements reserve fees, actual Harass returns and original cancellation. No new numeric rule or played-save conversion. Native selected families, World/phase/Discovery/Semuta, reaction-preview and other effect-specific bounds remain separate; inherited provisional timings are not promoted to publisher rulings. | [Source and exact native proof](NEXUS_CARD_RULES.md#classic-independent-treachery-composition--18-october-2026) |
| Classic independent native Nexus reactions | Fresh independent36/Nexus12 optional14 initializes the existing Ixian purchase and actual Harkonnen declared-Traitor effect stamps; played games remain unchanged. Public selection binds canonical faces and the original census, with a new independent marker only where selected. Under standing choose-recommended authority, inherit the existing bounded native-counter-first/neutral acknowledgement and replacement-before-Harkonnen-bonus conventions, visibly provisional, not new publisher or universal rulings. Personal/allied source, real Mentat replacement and original paid/card/force mechanics remain. Other native/module/reaction envelopes stay separate. | [Current source and actual controls](NEXUS_CARD_RULES.md#classic-selected-card-reactions--18-october-2026) |
+| Classic Nexus core / funded card shipments | Original Nexus independence admits fresh classic2–6 Basic/Advanced base33 or selected independent36, optionally all14, without a three-card dependency. Initialize actual Guild/Harkonnen/Ixian reaction states only at fresh setup. Original signed Cunning reserve/cross/return and Richese one-counter grants retain native counters, funding and physical continuation through the same Guild fee gate. One Guild singleton cannot simultaneously fund Cunning and rival Use; actual wrong-face Pass remains neutral. Actual third-party Bureaucrat sees the real recipient and printed payer exclusions. No new numerical/universal ruling, played reflag or wider module opening; inherited timing stays provisional. | [Current source and exact proof](NEXUS_CARD_RULES.md#classic-nexus-core-and-funded-card-shipments--18-october-2026) |
| Fresh native Ecaz/Moritani defaults | Fresh actual Eca/Mori roles reuse registered selected-deck all14 capability, including mixed families and original World/phase/cards without Nexus. Printed Eca participation and Advanced Mori assassination initialize only for actual seats; Basic gains no Advanced power and unused decks create no faction seat. Bare rosters and original Nexus/Discovery/Semuta retain their envelopes; Advanced role/shared Duke guards stay. No played-save forfeiture/opportunity history is fabricated. | [Current registered source envelope](LEADER_SKILLS_RUNTIME.md#registered-native-e3-selected-decks--18-october-2026), [Mori authority](MORITANI_ASSASSINATE_LEADERS.md#connected-scope-and-private-information) |
| Independent Treachery / original phase modules | Printed independence retains original Tech3–6 Basic/Advanced and/or Strongholds2–6 Advanced with classic/E1/E2 and original standalone/paired E3 bare cards; all14 also composes registered mixed selected families and Worlds. Real first-Storm/end-Mentat owners, separate delayed income and physical funding remain. Fees/H are not revival/shipment and +2 is not supported force. Native coalition/assassination and original queue stay source-labelled. Bare mixed E3 World/phase, original Nexus/Discovery/Semuta and role/effect guards remain. | [Original phase contract](ECAZ_TREACHERY_RULES.md#original-phase-module-composition--16-october-2026), [registered scope](LEADER_SKILLS_RUNTIME.md#registered-native-e3-selected-decks--18-october-2026) |
| Classic World independent cards / phase modules | Compose original unused Tech3+ and/or Advanced Strongholds2+ with the existing classic36-card Homeworld entry. Preserve native typed custody, current separate-request rates/paid ledgers, exact source-bearing R fees, original face/support, fee/dial disjointness and fee-before-Harass. Keep every own-World Harass ban; native returns are not shipment/free Revival. Original delayed income and earned cards add no personal subsidy or supported +2. This inherits existing provisional timing, not a new publisher ruling or native expansion/public gate. | [Source-bound contract](ECAZ_TREACHERY_RULES.md#classic-world-phase-module-composition--16-october-2026) |
diff --git a/game/engine.ts b/game/engine.ts
index 3781f8b1..ca97c15e 100644
--- a/game/engine.ts
+++ b/game/engine.ts
@@ -1,5 +1,5 @@
import { classicNexusModulesProfile, mixedE1E2NexusProfile, mixedE3NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
-import { classicIndependentNexusProfile, nativeFactionTechProfile } from './faction-module-profile';
+import { classicIndependentNexusProfile, classicNexusCoreProfile, nativeFactionTechProfile } from './faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, homeworldLeaderSkillsProfile, independentTreacheryLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, guildBetrayalNexusLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeEcazLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3LeaderSkillsProfile, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
@@ -9646,7 +9646,7 @@ export function initializeNexusGameForAudit(state: Game): Game {
if ((mixedE3 || standalone || pairedE3) && g.advanced && byFaction(g, 'moritani')) initializeMoritaniAssassinateState(g);
return g;
}
-/** Fresh audit admission only; ordinary games and public starts never opt in. */
+/** Narrow original standalone admission; normal classics share fresh effect state. */
export function initializeHarkonnenNexusBetrayalGameForAudit(state: Game): Game {
requireRule(!state.ecazTreachery && !state.leaderSkills && harkonnenNexusBetrayalModeSupported(state) &&
state.nexusCards?.cards === null && state.nexusCards.phase === null &&
@@ -9658,17 +9658,10 @@ export function initializeHarkonnenNexusBetrayalGameForAudit(state: Game): Game
state.nexusHarkonnenReplacementHistory === undefined,
'Harkonnen Nexus Betrayal requires a fresh classic Harkonnen lobby with only base Treachery and Nexus.');
requireFreshBaseRuntime(state);
- const g = initializeNexusGameForAudit(state);
- g.nexusHarkonnenBetrayalPreview = true;
- g.pendingNexusHarkonnenBetrayal = null;
- g.nexusHarkonnenBetrayalHistory = [];
- g.nexusHarkonnenBetrayalCursor = initialHarkonnenBetrayalCursor();
- g.pendingNexusHarkonnenReplacement = null;
- g.nexusHarkonnenReplacementHistory = [];
- return g;
+ return initializeNexusGameForAudit(state);
}
function harkonnenNexusBetrayalModeSupported(g: Game): boolean {
- if (classicIndependentNexusProfile(g)) return !!byFaction(g, 'harkonnen');
+ if (classicNexusCoreProfile(g)) return !!byFaction(g, 'harkonnen');
return typeof g.advanced === 'boolean' && !!g.nexusCards && g.expansions.length === 0 &&
g.players.length >= 2 && g.players.length <= 6 && !!byFaction(g,'harkonnen') &&
new Set(g.players.map(p => p.faction)).size === g.players.length &&
@@ -10011,7 +10004,7 @@ function decideIxianNexusBetrayal(g: Game,id: string,action: Action): void {
if (use) log(g,`${getPlayer(g,id).name} spent Ixian Nexus Betrayal to prevent the original native ${frame.source.kind} attempt.`);
finishIxianNativeAttempt(g,frame.continuation,use);
}
-/** Fresh classic, Tleilaxu or Richese profile; no public start or retrofit. */
+/** Original standalone family admission; normal classics share fresh effect state. */
export function initializeIxianNexusReplacementGameForAudit(state: Game): Game {
requireRule(!state.ecazTreachery && !state.leaderSkills && !state.nexusIxianReplacementPreview && !state.pendingNexusIxianReplacement &&
!state.pendingNexusIxianSpecialPurchase &&
@@ -10021,14 +10014,16 @@ export function initializeIxianNexusReplacementGameForAudit(state: Game): Game {
const g = initializeNexusGameForAudit(state);
// Ix supplies Treachery here, not the optional Sandtrout Spice card.
if (g.expansions.includes('ix')) g.spiceDeck = shuffle(spiceDeck());
- g.nexusIxianReplacementPreview = true;
- g.pendingNexusIxianReplacement = null;
- g.nexusIxianReplacementHistory = [];
- g.nexusIxianReplacementCursor = initialIxianReplacementCursor();
+ if (!g.nexusIxianReplacementPreview) {
+ g.nexusIxianReplacementPreview = true;
+ g.pendingNexusIxianReplacement = null;
+ g.nexusIxianReplacementHistory = [];
+ g.nexusIxianReplacementCursor = initialIxianReplacementCursor();
+ }
return g;
}
function ixianNexusReplacementModeSupported(g: Game): boolean {
- if (classicIndependentNexusProfile(g)) return true;
+ if (classicNexusCoreProfile(g)) return true;
return !!g.nexusCards && g.players.length >= 2 && g.players.length <= 6 &&
new Set(g.players.map(p => p.faction)).size === g.players.length &&
((!g.expansions.length && g.players.every(p => CLASSIC_FACTIONS[p.faction] === true)) ||
@@ -10178,6 +10173,13 @@ function decideIxianNexusReplacement(g: Game,id: string,action: Action): void {
g.pendingKarama = special.resume.pendingKarama;
} else continueAuctionSale(g,source.free);
}
+function initializeGuildBetrayalState(g: Game): void {
+ g.guildBetrayalPreview = true;
+ g.guildBetrayal = {sequence: 0, event: null, turn: null, sourceSignature: null, status: 'completed'};
+ g.nexusGuildBetrayalHistory = [];
+ g.guildBetrayalSourceReceipts = [];
+ g.pendingGuildBetrayal = null;
+}
/** Opt-in fresh classic setup through the real physical Nexus/Homeworld setup. */
export function initializeGuildBetrayalGameForAudit(state: Game): Game {
requireRule(!state.guildBetrayalPreview && !state.guildBetrayal && !state.pendingGuildBetrayal &&
@@ -10185,11 +10187,7 @@ export function initializeGuildBetrayalGameForAudit(state: Game): Game {
guildBetrayalModeSupported(state),
'Guild Betrayal needs a fresh classic native-Guild lobby with only Nexus and optional Homeworlds.');
const g = initializeNexusGameForAudit(state);
- g.guildBetrayalPreview = true;
- g.guildBetrayal = {sequence: 0, event: null, turn: null, sourceSignature: null, status: 'completed'};
- g.nexusGuildBetrayalHistory = [];
- g.guildBetrayalSourceReceipts = [];
- g.pendingGuildBetrayal = null;
+ if (!g.guildBetrayalPreview) initializeGuildBetrayalState(g);
return g;
}
/** Development-only composition of the bounded Betrayal preview with classic Leader Skills. */
@@ -10205,14 +10203,10 @@ export function initializeGuildBetrayalLeaderSkillsGameForAudit(state: Game): Ga
'Guild Betrayal with Leader Skills needs a fresh classic native-Guild lobby with only Nexus.');
const g = structuredClone(state);
g.nexusCards = {cards: null, phase: null};
- g.guildBetrayalPreview = true;
- g.guildBetrayal = {sequence: 0, event: null, turn: null, sourceSignature: null, status: 'completed'};
- g.nexusGuildBetrayalHistory = [];
- g.guildBetrayalSourceReceipts = [];
- g.pendingGuildBetrayal = null;
return initializeLeaderSkillsGameForAudit(g);
}
function guildBetrayalModeSupported(g: Game): boolean {
+ if (classicNexusCoreProfile(g)) return !!byFaction(g, 'guild');
return !!g.nexusCards && g.expansions.length === 0 && g.players.length >= 2 &&
g.players.length <= 6 && !!byFaction(g, 'guild') &&
g.players.every(p => CLASSIC_FACTIONS[p.faction] === true) &&
@@ -10221,6 +10215,9 @@ function guildBetrayalModeSupported(g: Game): boolean {
!g.richeseBetrayalPreview && !g.mobileStronghold;
}
function guildBetrayalParentSignature(g: Game, continuation: GuildBetrayalContinuation): string {
+ const cunning = currentGuildCunning(g);
+ const richese = continuation.source === 'reserve' && continuation.shipment.nexusEvent
+ ? currentNexusRicheseShipment(g, continuation.shipment) : null;
return JSON.stringify({
continuation, status: g.status, turn: g.turn, phase: g.phase, active: g.active,
order: g.order, storm: g.storm, movementRemaining: g.movementRemaining,
@@ -10234,6 +10231,8 @@ function guildBetrayalParentSignature(g: Game, continuation: GuildBetrayalContin
phaseOpening: g.phaseOpening, truthtrance: g.truthtrance,
pendingShipment: g.pendingShipment, pendingHomeworldShipment: g.pendingHomeworldShipment,
pendingGuildTransport: g.pendingGuildTransport,
+ ...(cunning ? { nexusGuildCunning: cunning } : {}),
+ ...(richese ? { nexusRichese: richese } : {}),
});
}
function guildBetrayalCleanParent(g: Game): void {
@@ -10269,8 +10268,9 @@ function guildBetrayalAuthority(g: Game, continuation: GuildBetrayalContinuation
'The saved reserve shipment contains foreign source fields.');
requireRule(!shipment.source && !shipment.noField && !shipment.alliedNoField &&
!shipment.smuggler && !shipment.smugglerCompanion && !shipment.richesePair &&
- !shipment.guildSecretEvent && !shipment.guildNexusEvent && !shipment.nexusEvent,
- 'This Guild Betrayal profile supports native physical reserve shipments only.');
+ !shipment.guildSecretEvent &&
+ ((!shipment.guildNexusEvent && !shipment.nexusEvent) || classicNexusCoreProfile(g)),
+ 'This Guild Betrayal profile requires a supported original physical shipment source.');
validatePhysicalShipment(g, shipment);
validateShipmentArrival(g, shipment);
checkShipmentIncomeFunding(g, getPlayer(g, shipment.player), shipment.cost, shipment.allyPayment);
@@ -10896,14 +10896,17 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
g.spiceBankerIncomePreview = true;
g.spiceBankerIncome = bankerIncomeRule(() => createBankerIncomeState(bankerIncomeContext(g)));
}
- if (classicIndependentNexusProfile(g)) {
+ if (classicNexusCoreProfile(g)) {
requireRule(g.nexusIxianReplacementPreview === undefined &&
g.nexusIxianReplacementHistory === undefined && g.nexusIxianReplacementCursor === undefined &&
!g.pendingNexusIxianReplacement && !g.pendingNexusIxianSpecialPurchase &&
g.nexusHarkonnenBetrayalPreview === undefined && g.nexusHarkonnenBetrayalHistory === undefined &&
g.nexusHarkonnenBetrayalCursor === undefined && !g.pendingNexusHarkonnenBetrayal &&
- !g.pendingNexusHarkonnenReplacement && g.nexusHarkonnenReplacementHistory === undefined,
- 'Native Nexus reactions initialize only with fresh independent-card setup.');
+ !g.pendingNexusHarkonnenReplacement && g.nexusHarkonnenReplacementHistory === undefined &&
+ g.guildBetrayalPreview === undefined && g.guildBetrayal === undefined &&
+ g.nexusGuildBetrayalHistory === undefined && g.guildBetrayalSourceReceipts === undefined &&
+ !g.pendingGuildBetrayal,
+ 'Native Nexus reactions initialize only with fresh classic setup.');
g.nexusIxianReplacementPreview = true;
g.pendingNexusIxianReplacement = null;
g.nexusIxianReplacementHistory = [];
@@ -10916,6 +10919,7 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
g.pendingNexusHarkonnenReplacement = null;
g.nexusHarkonnenReplacementHistory = [];
}
+ if (byFaction(g, 'guild')) initializeGuildBetrayalState(g);
}
initializeSetup(g, strongholdFactions || !!g.strongholdCards);
if (nativeTech && byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
@@ -28106,7 +28110,7 @@ function projectedNexusGuildCunning(g: Game, owner: string) {
hajrAvailable:record.stage !== 'pending' && !g.hajr.includes(owner)}};
if (g.nexusCards?.cards?.hands[owner] !== 'guild') return null;
let blocked: string | null = null;
- if (g.guildBetrayalPreview) blocked = 'This payment audit profile supports the original native shipment, not a Cunning second-shipment overlay.';
+ if (g.guildBetrayalPreview && !classicNexusCoreProfile(g)) blocked = 'This payment audit profile supports the original native shipment, not a Cunning second-shipment overlay.';
else if (p.ally) blocked = 'Guild Cunning requires an unallied native Guild player.';
else if (g.status !== 'playing' || g.phase !== 5 || g.active !== owner)
blocked = 'Use Cunning when finishing your own Shipment and Movement turn.';
@@ -28215,19 +28219,26 @@ function nexusGuildCunningIntegrity(g: Game) {
nexusRule(() => nexusGuildCunningMoves(active.receipt,g.hajr.includes(owner.id),active.movesAfter));
for (const response of responses) validateGuildCunningResponse(g,response!);
if (active.shipment?.stage === 'pending') {
+ const suspended = g.pendingGuildBetrayal?.continuation;
if (active.shipment.kind === 'reserve') {
- requireRule(g.pendingShipment?.guildNexusEvent === active.receipt.event, 'Guild Cunning lost its pending reserve shipment.');
- validateGuildCunningShipment(g,owner.id,active.receipt.event,'reserve',g.pendingShipment);
+ const pending = g.pendingShipment ?? (suspended?.source === 'reserve' ? suspended.shipment : null);
+ requireRule(pending?.guildNexusEvent === active.receipt.event, 'Guild Cunning lost its pending reserve shipment.');
+ validateGuildCunningShipment(g,owner.id,active.receipt.event,'reserve',pending);
const decisions = homeworldSavedDecisions(g).filter(d => d.kind === 'guildShipment');
const rates = savedNoFieldResponses(g).filter(response => response.kind === 'guildRate');
- requireRule(decisions.length > 0 || rates.length === 1,
- 'Guild Cunning lost its shipment interception or rate response.');
+ requireRule(suspended?.source === 'reserve'
+ ? decisions.length === 0 && rates.length === 0
+ : decisions.length > 0 || rates.length === 1,
+ 'Guild Cunning lost its shipment interception, rate response or original payment acknowledgement.');
for (const d of decisions) validateGuildShipmentDecision(g,d);
for (const rate of rates) validateGuildRateResponse(g,rate);
} else if (active.shipment.kind === 'homeworld') {
requireRule(g.pendingHomeworldShipment?.guildNexusEvent === active.receipt.event,'Guild Cunning lost its pending Homeworld shipment.');
validateGuildCunningShipment(g,owner.id,active.receipt.event,'homeworld',g.pendingHomeworldShipment);
homeworldShipmentIntegrity(g);
+ } else if (active.shipment.kind === 'guild') {
+ requireRule(suspended?.source === 'guildTransport', 'Guild Cunning lost its original transport payment acknowledgement.');
+ validateGuildCunningShipment(g,owner.id,active.receipt.event,'guild',suspended.frame);
} else throw new RuleError('Guild Cunning retained an unfinished immediate cross-shipment.');
} else requireRule(!g.pendingShipment?.guildNexusEvent && !g.pendingHomeworldShipment?.guildNexusEvent,
'Guild Cunning has a shipment without its original pending record.');
@@ -28300,7 +28311,7 @@ function nexusRicheseOffer(g: Game, owner: string) {
const p = g.players.find(p => p.id === owner);
if (!p || g.nexusCards?.cards?.hands[owner] !== 'richese' || byFaction(g,'richese')) return null;
let blocked: string | null = null;
- if (g.guildBetrayalPreview) blocked = 'This payment audit profile supports the original native shipment, not a Richese Secret Ally shipment overlay.';
+ if (g.guildBetrayalPreview && !classicNexusCoreProfile(g)) blocked = 'This payment audit profile supports the original native shipment, not a Richese Secret Ally shipment overlay.';
else if (p.ally) blocked = 'Richese Secret Ally requires an unallied holder.';
else if (g.status !== 'playing' || g.phase !== 5 || g.active !== owner || p.shipped)
blocked = 'Use Secret Ally for your unused reserve shipment before moving.';
@@ -28497,7 +28508,9 @@ function finishNexusRicheseShipment(g: Game, shipment: PendingShipment, stage: '
g.nexusRicheseLast = {event:record.receipt.event,stage};
}
function nexusRicheseIntegrity(g: Game) {
- const history = g.nexusRicheseHistory, pending = g.pendingShipment;
+ const suspended = g.pendingGuildBetrayal?.continuation;
+ const history = g.nexusRicheseHistory, pending = g.pendingShipment ??
+ (suspended?.source === 'reserve' ? suspended.shipment : null);
if (history === undefined) {
requireRule(!g.nexusRicheseLast && !pending?.nexusEvent, 'Richese Secret Ally has lost its saved shipment history.');
return;
@@ -28536,8 +28549,8 @@ function nexusRicheseIntegrity(g: Game) {
currentNexusRicheseShipment(g,pending);
const decisions = homeworldSavedDecisions(g).filter(d => d.kind === 'guildShipment');
const rates = savedNoFieldResponses(g).filter(response => response.kind === 'guildRate');
- requireRule(decisions.length + rates.length === 1,
- 'Richese Secret Ally has lost its unique Guild interception or native-rate response.');
+ requireRule(decisions.length + rates.length + (suspended?.source === 'reserve' ? 1 : 0) === 1,
+ 'Richese Secret Ally has lost its unique Guild interception, native rate or original payment acknowledgement.');
for (const decision of decisions) validateGuildShipmentDecision(g,decision);
for (const response of rates) validateGuildRateResponse(g,response);
}
diff --git a/game/faction-module-profile.ts b/game/faction-module-profile.ts
index a60aa4a9..d89fa2f2 100644
--- a/game/faction-module-profile.ts
+++ b/game/faction-module-profile.ts
@@ -26,20 +26,20 @@ export type FactionModuleProfile = {
nexusHarkonnenBetrayalPreview?: unknown;
};
-/** Independent36 + original Nexus12 in classic tables, optionally all14 Skills.
- * Fresh native Harkonnen/Ixian effect stamps are not additional selected modules. */
-export function classicIndependentNexusProfile(
- game: Partial & Pick & {
- sandtrout?: unknown; moritaniAssassinatePreview?: unknown;
- },
-): boolean {
+type ClassicNexusProfile = Partial & Pick & {
+ sandtrout?: unknown; moritaniAssassinatePreview?: unknown;
+};
+
+/** Classic Nexus12 with selected base33/independent36 and optional all14 Skills.
+ * Native Guild/Harkonnen/Ixian effect stamps are not additional selected modules. */
+export function classicNexusCoreProfile(game: ClassicNexusProfile): boolean {
const players = game.players;
- if (typeof game.advanced !== 'boolean' || game.ecazTreachery !== true || !game.nexusCards ||
+ if (typeof game.advanced !== 'boolean' || !game.nexusCards ||
game.expansions.length !== 0 || !players || players.length < 2 || players.length > 6 ||
game.homeworlds || game.techTokens || game.strongholdCards || game.sandtrout ||
game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
- game.guildBetrayalPreview || game.richeseBetrayalPreview ||
+ game.richeseBetrayalPreview ||
game.nexusIxianBetrayalPreview) return false;
for (let i = 0; i < players.length; i++) {
for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false;
@@ -51,6 +51,11 @@ export function classicIndependentNexusProfile(
return true;
}
+/** Card consumers require the actual independent-variant selection. */
+export function classicIndependentNexusProfile(game: ClassicNexusProfile): boolean {
+ return game.ecazTreachery === true && classicNexusCoreProfile(game);
+}
+
/** Original E1/E2 families or standalone Ecaz/Moritani; explicit Semuta context only. */
export function nativeFactionTechProfile(game: FactionModuleProfile, semuta = false): boolean {
if (typeof game.advanced !== 'boolean' || !game.techTokens ||
diff --git a/game/leader-skill-profile.ts b/game/leader-skill-profile.ts
index 0e1279b0..9570f44a 100644
--- a/game/leader-skill-profile.ts
+++ b/game/leader-skill-profile.ts
@@ -1,5 +1,5 @@
import { FACTIONS } from './catalog';
-import { classicIndependentNexusProfile, mixedE3NexusRosterProfile } from './faction-module-profile';
+import { classicIndependentNexusProfile, classicNexusCoreProfile, mixedE3NexusRosterProfile } from './faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile } from './discovery-module-profile';
/** Public configuration only; never gate a skill on a hidden card or random deal. */
@@ -53,7 +53,7 @@ function nexusLeaderSkillModulesSupported(game: LeaderSkillProfile, assassinatio
/** Fresh classic Nexus/Skills, optionally Homeworlds/Discovery/Tech/Advanced Strongholds. */
export function classicNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
- if (classicIndependentNexusProfile(game)) return true;
+ if (classicNexusCoreProfile(game)) return true;
if (game.expansions.length || !nexusLeaderSkillModulesSupported(game, false, true, false)) return false;
const players = game.players!;
for (let i = 0; i < players.length; i++) {
@@ -68,6 +68,8 @@ export function classicNexusLeaderSkillsProfile(game: LeaderSkillProfile): boole
/** Fresh classic Nexus/Skills composed only with the bounded Guild Betrayal preview. */
export function guildBetrayalNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
+ if (game.guildBetrayalPreview && classicNexusCoreProfile(game))
+ return game.players!.some(player => player.faction === 'guild');
if (game.expansions.length || !game.guildBetrayalPreview || game.homeworlds ||
game.techTokens || game.strongholdCards || game.semutaPreview ||
!nexusLeaderSkillModulesSupported(game, false, false, true)) return false;
diff --git a/game/nexus-emperor-purchase.ts b/game/nexus-emperor-purchase.ts
index bcd96a3c..5ed8c704 100644
--- a/game/nexus-emperor-purchase.ts
+++ b/game/nexus-emperor-purchase.ts
@@ -18,7 +18,7 @@ import type { RicheseAuction } from './richese-auction';
import { quoteRicheseSettlement, type RicheseSettlementQuote } from './richese-settlement';
import { quoteRicheseBetrayalInvoice } from './nexus-richese-betrayal';
import { RICHESE_CARD_DEFINITIONS, richeseCardDefinition } from './richese-cards';
-import { classicIndependentNexusProfile } from './faction-module-profile';
+import { classicNexusCoreProfile } from './faction-module-profile';
export type EmperorNexusBankPurchase = {
kind: 'purchase';
@@ -381,7 +381,7 @@ function nativeBankNexusProfile(g: Game): boolean {
* with the Emperor absent, the concealed face and any buyer wallet or hand left
* to the caller. */
export function emperorNexusBankPurchaseModeSupported(g: Game): boolean {
- if (classicIndependentNexusProfile(g))
+ if (classicNexusCoreProfile(g))
return !g.players.some(p => p.faction === 'emperor') && !g.sandtrout;
if (!g.nexusCards || g.players.some(p => p.faction === 'emperor') ||
g.ecazTreachery || g.sandtrout) return false;
diff --git a/game/nexus-emperor-secret-ally.ts b/game/nexus-emperor-secret-ally.ts
index a26edcbd..62c84036 100644
--- a/game/nexus-emperor-secret-ally.ts
+++ b/game/nexus-emperor-secret-ally.ts
@@ -1,7 +1,7 @@
import type { Game, Player } from './engine';
import { eliteRevivalRemaining, revivalPrevented } from './revival';
import { FACTIONS } from './catalog';
-import { classicIndependentNexusProfile } from './faction-module-profile';
+import { classicNexusCoreProfile } from './faction-module-profile';
import {
classicNexusModulesProfile,
pairedNexusModulesProfile,
@@ -206,7 +206,7 @@ export function validateEmperorNexusRevival(
/** Reuse original fresh roster/module predicates, independently of purchase admission. */
function revivalModeSupported(g: Game): boolean {
- if (classicIndependentNexusProfile(g)) return true;
+ if (classicNexusCoreProfile(g)) return true;
if (g.semutaPreview || g.advancedPreview || g.kullPreview || g.nexusKullPreview ||
g.guildBetrayalPreview || g.richeseBetrayalPreview || g.nexusIxianReplacementPreview ||
g.nexusIxianBetrayalPreview || g.nexusHarkonnenBetrayalPreview || g.ecazTreachery)
diff --git a/game/nexus-fremen-betrayal.ts b/game/nexus-fremen-betrayal.ts
index 261cbbc1..ac893f72 100644
--- a/game/nexus-fremen-betrayal.ts
+++ b/game/nexus-fremen-betrayal.ts
@@ -1,6 +1,6 @@
import type { Game } from './engine';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
-import { classicIndependentNexusProfile } from './faction-module-profile';
+import { classicNexusCoreProfile } from './faction-module-profile';
export type FremenNexusBetrayal = {
event: string;
@@ -40,7 +40,7 @@ export function fremenBetrayalOffer(
if (holder.ally) blocked = 'An allied player cannot use a Nexus card.';
else if (g.status !== 'playing' || (g.phase !== 1 && g.phase !== 5))
blocked = 'Use Fremen Betrayal before the first Spice Blow or before Fremen movement.';
- else if (!classicIndependentNexusProfile(g) &&
+ else if (!classicNexusCoreProfile(g) &&
(g.expansions.length || g.homeworlds || g.leaderSkills || g.discoveryEnabled ||
g.discoveries || g.discoveryStash || g.greatMaker || g.ecazTreachery ||
g.techTokens || g.strongholdCards ||
diff --git a/game/nexus-module-profile.ts b/game/nexus-module-profile.ts
index d68d1353..2cdab538 100644
--- a/game/nexus-module-profile.ts
+++ b/game/nexus-module-profile.ts
@@ -1,5 +1,5 @@
import { FACTIONS } from './catalog';
-import { classicIndependentNexusProfile, mixedE3NexusRosterProfile, type FactionModuleProfile } from './faction-module-profile';
+import { classicNexusCoreProfile, mixedE3NexusRosterProfile, type FactionModuleProfile } from './faction-module-profile';
import { classicDiscoveryNexusProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
/** Explicit Richese composition only; original predicates still own roster and module legality. */
@@ -11,7 +11,7 @@ function semutaNexusContext(game: FactionModuleProfile): boolean {
/** Original classic Nexus modules, optionally Homeworlds; initializer checks undealt components. */
export function classicNexusModulesProfile(game: FactionModuleProfile): boolean {
- if (classicIndependentNexusProfile(game)) return true;
+ if (classicNexusCoreProfile(game)) return true;
if (game.discoveryEnabled || game.discoveries) return classicDiscoveryNexusProfile(game);
if (typeof game.advanced !== 'boolean' || !game.nexusCards ||
(!game.techTokens && !game.strongholdCards) || game.expansions.length !== 0 ||
diff --git a/game/reference.ts b/game/reference.ts
index 66368d91..a5c7e531 100644
--- a/game/reference.ts
+++ b/game/reference.ts
@@ -4538,11 +4538,11 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Verified',
- summary: 'In the explicit Guild Betrayal preview, 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.',
+ 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: [
- 'This is the fresh local guild-betrayal development profile, not a public module start or an upgrade to saved games. It admits two to six ready classic-faction seats in Basic or Advanced, with native Guild and at least one other faction, physical base decks and Nexus. Homeworlds are optional only when genuinely enabled and seeded at setup. Other expansion factions/decks, Leader Skills, Discoveries, Tech Tokens, Stronghold Cards, Semuta, Kull and Richese previews remain outside this boundary.',
+ 'Fresh classic2–6 Basic/Advanced Nexus core uses original base33 or selected independent36 Treachery, optionally all14 Skills. Actual native Guild receives its original payment-replacement state at fresh setup alongside supported Harkonnen/Ixian reactions; played games are not reflagged. The original explicit native Guild/Homeworld entry retains its own seeded boundary. Other selected families, World/phase/Discovery/Semuta and reaction envelopes are not expanded by this core entry.',
'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.',
- 'Only original native shipment producers are admitted in this profile. Guild Nexus Cunning’s second shipment and Richese Nexus Secret Ally’s discounted shipment are blocked before spending a card or creating a native parent; those effects outside this profile remain unchanged. Guild Secret Ally is already unavailable because native Guild is seated, not because Betrayal adds a new prohibition to that panel.',
+ '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.',
'Use takes the full original charged payment, not merely eligible Guild income or its reduced half. The original payer and authorized ally contribution pay once, but Guild, bank and any occupied-Junction receiver get none of that invoice. This is replacement of one fee, not an extra award or second charge.',
'You may take your own payment, but you must afford it before receiving it back. A wholly self-funded payment is paid and returned for net zero spice; future receipt cannot finance the declaration. An ally’s pledge has already debited that donor into escrow: the original escrow is consumed once, not charged to the donor again. Taking the fee does not change the payer’s own remainder or donor split. Your live voluntary-spend promises and reservations still apply.',
@@ -4553,7 +4553,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'The response adds no price, source/family/world, destination, force count, donor, balance, recipient or saved-receipt disclosure. Existing own-source and public inspector entitlements do not expand. Neutral acknowledgement logs no secret fee or source; the original committed shipment retains its normal result logging. Inspecting a printed Nexus reference is not evidence of another player’s held card.',
'Pass spends nothing and keeps Nexus. A recorded pass cannot be undone for this event. All required passes settle the original shipment once with its original receiver; with no publicly possible responder it settles automatically. Accepted Use spends the physical Nexus and redirects the same fee. Both actions submit only the offered event: the server binds payment and recipient. Finish this gate before shipping, moving, pledging, playing cards or opening another decision.',
'Refresh resumes the same typed producer, original invoice, passes and pending/completed ownership without a second donor debit, fee, card spend or force move. Stale or foreign events, malformed/orphaned saves and competing Use/final-Pass requests cannot select a new shipment or replay a raw action. Bounded direct engine, authenticated SQLite and actual Basic/Advanced CLI/phone source paths are verified locally; this does not certify complete Guild or Nexus rules.',
- 'Easy, Medium, Hard and Brutal share a minimal legal projected Use/Pass path; this is not Guild strategy or calibrated difficulty. Full Guild/Nexus/Homeworld families, wider compositions, full games, public starts and live deployment remain separate closed gates.',
+ 'Easy, Medium, Hard and Brutal share canonical legal private choices. Current classic-core policies complete real Mentat replacement, purchased-card choice, discounted fee claim, third-party Bureaucrat and native Cunning return/turn closure. These are bounded playability paths, not strategy calibration. Complete families, wider combinations, comprehensive assurance and deployed acceptance remain open.',
],
example: 'Your original shipment costs four spice and you already have four available: Use pays that fee once and returns all four to you, spending Guild Nexus but granting no extra shipment. If another shipper pays four, you receive four instead of Guild, bank or an occupied Junction receiver. Passing leaves the original routing unchanged.',
related: ['nexus-cards', 'movement', 'alliance-funding', 'homeworlds', 'privacy', 'faction-guild', 'implementation-checklist'],
@@ -4562,7 +4562,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'The bounded contract separates uniform acknowledgement from private Use and own blocked reason, with event-only actions, unchanged inspector entitlements and no new fee/source disclosure. Broader presentation and acceptance remain open.', evidence: ['components/nexus-guild-betrayal.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four profiles use only canonical projected legal Use/Pass choices while the payment gate suspends underlying actions. Full strategy and calibration remain unfinished.', evidence: ['game/bot-nexus-guild-betrayal.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Printed full-fee and own-payment clauses, escrow funding, explicit occupied-Junction override, four native adapters and privacy protocol are recorded. Ordinary whole-invoice rounding follows the separate user ruling; unresolved occupation/priority boundaries and full-family acceptance remain open.', evidence: ['docs/NEXUS_GUILD_RULES.md', 'docs/NEXUS_CARD_RULES.md', 'docs/HOMEWORLD_PAYMENT_INCOME_RULES.md', 'docs/RULE_DECISIONS.md'] },
- { area: 'Verification', status: 'Partial', detail: '147 selected offline cases and 6,114 full offline checks cover four native adapters, funded own refunds, original escrow, full low-Junction override, face-neutral acknowledgement, four legal bot profiles, actual handlers, authenticated restart/races and independent native-grant/orphan rejection. Eight real backed-up CLI entries and 390-pixel controls prove own/other/escrow/Guild-return/Homeworld/Junction Use, all-pass, free Fremen and refresh with exact saved force/card/spice custody. Local HTTP 48/55 has five 503s and two timeouts; broader and deployed acceptance remain open.', evidence: ['tests/nexus-guild-betrayal.test.ts', 'tests/nexus-guild-betrayal-engine.test.ts', 'tests/nexus-guild-betrayal-controls.test.tsx', 'tests/bot-nexus-guild-betrayal.test.ts', 'tests/nexus-guild-betrayal-recovery.test.ts', 'docs/NEXUS_GUILD_RULES.md'] },
+ {area:'Verification',status:'Partial',detail:"Classic core base33 or selected36 with optional14 now initializes the original actual-Guild fee source. Ten combined consumer cases, three390px journeys and twenty-eight legal policy choices preserve discounted5/price1, full-fee claim, actual third-party Bureaucrat2, all three Cunning routes and native cancellation. Earlier native/HTTP/saved results remain dated in linked contracts; broader and deployed assurance stay open.",evidence:['tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/nexus-guild-betrayal.test.ts', 'tests/nexus-guild-betrayal-engine.test.ts', 'tests/nexus-guild-betrayal-controls.test.tsx', 'tests/bot-nexus-guild-betrayal.test.ts', 'tests/nexus-guild-betrayal-recovery.test.ts', 'docs/NEXUS_GUILD_RULES.md']},
],
},
{
@@ -4574,7 +4574,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'An unallied holder with Ixians absent may spend Ixian Nexus to replace the exact card just bought in a normal auction or supported native Richese sale, preserving original settlement and drawing the actual deck top privately.',
searchText: 'Ixian Nexus Secret Ally purchased card replacement normal auction cache Black Market Karama full hand empty deck same card',
steps: [
- 'Original standalone entries admit classic/base33, Tleilaxu plus classics/Ix47, or Richese plus classics/CHOAM35 and cache, with Ixians absent and Nexus alone. Fresh normal classic2–6 Basic/Advanced independent36/Nexus12 now also initializes this same effect, optionally all14 Skills. Native selected families, Worlds, phase/Discovery/Semuta and other reaction envelopes retain their separate bounds; no played game gains a new stamp.',
+ 'Original standalone entries admit classic/base33, Tleilaxu plus classics/Ix47, or Richese plus classics/CHOAM35 and cache, with Ixians absent. Fresh classic2–6 Basic/Advanced Nexus core initializes this same effect with base33 or selected independent36, optionally all14 Skills; independent cards are not a prerequisite. Other native/module envelopes retain their own bounds, and no played game gains a new stamp.',
'Nexus has no starting deal. A draw requires a qualifying closing Nexus with an alliance. A two-seat profile can continue its base game but cannot naturally create an unallied Secret Ally holder; actual replacement needs at least three seats, or four for two simultaneous unallied recipients.',
'The photographed publisher face grants an independent Secret Ally operation when Ixians are absent. Remain unallied and hold the actual singleton Ixian Nexus. Native Ixian allied replacement is a different cancelable advantage and is not removed or used as a fallback. The adopted unofficial Advanced rulebook supplies relevant core rules, not this absent Nexus face or a new Nexus ruling.',
'Normal paid/Karama purchases, genuine native Richese sold lots and actual Advanced special-Karama acquisition during Bidding supply their own purchase authority. A direct purchase has already spent its Karama, three spice and once-use; its target/price3 are not an unrelated row or sale. Use/Keep resumes original income, active player and suspended controls. Only unpaid bids remain reserved; already-delivered sale commitments are not held twice. Other phases keep native acquisition without a Bidding-only Nexus choice. Unbid keeps, gifts, bonus draws, Technology swaps and old hand cards supply no purchase.',
@@ -4595,7 +4595,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'Actual390px native special purchase controls are enabled at three remaining spice during older paid income. Direct Use preserves the older card and both credits; reserved-bid Keep reaches a distinct genuine normal offer, then one row advance. Guidance states original cost/once-use committed and no new auction. Provisional cache/Harkonnen scope remains visible. No overflow/browser/application error; broader acceptance remains open.', evidence: ['components/nexus-ixian-replacement.tsx', 'components/richese-special-karama.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Four minimal legal policies use their own projected offer while original settlement waits. Sixteen original-state actions cover quiet Emperor/bank, reserved bid and nested paid income direct purchases; previous source-qualified paths remain in the contract. No strategy or difficulty calibration is claimed.', evidence: ['game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Actual auction/direct authority, native price3 and cost commitment, once-use, active/control restoration, paid-versus-unpaid reservations, source-v1 preservation and provisional cache/Harkonnen interpretations are documented. Outside Bidding the native special purchase adds no Nexus offer. Other modules and universal/publisher priority remain separate.', evidence: ['docs/NEXUS_IXIAN_REPLACEMENT_RULES.md', 'docs/NEXUS_CARD_RULES.md', 'docs/RULE_DECISIONS.md'] },
- {area:'Verification',status:'Partial',detail:"Classic independent36/Nexus12 optional14 now initializes the original effect at fresh setup. Actual paid2 delivers selected Reinforcements; Use discards only that card for the real deck top with no second invoice, while Harkonnen Pass keeps both cards and allows exactly one original bonus. Six combined consumer regressions, three390px journeys and four legal policies pass. Older native/direct and saved evidence remains dated; broader modules and deployed assurance stay open.",evidence:['tests/classic-independent-nexus-reactions.test.ts','tests/fixture-classic-independent-nexus-reactions.ts','tests/nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-engine.test.ts', 'tests/fixtures/nexus-ixian-native-depleted.json', 'tests/fixture-nexus-ixian-replacement.ts', 'tests/fixture-native-ixian-replacement.ts', 'tests/native-ixian-replacement.test.ts', 'tests/bg-ixian-replacement.test.ts', 'tests/fixture-bg-ixian-replacement.ts', 'tests/harkonnen-ixian-replacement.test.ts', 'tests/fixture-harkonnen-ixian-replacement.ts', 'tests/richese-ixian-replacement.test.ts', 'tests/fixture-richese-ixian-replacement.ts', 'tests/richese-ixian-special-replacement.test.ts', 'tests/fixture-richese-ixian-special-replacement.ts', 'tests/richese-ixian-direct-replacement.test.ts', 'tests/fixture-richese-ixian-direct-replacement.ts']},
+ {area:'Verification',status:'Partial',detail:"Fresh classic core base33 or selected36 optional14 initializes original Ixian replacement without requiring independent cards. Actual paid base-card and selected independent purchases retain one invoice, exact discard/draw and original native bonus. Ten combined consumer cases and four legal policies pass; prior native/direct evidence remains dated. Broader modules, full games and deployed assurance stay open.",evidence:['tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-reactions.test.ts','tests/fixture-classic-independent-nexus-reactions.ts','tests/nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-engine.test.ts', 'tests/fixtures/nexus-ixian-native-depleted.json', 'tests/fixture-nexus-ixian-replacement.ts', 'tests/fixture-native-ixian-replacement.ts', 'tests/native-ixian-replacement.test.ts', 'tests/bg-ixian-replacement.test.ts', 'tests/fixture-bg-ixian-replacement.ts', 'tests/harkonnen-ixian-replacement.test.ts', 'tests/fixture-harkonnen-ixian-replacement.ts', 'tests/richese-ixian-replacement.test.ts', 'tests/fixture-richese-ixian-replacement.ts', 'tests/richese-ixian-special-replacement.test.ts', 'tests/fixture-richese-ixian-special-replacement.ts', 'tests/richese-ixian-direct-replacement.test.ts', 'tests/fixture-richese-ixian-direct-replacement.ts']},
],
},
{
@@ -4643,7 +4643,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'Fresh supported classic starts cancel one original native Harkonnen traitor call, return that physical card immediately and give Harkonnen a private replacement during Mentat Pause.',
searchText: 'Harkonnen Nexus Betrayal revealed declared traitor ally caller beneficiary shuffle replacement Mentat neutral acknowledgement',
steps: [
- 'The original standalone preview requires native Harkonnen, classic2–6 Basic/Advanced, base33 and Nexus alone. Fresh normal classic independent36/Nexus12 now initializes the same effect, optionally all14 Skills, alongside native Ixian replacement. Original setup and public selection bind the physical census. No played game is reflagged; native selected families, Worlds, phase/Discovery/Semuta and other reaction envelopes stay separate.',
+ 'The original standalone preview requires native Harkonnen, classic2–6 Basic/Advanced, base33 and Nexus alone. Fresh classic Nexus core initializes the same effect with base33 or selected independent36, optionally all14 Skills, alongside actual native Guild and Ixian reactions. Original setup and public selection bind the census. No played game is reflagged; other native/module/reaction envelopes retain their bounds.',
'Nexus has no initial deal. An actual alliance-qualified closing Nexus must supply the unallied holder. An effect scenario needs at least three seats; two seats can continue ordinary play without inventing a card. Two simultaneous receivers need four actual seats.',
'The photographed publisher face cancels a revealed Harkonnen traitor, shuffles that exact card back immediately, and gives Harkonnen one replacement during Mentat Pause. It is not a draw-now exchange, whole-battle ban or refund of an already resolved outcome. Native Cunning and absent-Harkonnen Secret Ally remain separate effects.',
'Only a genuine legally matching native personal or allied Harkonnen call is a source. Preserve the actual caller, combatant beneficiary, target, original revealed plans and physical identity. A false or invalid call, protected leader, another faction or finished battle cannot offer this effect.',
@@ -4663,7 +4663,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'Public declared-Traitor inspector, own eligible event-only Use/Pass/Nexus inspector, sole/multiple public ownership and every-seat independent autopilot are connected. Actual phone Use, wrong-face Pass, partial refresh and raw administrator owner agree.', evidence: ['components/nexus-harkonnen-betrayal.tsx', 'components/game-table.tsx', 'game/table-turn.ts', 'db/admin-directory.ts'] },
{ area: 'AI', status: 'Partial', detail: 'All four minimal policies consume canonical own-view choices and native pending priority with legal paced continuation; full strategy and calibration remain separate.', evidence: ['game/bot-nexus-harkonnen-betrayal.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Printed immediate/delayed operations, personal/allied roles, inferred local counter/privacy convention, fresh natural card rights and unchanged outside-profile/module/public/deployment questions are explicit.', evidence: ['docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_CARD_RUNTIME.md', 'docs/RULE_DECISIONS.md'] },
- {area:'Verification',status:'Partial',detail:"Classic independent36/Nexus12 optional14 now initializes the existing effect only at fresh setup. Genuine personal Use, all-pass and allied native counter or allowance preserve actual Harass, the declared physical Traitor and one private real Mentat draw. Six combined consumer regressions, three390px native journeys and four legal policies pass. Earlier native/saved reviews remain dated; universal timing, wider modules, assurance and deployed acceptance remain open.",evidence:['tests/classic-independent-nexus-reactions.test.ts','tests/fixture-classic-independent-nexus-reactions.ts','tests/fixture-nexus-harkonnen-betrayal.ts', 'tests/nexus-harkonnen-betrayal.test.ts', 'tests/nexus-harkonnen-betrayal-runtime.test.ts', 'tests/nexus-harkonnen-betrayal-controls.test.tsx', 'tests/bot-nexus-harkonnen-betrayal.test.ts', 'tests/nexus-harkonnen-betrayal-recovery.test.ts', 'tests/prototype-room.test.ts', 'tests/admin-directory.test.ts']},
+ {area:'Verification',status:'Partial',detail:"Fresh classic core base33 or selected36 optional14 initializes the original actual-Harkonnen source without a card-variant dependency. Genuine base33 all14 Use reaches one real Mentat draw; earlier independent Harass, Pass and allied-counter cases remain. Ten combined consumer cases and four legal policies pass. Other modes, universal timing and deferred assurance remain open.",evidence:['tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-reactions.test.ts','tests/fixture-classic-independent-nexus-reactions.ts','tests/fixture-nexus-harkonnen-betrayal.ts', 'tests/nexus-harkonnen-betrayal.test.ts', 'tests/nexus-harkonnen-betrayal-runtime.test.ts', 'tests/nexus-harkonnen-betrayal-controls.test.tsx', 'tests/bot-nexus-harkonnen-betrayal.test.ts', 'tests/nexus-harkonnen-betrayal-recovery.test.ts', 'tests/prototype-room.test.ts', 'tests/admin-directory.test.ts']},
],
},
{
@@ -4675,7 +4675,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.',
- 'Classic two-through-six-seat Basic/Advanced tables may select the independent Recruits, Reinforcements and Harass & Withdraw variant with Nexus, optionally all fourteen Skills. Keep original36 Treachery,12 Nexus and14 Skill cards distinct. Starting hands/skills use their original order; the real closing draw still requires an actual Nexus and alliance. This entry adds no native selected families, Worlds, phase modules, Discoveries, Semuta or extra reaction-preview protocol.',
+ 'Fresh classic two-through-six-seat Basic/Advanced Nexus core admits base33 or selected independent36 Treachery, optionally all14 Skills. Keep the actual Treachery, twelve Nexus and fourteen Skill stocks distinct; no independent-card dependency or silent addition is invented. Original fresh Guild/Harkonnen/Ixian reaction states use their actual role and card sources. Starting hands/training and real alliance-qualified closing draws remain. Other native families, World/phase/Discovery/Semuta and reaction envelopes stay 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.',
@@ -4735,13 +4735,13 @@ export const RULE_TOPICS: RuleTopic[] = [
'When Richese is absent, its Secret Ally replaces your ordinary reserve shipment with up to five physical forces charged as one. Forces, elite identity, destination restrictions, native Homeworld sources and movement remain unchanged. Fremen retain free reinforcement. Native Guild rate cancellation changes the one-counter sand bill from1 to2 without canceling the Nexus source; an already canceled rate is reflected in the quote. Unfunded declared Nexus stays spent without refund/reusable coupon while ordinary shipment remains available under the selected affordability policy. An independently purchased ordinary price remains separate. This is no concealed marker or extra shipment.',
'Native Richese Cunning uses the original signed two-No-Field shipment in supported single/paired E2 or selected mixed Ix+CHOAM native Nexus tables with classics, optional Skills/Homeworlds/Discovery/Tech/Advanced Strongholds. Select two distinct unused physical tokens: one reveals immediately into reserve-capped forces, one remains concealed; pay one marker invoice and spend the Nexus. The previous shipment token cannot be either choice, and the concealed one becomes last-used. Native/Guild response and actual payer/counter sources remain; a nested destination must be revealed. Prevention keeps physical tokens and unpaid spice, after any printed card cost. Sandtrout, arbitrary companions, shared allocation and other pending combinations retain their guards.',
'Richese Betrayal has a separate opt-in auction preview. Before payment, an unallied rival may spend its physical Richese Nexus card to discard a public self-cache win without any purchase debit, or redirect another buyer’s cache/Black Market payment to the bank while preserving delivery and earned bonus. Every publicly possible holder acknowledges the same opportunity; private Use eligibility cannot determine public timing. Normal hidden self-purchases and private special-Karama acquisitions are not connected. See the dedicated guide for its existing local runtime/browser evidence and remaining release gates; that evidence does not verify Nexus Kull.',
- 'Guild Cunning is declared when finishing your ordinary Shipment and Movement turn. It spends the Nexus and opens one Karama response. Allowance grants a second native shipment at normal Guild prices, including supported cross-shipment, return and Homeworld routes. Cancellation leaves the completed first turn intact. Only an unused Hajr move can follow the second shipment; no ordinary movement or third move is granted. The physical Ornithopter card combination remains unavailable. The separate guild-betrayal profile admits only original native shipment producers and explicitly blocks this second-shipment offer before Nexus spending; Cunning outside that profile is unchanged.',
+ 'Guild Cunning is declared when finishing the ordinary Shipment and Movement turn. It spends the Nexus and opens its native Karama response. Allowance grants one second native shipment at normal Guild prices, including supported cross, return and Homeworld routes; cancellation preserves the first shipment and movement. Only an unused Hajr move can follow, never another ordinary movement or third move. Classic core retains the actual grant and funded reserve/cross/return through Guild fee acknowledgement; other envelopes keep their own source limits. The physical Ornithopter card combination remains separate.',
'Cunning native cross/return permission and rate use separate unpaid controls while original shipped history stays unchanged. A denial within its second opportunity allows a legal reserve replacement; denial of an earlier ordinary opportunity does not remove that new signed grant. Typed sources, original prices and approval stay bound; neither first payment nor ordinary movement reopens. Independent purchased price, immune Junction and other card sources remain distinct.',
'With Guild absent, its Secret Ally is an explicit optional source for one ordinary shipment at Guild prices. It supports physical reserve shipment, cross-shipment and return to the holder’s own reserves, including the native-Homeworld return when Homeworlds are enabled, plus half-price independently legal world-to-world shipment. Fremen may retain free reinforcement or choose the paid card route beyond its normal range, outside the storm. Native faction, typed sources and normal movement stay unchanged. Arbitrary foreign-world transport and concealed No-Fields are not granted.',
'Borrowed Guild/Richese tariffs do not change native faction or physical reserve origin. Non-Guild/non-Fremen off-planet arrivals can accrue Heighliners once for collection at the original phase end. A paid borrowed Guild route from Fremen southern reserves is still on-planet and does not trigger it; stopped and native-Guild-only shipments likewise remain excluded. The existing Emperor bank-auction purchase retains the actual buyer wallet and creates no industry activity.',
- 'Guild Betrayal has a separate fresh classic/native-Guild development preview. Before a fully funded positive shipment settles, an unallied holder may spend the physical Guild Nexus to take the full original fee, including their own pre-funded payment, instead of Guild, bank or occupied Junction income. Original donor escrow is consumed once and original physical delivery is unchanged. Neutral public acknowledgements reveal no fee/source; only private eligibility exposes Use. Zero/stopped shipments create no award or Nexus cost. See the dedicated guide; new verification remains Planned and ordinary Homeworld occupation/rounding questions stay local.',
+ 'Guild Betrayal in fresh classic core or supported original native-Guild entries replaces one fully funded positive shipment fee, including an already-affordable own payment. Original escrow and physical delivery occur once; neutral acknowledgement reveals no fee/source, while private actual card authority controls Use. Core preserves Richese five-for-one and Guild Cunning source records through this same gate. The single Guild card spent for Cunning cannot also be held for rival Use. Zero/stopped shipments create no fee or Nexus award; wider World and income boundaries retain their own contracts.',
'Cunning spends its Nexus card and uses the native Mentat placement opportunity. A Karama response precedes placement; cancellation leaves the token where it was and the Nexus spent. Later Terror entry still uses the existing one-token stack selection and supported effect rules. If Extortion was revealed this turn, its bank award and payment choices follow that placement response; this does not implement Atomics or complete Moritani games.',
- 'Hidden reactive Betrayal timing remains unresolved for other effects. Complete module games are unfinished, so public module starts remain disabled.',
+ 'Hidden reactive timing remains unresolved outside supported original protocols. Fresh normal previews do not certify complete Nexus games, every combination, comprehensive assurance or deployed acceptance.',
],
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal', 'nexus-guild-betrayal', 'nexus-ixian-replacement', 'nexus-ixian-betrayal'],
checklist: [
@@ -4749,7 +4749,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 independent36/Nexus12 with optional14 uses original normal setup, real closing draws, native Recruits plus fixed Tleilaxu grant, and Cunning with separate reserve fees and undialed returns. Selected bank-card purchase and native cancellation remain. Three actual390px surfaces and four legal all14 policies pass; older native/module and saved evidence stays dated. Full Nexus, added overlays, comprehensive assurance and deployed acceptance remain open.", evidence: ['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:"One public classic-core capability composes base33 or selected independent36 with Nexus12, optionally all14, and initializes native reaction states only at fresh setup. Genuine card draws, original shipment grants, native counters, exact funded fee and actual delayed replacement remain. Ten combined consumer regressions, three390px journeys and twenty-eight legal policy choices pass. Other native/module envelopes, universal timing, comprehensive assurance and deployed acceptance remain open.",evidence:['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"]},
],
},
{
@@ -4935,8 +4935,8 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'Fourteen physical skills, private assignment choices, five battle disciplines, Planetologist movement/battle effects and Suk Graduate rescues, Mentat/Bureaucrat scoring and Bureaucrat payment redirection, Sandmaster victory spice, Rihani exchanges, optional Smuggler shipping and provisional modifier-inclusive battle collection, Sandmaster route and worm-ride collection, sealed Spice Banker battle spending and optional Diplomat base-defense copying in development tables. Other effects and combined-module acceptance remain unfinished.',
steps: [
'Fresh all14 uses one selected-deck capability for every registered actual seat, Basic/Advanced, including mixed native Ecaz/Moritani with selected E1/E2 families. Neither an expansion faction, independent cards, Nexus nor one exact E3 family is required just to use selected ordinary decks. An unused deck creates no seated faction. Original World/phase envelopes retain components and native power bindings; original Nexus/Discovery/Semuta and Advanced role limits stay separate. Ix drafts before training; Auditor/shared Duke never train. No Advanced power, income, owner or played-save history is invented.',
- 'The separate classic independent36/Nexus12 entry also permits optional all14 in Basic/Advanced, two through six base-faction seats, without other overlays. Actual offers, starting-card order, mandatory normal Banker and original Nexus ability sources remain. New ordinary-card recognition is selected by configuration, never by a hidden hand or Nexus face. Native expansion, World/phase/Discovery/Semuta and extra reaction-preview card contexts retain their separate bounds.',
- 'Explicit guild-betrayal --leader-skills retains the original classic native-Guild Nexus profile without Homeworlds, Tech, Strongholds, Discovery, Semuta or other overlays. All14 training remains native. Ordinary skill quotes now admit the composition, restoring actual Mentat private questions before posture and Sandmaster ground collection rather than only enabling Bureaucrat. Native card custody, paid forces, original Betrayal Use/Pass and movement/plan continuation stay unchanged. Other combinations and complete-mode acceptance remain separate.',
+ 'Classic Nexus core with original base33 or selected independent36 permits optional all14 in Basic/Advanced, two through six base seats, without other overlays. Actual offers, starting-card order, mandatory normal Banker and native reaction sources remain. Original Guild fee replacement also retains actual third-party Bureaucrat receipts, not a fictitious Guild recipient. Selection and printed roles are public capabilities, never inferred from hidden hands. Other native and module envelopes stay separate.',
+ 'Explicit guild-betrayal --leader-skills remains a bounded classic preset of the original all14 setup. It does not require independent cards or another overlay. Native Mentat private questions, skilled-disc posture, Sandmaster collection and normal income retain their shared quotes; exact payer/recipient and printed Bureaucrat third-party conditions remain. Native cards, paid forces, Use/Pass and movement/plan continuation stay physical. Wider combinations and complete-mode acceptance remain separate.',
'Basic Moritani keeps legal Terror but gains no Advanced assassination, including registered mixed native Skills seats. Ecaz/Moritani with selected E1/E2 or unused decks keeps original Worlds/Tech3+/Advanced Strongholds2+ and optional independent cards without requiring Nexus. Advanced E3/Harkonnen remains guarded. Winner Suk and Rihani precede a different unused trained-disc death, one skill return, printed bounty and one original Mentat replacement. Native card/force sources, normal-call forfeiture and shared-Duke exclusion remain; unrelated overlays retain their original envelopes.',
'Basic and Advanced selected-deck Skills admits registered base, Ixian/Tleilaxu, CHOAM/Richese and Ecaz/Moritani seats only when their family is selected. Unused decks neither create a seat nor require one. All14/native setup remain. Auditor is Advanced and unskilled; foreign-ghola authorization still explicitly requires Advanced, grants no new skill and retains original Duke/custody limits. Cyborg losses, native funding and capped markers stay physical. Supplied Advanced p34 excludes Richese auctions from Ixian Technology; existing capture/KH/copy/role guards keep their own rules.',
'Independent Recruits, Reinforcements and Harass compose registered selected-family all14 Basic/Advanced, including mixed E3, original Worlds and Tech3+/Advanced Strongholds2+. Bare E3 World cards keep their original roster envelope without requiring Skills. Original36/38/50, cache, native setup/prices and selected-hand capability stay. Own-World fee/dial pools are disjoint; visitor returns retain typed homes and Suk cannot rescue fees. Earned support is not personal money or reserve funding. Original Nexus/Discovery/Semuta, shared Duke and Advanced role/effect guards remain.',
@@ -4977,7 +4977,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area:'Player controls',status:'Partial',detail:'Private card/leader selection, readable inspectors, public battle posture and revival draw/decline, Planetologist range/gather controls, Special weapon selection and Suk Graduate physical rescue choices are connected. Rihani has private history and separate keep-new/reveal-old controls; automatic battle effects need no confirmation. Smuggler has explicit ordinary shipping opt-out and No-Field companion opt-in with physical-versus-priced force counts, plus surviving-leader battle collection with reveal-time guidance and owned multi-pile allocations. Banker exposes separate sealed spending and authorized inspection. Sandmaster exposes explicit legal routes, per-territory collection choices, optional worm/physical Ambassador destination checkboxes and direct victory-pile placement when several piles remain. Diplomat offers a named committed Worthless choice or decline after public reveal, and a separate surviving-loser destination/normal/elite retreat or decline. Bureaucrat offers redirect or full payment with explicit recipient amounts and deferred bribe custody. Remaining combined-module and exceptional-timing controls are incomplete.',evidence:['components/leader-skills.tsx','components/planetologist-movement.tsx','components/suk-graduate.tsx','components/rihani-decipherer.tsx','components/mentat-question.tsx','components/bureaucrat-payment.tsx','components/leader-skill-battle-guide.tsx','components/smuggler-shipment.tsx','components/smuggler-battle-collection.tsx','components/spice-banker.tsx','components/sandmaster-movement.tsx','components/sandmaster-worm.tsx','components/sandmaster-victory-spice.tsx','components/ambassador-movement.tsx','components/diplomat-defense.tsx','components/diplomat-retreat.tsx','components/game-table.tsx'] },
{ area:'AI',status:'Partial',detail:'All four profiles use private offers, legal visibility/revival actions and Planetologist movement/battle candidates and legal Suk Graduate rescues, Rihani exchanges, shared battle scoring, Smuggler reserve/owned No-Field shipments and modifier-inclusive collection plans, exact multi-pile Smuggler allocations, Sandmaster route, worm, physical Ambassador destination collection and victory-pile placement, funded Banker commitments and optional Diplomat copying from public revealed defenses or choosing legal losing-battle retreat. Bureaucrat uses the public payee alliance to redirect enemy income or preserve allied income. These are legal prototype paths; complete skill strategy and difficulty calibration remain unfinished.',evidence:['game/bots.ts'] },
{ area:'Documentation',status:'Partial',detail:'All fourteen faces and common rules are recorded. Skilled capture follows public physical-card custody. The 13 October 2026 ruling resolves the captured-card replacement question: the original assignment is retained and no replacement is drawn while it exists; other material timing interpretations remain explicit.',evidence:['docs/LEADER_SKILLS_RULES.md','docs/LEADER_SKILLS_CAPTURE.md','docs/LEADER_SKILLS_RUNTIME.md','docs/PLANETOLOGIST_RULES.md','docs/SUK_GRADUATE_RULES.md','docs/LEADER_BATTLE_EFFECTS.md','docs/MENTAT_QUESTION.md','docs/BUREAUCRAT_PAYMENTS.md','docs/SMUGGLER_SHIPMENT.md','docs/SPICE_BANKER_RUNTIME.md','docs/SANDMASTER_WORM.md','docs/DIPLOMAT_DEFENSE.md'] },
- {area: 'Verification', status: 'Partial', detail: "Earlier native, Richese, Smuggler, optional-module, Diplomat and saved evidence remains dated in linked contracts. Classic independent36/Nexus12 optional14 now retains original setup and native effect consumers. Actual normal module selection, Recruits fixed-grant revival and Cunning reserve-versus-board battle controls pass; four all14 legal policies retain stock and complete bounded progression. Full modes, strategy, deferred assurance and deployed acceptance remain unfinished.", evidence: ['tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', 'tests/bureaucrat-richese-cache.test.ts','tests/bureaucrat-richese-black-market.test.ts','tests/fixture-bureaucrat-richese-funding.ts','docs/BUREAUCRAT_PAYMENTS.md','tests/diplomat-used-defense.test.ts', 'tests/diplomat-late-journey.test.ts', 'tests/fixture-diplomat-late-journey.ts', 'tests/diplomat-context-journey.test.ts', 'tests/fixture-diplomat-context-journey.ts', 'docs/DIPLOMAT_DEFENSE.md', 'tests/smuggler-kwisatz-journey.test.ts', 'tests/fixture-smuggler-kwisatz-journey.ts', 'tests/smuggler-artillery-journey.test.ts', 'tests/fixture-smuggler-artillery-journey.ts', 'docs/SMUGGLER_BATTLE.md', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts','tests/homeworld-native-e1e2-modules-runtime.test.ts','tests/homeworld-native-e3-modules-runtime.test.ts','tests/homeworld-classic-skills-runtime.test.ts','tests/homeworld-skills-discovery-runtime.test.ts','tests/homeworld-optional-modules-runtime.test.ts','tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/captured-skill-revival.test.ts','tests/fixture-captured-skill-revival.ts','tests/captured-skill-special-revival.test.ts','tests/fixture-captured-skill-special-revival.ts','tests/leader-battle-effects.test.ts','tests/suk-graduate.test.ts','tests/mentat-question.test.ts','tests/bureaucrat-payment.test.ts','tests/smuggler-battle.test.ts','tests/spice-banker.test.ts','tests/advanced-native-skills.test.ts','tests/fixture-advanced-native-skills.ts','tests/skills-tech-payments-runtime.test.ts','tests/skills-tech-battle-runtime.test.ts','tests/ix-choam-skills-tech-runtime.test.ts','tests/tleilaxu-skills-tech-runtime.test.ts','tests/richese-skills-tech-runtime.test.ts','tests/classic-skills-stronghold-runtime.test.ts','tests/native-skills-stronghold-runtime.test.ts','tests/ecaz-skills-modules-runtime.test.ts','tests/moritani-skills-modules-runtime.test.ts','docs/LEADER_SKILLS_RUNTIME.md','docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md']},
+ {area:'Verification',status:'Partial',detail:"Classic Nexus core no longer requires independent cards for all14 native reactions. Original offers, starting-card order and mandatory Banker remain; actual Guild fee claims retain the real recipient and printed Bureaucrat third-party conditions. Base33 Mentat replacement, purchase, native Cunning and three390px source controls pass with four legal policies. Earlier native/Richese evidence stays dated; full modes, strategy and deferred assurance remain open.",evidence:['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/bureaucrat-richese-cache.test.ts','tests/bureaucrat-richese-black-market.test.ts','tests/fixture-bureaucrat-richese-funding.ts','docs/BUREAUCRAT_PAYMENTS.md','tests/diplomat-used-defense.test.ts', 'tests/diplomat-late-journey.test.ts', 'tests/fixture-diplomat-late-journey.ts', 'tests/diplomat-context-journey.test.ts', 'tests/fixture-diplomat-context-journey.ts', 'docs/DIPLOMAT_DEFENSE.md', 'tests/smuggler-kwisatz-journey.test.ts', 'tests/fixture-smuggler-kwisatz-journey.ts', 'tests/smuggler-artillery-journey.test.ts', 'tests/fixture-smuggler-artillery-journey.ts', 'docs/SMUGGLER_BATTLE.md', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts','tests/homeworld-native-e1e2-modules-runtime.test.ts','tests/homeworld-native-e3-modules-runtime.test.ts','tests/homeworld-classic-skills-runtime.test.ts','tests/homeworld-skills-discovery-runtime.test.ts','tests/homeworld-optional-modules-runtime.test.ts','tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/captured-skill-revival.test.ts','tests/fixture-captured-skill-revival.ts','tests/captured-skill-special-revival.test.ts','tests/fixture-captured-skill-special-revival.ts','tests/leader-battle-effects.test.ts','tests/suk-graduate.test.ts','tests/mentat-question.test.ts','tests/bureaucrat-payment.test.ts','tests/smuggler-battle.test.ts','tests/spice-banker.test.ts','tests/advanced-native-skills.test.ts','tests/fixture-advanced-native-skills.ts','tests/skills-tech-payments-runtime.test.ts','tests/skills-tech-battle-runtime.test.ts','tests/ix-choam-skills-tech-runtime.test.ts','tests/tleilaxu-skills-tech-runtime.test.ts','tests/richese-skills-tech-runtime.test.ts','tests/classic-skills-stronghold-runtime.test.ts','tests/native-skills-stronghold-runtime.test.ts','tests/ecaz-skills-modules-runtime.test.ts','tests/moritani-skills-modules-runtime.test.ts','docs/LEADER_SKILLS_RUNTIME.md','docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md']},
],
},
{
diff --git a/tests/classic-nexus-core.test.ts b/tests/classic-nexus-core.test.ts
new file mode 100644
index 00000000..fc56e136
--- /dev/null
+++ b/tests/classic-nexus-core.test.ts
@@ -0,0 +1,133 @@
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { viewGame } from '../game/engine';
+import { createIndependentHarkonnenCall, createIndependentIxianPurchase,
+ reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
+import { advanceClassicIndependentNexus } from './fixture-classic-independent-nexus';
+import { classicDiscoveryNexusClean } from './fixture-discovery-classic-nexus';
+import { createClassicNexusGuildPayment, createClassicNexusCunning,
+ submitClassicNexusSecondShipment } from './fixture-classic-nexus-core';
+
+void test('ordinary base33 all14 supports the native declared-Traitor cancellation and real Mentat draw', () => {
+ const f = createIndependentHarkonnenCall({ independent: false, advanced: true, skills: true });
+ assert.equal(f.game.deck.length + f.game.discard.length + f.game.players.reduce((sum, player) => sum + player.hand.length, 0), 33);
+ let game = reactionAction(f.game, 'atreides', { type: 'nexusHarkonnenBetrayalUse',
+ event: f.game.pendingNexusHarkonnenBetrayal!.source.event });
+ assert.equal(reactionPlayer(game, 'atreides').leaders.find(leader => leader.id === f.identity)?.dead, false);
+ assert.equal(reactionPlayer(game, 'atreides').tanks, 1, 'Without Harass, the actual losing board counter is lost normally.');
+ assert.equal(reactionPlayer(game, 'harkonnen').traitors.includes(f.identity), false);
+ const top = game.traitorReserve![0];
+ game = advanceClassicIndependentNexus(game, state => state.phase === 8 && classicDiscoveryNexusClean(state));
+ assert.equal(game.nexusHarkonnenReplacementHistory!.length, 1);
+ assert.equal(game.nexusHarkonnenReplacementHistory![0].drawn, top);
+ assert.equal(reactionPlayer(game, 'harkonnen').traitors.length, 4);
+ assert.deepEqual(game.nexusCards!.cards!.discard, ['harkonnen']);
+});
+
+void test('ordinary base33 all14 replaces the actual paid lot without introducing independent cards', () => {
+ const f = createIndependentIxianPurchase({ independent: false, advanced: true, skills: true });
+ const purchased = f.game.pendingNexusIxianReplacement!.card, top = f.game.deck[0];
+ const paid = reactionPlayer(f.game, 'atreides').spice;
+ const game = reactionAction(f.game, 'atreides', { type: 'nexusIxianReplacementUse',
+ event: f.game.pendingNexusIxianReplacement!.event });
+ assert.equal(reactionPlayer(game, 'atreides').spice, paid);
+ assert.equal(paid, reactionPlayer(f.beforePayment, 'atreides').spice - 2);
+ assert.equal(reactionPlayer(game, 'atreides').hand.some(card => card.id === purchased.id), false);
+ assert.deepEqual(reactionPlayer(game, 'atreides').hand.find(card => card.id === top.id), top);
+ assert.equal(game.discard.filter(card => card.id === purchased.id).length, 1);
+ assert.equal(game.auction?.index, f.game.auction!.index + 1);
+ assert.deepEqual(game.nexusCards!.cards!.discard, ['ixians']);
+});
+
+void test('Richese Secret Ally five-force price and a distinct Guild fee claim retain both original singleton costs', () => {
+ const f = createClassicNexusGuildPayment({ advanced: true, skills: true });
+ assert.equal(f.game.pendingGuildBetrayal!.invoice.price, 1);
+ assert.equal(reactionPlayer(f.game, 'atreides').reserves, reactionPlayer(f.beforeShipment, 'atreides').reserves);
+ assert.equal(f.game.nexusRicheseHistory!.at(-1)!.stage, 'pending');
+ assert.deepEqual(f.game.nexusCards!.cards!.discard, ['richese']);
+ const game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalUse',
+ event: f.game.pendingGuildBetrayal!.invoice.event });
+ assert.equal(reactionPlayer(game, 'atreides').spice, reactionPlayer(f.beforeShipment, 'atreides').spice - 1);
+ assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice + 1);
+ assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.beforeShipment, 'guild').spice);
+ assert.equal(reactionPlayer(game, 'atreides').reserves, reactionPlayer(f.beforeShipment, 'atreides').reserves - 5);
+ assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
+ assert.equal(game.nexusRicheseHistory!.at(-1)!.stage, 'shipped');
+ assert.deepEqual(game.nexusCards!.cards!.discard, ['richese', 'guild']);
+});
+
+void test('passing the discounted shipment keeps Guild Nexus and pays only the original one-spice Guild receipt', () => {
+ const f = createClassicNexusGuildPayment({ independent: true });
+ const game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalPass',
+ event: f.game.pendingGuildBetrayal!.invoice.event });
+ assert.equal(reactionPlayer(game, 'atreides').spice, reactionPlayer(f.beforeShipment, 'atreides').spice - 1);
+ assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.beforeShipment, 'guild').spice + 1);
+ assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice);
+ assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
+ assert.equal(game.nexusCards!.cards!.hands.emperor, 'guild');
+ assert.deepEqual(game.nexusCards!.cards!.discard, ['richese']);
+});
+
+void test('the paying Bureaucrat cannot divert its own five-spice Guild Nexus payment', () => {
+ const f = createClassicNexusGuildPayment({ advanced: true, skills: true, discounted: false });
+ const game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalUse',
+ event: f.game.pendingGuildBetrayal!.invoice.event });
+ assert.notEqual(game.decision?.kind, 'bureaucratPayment');
+ assert.equal(reactionPlayer(game, 'atreides').spice, reactionPlayer(f.beforeShipment, 'atreides').spice - 5);
+ assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice + 5);
+ assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
+});
+
+void test('a genuine third-party Bureaucrat redirects two from the actual Guild Nexus recipient without repeating delivery', () => {
+ const f = createClassicNexusGuildPayment({ advanced: true, skills: true, discounted: false,
+ training: { atreides: 'diplomat', guild: 'mentat', harkonnen: 'bureaucrat', emperor: 'sandmaster' } });
+ let game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalUse',
+ event: f.game.pendingGuildBetrayal!.invoice.event });
+ const offer = viewGame(game, 'harkonnen').bureaucrat!.pending!;
+ assert.equal(offer.payee, 'emperor');
+ assert.equal(offer.amount, 5);
+ game = reactionAction(game, 'harkonnen', { type: 'decision', event: offer.event, redirect: true });
+ assert.equal(reactionPlayer(game, 'atreides').spice, reactionPlayer(f.beforeShipment, 'atreides').spice - 5);
+ assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice + 3);
+ assert.equal(reactionPlayer(game, 'harkonnen').spice, reactionPlayer(f.beforeShipment, 'harkonnen').spice);
+ assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.beforeShipment, 'guild').spice);
+ assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
+ assert.equal(reactionPlayer(game, 'atreides').reserves, reactionPlayer(f.beforeShipment, 'atreides').reserves - 5);
+ assert.deepEqual(game.nexusCards!.cards!.discard, ['guild']);
+});
+
+for (const kind of ['reserve', 'cross', 'return'] as const) {
+ void test(`native Guild Cunning ${kind} shipment survives the neutral fee gate with one real paid group`, () => {
+ const f = submitClassicNexusSecondShipment(createClassicNexusCunning({ advanced: true, skills: true }), kind);
+ const offer = viewGame(f.game, 'emperor').guildBetrayalReaction!;
+ assert.equal(offer.canUse, false, 'The actual Guild singleton was already spent for Cunning.');
+ assert.equal(f.game.pendingGuildBetrayal!.invoice.price, 1);
+ const before = reactionPlayer(f.beforeSecond, 'guild');
+ let game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalPass', event: offer.event });
+ const guild = reactionPlayer(game, 'guild'), destination = `${f.second}:${f.secondSector}`;
+ assert.equal(guild.spice, before.spice - 1);
+ assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeSecond, 'emperor').spice);
+ assert.equal(guild.reserves, before.reserves + (kind === 'reserve' ? -1 : kind === 'return' ? 1 : 0));
+ assert.equal(guild.forces[f.origin] ?? 0, kind === 'reserve' ? 1 : 0);
+ assert.equal(guild.forces[destination] ?? 0, kind === 'return' ? 0 : 1);
+ assert.deepEqual(game.nexusCards!.cards!.discard, ['guild']);
+ assert.equal(game.nexusCards!.cards!.hands.emperor, 'harkonnen');
+ assert.equal(viewGame(game, 'guild').nexusGuildCunning!.active!.movesLeft, 0);
+ const settledForces = structuredClone(guild.forces);
+ game = reactionAction(game, 'guild', { type: 'endMovement' });
+ assert.deepEqual(reactionPlayer(game, 'guild').forces, settledForces);
+ assert.equal(game.nexusGuildCunningHistory!.at(-1)!.stage, 'completed');
+ });
+}
+
+void test('native Karama cancels only Cunning and retains the completed first shipment and its price', () => {
+ const f = createClassicNexusCunning({ advanced: true, skills: true });
+ const game = reactionAction(f.game, 'atreides', { type: 'card', card: f.counter, mode: 'cancel' });
+ assert.deepEqual(reactionPlayer(game, 'guild').forces, reactionPlayer(f.beforeCunning, 'guild').forces);
+ assert.equal(reactionPlayer(game, 'guild').reserves, reactionPlayer(f.beforeCunning, 'guild').reserves);
+ assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.beforeCunning, 'guild').spice);
+ assert.equal(game.discard.filter(card => card.id === f.counter).length, 1);
+ assert.deepEqual(game.nexusCards!.cards!.discard, ['guild']);
+ assert.equal(game.nexusGuildCunningHistory!.at(-1)!.stage, 'canceled');
+ assert.notEqual(game.active, 'guild');
+});
diff --git a/tests/fixture-classic-independent-nexus-reactions.ts b/tests/fixture-classic-independent-nexus-reactions.ts
index 876bbf68..a92bee6a 100644
--- a/tests/fixture-classic-independent-nexus-reactions.ts
+++ b/tests/fixture-classic-independent-nexus-reactions.ts
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import { baseDeck } from '../game/cards';
+import type { FactionId } from '../game/catalog';
import { territory } from '../game/board';
import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
@@ -20,10 +21,10 @@ export function reactionAction(game: Game, actor: string, action: Action, action
/** Original setup and turn-one play, followed by a real worm, reciprocal
* alliance and closing draw. Only unused original deck singletons move. */
-function drawIndependentReaction(face: 'harkonnen' | 'ixians', owner: string,
+export function drawClassicNexusReaction(face: FactionId, owner: string,
options: ClassicIndependentNexusOptions) {
const fixture = createClassicIndependentNexusFixture({ ...options,
- roster: ['atreides', 'harkonnen', 'guild'] });
+ roster: options.roster ?? ['atreides', 'harkonnen', 'guild'] });
const actions = fixture.actions;
let game = fixture.game;
const take = (worm: boolean) => {
@@ -48,10 +49,11 @@ function drawIndependentReaction(face: 'harkonnen' | 'ixians', owner: string,
return { ...fixture, game, owner, actions, beforeDraw, afterDraw: structuredClone(game) };
}
-export function createIndependentHarkonnenCall(options: { advanced?: boolean; skills?: boolean; remote?: boolean } = {}) {
+export function createIndependentHarkonnenCall(options: { advanced?: boolean; skills?: boolean; remote?: boolean; independent?: boolean } = {}) {
const counter = baseDeck().find(card => card.effect === 'karama')!.id;
- const fixture = drawIndependentReaction('harkonnen', 'atreides', { ...options,
- startingCards: { atreides: [HARASS_WITHDRAW_CARD], guild: [counter] } });
+ const starting = options.independent === false ? baseDeck().find(card => card.kind === 'worthless')!.id : HARASS_WITHDRAW_CARD;
+ const fixture = drawClassicNexusReaction('harkonnen', 'atreides', { ...options,
+ startingCards: { atreides: [starting], guild: [counter] } });
const actions = fixture.actions;
let game = advanceClassicIndependentNexus(fixture.game, state => state.phase === 5 &&
state.active === 'atreides' && classicDiscoveryNexusClean(state), actions);
@@ -81,7 +83,7 @@ export function createIndependentHarkonnenCall(options: { advanced?: boolean; sk
for (const actor of [game.battle!.attacker, game.battle!.defender]) {
game = reactionAction(game, actor, { type: 'battlePlan', dial: 0, support: 0,
leader: actor === 'atreides' ? identity : opponentLeader,
- weapon: actor === 'atreides' ? HARASS_WITHDRAW_CARD : null, defense: null }, actions);
+ weapon: actor === 'atreides' && options.independent !== false ? HARASS_WITHDRAW_CARD : null, defense: null }, actions);
while (game.response || game.phaseOpening || game.decision?.kind === 'fullPlanOffer') {
const step = nextClassicDiscoveryNexusStep(game); assert.ok(step);
game = applyClassicDiscoveryNexusStep(game, step, actions);
@@ -96,13 +98,17 @@ export function createIndependentHarkonnenCall(options: { advanced?: boolean; sk
return { ...fixture, game, beforeCall, beforeShipment, afterShipment, identity, opponent, key, counter };
}
-export function createIndependentIxianPurchase(options: { advanced?: boolean; skills?: boolean; owner?: 'atreides' | 'harkonnen' } = {}) {
+export function createIndependentIxianPurchase(options: { advanced?: boolean; skills?: boolean; owner?: 'atreides' | 'harkonnen'; independent?: boolean } = {}) {
const owner = options.owner ?? 'atreides';
- const fixture = drawIndependentReaction('ixians', owner, { ...options,
- startingCards: { [owner]: ['ecaz-recruits'] } });
+ const starting = options.independent === false ? baseDeck().find(card => card.kind === 'worthless')!.id : 'ecaz-recruits';
+ const fixture = drawClassicNexusReaction('ixians', owner, { ...options,
+ startingCards: { [owner]: [starting] } });
const actions = fixture.actions;
let game = advanceClassicIndependentNexus(fixture.game, state => state.phase === 2 && classicDiscoveryNexusClean(state), actions);
- const ordered = [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD].map(id => {
+ const lots = options.independent === false
+ ? [baseDeck().find(card => card.kind === 'poison')!.id, baseDeck().find(card => card.kind === 'shield')!.id]
+ : [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD];
+ const ordered = lots.map(id => {
const index = game.deck.findIndex(card => card.id === id); assert.ok(index >= 0);
return game.deck.splice(index, 1)[0];
});
diff --git a/tests/fixture-classic-independent-nexus.ts b/tests/fixture-classic-independent-nexus.ts
index 983c808c..09696e6e 100644
--- a/tests/fixture-classic-independent-nexus.ts
+++ b/tests/fixture-classic-independent-nexus.ts
@@ -12,6 +12,8 @@ import { nextClassicDiscoveryNexusStep, applyClassicDiscoveryNexusStep } from '.
export type ClassicIndependentNexusOptions = {
advanced?: boolean;
skills?: boolean;
+ /** False exercises the original base33 without selecting the independent variant. */
+ independent?: boolean;
roster?: readonly FactionId[];
training?: Partial>;
/** Only original, still-undealt starting Treachery slots are ordered. */
@@ -22,6 +24,7 @@ export type ClassicIndependentNexusStep = { actor: string; action: Action };
export function createClassicIndependentNexusFixture(options: ClassicIndependentNexusOptions = {}) {
const roster = options.roster ?? ['atreides', 'guild', 'emperor'];
const advanced = options.advanced ?? false;
+ const independent = options.independent ?? true;
let game = createGame('CLASSICINDEPENDENTNEXUS', newPlayer(roster[0], roster[0], roster[0]), advanced, []);
for (const faction of roster.slice(1)) joinGame(game, newPlayer(faction, faction, faction));
const actions: ClassicIndependentNexusStep[] = [], staging: string[] = [];
@@ -31,14 +34,14 @@ export function createClassicIndependentNexusFixture(options: ClassicIndependent
? applyClassicDiscoveryNexusStep(game, { actor, action })
: applyAction(game, actor, action);
};
- for (const option of ['ecazTreachery', 'nexusCards', ...(options.skills ? ['leaderSkills'] : [])])
+ for (const option of [...(independent ? ['ecazTreachery'] : []), 'nexusCards', ...(options.skills ? ['leaderSkills'] : [])])
act(game.host, { type: 'moduleOption', module: option, enabled: true });
for (const player of game.players) act(player.id, { type: 'ready' });
const lobby = structuredClone(game);
const rolls = options.skills ? skillOfferRolls(game, options.training) : [];
rolls.push(...Array(NEXUS_FACTIONS.length - 1).fill(0xffffffff));
if (options.startingCards) {
- const source = [...treacheryDeck([]), ...ecazTreacheryCards()].map(card => card.id);
+ const source = [...treacheryDeck([]), ...(independent ? ecazTreacheryCards() : [])].map(card => card.id);
const used = new Set(), prefix: string[] = [];
const reserved = new Set(Object.values(options.startingCards).flatMap(cards => cards ?? []));
for (const player of game.players) {
@@ -109,7 +112,16 @@ export function advanceClassicIndependentNexus(game: Game, until: (state: Game)
actions?: ClassicIndependentNexusStep[], max = 1200): Game {
for (let count = 0; !until(game) && count < max; count++) {
assert.notEqual(game.status, 'finished', 'Original lifecycle ended before requested continuation');
- const step = nextClassicDiscoveryNexusStep(game);
+ let step: ClassicIndependentNexusStep | null;
+ if (game.pendingGuildBetrayal) {
+ const actor = game.pendingGuildBetrayal.required.find(id => !game.pendingGuildBetrayal!.passed.includes(id));
+ assert.ok(actor);
+ const view = viewGame(game, actor);
+ view.players.find(player => player.id === actor)!.bot = 'Easy';
+ const action = botActions(view)[0];
+ assert.ok(action && ['guildBetrayalUse', 'guildBetrayalPass'].includes(action.type));
+ step = { actor, action };
+ } else step = nextClassicDiscoveryNexusStep(game);
assert.ok(step, `Use the real sealed control at ${game.turn}/${game.phase}/${game.decision?.kind}`);
game = applyClassicDiscoveryNexusStep(game, step, actions);
}
diff --git a/tests/fixture-classic-nexus-core.ts b/tests/fixture-classic-nexus-core.ts
new file mode 100644
index 00000000..ceaddec7
--- /dev/null
+++ b/tests/fixture-classic-nexus-core.ts
@@ -0,0 +1,94 @@
+import assert from 'node:assert/strict';
+import { viewGame, type Action, type Game } from '../game/engine';
+import { TERRITORIES, territory } from '../game/board';
+import { baseDeck } from '../game/cards';
+import type { FactionId } from '../game/catalog';
+import { drawClassicNexusReaction, reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
+import { advanceClassicIndependentNexus, type ClassicIndependentNexusOptions, type ClassicIndependentNexusStep } from './fixture-classic-independent-nexus';
+import { classicDiscoveryNexusClean } from './fixture-discovery-classic-nexus';
+
+export type ClassicNexusCoreOptions = Pick;
+function drawSecondFace(game: Game, owner: string, face: FactionId, actions: ClassicIndependentNexusStep[]) {
+ assert.equal(game.nexusCards!.phase!.stage, 'drawing');
+ const cards = game.nexusCards!.cards!, index = cards.deck.indexOf(face);
+ assert.ok(index >= 0);
+ cards.deck.unshift(cards.deck.splice(index, 1)[0]);
+ return reactionAction(game, owner, { type: 'nexusCardChoice', turn: game.turn,
+ card: cards.hands[owner], choice: 'draw', ownRedraws: 0 }, actions);
+}
+
+/** Two distinct original cards: A holds Richese (or an irrelevant Harkonnen),
+ * Emperor holds Guild, and actual Guild/Harkonnen are allied. */
+export function createClassicNexusGuildPayment(options: ClassicNexusCoreOptions & { discounted?: boolean } = {}) {
+ const discounted = options.discounted ?? true;
+ const fixture = drawClassicNexusReaction(discounted ? 'richese' : 'harkonnen', 'atreides', {
+ ...options, independent: options.independent ?? false,
+ roster: ['atreides', 'guild', 'harkonnen', 'emperor'],
+ training: options.training ?? (options.skills ? { atreides: 'bureaucrat' } : undefined),
+ });
+ const actions = fixture.actions;
+ let game = drawSecondFace(fixture.game, 'emperor', 'guild', actions);
+ const afterDraws = structuredClone(game);
+ game = advanceClassicIndependentNexus(game, state => state.phase === 5 && state.active === 'atreides' &&
+ classicDiscoveryNexusClean(state), actions);
+ const beforeShipment = structuredClone(game), place = territory('carthag');
+ assert.ok(!place.sectors.includes(game.storm));
+ const amount = 5;
+ const action: Action = { type: 'ship', territory: place.id, sector: place.sectors[0],
+ amount, elite: 0, allyPayment: 0 };
+ if (discounted) {
+ const offer = viewGame(game, 'atreides').nexusRichese!;
+ assert.equal(offer.blocked, null);
+ action.nexus = offer.event;
+ }
+ game = reactionAction(game, 'atreides', action, actions);
+ game = advanceClassicIndependentNexus(game, state => !!state.pendingGuildBetrayal &&
+ classicDiscoveryNexusClean(state), actions);
+ return { ...fixture, game, afterDraws, beforeShipment, actions, action, amount,
+ key: `${place.id}:${place.sectors[0]}`, holder: 'emperor' };
+}
+
+/** A/H form the real alliance; native Guild and Emperor draw distinct faces.
+ * The latter holds Harkonnen, never a second copy of Guild's singleton. */
+export function createClassicNexusCunning(options: ClassicNexusCoreOptions = {}) {
+ const counter = baseDeck().find(card => card.effect === 'karama')!.id;
+ const fixture = drawClassicNexusReaction('guild', 'guild', { ...options,
+ independent: options.independent ?? false, roster: ['atreides', 'harkonnen', 'guild', 'emperor'],
+ startingCards: { atreides: [counter] },
+ });
+ const actions = fixture.actions;
+ let game = drawSecondFace(fixture.game, 'emperor', 'harkonnen', actions);
+ game = advanceClassicIndependentNexus(game, state => state.phase === 5 && state.active === 'guild' &&
+ classicDiscoveryNexusClean(state), actions);
+ const empty = TERRITORIES.filter(place => place.type === 'stronghold' &&
+ !place.sectors.includes(game.storm) && game.players.every(player =>
+ Object.keys(player.forces).every(key => !key.startsWith(`${place.id}:`))));
+ assert.ok(empty.length >= 2);
+ const first = empty[0], second = empty[1], origin = `${first.id}:${first.sectors[0]}`;
+ const beforeFirst = structuredClone(game);
+ game = reactionAction(game, 'guild', { type: 'ship', territory: first.id, sector: first.sectors[0],
+ amount: 1, elite: 0, allyPayment: 0 }, actions);
+ game = advanceClassicIndependentNexus(game, state => reactionPlayer(state, 'guild').shipped &&
+ !state.pendingGuildBetrayal && classicDiscoveryNexusClean(state), actions);
+ const beforeCunning = structuredClone(game), offer = viewGame(game, 'guild').nexusGuildCunning!.offer!;
+ assert.equal(offer.blocked, null);
+ game = reactionAction(game, 'guild', { type: 'endMovement', nexus: offer.event }, actions);
+ return { ...fixture, game, beforeFirst, beforeCunning, actions, counter, origin,
+ second: second.id, secondSector: second.sectors[0] };
+}
+
+export function submitClassicNexusSecondShipment(fixture: ReturnType,
+ kind: 'reserve' | 'cross' | 'return') {
+ let game = advanceClassicIndependentNexus(fixture.game, state =>
+ state.nexusGuildCunningHistory?.at(-1)?.stage === 'secondShipment' && classicDiscoveryNexusClean(state), fixture.actions);
+ const beforeSecond = structuredClone(game);
+ const action: Action = kind === 'reserve'
+ ? { type: 'ship', territory: fixture.second, sector: fixture.secondSector, amount: 1, elite: 0, allyPayment: 0 }
+ : { type: 'guildShip', from: fixture.origin,
+ territory: kind === 'return' ? 'reserves' : fixture.second,
+ ...(kind === 'return' ? {} : { sector: fixture.secondSector }), amount: 1, elite: 0, allyPayment: 0 };
+ game = reactionAction(game, 'guild', action, fixture.actions);
+ game = advanceClassicIndependentNexus(game, state => !!state.pendingGuildBetrayal &&
+ classicDiscoveryNexusClean(state), fixture.actions);
+ return { ...fixture, game, beforeSecond, action, kind };
+}
diff --git a/tests/nexus-guild-betrayal-engine.test.ts b/tests/nexus-guild-betrayal-engine.test.ts
index 0ded9f1a..3f6ff3ff 100644
--- a/tests/nexus-guild-betrayal-engine.test.ts
+++ b/tests/nexus-guild-betrayal-engine.test.ts
@@ -249,18 +249,6 @@ void test('No publicly possible holder auto-settles the native fee without a fab
validateNexusCards(done.nexusCards!.cards!, done.players);
});
-void test('Unsupported Cunning producer is visibly blocked before its singleton is spent in the bounded payment profile', () => {
- const fixture = createGuildBetrayalFixture({payer: 'guild'});
- const before = structuredClone(fixture.before);
- const cards = before.nexusCards!.cards!;
- cards.hands[fixture.holder] = cards.hands[fixture.guild];
- cards.hands[fixture.guild] = 'guild';
- const offer = viewGame(before, fixture.guild).nexusGuildCunning!.offer!;
- assert.match(offer.blocked!, /second-shipment overlay/);
- rejects(before, fixture.guild, {type: 'endMovement', nexus: offer.event});
- assert.equal(before.nexusCards!.cards!.hands[fixture.guild], 'guild');
- assert.equal(before.nexusGuildCunningHistory, undefined);
-});
function outsidePaymentProfile(game: Game): Game {
const native = structuredClone(game);
@@ -272,47 +260,6 @@ function outsidePaymentProfile(game: Game): Game {
return native;
}
-for (const advanced of [false, true]) {
- void test(`${advanced ? 'Advanced' : 'Basic'} Richese overlay rejects before spend while ordinary and outside-profile shipments finish`, () => {
- const fixture = createGuildBetrayalFixture({advanced, payer: 'holder'});
- const before = structuredClone(fixture.before);
- const cards = before.nexusCards!.cards!;
- const index = cards.deck.indexOf('richese');
- assert.ok(index >= 0);
- cards.deck[index] = cards.hands[fixture.holder]!;
- cards.hands[fixture.holder] = 'richese';
- validateNexusCards(cards, before.players);
- const offer = viewGame(before, fixture.holder).nexusRichese!;
- assert.match(offer.blocked!, /Richese Secret Ally shipment overlay/);
- const action: Action = {...fixture.declaration, amount: 3, nexus: offer.event};
- const saved = structuredClone(before);
- assert.throws(() => applyAction(before, fixture.holder, action), /Richese Secret Ally shipment overlay/);
- assert.deepEqual(before, saved, 'No singleton spend, parent, history, fee or forces change on rejection.');
- assert.equal(before.nexusRicheseHistory, undefined);
-
- const ordinary = settleGuildBetrayalFixture(applyAction(before, fixture.holder,
- {...fixture.declaration, amount: 3}));
- const prior = before.players.find(p => p.id === fixture.holder)!;
- const shipped = ordinary.players.find(p => p.id === fixture.holder)!;
- assert.equal(shipped.reserves, prior.reserves - 3);
- assert.equal(shipped.spice, prior.spice - 3);
- assert.equal(shipped.shipped, true);
- assert.equal(ordinary.nexusCards!.cards!.hands[fixture.holder], 'richese');
-
- const native = outsidePaymentProfile(before);
- const nativeOffer = viewGame(native, fixture.holder).nexusRichese!;
- assert.equal(nativeOffer.blocked, null);
- const done = settleGuildBetrayalFixture(applyAction(native, fixture.holder,
- {...action, nexus: nativeOffer.event}));
- const recipient = done.players.find(p => p.id === fixture.holder)!;
- assert.equal(recipient.reserves, prior.reserves - 3);
- assert.equal(recipient.spice, prior.spice - 1);
- assert.equal(recipient.shipped, true);
- assert.equal(done.nexusCards!.cards!.hands[fixture.holder], null);
- assert.equal(done.nexusRicheseHistory!.at(-1)!.stage, 'shipped');
- assert.equal(done.nexusCards!.cards!.discard.filter(face => face === 'richese').length, 1);
- });
-}
void test('Saved native cross permission is independently required even for a self-consistent paid frame or purchased Karama rate', () => {
for (const purchasedRate of [false, true]) {