Compose classic Nexus with earned phase modules

This commit is contained in:
Quintine committed 2026-10-18 06:21:11 +11:00
1 parent a7cc25ae29
commit ce716f3438
21 files changed
+875 -31

No files matched your search

+4 -3
View File
@@ -1293,9 +1293,10 @@ export function GameTable({
{classicNexusPreview && (
<p className="notice" role="status">
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
or selected independent36 Treachery, optionally all14 Leader Skills. Original
Tech (3+ seats) and Advanced Strongholds (2+ seats) may compose. Native reactions
retain original card, force and payment authority. Acknowledgement timing and
purchase/bonus ordering remain provisional. Other modules and complete Nexus
acceptance remain separate.{' '}
<a href="/rules?topic=nexus-cards#nexus-cards">Preview rules and limits</a>
</p>
+23
View File
@@ -29,6 +29,29 @@ optional skill bands and combinations remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Classic core phase modules — 18 October. Prototyped / Partial.**
Fresh classic Nexus now composes original Tech3+ and Advanced Strongholds2+
with selected33/36 and optional14. Normal host controls retain all five
requested modules through actual training/Traitors into first Storm. Real
first-Storm ownership, phase payout and first-Mentat claims are not assigned
or retrofitted; other native/World/Discovery/Semuta envelopes stay separate.
Earned Arrakeen support3 uses bank2/personal1 beside real reserve fees;
trained base33 Carthag Shield survives poison after native call cancellation
and reaches original cleanup/private replacement. Fixed paid5 then ordinary
free2 retains Axlotl pending2 at purse8 until actual phase4 close pays10.
Combined phase funding/payout and paid2 replacement preserve original owners.
Frozen43-file **439/439**, focused1/1 and repaired Arrakeen2/2, types/lint/
build pass. Fixture corrections preserve original Guild-ally3 tariff and
separate city Collection2; no game calculation changed. Four actual390px
journeys and four legal policies/66 owned choices finish; no calibration claim.
No gameplay/application exception or overflow. Two DELETE204 cleanup fetches
logged ERR_ABORTED and an idle wait timed out; tool diagnostics are preserved.
Temporary memory/routes/executable scripts removed/released204, no stored room/
reset/restart/deployment. Full modes, universal timing and assurance stay open.
[Source-bound owner verification](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--18-october-2026).
**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
+43
View File
@@ -1,5 +1,48 @@
# Dune implementation status
## 18 October 2026 — Classic Nexus with earned phase modules
Original independent36/Nexus+Tech and all14/phase starts rejected through
native lobby actions; plain33/all14/phase started without reaction states.
The public classic core now composes original Tech3+ and Advanced Strongholds2+,
preserving Basic/Advanced prerequisites, selected33/36 and optional14. Bare
independent-card consumers share that phase capability. Fee source snapshots
bind only present modules; no played state, token/card owner or history is
fabricated. Other native/World/Discovery/Semuta and reaction envelopes stay.
Two disjoint lanes add original-action Tech and held-Stronghold sources plus
eleven meaningful regressions. Real first-Storm owners and first-Mentat cards
precede actual death, Revival, funded delivery and battle. Parent removed
setup/wiring and same-prestate replay pins, kept real error/cost boundaries,
added a second genuine free return for one-pile behavior and removed an unused
copied drain snapshot. The Tech worker ran its four-case file twice despite
the freeze instruction; those repeated lane checks are not owner certification.
Parent frozen43-file **439/439**, focused second-free1/1, repaired native
Arrakeen2/2, types/lint/build pass. Initial437/439 and the narrow repair exposed
incorrect fixture expectations for real Guild-ally shipping (five cost3) and
later Carthag Collection2; the original game calculations are unchanged.
Four legal policies perform66 owned choices through native income/Revival
and earned battles, with varying outcomes rather than a calibration claim.
Four actual390px journeys finish original five-option lobby readiness/
training/Traitors/Storm (stock36/Nexus12, no premature owners), earned Arrakeen
support3 bank2/personal1 with three reserve fees and separate Collection2,
trained base33 Carthag Shield survival/native call cancellation/cleanup/private
Mentat replacement, and fixed paid5/free2 Axlotl pending2→actual phase-end
purse8→10. Combined original paid5/Bureaucrat2 also retains claimedTurn1 cards
and Heighliners pending1 until Guild6→7 at native phase5 close. Actual paid2
Ixian replacement with both earned modules preserves holders/piles/wallet10.
No application/gameplay exception or horizontal overflow. Two isolated cleanup
DELETEs returned204 but browser diagnostics reported ERR_ABORTED; a network-idle
wait timed out. Diagnostics are preserved and reported, not replayed away.
Temporary routes/memory/executable programmes removed/released204; no saved room,
reset, server restart or deployment. Existing build warnings and full-mode/
universal-timing/assurance gates remain. Tech now has canonical helper/consumer
evidence in the existing checklist instead of another broad history lookup.
[Canonical current source and proof](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--18-october-2026).
## 18 October 2026 — Classic Nexus core and funded native card shipments
Ordinary base33 all14 genuine Harkonnen call and Ixian purchase still lacked
+12
View File
@@ -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 with original phase modules — 18 October 2026
Classic base33/selected36 Nexus now retains optional all14 with original
Tech3+ and Advanced Strongholds2+. Public capability and original setup
preserve actual first-Storm tokens/end-Mentat cards; none is assigned by a
fixture or retrofitted into played state. Native bank subsidy funds actual
holder support, not reserve fees or personal money. Actual trained Carthag
Shield defense, original call/cancellation, winner cleanup and private Mentat
replacement remain. Fixed paid groups/free returns and phase Tech income
stay separate from mandatory normal Banker.
[439/439, actual normal controls and legal policies](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--18-october-2026).
## Classic Nexus core without card dependency — 18 October 2026
Original base33 or explicitly selected independent36 Nexus core admits
+60
View File
@@ -2,6 +2,66 @@
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 original phase modules — 18 October 2026
**Prototyped / Partial.** Fresh classic Nexus core now retains original
Tech3–6 Basic/Advanced and/or Strongholds2–6 Advanced with base33 or explicitly
selected independent36, optionally all14 Skills. The shared public predicate
keeps the printed player-count/rules prerequisites. Other native families,
Worlds, Discovery/Semuta and reaction envelopes remain separate. No played
state is reflagged or assigned a token/card holder.
Actual host controls retain all five requested modules through original
readiness, starting cards, four private skill assignments and native Traitor
selection into first Storm. Stock36/Nexus12 remain; Tech owners are still
unassigned and Stronghold claim0 until their original earning boundaries.
Only original setup lotteries/unopened decks select fixture sources.
Original Guild fee parents conditionally bind the real Tech/Stronghold state;
bare ordinary parents gain no unconditional new keys. Existing signed card
shipments, native counters, original currency and physical continuation remain.
The combined original-action payment reaches payer7/recipient13 after Bank2,
Heighliners pending1 while Guild stays6, then actual phase5 close pays Guild7.
Actual first-Mentat holders remain unchanged through that contested arrival.
Paid2 Ixian replacement likewise preserves both earned module states, exact
discard/draw, wallet10 and one following lot.
New original-action consumer evidence:
- Tech: real first-Storm owner/count, funded five-for-one delivery, matching
phase-end Heighliners income, fixed Tleilaxu paid5 versus ordinary free
returns, a second genuine free request without another pile, pending-spice
affordability and native winner transfer after Harass.
- Earned Arrakeen: temporary5 support3 uses bank2/personal1, while independent
Reinforcements pays three actual reserves. Observed board2/Tanks6/reserves12
and purse6 before separate Collection2→8; real starred reserves stay unchanged.
- Earned Carthag: an actual all14 trained disc behind the shield survives
poison with its physical Shield after the original call is canceled.
Nine forces survive; original winner card choices, losing retention,
capture decline, inspection and one private actual Mentat replacement remain.
Empty defense still loses the disc/army and held custody changes only at Mentat.
Frozen43-file **439/439**, focused second-free1/1 and repaired Arrakeen2/2,
types/lint/build pass. Initial437/439 exposed two new fixture assumptions:
the real Guild ally pays rounded3 for five counters, and later native
Carthag Collection2 is separate from support1. Assertions now preserve those
existing rules; no tariff/income calculation changed. Setup/wiring and
same-snapshot replay pins were removed in favor of actual consumer transitions.
Tech now has canonical helper/consumer/check evidence in the existing checklist.
Four original-control390px journeys finish normal five-module setup,
Arrakeen support/fees, trained Carthag defense/Mentat and fixed5+free2 Axlotl.
The last preserves purse8 while token2 waits, then actual phase4 close pays10.
Four legal policies execute66 owned choices through original income, Revival
and battles; outcomes vary and are not strength calibration.
All gameplay requests/controls complete without application exceptions or
horizontal overflow. Two isolated DELETE cleanup fetches returned204 but
browser diagnostics logged `ERR_ABORTED`; a cleanup network-idle wait timed
out. These tool/lifecycle diagnostics are retained, not erased or represented
as failed gameplay. Temporary routes, memory and executable smoke scripts are
removed/released204; no stored room/reset/restart/deployment.
Source-bound proof: `/tmp/dune-classic-nexus-phase-18oct.json`. Complete modes,
all combinations, universal timing and deferred assurance remain unfinished.
## Classic Nexus core and funded card shipments — 18 October 2026
**Prototyped / Partial.** Original E3 pp4/9/11 permits Nexus independently of
+11
View File
@@ -4,6 +4,17 @@
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 original phase-module funding — 18 October 2026
The classic core entry now retains original Tech3+ and Advanced Strongholds2+,
base33 or selected36 and optional14. Guild fee parents bind only genuinely
enabled module state; original bare-parent grammar remains. Real token/card
owners are earned at their original boundaries. Actual combined paid5/
Guild-card receipt/Bureaucrat2 leaves payer7/recipient13/Guild6 while one
Heighliners spice waits; actual phase5 close pays its owner Guild7, independent
of the diverted shipping receiver. No phase income finances earlier fees.
[Source and complete bounded evidence](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--18-october-2026).
## Classic core funded card shipments — 18 October 2026
Fresh classic2–6 Basic/Advanced Nexus uses original base33 or selected
+11
View File
@@ -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.
## Earned phase cards and original replacement — 18 October 2026
Fresh classic core now retains Tech3+ and Advanced Strongholds2+ with
selected33/36, optionally14. Actual all14 base33 Carthag controls reveal the
native matching call, spend the genuinely drawn Nexus, preserve physical
Shield poison survival, finish original card/retention/capture/inspection
priority and draw one private actual replacement at Mentat. The separate
held module card is never added to the Treachery census or transferred early.
Harass return and native winner token transfer create no free-Revival income.
[Exact bounded proof](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--18-october-2026).
## Original base33 core without card dependency — 18 October 2026
Fresh classic2–6 Basic/Advanced Nexus now initializes the original native
+10
View File
@@ -2,6 +2,16 @@
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 phase-module purchased-card source — 18 October 2026
Classic core retains Tech3+ and Advanced Strongholds2+, selected33/36 and
optional14, through original setup/owner timing. Actual paid2 replacement
with both earned modules keeps wallet10, discards only bought Reinforcements
for the real deck top, spends Ixian Nexus once and reaches following lot1.
Original module holders/piles remain; neither creates an extra purchase,
card, subsidy or source alias. Other native/module envelopes stay separate.
[Current source and evidence](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--18-october-2026).
## Original base33 core without card dependency — 18 October 2026
Fresh classic2–6 Basic/Advanced Nexus initializes original purchase replacement
+10
View File
@@ -2,6 +2,16 @@
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.
## Original Tech and Stronghold composition — 18 October 2026
Classic Nexus core retains the original phase modules with selected33/36
and optional14. A real funded five-for-one arrival still delivers five and
uses the one-counter invoice; original Heighliners accrues its actual owner's
token count only after delivery and pays only at phase end. Neither the fee
claim nor a held Stronghold card invents another trigger or personal subsidy.
No other native/World/Discovery envelope or numerical rule changes.
[Current phase consumers and controls](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--18-october-2026).
## Classic core Guild fee composition — 18 October 2026
Classic base33 or selected independent36 Nexus, optionally all14, now retains
+1
View File
@@ -705,6 +705,7 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP
| 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) |
| Classic Nexus core / original phase modules | Fresh classic base33 or selected36/Nexus12, optionally14, composes original Tech3–6 Basic/Advanced and Strongholds2–6 Advanced through the public core capability. Real first-Storm tokens, owner-count pending income, actual end-Mentat held cards and original matching payout remain. Fixed paid force groups do not become free under Recruits; free requests accrue once, pending income cannot fund earlier costs, and fees/Harass are not Revival/shipment. Earned local bank support does not fund reserve fees; physical effective Carthag defense, native counters/cleanup/winner token/private replacement keep their original order. No new numerical or universal policy, played reflag or wider family/World/Discovery opening. | [Source, controls and owner-frozen439/439](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--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) |
+48
View File
@@ -2,6 +2,54 @@
Historical development module checkpoint, 7 September 2026: the six printed effects, public card custody, mobile declaration, support accounting, AI choices and readable faces are integrated. The later user-authorized [classic Advanced preview](ADVANCED_PREVIEW.md) already permits Stronghold Cards; preserve that access. Complete Advanced/expansion release and arbitrary combinations remain gated.
## Classic original Nexus composition — 18 October 2026
The rules-first classic Advanced Nexus12 entry retains original Strongholds2–6,
base33 or explicitly selected independent36, and optional all14 Skills. The six
printed effects and original custody timing remain unchanged. This effect
proof keeps Tech off; the shared core also composes original Tech3+. No
World/Discovery/native-family envelope, numerical rule or universal timing
ruling is added, and complete public-release acceptance remains gated.
`tests/fixture-nexus-core-strongholds.ts` exports
`createNexusCoreArrakeenFixture`, `createNexusCoreCarthagFixture`,
`revealNexusCoreStrongholdBattle`, `callNexusCoreCarthagTraitor` and
`finishNexusCoreStrongholdBattle`. Both scenes stop at an original pre-plan
boundary after genuine first-end-Mentat claims. Arrakeen is earned by an actual
turn-one five-reserve shipment; Carthag is retained from actual setup forces.
Only unopened decks and original deal lotteries select source cards. No
holders, hands, forces, deaths, clocks or wallets are assigned. Shared native
controls handle legal participation; the fixture supplies its explicit plans.
The worker's finite original-action programme was exercised locally; its
source and reported branches are preserved with the parent's actual control
proof in `/tmp/dune-classic-nexus-phase-18oct.json`. The executable throwaway
was removed. Observed base33/independent Arrakeen results: five physical counters
for dial6/support3, bank2/personal1; wallet7→8 includes the separate Collection2,
with two board survivors. Independent Reinforcements additionally spends three
real reserves (15→12), producing six total ordinary Tanks counters rather than
three and never adding supported fighters. Without Cunning, dial5/support5
costs personal3, consumes all five board counters plus three reserve fees, and
keeps the now-empty city's card until actual next Mentat sets it aside.
Actual Carthag runs cancel the original Harkonnen call with the genuinely drawn
Harkonnen Nexus. The physical Shield protects its holder from poison: nine
forces and the leader survive, and the Shield stays in the original hand.
All14 uses the actually trained Prana Bindu disc after the original
behind-shield choice. Omitting that defense kills the disc and all ten forces;
Atreides controls Carthag immediately, but the old holder keeps the module card
until Mentat transfers it. Original cancellation replaces one Harkonnen
Traitor at Mentat; Pass instead preserves the single-traitor win, no ordinary
winner dial casualty, unspent Nexus and no replacement.
`tests/nexus-core-strongholds-runtime.test.ts` records payment, physical-counter,
defense, custody and cleanup regressions. Parent frozen43-file439/439,
types/lint/build and actual390px Arrakeen plus trained base33 Carthag controls
pass. Initial fixture pricing/Collection assumptions were repaired against
the original Guild-ally3 tariff and separate native city2 income. No all-six
combined, full-mode, strategic or deployed acceptance claim.
[Shared current proof and cleanup limitations](NEXUS_CARD_RULES.md#classic-nexus-original-phase-modules--18-october-2026).
## All14 Skills with independent cards — 17 October 2026
Supported original classic, selected E1/E2 and standalone/paired E3 all14
+4 -1
View File
@@ -10233,6 +10233,8 @@ function guildBetrayalParentSignature(g: Game, continuation: GuildBetrayalContin
pendingGuildTransport: g.pendingGuildTransport,
...(cunning ? { nexusGuildCunning: cunning } : {}),
...(richese ? { nexusRichese: richese } : {}),
...(g.techTokens ? { techTokens: g.techTokens } : {}),
...(g.strongholdCards ? { strongholdCards: g.strongholdCards } : {}),
});
}
function guildBetrayalCleanParent(g: Game): void {
@@ -10738,7 +10740,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
const homeworlds = homeworldLeaderSkillsProfile(state);
const independent = independentTreacheryLeaderSkillsProfile(state);
if (state.ecazTreachery) {
requireRule(independent, 'Independent Skills cards require supported selected decks with original World/phase modules, or classic Nexus without other overlays; Discovery and unrelated preview contexts remain gated.');
requireRule(independent, 'Independent Skills cards require supported selected decks with original World/phase modules, or classic Nexus with original phase modules; Discovery and unrelated preview contexts remain gated.');
requireFreshBaseRuntime(state);
requireFreshFactionInventory(state);
}
@@ -10800,6 +10802,7 @@ function ecazHomeworldProfile(g: Game): boolean {
}
/** Independent cards compose original phase modules, not unrelated previews. */
function independentCardModulesProfile(g: Game): boolean {
if ((g.techTokens || g.strongholdCards) && classicIndependentNexusProfile(g)) return true;
if ((g.techTokens || g.strongholdCards) && g.leaderSkills && !g.sandtrout &&
independentTreacheryLeaderSkillsProfile(g)) return true;
if (!g.ecazTreachery || (!g.techTokens && !g.strongholdCards) || !independentCardModuleRoster(g) ||
+3 -3
View File
@@ -30,13 +30,13 @@ type ClassicNexusProfile = Partial<FactionModuleProfile> & Pick<FactionModulePro
sandtrout?: unknown; moritaniAssassinatePreview?: unknown;
};
/** Classic Nexus12 with selected base33/independent36 and optional all14 Skills.
/** Classic Nexus12 with selected base33/independent36, optional all14 and original phase modules.
* 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.nexusCards ||
game.expansions.length !== 0 || !players || players.length < 2 || players.length > 6 ||
game.homeworlds || game.techTokens || game.strongholdCards || game.sandtrout ||
game.expansions.length !== 0 || !players || players.length < (game.techTokens ? 3 : 2) || players.length > 6 ||
game.homeworlds || (game.strongholdCards && !game.advanced) || game.sandtrout ||
game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
game.richeseBetrayalPreview ||
+22 -21
View File
@@ -3419,6 +3419,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'The mobile card copies a currently controlled stronghold, even if another faction still holds that location’s card. Holding a card after leaving its location does not itself make that location an eligible copy. The choice is public and fixed before battle plans; a sole eligible advantage is announced automatically.',
'Arrakeen pays only for actual support and creates no personal cash. CHOAM force-payment income includes the bank contribution, subject to its normal traitor and donor exclusions. Carthag does not protect an empty or Worthless defense, stop Poison Tooth, or remove a Shield’s Lasgun explosion risk.',
'Sietch Tabr uses the opposing dial, excluding leaders and other bonuses. A single traitor victory qualifies; mutual destruction does not. Tuek pays after an ordinary loss or a single traitor outcome as well as a win; a double traitor gives neither faction spice. Habbanya also decides a tied Stone Burner comparison.',
'Classic Advanced Nexus core also retains original Strongholds2–6 and optional Tech3–6 with base33 or selected independent36, optionally all14. Earn cards only at actual end-Mentat. Earned Arrakeen composes temporary-Sardaukar support and real Reinforcements reserve fees without making bank money personal; earned Carthag still requires a physical effective defense after an original Harkonnen call is canceled. Original cleanup, token reward, held custody and private Mentat replacement remain.',
'Fresh Advanced all14 Skills uses registered actual seats and selected ordinary decks with original Strongholds2–6 and optional Tech3–6, including its independent-card/World envelope. Native E3 needs no Nexus or exact single family merely to use Skills. Original Nexus/Discovery/Semuta card contexts and Advanced role guards remain separate. Actual end-Mentat claims stay. Arrakeen funds only local holder-plan support, never native World support, personal cash or reserve fees; normal Banker uses the final payer debit. Rescue, cleanup, Tech and Face Dance retain their order.',
'Ecaz/Moritani Skills may include other registered selected-family seats and unused selected ordinary decks, with original Worlds/phase modules and optional independent cards. Native five-disc training and original role exclusions remain. Normal Warmaster one on a temporary Duke never grants trained three. Winner Suk precedes unused-disc assassination, bounty, winner Tech and one Mentat replacement. Ecaz-held support belongs only to the selected Ecaz plan, not an allied plan; fixed and variable armies remain separate. Advanced E3/Harkonnen, shared Duke and effect-specific guards stay.',
'Fresh independent Treachery previews retain classic/E1/E2 and original standalone/paired E3 bare rosters; all14 additionally admits registered mixed selected families. Original Worlds, Advanced Strongholds2–6 and optional Tech3–6 retain actual end-Mentat custody. Reinforcements adds no supported counters; Harass vacates control without transferring a held card early. Exact native fees/returns, card effects and winner Tech remain; bare mixed-E3 World/phase, original Nexus/Discovery card and role/effect guards stay separate.',
@@ -3470,20 +3471,12 @@ export const RULE_TOPICS: RuleTopic[] = [
'All six full effect guides, custody timing, examples and interaction explanations are available here.',
evidence: ['game/reference.ts', 'docs/STRONGHOLD_CARDS.md'],
},
{
area: 'Verification',
status: 'Partial',
detail:
'Dated standalone and E1/E2 Skills/Stronghold cases remain. Standalone E3 adds15 meaningful cases,152 affected, types/lint/build, nine actual programs and eight E3 games (1531 actions/no rejection,38 JSON continuations); the expanded47-game batch completes13936 actions/358 continuations. Human390px normal Duke/Carthag poison survival and Guild Suk before unused-disc assassination preserve original cards/Tech/income/replacement on refresh. Full/deployed acceptance remains unfinished.',
evidence: [
'tests/stronghold-cards.test.ts',
'tests/stronghold-engine.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',
],
},
{area:'Verification',status:'Partial',detail:"Original-action classic Nexus phase sources now earn Arrakeen support beside temporary Sardaukar and real reserve fees, and Carthag Shield poison survival after native call cancellation. Actual normal five-module setup plus earned combat and Axlotl controls complete; four legal policies finish native income and battles. Prior E3 and broader evidence remains dated. Full combinations, deployed acceptance and comprehensive assurance remain open.",evidence:['tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/stronghold-cards.test.ts',
'tests/stronghold-engine.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',]},
],
},
{
@@ -3530,6 +3523,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Token ownership and pending income are public and persist. Fresh registered selected-family all14 keeps original Tech3–6 Basic/Advanced and optional Advanced Strongholds2–6, including supported Worlds and independent cards, without an incidental Nexus or single-family E3 requirement. Original Nexus/Discovery/Semuta and role/effect guards remain. Rescue/board-spice precedes winner Tech and Face Dance. Mandatory normal Banker waits for Mentat, separate from phase-end Tech; fees/H returns are not revival/shipment. Native CHOAM opening income is not Charity. Two-player Tech and full combined acceptance remain unfinished.',
'Without Skills, fresh native E1/E2 families or standalone Ecaz OR Moritani with classic opponents may preserve unused original Tech Tokens through the local factions entry, Basic/Advanced three through six players; Advanced stronghold-factions adds the original six Stronghold Cards. Native payments, typed casualties, selected Ecaz lead rewards and Moritani original assassination remain separate. Split allied holdings never qualify as a complete set. Basic odd Occupy, Richese cache/No-Field rulings, unrelated overlays, public starts and saved-game conversion remain guarded.',
'Classic/E1/E2 and original standalone/paired E3 bare independent-card previews retain Worlds, Tech3–6 and optional Advanced Strongholds2–6. All14 extends this to registered mixed selected families. Recruits changes current native rates without repeating Axlotl or advancing payout. Harass deposits and reserve fees are not free Revival or off-planet shipment. Original phase income/native custody/winner rewards remain; bare mixed-E3 World/phase and unrelated module/role guards stay separate.',
'Classic Nexus core composes original Tech3–6 Basic/Advanced and optional Advanced Strongholds2–6 with base33 or selected independent36, optionally all14. Real first-Storm owners and actual token counts set income. A paid fixed Tleilaxu force group does not become free because Recruits is active; subsequent genuine free returns accrue one Axlotl pile, not one pile per request. Pending spice cannot fund Revival before phase end. Harass returns and reserve fees are not token-triggering Revival or shipment; actual battle winners take one original opposing token.',
],
example:
'You own Heighliners and Axlotl Tanks. When Emperor ships from reserves, place two spice on Heighliners. A later shipment adds nothing. Collect the two spice when Shipping and Movement ends.',
@@ -3541,6 +3535,13 @@ export const RULE_TOPICS: RuleTopic[] = [
'victory',
'ix-modules',
],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Original first-Storm ownership, owner-count pending income, matching phase-end payout and native winner transfer remain. Classic Nexus core now admits original Tech with selected33/36 and optional14, preserving funded card shipment and fixed-versus-free Revival sources. Other module/native envelopes keep their bounds.', evidence: ['game/tech-tokens.ts', 'game/engine.ts', 'game/phase-resource-quote.ts', 'game/revival-resume.ts', 'game/faction-module-profile.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Actual normal five-module lobby reaches original training/Storm. Native fixed-five and ordinary-free controls show original prices, physical Tanks and pending pile separately; actual phase close pays it. Stronghold and card inspectors retain their own entitlements.', evidence: ['components/game-table.tsx', 'tests/fixture-nexus-core-tech.ts'] },
{ area: 'AI', status: 'Partial', detail: 'Four existing own-view policies legally claim funded card shipment, complete real Revival into phase-end payout and finish earned Stronghold/Nexus battles. No strategic calibration or fixed-choice pin.', evidence: ['game/bots.ts', 'tests/fixture-nexus-core-tech.ts', 'tests/fixture-nexus-core-strongholds.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Printed trigger/exception, owner count, pending-versus-spendable spice, original payout, winner reward and complete-set victory remain distinct. Current classic core phase scope and inherited timing are explicit; full module/faction combinations and assurance remain open.', evidence: ['game/reference.ts', 'docs/NEXUS_CARD_RULES.md', 'docs/STRONGHOLD_CARDS.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Original-action consumers cover funded five-for-one delivery, paid fixed5 versus free2, a second genuine free request, pending-income affordability and Harass/winner transfer. Actual all14 Revival UI and four legal policies reach payout; older broader and saved evidence remains dated.', evidence: ['tests/nexus-core-tech-runtime.test.ts', 'tests/fixture-nexus-core-tech.ts', 'tests/tech-tokens.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts'] },
],
},
{
id: 'captured-leaders',
@@ -4540,7 +4541,7 @@ export const RULE_TOPICS: RuleTopic[] = [
developmentStage: 'Verified',
summary: 'An unallied rival holding Guild Nexus may take one full original positive shipment payment, including their own pre-funded payment, instead of Guild, bank or occupied Junction income in fresh classic core or supported original standalone entries.',
steps: [
'Fresh classic2–6 Basic/Advanced Nexus core uses 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.',
'Fresh classic2–6 Basic/Advanced Nexus core uses base33 or selected independent36, optionally all14 Skills, original Tech3+ and Advanced Strongholds2+. Actual Guild receives original fee state at fresh setup alongside supported Harkonnen/Ixian reactions; played games are not reflagged. Original explicit native Guild/Homeworld entry retains its seeded boundary. Other selected families, Worlds, Discovery/Semuta and reaction envelopes stay separate.',
'Remain unallied and hold the actual singleton Guild Nexus while another seated player is native Guild. Use spends that physical Nexus once, with no Treachery Card, hand slot or extra activation spice. Cunning and absent-Guild Secret Ally are separate modes. An empty Guild board or temporary leader control does not change the mode.',
'Within classic core, original Guild Cunning second shipments and Richese Secret Ally discounted shipments retain their actual signed grant, physical group, paid tariff and native continuation through the same funded fee gate. One Guild singleton cannot simultaneously fund native Cunning and rival Betrayal Use; other held faces retain neutral Pass. Outside core, original producer envelopes remain separate. Guild Secret Ally still requires Guild to be absent and is not another native-Guild producer.',
'Wait for a legal positive shipment fee after any applicable native stop and rate responses. The original source, tariff and physical forces must already be legal and fully funded. Stopped, declined, invalid or zero-cost shipments cannot award spice or spend this Nexus; ordinary free Fremen reinforcement and free Guild Ambassador transport stay distinct.',
@@ -4574,7 +4575,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. 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.',
'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 the same effect with base33 or selected36, optional14, original Tech3+ and Advanced Strongholds2+; independent cards are not required. Other native/module envelopes keep their bounds; 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.',
@@ -4643,7 +4644,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 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.',
'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 selected36, optional14, original Tech3+ and Advanced Strongholds2+, alongside actual Guild/Ixian reactions. Public selection binds the census; real held-card effects and cleanup stay native. No played game is reflagged; other native/module envelopes remain separate.',
'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.',
@@ -4675,7 +4676,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'A separate twelve-card deck rewards remaining unallied. Private draws and selected Atreides, Harkonnen, Tleilaxu, Ixian, Bene Gesserit, Emperor, Fremen, CHOAM, Moritani, Richese and Guild effects are connected. Other effects and complete module play remain in development.',
steps: [
'This optional module can be combined with Basic or Advanced rules independently of the factions and other expansion modules selected. Include one Nexus Card for each of the twelve factions.',
'Fresh 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.',
'Fresh classic2–6 Basic/Advanced Nexus core admits base33 or selected independent36, optionally all14, original Tech3+ and Advanced Strongholds2+. Actual Treachery, twelve Nexus, fourteen Skills, three Tech and six held-card stocks remain distinct; no selection is silently added. Original fresh Guild/Harkonnen/Ixian states use actual role/card authority. Starting hands/training, real closing draws, first-Storm tokens and end-Mentat claims stay native. Other families, Worlds, Discovery/Semuta and reaction envelopes remain separate.',
'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.',
@@ -4749,7 +4750,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:"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-nexusLine truncated
{area:"Verification",status:'Partial',detail:"Classic core now composes original Tech3+ and Advanced Strongholds2+ with base33/selected36 and optional14. Real first-Storm owners, phase-end income and first-Mentat held cards precede actual card grants, funded receipts, support and casualties. Four390px paths and four legal policies complete earned income/combat; new regressions cover fixed/free returns, spendability, one-pile accrual, native winner transfer and held custody. Other native/World/Discovery/Semuta envelopes and full assurance remain open.",evidence:['tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', 'tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SLine truncated
],
},
{
@@ -4935,7 +4936,7 @@ 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.',
'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.',
'Classic Nexus core with base33 or selected36 permits optional all14, original Tech3+ and Advanced Strongholds2+. Offers, starting-card order, mandatory normal Banker and native reactions remain. Actual third-party Bureaucrat sees the real Guild-card recipient. Tech pays only at its matching phase end; Banker waits for Mentat. Earned local support does not become personal money or reserve fees. Selection/printed roles are public capabilities; other native/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.',
@@ -4977,7 +4978,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:"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']},
{area:'Verification',status:'Partial',detail:"Classic Nexus optional14 now retains original Tech3+ and Advanced Strongholds2+ with selected33/36. Mandatory Banker remains separate from native phase income; actual third-party receipts, trained-disc posture, local bank support and original cleanup/replacement stay distinct. Real normal lobby, earned Carthag/Arrakeen and fixed/free Axlotl controls complete with four legal policies. Earlier native/Richese evidence remains dated; full modes, strategy and deferred assurance stay open.",evidence:['tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', 'tests/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']},
],
},
{
@@ -49,7 +49,7 @@ export function drawClassicNexusReaction(face: FactionId, owner: string,
return { ...fixture, game, owner, actions, beforeDraw, afterDraw: structuredClone(game) };
}
export function createIndependentHarkonnenCall(options: { advanced?: boolean; skills?: boolean; remote?: boolean; independent?: boolean } = {}) {
export function createIndependentHarkonnenCall(options: { advanced?: boolean; skills?: boolean; remote?: boolean; independent?: boolean; tech?: boolean; strongholds?: boolean } = {}) {
const counter = baseDeck().find(card => card.effect === 'karama')!.id;
const starting = options.independent === false ? baseDeck().find(card => card.kind === 'worthless')!.id : HARASS_WITHDRAW_CARD;
const fixture = drawClassicNexusReaction('harkonnen', 'atreides', { ...options,
@@ -98,7 +98,7 @@ 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'; independent?: boolean } = {}) {
export function createIndependentIxianPurchase(options: { advanced?: boolean; skills?: boolean; owner?: 'atreides' | 'harkonnen'; independent?: boolean; tech?: boolean; strongholds?: boolean } = {}) {
const owner = options.owner ?? 'atreides';
const starting = options.independent === false ? baseDeck().find(card => card.kind === 'worthless')!.id : 'ecaz-recruits';
const fixture = drawClassicNexusReaction('ixians', owner, { ...options,
@@ -14,6 +14,8 @@ export type ClassicIndependentNexusOptions = {
skills?: boolean;
/** False exercises the original base33 without selecting the independent variant. */
independent?: boolean;
tech?: boolean;
strongholds?: boolean;
roster?: readonly FactionId[];
training?: Partial<Record<FactionId, LeaderSkillId>>;
/** Only original, still-undealt starting Treachery slots are ordered. */
@@ -36,6 +38,8 @@ export function createClassicIndependentNexusFixture(options: ClassicIndependent
};
for (const option of [...(independent ? ['ecazTreachery'] : []), 'nexusCards', ...(options.skills ? ['leaderSkills'] : [])])
act(game.host, { type: 'moduleOption', module: option, enabled: true });
if (options.tech) act(game.host, { type: 'techTokens', enabled: true });
if (options.strongholds) act(game.host, { type: 'strongholdCards', 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) : [];
+1 -1
View File
@@ -7,7 +7,7 @@ import { drawClassicNexusReaction, reactionAction, reactionPlayer } from './fixt
import { advanceClassicIndependentNexus, type ClassicIndependentNexusOptions, type ClassicIndependentNexusStep } from './fixture-classic-independent-nexus';
import { classicDiscoveryNexusClean } from './fixture-discovery-classic-nexus';
export type ClassicNexusCoreOptions = Pick<ClassicIndependentNexusOptions, 'advanced' | 'skills' | 'independent' | 'training'>;
export type ClassicNexusCoreOptions = Pick<ClassicIndependentNexusOptions, 'advanced' | 'skills' | 'independent' | 'training' | 'tech' | 'strongholds'>;
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);
+171
View File
@@ -0,0 +1,171 @@
import assert from 'node:assert/strict';
import { viewGame, type Game } from '../game/engine';
import { baseDeck } from '../game/cards';
import { territory } from '../game/board';
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
import { createClassicIndependentNexusFixture, advanceClassicIndependentNexus,
type ClassicIndependentNexusStep } from './fixture-classic-independent-nexus';
import { drawClassicNexusReaction, reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
import { classicDiscoveryNexusClean as clean, nextClassicDiscoveryNexusStep,
applyClassicDiscoveryNexusStep } from './fixture-discovery-classic-nexus';
export { reactionPlayer as nexusCoreStrongholdPlayer };
export type NexusCoreStrongholdOptions = { independent?: boolean; skills?: boolean };
export type NexusCoreStrongholdFixture = {
game: Game; setup: Game; actions: ClassicIndependentNexusStep[]; staging: string[];
claimed: Game; beforeShipment: Game; afterShipment: Game;
owner: string; opponent: string; key: string; ownerLeader: string; opponentLeader: string;
weapon: string | null; defense: string | null;
};
function prepare(game: Game, actions: ClassicIndependentNexusStep[]): Game {
for (let count = 0; count < 120; count++) {
const next = nextClassicDiscoveryNexusStep(game);
if (!next) {
assert.ok(game.battle && !game.battle.revealed && clean(game));
return game;
}
// The original decision must put the trained disc behind the shield
// before it can be selected in a sealed plan.
if (next.action.type === 'leaderSkillVisibility') next.action.hide = true;
game = applyClassicDiscoveryNexusStep(game, next, actions);
}
throw Error('Original Stronghold battle did not reach its sealed-plan boundary');
}
function bestLeader(game: Game, actor: string): string {
const leader = reactionPlayer(game, actor).leaders.filter(disc => !disc.dead && !disc.usedAt)
.sort((one, two) => two.strength - one.strength)[0];
assert.ok(leader);
return leader.id;
}
function chooseBattle(game: Game, location: string, actions: ClassicIndependentNexusStep[]): Game {
return prepare(reactionAction(game, game.active!, { type: 'chooseBattle', territory: location,
target: game.active === 'emperor' ? 'harkonnen' : 'emperor' }, actions), actions);
}
/** Emperor genuinely ships five original reserves into empty Arrakeen on turn
* one. Only unopened Spice/Nexus decks are ordered; every counter, payment,
* alliance and first end-Mentat card claim comes from original actions. */
export function createNexusCoreArrakeenFixture(options: NexusCoreStrongholdOptions = {}): NexusCoreStrongholdFixture {
const independent = options.independent ?? true;
const original = createClassicIndependentNexusFixture({ advanced: true, independent,
skills: options.skills, strongholds: true, roster: ['emperor', 'harkonnen', 'guild'],
startingCards: independent ? { emperor: [REINFORCEMENTS_CARD] } : undefined });
const actions = original.actions;
let game = original.game;
const take = (worm: boolean) => {
const index = game.spiceDeck.findIndex(card => worm ? 'worm' in card : 'territory' in card);
assert.ok(index >= 0);
return game.spiceDeck.splice(index, 1)[0];
};
game.spiceDeck.unshift(take(false), take(false), take(true), take(false), take(false));
game = advanceClassicIndependentNexus(game, state => state.turn === 1 && state.phase === 5 &&
state.active === 'emperor' && clean(state), actions);
const sector = territory('arrakeen').sectors[0], key = `arrakeen:${sector}`;
assert.ok(!territory('arrakeen').sectors.includes(game.storm));
game = reactionAction(game, 'emperor', { type: 'ship', territory: 'arrakeen', sector,
amount: 5, elite: 0, allyPayment: 0 }, actions);
game = advanceClassicIndependentNexus(game, state => state.strongholdCards?.claimedTurn === 1, actions);
const claimed = structuredClone(game);
assert.equal(claimed.strongholdCards!.owners.arrakeen, 'emperor');
game = advanceClassicIndependentNexus(game, state => state.turn === 2 && state.nexus &&
!state.spiceWindow && !state.spiceResolution && clean(state), actions);
game = reactionAction(game, 'harkonnen', { type: 'alliance', target: 'guild' }, actions);
game = reactionAction(game, 'guild', { type: 'alliance', target: 'harkonnen' }, actions);
game = advanceClassicIndependentNexus(game, state => state.nexusCards?.phase?.stage === 'drawing' && clean(state), actions);
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf('emperor');
assert.ok(index >= 0);
cards.deck.unshift(cards.deck.splice(index, 1)[0]);
game = reactionAction(game, 'emperor', { type: 'nexusCardChoice', turn: game.turn,
card: cards.hands.emperor, choice: 'draw', ownRedraws: 0 }, actions);
game = advanceClassicIndependentNexus(game, state => state.phase === 5 && state.active === 'harkonnen' && clean(state), actions);
assert.ok(!territory('arrakeen').sectors.includes(game.storm));
const beforeShipment = structuredClone(game);
game = reactionAction(game, 'harkonnen', { type: 'ship', territory: 'arrakeen', sector,
amount: 5, elite: 0, allyPayment: 0 }, actions);
game = advanceClassicIndependentNexus(game, state => state.phase === 6 && clean(state), actions);
const afterShipment = structuredClone(game);
game = chooseBattle(game, 'arrakeen', actions);
return { ...original, game, actions, claimed, beforeShipment, afterShipment,
owner: 'emperor', opponent: 'harkonnen', key, ownerLeader: bestLeader(game, 'emperor'),
opponentLeader: bestLeader(game, 'harkonnen'), weapon: independent ? REINFORCEMENTS_CARD : null,
defense: null };
}
/** Harkonnen retains its genuinely earned Carthag card. Atreides draws the
* physical Harkonnen Nexus, ships one counter and plays an actually offered
* Harkonnen Traitor. Optional all14 uses the original trained-disc choice. */
export function createNexusCoreCarthagFixture(options: NexusCoreStrongholdOptions = {}): NexusCoreStrongholdFixture {
const poison = baseDeck().find(card => card.kind === 'poison')!.id;
const shield = baseDeck().find(card => card.kind === 'shield')!.id;
const original = drawClassicNexusReaction('harkonnen', 'atreides', { advanced: true,
independent: options.independent ?? true, skills: options.skills, strongholds: true,
training: options.skills ? { harkonnen: 'prana-bindu-adept' } : undefined,
startingCards: { atreides: [poison], harkonnen: [shield] } });
const actions = original.actions;
const claimed = structuredClone(original.beforeDraw);
assert.equal(claimed.strongholdCards!.owners.carthag, 'harkonnen');
let game = advanceClassicIndependentNexus(original.game, state => state.phase === 5 &&
state.active === 'atreides' && clean(state), actions);
const opponentLeader = reactionPlayer(game, 'atreides').leaders.find(leader =>
reactionPlayer(game, 'harkonnen').traitors.includes(leader.id))?.id;
assert.ok(opponentLeader, 'Use the actual offered Traitor, never assign a leader identity');
const sector = territory('carthag').sectors[0], key = `carthag:${sector}`;
assert.ok(!territory('carthag').sectors.includes(game.storm));
const beforeShipment = structuredClone(game);
game = reactionAction(game, 'atreides', { type: 'ship', territory: 'carthag', sector,
amount: 1, elite: 0, allyPayment: 0 }, actions);
game = advanceClassicIndependentNexus(game, state => state.phase === 6 && clean(state), actions);
const afterShipment = structuredClone(game);
game = reactionAction(game, game.active!, { type: 'chooseBattle', territory: 'carthag',
target: game.active === 'atreides' ? 'harkonnen' : 'atreides' }, actions);
game = prepare(game, actions);
const trained = game.leaderSkills?.assignments.find(assignment => assignment.owner === 'harkonnen');
return { ...original, game, claimed, beforeShipment, afterShipment, owner: 'harkonnen',
opponent: 'atreides', key, ownerLeader: trained?.leader ?? bestLeader(game, 'harkonnen'),
opponentLeader, weapon: poison, defense: shield };
}
/** Return the revealed original battle, before any Traitor vote or cleanup. */
export function revealNexusCoreStrongholdBattle(fixture: NexusCoreStrongholdFixture,
options: { cunning?: boolean; shield?: boolean; ownerDial?: number; opponentDial?: number } = {}): Game {
let game = structuredClone(fixture.game);
const arrakeen = fixture.key.startsWith('arrakeen:');
if (arrakeen && options.cunning !== false) {
const offer = viewGame(game, fixture.owner).nexusSardaukar!.offer!;
assert.equal(offer.blocked, null);
game = reactionAction(game, fixture.owner, { type: 'nexusSardaukar', event: offer.event });
game = advanceClassicIndependentNexus(game, clean);
}
const ownerDial = options.ownerDial ?? (arrakeen ? 6 : 0.5);
const opponentDial = options.opponentDial ?? (arrakeen ? 1 : 0);
for (const actor of [game.battle!.attacker, game.battle!.defender]) {
const owner = actor === fixture.owner;
game = reactionAction(game, actor, { type: 'battlePlan',
dial: owner ? ownerDial : opponentDial,
support: owner ? arrakeen ? options.cunning === false ? ownerDial : ownerDial / 2 : 0 : opponentDial,
leader: owner ? fixture.ownerLeader : fixture.opponentLeader,
weapon: arrakeen ? owner ? fixture.weapon : null : owner ? null : fixture.weapon,
defense: !arrakeen && owner && options.shield !== false ? fixture.defense : null });
while (game.response || game.phaseOpening || game.decision?.kind === 'fullPlanOffer') {
const next = nextClassicDiscoveryNexusStep(game); assert.ok(next);
game = applyClassicDiscoveryNexusStep(game, next);
}
}
assert.ok(game.battle?.revealed);
return game;
}
/** Original votes and native cleanup; no plan or strategy is invented. */
export function finishNexusCoreStrongholdBattle(game: Game, stopAtCleanup = false): Game {
return advanceClassicIndependentNexus(game, state => (!state.battle && clean(state)) ||
(stopAtCleanup && state.decision?.kind === 'battleCards'));
}
/** Preserve the original native Harkonnen call and pending cancellation window. */
export function callNexusCoreCarthagTraitor(game: Game): Game {
for (const voter of viewGame(game, 'atreides').battle!.traitorVoters)
if (voter !== 'harkonnen' && game.battle!.traitorCalls[voter] === undefined)
game = reactionAction(game, voter, { type: 'traitorCall', call: false });
return reactionAction(game, 'harkonnen', { type: 'traitorCall', call: true });
}
+123
View File
@@ -0,0 +1,123 @@
import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import { TERRITORIES } from '../game/board';
import { treacheryDeck } from '../game/cards';
import { ownedTech, type TechId } from '../game/tech-tokens';
import { createClassicNexusGuildPayment, type ClassicNexusCoreOptions } from './fixture-classic-nexus-core';
import { createIndependentHarkonnenCall, reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
import { advanceClassicIndependentNexus, createClassicIndependentNexusFixture,
type ClassicIndependentNexusStep } from './fixture-classic-independent-nexus';
import { classicDiscoveryNexusClean, nextClassicDiscoveryNexusStep } from './fixture-discovery-classic-nexus';
/** Read the actual first-Storm custody; never select or retrofit a token owner. */
export function nexusCoreTechHolding(game: Game, token: TechId) {
const owner = game.techTokens?.[token].owner;
assert.ok(owner, 'The original first Storm must assign this physical token');
return { owner, count: ownedTech(game.techTokens, owner).length };
}
/** A real five-counter Richese order, stopped before its funded Guild invoice. */
export function createNexusCoreTechShipment(options: ClassicNexusCoreOptions = {}) {
const fixture = createClassicNexusGuildPayment({ independent: true, ...options, tech: true });
return { ...fixture, token: nexusCoreTechHolding(fixture.game, 'heighliners') };
}
/** Original Harass plan and native Harkonnen declaration, before Nexus Use/Pass.
* The reused producer forwards these original module options into its lobby. */
export function createNexusCoreTechHarass() {
const options = { tech: true, independent: true };
return createIndependentHarkonnenCall(options);
}
/** Actual battle losses supply eight Tanks and one own dead leader. Recruits
* stays in its original starting hand, and a real closing draw supplies Tleilaxu.
* Only unopened original Spice/Nexus singletons are positioned. With `drain`,
* a genuine turn-one four-counter desert shipment spends eight of Atreides'
* own spice so later Revival prices face a genuinely thin wallet. */
export function createNexusCoreTechRevival(options: Pick<ClassicNexusCoreOptions,
'advanced' | 'skills' | 'training'> & { drain?: boolean } = {}) {
const weapon = treacheryDeck([]).find(card => card.kind === 'projectile')!;
const harmless = treacheryDeck([]).find(card => card.kind === 'worthless')!;
const { drain, ...modules } = options;
const fixture = createClassicIndependentNexusFixture({ advanced: true, skills: true, ...modules,
independent: true, tech: true,
training: options.training ?? { atreides: 'diplomat', guild: 'mentat', emperor: 'sandmaster' },
startingCards: { atreides: ['ecaz-recruits'], guild: [harmless.id], emperor: [weapon.id] } });
const actions = fixture.actions;
let game = fixture.game;
const take = (worm: boolean) => {
const index = game.spiceDeck.findIndex(card => worm
? 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper
: 'territory' in card);
assert.ok(index >= 0);
return game.spiceDeck.splice(index, 1)[0];
};
game.spiceDeck.unshift(take(false), ...(game.advanced ? [take(false)] : []),
take(true), take(false), ...(game.advanced ? [take(false)] : []));
if (drain) {
game = advanceClassicIndependentNexus(game, state => state.turn === 1 &&
state.phase === 5 && state.active === 'atreides' && classicDiscoveryNexusClean(state), actions);
const desert = TERRITORIES.find(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(desert);
const wallet = reactionPlayer(game, 'atreides').spice;
assert.ok(wallet >= 8, 'The genuine drain shipment needs its real turn-one wallet');
game = reactionAction(game, 'atreides', { type: 'ship', territory: desert.id,
sector: desert.sectors[0], amount: 4, elite: 0, allyPayment: 0 }, actions);
game = advanceClassicIndependentNexus(game, state => state.turn === 1 && state.phase === 5 &&
state.active !== 'atreides' && classicDiscoveryNexusClean(state), actions);
assert.equal(reactionPlayer(game, 'atreides').spice, wallet - 8);
}
game = advanceClassicIndependentNexus(game, state => state.phase === 5 &&
state.active === 'emperor' && classicDiscoveryNexusClean(state), actions);
const beforeShipment = structuredClone(game);
game = reactionAction(game, 'emperor', { type: 'ship', territory: 'arrakeen', sector: 10,
amount: 1, elite: 0, allyPayment: 0 }, actions);
game = advanceClassicIndependentNexus(game, state => state.phase === 6 && classicDiscoveryNexusClean(state), actions);
if (!game.battle) game = reactionAction(game, game.active!, { type: 'chooseBattle', territory: 'arrakeen',
target: game.active === 'atreides' ? 'emperor' : 'atreides' }, actions);
game = advanceClassicIndependentNexus(game, state => !!state.battle && !state.battle.revealed &&
classicDiscoveryNexusClean(state) && !nextClassicDiscoveryNexusStep(state), actions);
const leader = reactionPlayer(game, 'atreides').leaders.filter(disc => !disc.dead)
.sort((a, b) => a.strength - b.strength)[0].id;
const opponentLeader = reactionPlayer(game, 'emperor').leaders.filter(disc => !disc.dead)
.sort((a, b) => a.strength - b.strength)[0].id;
game = reactionAction(game, 'atreides', { type: 'battlePlan', dial: game.advanced ? 4 : 8, support: 0,
leader, weapon: null, defense: null }, actions);
game = reactionAction(game, 'emperor', { type: 'battlePlan', dial: 0, support: 0,
leader: opponentLeader, weapon: weapon.id, defense: null }, actions);
game = advanceClassicIndependentNexus(game, state => state.turn === 2 && state.nexus &&
!state.spiceWindow && !state.spiceResolution && classicDiscoveryNexusClean(state), actions);
const afterBattle = structuredClone(game);
game = reactionAction(game, 'guild', { type: 'alliance', target: 'emperor' }, actions);
game = reactionAction(game, 'emperor', { type: 'alliance', target: 'guild' }, actions);
game = advanceClassicIndependentNexus(game, state => state.nexusCards?.phase?.stage === 'drawing', actions);
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf('tleilaxu');
assert.ok(index >= 0);
cards.deck.unshift(cards.deck.splice(index, 1)[0]);
const beforeDraw = structuredClone(game);
game = reactionAction(game, 'atreides', { type: 'nexusCardChoice', turn: game.turn,
card: cards.hands.atreides, choice: 'draw', ownRedraws: 0 }, actions);
const afterDraw = structuredClone(game);
game = advanceClassicIndependentNexus(game, state => state.phase === 4 && classicDiscoveryNexusClean(state), actions);
const offer = viewGame(game, 'atreides').nexusTleilaxu!.secretAlly!;
assert.equal(offer.blocked, null);
const action: Action = { type: 'nexusTleilaxuRevival', event: offer.event, leader, amount: 5, elite: 0 };
return { ...fixture, game, actions, beforeShipment, afterBattle, beforeDraw, afterDraw,
leader, action,
token: nexusCoreTechHolding(game, 'axlotl') };
}
/** Retain the exact native phase-ending action and its pre-payout wallets. */
export function finishNexusCoreTechPhase(state: Game, actions: ClassicIndependentNexusStep[] = []) {
let game = state;
const phase = game.phase;
for (let count = 0; count < 300; count++) {
const before = structuredClone(game), step = nextClassicDiscoveryNexusStep(game);
assert.ok(step, 'Finish the real pending control before leaving this phase');
game = reactionAction(game, step.actor, step.action, actions);
if (game.phase !== phase) return { before, after: game, step, actions };
}
throw new Error('The native phase did not finish');
}
@@ -0,0 +1,161 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { maxCombatDial } from '../game/combat';
import { strongholdControllers } from '../game/stronghold-cards';
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
import { advanceClassicIndependentNexus } from './fixture-classic-independent-nexus';
import { reactionAction } from './fixture-classic-independent-nexus-reactions';
import { classicDiscoveryNexusClean as clean } from './fixture-discovery-classic-nexus';
import { createNexusCoreArrakeenFixture, createNexusCoreCarthagFixture,
revealNexusCoreStrongholdBattle, finishNexusCoreStrongholdBattle,
callNexusCoreCarthagTraitor, nexusCoreStrongholdPlayer as player } from './fixture-nexus-core-strongholds';
function physicalCounters(game: Game): void {
for (const seat of game.players)
assert.equal(seat.reserves + seat.tanks + Object.values(seat.forces).reduce((sum, count) => sum + count, 0), 20);
}
function nextMentat(game: Game): Game {
return advanceClassicIndependentNexus(game, state => state.turn === 3 &&
state.strongholdCards?.claimedTurn === 2 && clean(state));
}
for (const independent of [false, true]) {
void test(`earned Arrakeen pays only actual holder support beside native Emperor Cunning${independent ? ' and three real Reinforcements reserves' : ' on base33'}`, () => {
const f = createNexusCoreArrakeenFixture({ independent });
assert.equal(f.setup.strongholdCards!.claimedTurn, 0);
assert.ok(Object.values(f.setup.strongholdCards!.owners).every(owner => owner === null));
assert.equal(f.claimed.strongholdCards!.owners.arrakeen, f.owner);
assert.equal(player(f.claimed, f.owner).forces[f.key], 5);
assert.equal(player(f.claimed, f.owner).reserves, player(f.setup, f.owner).reserves - 5,
'First end-Mentat ownership follows an original five-counter shipment');
const before = player(f.game, f.owner), enemy = player(f.game, f.opponent);
assert.equal(player(f.afterShipment, f.opponent).reserves, player(f.beforeShipment, f.opponent).reserves - 5);
assert.equal(player(f.afterShipment, f.opponent).spice, player(f.beforeShipment, f.opponent).spice - 3,
'The real reciprocal Guild alliance retains the rounded half-price tariff for five counters');
const revealed = revealNexusCoreStrongholdBattle(f);
const forces = viewGame(revealed, f.owner).battle!.ownForces!;
assert.equal(forces.normal, 5);
assert.equal(forces.temporaryElite, 5);
assert.equal(maxCombatDial(forces), 10);
assert.equal(revealed.battle!.plans[f.owner].dial, 6);
assert.equal(revealed.battle!.plans[f.owner].support, 3);
assert.equal(player(revealed, f.owner).spice, before.spice,
'Sealing the plan neither pays support early nor credits bank money to the holder');
const done = finishNexusCoreStrongholdBattle(revealed);
assert.equal(done.lastBattleContext!.winner, f.owner);
assert.equal(player(done, f.owner).spice, before.spice - 1 + 2,
'Actual support3 uses bank2/personal1 before separate native Arrakeen Collection2');
assert.equal(player(done, f.opponent).spice, enemy.spice - 1 + 2,
'The opposing Carthag holder pays personal support1 in Arrakeen before its separate native city Collection2');
assert.equal(player(done, f.owner).reserves, before.reserves - (independent ? 3 : 0));
assert.equal(player(done, f.owner).tanks, before.tanks + 3 + (independent ? 3 : 0),
'Three dialed temporary-Sardaukar casualties and three reserve fees retain distinct sources');
assert.equal(player(done, f.owner).forces[f.key], 2,
'Reinforcements never adds supported board counters');
assert.deepEqual(player(done, f.owner).elites, before.elites);
assert.equal(player(done, f.opponent).tanks, enemy.tanks + 5);
assert.equal(done.nexusCards!.cards!.hands[f.owner], null);
assert.equal(done.nexusCards!.cards!.discard.filter(face => face === 'emperor').length, 1);
assert.equal(done.discard.filter(card => card.id === REINFORCEMENTS_CARD).length, independent ? 1 : 0);
assert.equal(done.nexusSardaukarHistory!.length, 1);
assert.equal(done.nexusSardaukarHistory![0].casualties!.outcome, 'complete');
assert.equal(done.strongholdCards!.owners.arrakeen, f.owner);
assert.equal(done.phase, 7);
physicalCounters(done);
});
}
void test('Arrakeen bank support cannot replace the Reinforcements reserve fee or ordinary five-counter dial losses when Cunning is not used', () => {
const f = createNexusCoreArrakeenFixture();
const before = player(f.game, f.owner);
const done = finishNexusCoreStrongholdBattle(revealNexusCoreStrongholdBattle(f, { cunning: false, ownerDial: 5 }));
assert.equal(done.lastBattleContext!.winner, f.owner);
assert.equal(player(done, f.owner).spice, before.spice - 3,
'Support5 costs personal3 after bank2; no surviving city counter means no Collection2');
assert.equal(player(done, f.owner).reserves, before.reserves - 3);
assert.equal(player(done, f.owner).tanks, before.tanks + 8);
assert.equal(player(done, f.owner).forces[f.key] ?? 0, 0);
assert.equal(done.nexusCards!.cards!.hands[f.owner], 'emperor');
assert.equal(done.strongholdCards!.owners.arrakeen, f.owner,
'Vacating the city through actual dial casualties does not transfer held custody early');
const settled = nextMentat(done);
assert.equal(settled.strongholdCards!.owners.arrakeen, null);
physicalCounters(settled);
});
for (const skills of [false, true]) {
void test(`earned Carthag physical Shield survives poison after an original Harkonnen call is canceled${skills ? ' with its actually trained all14 disc behind the shield' : ' on base33'}`, () => {
const f = createNexusCoreCarthagFixture({ skills, independent: skills });
const before = player(f.game, f.owner);
assert.equal(f.claimed.strongholdCards!.owners.carthag, f.owner);
assert.equal(f.claimed.strongholdCards!.claimedTurn, 1);
assert.equal(player(f.afterShipment, f.opponent).reserves, player(f.beforeShipment, f.opponent).reserves - 1);
assert.equal(player(f.afterShipment, f.opponent).spice, player(f.beforeShipment, f.opponent).spice - 1);
const revealed = revealNexusCoreStrongholdBattle(f);
const called = callNexusCoreCarthagTraitor(revealed);
assert.equal(player(called, f.opponent).leaders.find(disc => disc.id === f.opponentLeader)!.dead, false,
'The original call is pending, not already a paid traitor outcome');
const event = called.pendingNexusHarkonnenBetrayal!.source.event;
const canceled = reactionAction(called, f.opponent, { type: 'nexusHarkonnenBetrayalUse', event });
assert.equal(canceled.nexusCards!.cards!.hands[f.opponent], null);
assert.equal(player(canceled, f.owner).traitors.includes(f.opponentLeader), false);
assert.equal(canceled.traitorReserve!.includes(f.opponentLeader), true);
const done = finishNexusCoreStrongholdBattle(canceled);
assert.equal(done.lastBattleContext!.winner, f.owner);
assert.equal(player(done, f.owner).leaders.find(disc => disc.id === f.ownerLeader)!.dead, false);
assert.equal(player(done, f.owner).forces[f.key], before.forces[f.key] - 1);
assert.equal(player(done, f.owner).tanks, before.tanks + 1);
assert.equal(player(done, f.owner).reserves, before.reserves);
assert.equal(player(done, f.owner).hand.some(card => card.id === f.defense), true,
'Snooper protection never replaces the physical Shield or moves it to a new hand');
assert.equal(player(done, f.opponent).tanks, player(f.game, f.opponent).tanks + 1);
const top = done.traitorReserve![0];
const mentat = nextMentat(done);
assert.equal(mentat.nexusHarkonnenReplacementHistory!.length, 1);
assert.equal(mentat.nexusHarkonnenReplacementHistory![0].drawn, top);
assert.equal(player(mentat, f.owner).traitors.length, before.traitors.length);
assert.equal(mentat.strongholdCards!.owners.carthag, f.owner);
physicalCounters(mentat);
});
}
void test('a retained Carthag card cannot protect an empty defense; actual loss changes control now but held custody only at Mentat', () => {
const f = createNexusCoreCarthagFixture({ skills: true });
let game = callNexusCoreCarthagTraitor(revealNexusCoreStrongholdBattle(f, { shield: false }));
game = reactionAction(game, f.opponent, { type: 'nexusHarkonnenBetrayalUse',
event: game.pendingNexusHarkonnenBetrayal!.source.event });
const done = finishNexusCoreStrongholdBattle(game);
assert.equal(done.lastBattleContext!.winner, f.opponent);
assert.equal(player(done, f.owner).leaders.find(disc => disc.id === f.ownerLeader)!.dead, true);
assert.equal(player(done, f.owner).forces[f.key] ?? 0, 0);
assert.equal(player(done, f.owner).tanks, player(f.game, f.owner).tanks + 10);
assert.equal(player(done, f.owner).hand.some(card => card.id === f.defense), true,
'An unplayed Shield stays physically held and grants no Carthag defense');
assert.equal(strongholdControllers(done.players, false).carthag, f.opponent);
assert.equal(done.strongholdCards!.owners.carthag, f.owner);
assert.equal(done.strongholdCards!.claimedTurn, 1);
const mentat = nextMentat(done);
assert.equal(mentat.strongholdCards!.owners.carthag, f.opponent);
assert.equal(mentat.strongholdCards!.claimedTurn, 2);
assert.equal(player(mentat, f.owner).hand.some(card => card.id === f.defense), true);
physicalCounters(mentat);
});
void test('passing the real Harkonnen Nexus acknowledgment retains the original single-traitor outcome and does not spend or replace the held card', () => {
const f = createNexusCoreCarthagFixture();
let game = callNexusCoreCarthagTraitor(revealNexusCoreStrongholdBattle(f));
game = reactionAction(game, f.opponent, { type: 'nexusHarkonnenBetrayalPass',
event: game.pendingNexusHarkonnenBetrayal!.source.event });
const done = finishNexusCoreStrongholdBattle(game);
assert.equal(done.lastBattleContext!.winner, f.owner);
assert.equal(player(done, f.opponent).leaders.find(disc => disc.id === f.opponentLeader)!.dead, true);
assert.equal(player(done, f.owner).traitors.includes(f.opponentLeader), true);
assert.equal(player(done, f.owner).tanks, player(f.game, f.owner).tanks,
'A successful single-traitor call does not pay the ordinary half-dial casualty');
assert.equal(done.nexusCards!.cards!.hands[f.opponent], 'harkonnen');
assert.deepEqual(done.nexusCards!.cards!.discard, []);
assert.equal(done.pendingNexusHarkonnenReplacement, null);
assert.equal(done.strongholdCards!.owners.carthag, f.owner);
physicalCounters(done);
});
+151
View File
@@ -0,0 +1,151 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { applyAction, type Action, type Game } from '../game/engine';
import { TECH_TOKENS, ownedTech } from '../game/tech-tokens';
import { forceRevivalQuote, normalRevivalAllowance } from '../game/revival';
import { reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
import { advanceClassicIndependentNexus } from './fixture-classic-independent-nexus';
import { classicDiscoveryNexusClean } from './fixture-discovery-classic-nexus';
import { createNexusCoreTechHarass, createNexusCoreTechRevival,
createNexusCoreTechShipment, finishNexusCoreTechPhase } from './fixture-nexus-core-tech';
const wallets = (game: Game) => Object.fromEntries(game.players.map(player => [player.id, player.spice]));
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before, 'A rejected consumer action cannot move cards, counters, wallets or tokens');
}
const quietPhaseFour = (game: Game) => advanceClassicIndependentNexus(game,
state => state.phase === 4 && classicDiscoveryNexusClean(state));
function firstStormCustody(game: Game) {
assert.ok(game.techTokens);
for (const token of TECH_TOKENS) {
const owner = game.techTokens[token.id].owner;
assert.ok(owner, `The original first Storm must have assigned ${token.name}`);
assert.equal(game.techTokens[token.id].spice, 0, 'No token income exists before its phase');
}
}
void test('Basic independent36 Tech keeps first-Storm custody and pays the funded delivery Heighliners once', () => {
const f = createNexusCoreTechShipment();
firstStormCustody(f.game);
assert.equal(f.game.turn, 2);
assert.equal(f.game.pendingGuildBetrayal!.invoice.price, 1);
const owner = f.token.owner, pending = f.token.count;
const before = reactionPlayer(f.beforeShipment, 'atreides');
const game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalUse',
event: f.game.pendingGuildBetrayal!.invoice.event });
// The delivered five counters, not the fee itself, are the real shipment.
assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
assert.equal(reactionPlayer(game, 'atreides').reserves, before.reserves - 5);
assert.equal(reactionPlayer(game, 'atreides').spice, before.spice - 1);
assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice + 1);
assert.equal(game.techTokens!.heighliners.owner, owner);
assert.equal(game.techTokens!.heighliners.spice, pending, 'Delivery accrues the owner-count income as pending spice');
assert.equal(game.techTokens!.heighliners.triggeredTurn, game.turn);
const closed = finishNexusCoreTechPhase(game);
assert.equal(closed.after.techTokens!.heighliners.spice, 0, 'Phase end pays the pending pile exactly once');
assert.equal(closed.after.techTokens!.heighliners.triggeredTurn, game.turn);
const beforeClose = wallets(closed.before), afterClose = wallets(closed.after);
assert.equal(afterClose[owner], beforeClose[owner] + pending);
for (const [id, spice] of Object.entries(beforeClose))
if (id !== owner) assert.equal(afterClose[id], spice, 'No other seat collects another token owner\'s pile');
});
void test('Advanced all14 Tech fixed Tleilaxu group pays real bank spice and only a free return accrues Axlotl', () => {
const f = createNexusCoreTechRevival();
firstStormCustody(f.game);
const owner = f.token.owner, pending = f.token.count;
const start = reactionPlayer(f.game, 'atreides');
assert.equal(start.tanks, 8, 'Actual battle losses supply the physical Tanks');
assert.equal(start.leaders.find(disc => disc.id === f.leader)!.dead, true);
let game = reactionAction(f.game, 'atreides', { type: 'card', card: 'ecaz-recruits' });
const rate = normalRevivalAllowance(game, reactionPlayer(game, 'atreides'));
assert.equal(rate.freeRate, 4);
assert.equal(rate.limit, 7);
rejected(game, 'atreides', { ...f.action, elite: 1 });
rejected(game, 'atreides', { ...f.action, leader: '' });
rejected(game, 'atreides', { ...f.action, amount: 6 });
game = quietPhaseFour(reactionAction(game, 'atreides', f.action));
const fixed = reactionPlayer(game, 'atreides');
assert.equal(fixed.spice, start.spice - 5, 'Five exact counters cost one bank spice each');
assert.equal(fixed.tanks, 3);
assert.equal(fixed.reserves, start.reserves + 5);
assert.equal(fixed.revived, 5);
assert.equal(fixed.freeForcesRevived, 0, 'The paid card group never consumes the free quota');
assert.equal(fixed.leaderRevived, true);
assert.equal(fixed.leaders.find(disc => disc.id === f.leader)!.dead, false);
assert.equal(game.nexusCards!.cards!.hands.atreides, null);
assert.equal(game.discard.filter(card => card.id === 'ecaz-recruits').length, 1);
assert.equal(game.techTokens!.axlotl.owner, owner);
assert.equal(game.techTokens!.axlotl.spice, 0, 'A paid fixed group is not a free revival and accrues nothing');
assert.equal(game.techTokens!.axlotl.triggeredTurn, undefined);
game = quietPhaseFour(reactionAction(game, 'atreides', { type: 'revive', amount: 1 }));
const freeSeat = reactionPlayer(game, 'atreides');
assert.equal(freeSeat.spice, fixed.spice, 'The ordinary free return itself costs nothing');
assert.equal(freeSeat.freeForcesRevived, 1);
assert.equal(freeSeat.tanks, 2);
assert.equal(game.techTokens!.axlotl.spice, pending, 'One genuine free return accrues the owner-count pile');
assert.equal(game.techTokens!.axlotl.triggeredTurn, game.turn);
game = quietPhaseFour(reactionAction(game, 'atreides', { type: 'revive', amount: 1 }));
assert.equal(reactionPlayer(game, 'atreides').spice, fixed.spice);
assert.equal(reactionPlayer(game, 'atreides').reserves, start.reserves + 7);
assert.equal(reactionPlayer(game, 'atreides').tanks, 1);
assert.equal(game.techTokens!.axlotl.spice, pending,
'A second genuine free return in this Revival does not mint another pile');
const closed = finishNexusCoreTechPhase(game);
assert.equal(closed.after.techTokens!.axlotl.spice, 0, 'Revival phase end pays the Axlotl pile once');
const beforeClose = wallets(closed.before), afterClose = wallets(closed.after);
assert.equal(afterClose[owner], beforeClose[owner] + pending);
});
void test('pending Axlotl spice cannot fund an unaffordable further paid revival', () => {
const f = createNexusCoreTechRevival({ drain: true });
const owner = f.token.owner, pending = f.token.count;
let game = reactionAction(f.game, 'atreides', { type: 'card', card: 'ecaz-recruits' });
const rate = normalRevivalAllowance(game, reactionPlayer(game, 'atreides'));
assert.ok(reactionPlayer(game, 'atreides').spice < 6,
'The genuine turn-one shipment leaves a thin wallet for this boundary');
game = quietPhaseFour(reactionAction(game, 'atreides', { type: 'revive', amount: rate.freeRate }));
const seat = reactionPlayer(game, 'atreides');
assert.equal(game.techTokens!.axlotl.spice, pending, 'The owner\'s own free return created the pending pile');
const quote = forceRevivalQuote(game, seat, 3);
assert.ok(quote.cost > seat.spice && quote.cost - seat.spice <= pending,
'Choose the smallest legal overage the pending pile would cover if it were spendable');
rejected(game, 'atreides', { type: 'revive', amount: 3 });
assert.equal(game.techTokens!.axlotl.spice, pending, 'The refusal cannot mint or clear token income');
game = quietPhaseFour(reactionAction(game, 'atreides', { type: 'revive', amount: 2 }));
const paid = reactionPlayer(game, 'atreides');
assert.equal(paid.spice, seat.spice - 4, 'Two genuinely affordable paid counters cost their printed four bank spice');
assert.equal(paid.tanks, 2);
assert.equal(game.techTokens!.axlotl.spice, pending, 'Axlotl accrues once per turn, not once per return');
const closed = finishNexusCoreTechPhase(game);
assert.equal(closed.after.techTokens!.axlotl.spice, 0);
assert.equal(wallets(closed.after)[owner], wallets(closed.before)[owner] + pending);
});
void test('Basic independent36 Harass return and native winner transfer mint no token income', () => {
const f = createNexusCoreTechHarass();
const before = f.beforeCall;
const game = reactionAction(f.game, 'atreides', { type: 'nexusHarkonnenBetrayalUse',
event: f.game.pendingNexusHarkonnenBetrayal!.source.event });
const lost = ownedTech(before.techTokens, 'atreides');
assert.ok(lost.length, 'The defeated seat genuinely held tokens');
const moved = TECH_TOKENS.filter(token =>
before.techTokens![token.id].owner !== game.techTokens![token.id].owner);
assert.equal(moved.length, 1, 'The battle winner takes exactly one token from the loser');
const token = moved[0];
assert.equal(before.techTokens![token.id].owner, 'atreides');
assert.equal(game.techTokens![token.id].owner, 'harkonnen');
assert.ok(lost.includes(token.id), 'The native choice belongs to the loser\'s actual holdings');
assert.equal(game.pendingTech ?? null, null);
for (const rule of TECH_TOKENS) {
assert.equal(game.techTokens![rule.id].spice, 0, `${rule.name} mints no income from a traitor battle`);
}
assert.equal(game.techTokens!.heighliners.triggeredTurn, game.turn,
'The earlier genuine shipment accrual was already paid at its own phase end');
assert.equal(reactionPlayer(game, 'atreides').reserves,
reactionPlayer(f.beforeShipment, 'atreides').reserves, 'The Harass return is not a Revival grant');
assert.equal(reactionPlayer(game, 'atreides').tanks, 0);
assert.equal(reactionPlayer(game, 'atreides').leaders.find(disc => disc.id === f.identity)?.dead, false);
});