Compose independent Skills with original Tech and Stronghold modules
Unify fresh/live classic and native phase admission, retaining original counts, earned custody, separate income ledgers and physical card/skill continuations. Plain paired E3 phases no longer require cards or Nexus. Coupled44-file471/471, types/lint/build, genuine programmes and390px normal entry pass. Controlled card inputs explicit; no save retrofit or deployment.
This commit is contained in:
1 parent
17adffa6a7
commit
b9330d489e
31 files changed
+792
-99
No files matched your search
@@ -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.
|
||||
|
||||
**Skills/cards with original phase modules — 17 October.** Fresh all14 Skills
|
||||
and independent cards now retains original Tech3+ Basic/Advanced and
|
||||
Strongholds2+ Advanced, optionally both, across existing classic, selected
|
||||
E1/E2 and standalone/paired E3 rosters. Paired plain E3 Skills also retains
|
||||
that phase envelope without requiring Nexus or cards. World/Nexus/Discovery,
|
||||
mixed-E3 and existing special-role guards remain; no rules arithmetic or saved
|
||||
history is invented.
|
||||
|
||||
Real revival preserves prior payment, one Axlotl accrual/phase-end payout and
|
||||
the separate normal Banker threshold/Mentat grant. Real held Arrakeen support
|
||||
is Bank contribution, not personal money or reserve-fee funding. Typed fees,
|
||||
Harass/Suk, native prices, selected-holder versus ally, winner Tech and native
|
||||
assassination/Mentat retain physical sources and original continuation.
|
||||
|
||||
Frozen44-file **471/471**, types/lint/build and actual programmes pass.
|
||||
Eight natural composed tables reach turn3 or earlier wins; real390px normal
|
||||
lobby selects all four modules, trains an offered Smuggler and reaches Storm
|
||||
with no browser errors/root overflow. Targeted programmes, not those natural
|
||||
tables, supply the detailed fee/income/claim evidence. Native controlled-card
|
||||
positioning is labelled, including a repeated Richese R input; no natural
|
||||
reacquisition or full-mode/deployed/deferred assurance claim follows.
|
||||
[Current phase scope and evidence](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026).
|
||||
|
||||
**Paired E3 Skills and native arrivals — 17 October.** Native Ecaz+Moritani/
|
||||
classics all14 Skills no longer requires Nexus Cards, Basic/Advanced2–6,
|
||||
optionally independent cards only. Four normal plain/card starts failed before
|
||||
|
||||
@@ -5,11 +5,15 @@ Historical primary-source audit, 2026-09-07, corrected after independent verific
|
||||
## Paired native Skills — 17 October 2026
|
||||
|
||||
Native Ecaz/Moritani/classics now retains ordinary all14 Skills without Nexus,
|
||||
optionally independent cards only, in Basic/Advanced. Actual native6/6,
|
||||
optionally independent cards and original Tech3+/Advanced Strongholds2+. Actual native6/6,
|
||||
selected lead, card-user fee/return and fixed versus variable losses remain
|
||||
separate; trained Ecaz Suk rescues only its committed own losses. Native
|
||||
Moritani contributes normal counters rather than invented Imperial elites.
|
||||
[Paired source scope and323/323 proof](LEADER_SKILLS_RUNTIME.md#paired-e3-without-nexus--17-october-2026).
|
||||
[Current phase programme and471/471 proof](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026)
|
||||
uses actual turn1 paid4/earned Arrakeen before the real worm coalition. Ecaz
|
||||
support is Bank2/personal0; an allied Moritani plan does not inherit it.
|
||||
|
||||
|
||||
## Standalone Ecaz Leader Skills — 17 October 2026
|
||||
|
||||
|
||||
@@ -407,8 +407,8 @@ No deployed, full-module, strategic calibration or deferred assurance claim foll
|
||||
## Paired E3 Skills and native arrival independence — 17 October 2026
|
||||
|
||||
Ecaz+Moritani/classics now also uses all14 Skills without Nexus, optionally
|
||||
independent cards, Basic/Advanced2–6 and no other modules. Original native6/6,
|
||||
Loyalty, assignment, separate card-user costs/armies and one native Mentat
|
||||
independent cards, Basic/Advanced2–6, plus original Tech3+/Advanced Strongholds2+.
|
||||
Original native6/6, Loyalty, assignment, separate card-user costs/armies and one native Mentat
|
||||
replacement remain; Advanced Harkonnen/shared-Duke/mixed-family guards stay.
|
||||
[Canonical paired programme and323/323 evidence](LEADER_SKILLS_RUNTIME.md#paired-e3-without-nexus--17-october-2026).
|
||||
|
||||
@@ -419,3 +419,20 @@ requiring Skills. The earlier storm-order policy, actual control/effect-specific
|
||||
guards and physical payment/force rules are unchanged, not newly adjudicated.
|
||||
[Native queue and genuine proof](MORITANI_ENTRY_TIMING.md#paired-skills-and-independent-card-entries--17-october-2026).
|
||||
|
||||
## All14 Skills and original phase modules — 17 October 2026
|
||||
|
||||
The printed variant independence now composes original all14 with Tech3–6
|
||||
Basic/Advanced and/or Strongholds2–6 Advanced across existing classic, selected
|
||||
E1/E2 and standalone/paired E3 rosters, without World/Nexus/Discovery or
|
||||
unrelated previews. Plain paired E3 phase Skills does not require these cards.
|
||||
Native setup, decks/cache, card-user source/fees and unchanged provisional
|
||||
timing remain. Original phase-end Tech and real-Mentat Banker are separate;
|
||||
earned support is not money or reserve-fee funding, and R/H are not Revival.
|
||||
|
||||
Frozen44-file471/471, types/lint/build, actual rule programmes and eight bounded
|
||||
natural tables pass their changed paths. Real390px four-module normal entry
|
||||
reaches Storm after actual skill/Traitor choices without errors/overflow.
|
||||
Controlled physical card staging is labelled; the Richese second R is not a
|
||||
claimed natural reacquisition. No complete-mode or deployed acceptance follows.
|
||||
[Canonical composition/evidence](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026).
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
# Harass & Withdraw development prototype
|
||||
|
||||
Updated 16 October 2026. This connects the physical card to battle plans,
|
||||
Updated 17 October 2026. This connects the physical card to battle plans,
|
||||
private inspections, force returns, controls, AI and saved continuation. It does
|
||||
not complete the independent three-card variant or open public mode gates.
|
||||
not certify the complete independent card variant or a complete rules mode.
|
||||
|
||||
## Original Skills/phase composition — 17 October 2026
|
||||
|
||||
Supported classic, selected E1/E2 and standalone/paired E3 all14/card profiles
|
||||
retain original Tech3–6 Basic/Advanced and Advanced Strongholds2–6 without
|
||||
World/Nexus/Discovery. Exact typed undialed returns, fixed/variable ownership,
|
||||
fee-before-return funding and committed-only Suk stay. An earned Stronghold
|
||||
funds eligible support, not personal cash or reserve fees; Harass is neither
|
||||
free revival nor off-planet shipment and cannot accrue technology that way.
|
||||
[Current471/471 composition and evidence](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026).
|
||||
|
||||
|
||||
## Authority and interpretation
|
||||
|
||||
@@ -227,8 +238,8 @@ route warnings remain; no HTTP or database boundary changed.
|
||||
- Clean classic Homeworlds use the [native reserve-routing contract](#classic-homeworld-reserve-routing--16-october-2026).
|
||||
Classic or selected Ix/CHOAM rosters, including Worlds, may retain
|
||||
[original Tech3+ and Advanced Strongholds2+](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-october-2026).
|
||||
Nexus, Skills, Discoveries, broader World/faction composition and full-mode
|
||||
acceptance remain guarded.
|
||||
All14/card profiles without Worlds now retain those original phase modules.
|
||||
World+Skills, Nexus/Discovery and broader combinations retain their guards.
|
||||
- Richese card-family configurations now admit Harass under the
|
||||
[provisional post-withdrawal policy](#provisional-stone-burner-timing--16-october-2026).
|
||||
The ordinary invariant-allocation guard remains. Clean selected Ix follows
|
||||
@@ -237,8 +248,8 @@ route warnings remain; no HTTP or database boundary changed.
|
||||
Own concealed No-Field use follows the
|
||||
[materialization-first contract](#own-no-field-composition--16-october-2026).
|
||||
Other profile/module boundaries remain unchanged.
|
||||
- Co-present armies remain guarded in Basic or without the supported Advanced
|
||||
Occupy profile. The [combined-army follow-up](ECAZ_OCCUPY_RULES.md#independent-cards-with-combined-armies--2-october-2026)
|
||||
- Unsupported co-present armies and Basic mixed markers remain guarded.
|
||||
Supported native Basic/Advanced [Occupy](ECAZ_OCCUPY_RULES.md#paired-native-skills--17-october-2026)
|
||||
returns only actual card-user forces: original fixed Ecaz commitment or the
|
||||
selected ally's native variable commitment. It never returns the other ally
|
||||
or recomputes mandatory fixed losses from the diminished board.
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 17 October 2026 — Skills/cards with original phase modules
|
||||
|
||||
Eight original normal card/Skills phase starts and two plain paired starts
|
||||
rejected before; the shared independent profile now retains original Tech3+
|
||||
Basic/Advanced and Strongholds2+ Advanced, optionally both. Classic, selected
|
||||
E1/E2 and standalone/paired E3 keep their existing roster/source limits.
|
||||
Live card consumers reuse that same profile despite mandatory Banker/native
|
||||
markers. Plain paired E3 no longer requires a card or Nexus for these original
|
||||
modules. World/Nexus/Discovery and unrelated guards remain.
|
||||
|
||||
Disjoint genuine programmes preserve paid/free revival ledgers, Axlotl once at
|
||||
phase end and normal Banker separately at Mentat; real earned Arrakeen funds
|
||||
only selected-holder support. Ix retains typed fee/Harass/Suk and native
|
||||
Cyborg5 paid to Tleilaxu. Richese retains reserve-limited materialization and
|
||||
actual held support. E3 retains separate armies/selected reward, unused native
|
||||
trained-disc death and one Mentat replacement. No owner, wallet or phase is
|
||||
assigned to simulate earned effects; controlled card positioning is explicit.
|
||||
The Richese second battle restages the original physical R, not a natural draw.
|
||||
|
||||
Frozen44-file **471/471**, types/lint/build and actual rule programmes pass.
|
||||
Support assertions now observe payment before printed Collection income;
|
||||
Recruits tests actual remaining Tanks rather than a larger abstract allowance.
|
||||
An old charity fixture now conserves printed Karama from its actual hand/deck/
|
||||
discard instead of assuming it remains undealt. Eight natural composed tables
|
||||
reach turn3 or earlier wins with29 battles/eight Mentat decision steps.
|
||||
Real390px normal lobby selects all four modules, assigns offered Smuggler and
|
||||
reaches Storm without browser errors/root overflow. Existing build warnings,
|
||||
provisional card/Basic arithmetic and full-mode/deployed/deferred assurance
|
||||
limits remain. No game reset or saved-game conversion occurs.
|
||||
[Canonical current envelope](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026).
|
||||
|
||||
## 17 October 2026 — Paired native E3 Skills without Nexus
|
||||
|
||||
Four actual paired Basic/Advanced plain/card Skills starts rejected before the
|
||||
|
||||
@@ -7,11 +7,50 @@ 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).
|
||||
|
||||
## Independent cards with original phase modules — 17 October 2026
|
||||
|
||||
Fresh original all14 Skills plus independent Recruits/Reinforcements/Harass now
|
||||
retains Tech3–6 Basic/Advanced and/or Strongholds2–6 Advanced (both3–6).
|
||||
Existing classic, selected Ix/CHOAM native/base and standalone/paired E3
|
||||
ecaz rosters/decks36/38/50 remain; the Richese cache stays separate. Plain
|
||||
paired E3 Skills also uses these original modules without a card dependency.
|
||||
World/Nexus/Discovery, mixed E3, Advanced E3/Harkonnen, shared-Duke training and
|
||||
effect-specific guards are unchanged. No new state shape or save retrofit.
|
||||
|
||||
One shared independent profile feeds setup, native skills and live card
|
||||
availability, including mandatory normal Banker and native markers. Real
|
||||
claims and first-Storm owners remain authoritative. Arrakeen funds only actual
|
||||
selected-holder support; it creates neither personal cash nor reserve fees.
|
||||
Source-labelled R/H/Suk, typed aftercare, winner Tech and native assassination
|
||||
retain existing order and provisional interpretations.
|
||||
|
||||
The existing classic independent-Skills fixture/test pairs add actual revival/
|
||||
income and zero-personal-spice held-support programmes; the Ix/CHOAM pairs add
|
||||
typed reward/revival and real native marker/earned support. The Ecaz/Moritani
|
||||
pairs add selected-holder versus ally and plain no-card phase continuation.
|
||||
Original defaults remain off. Shared module setup controls undealt lotteries;
|
||||
the coalition helper actually ships4 in turn1, earns Arrakeen at real Mentat
|
||||
and retains the same4 for turn2. No module owner, money, army or phase is faked.
|
||||
Conserved card inputs remain explicit: the native Richese held case restages
|
||||
the original R between battles without claiming a natural reacquisition.
|
||||
|
||||
Frozen44-file471/471, types/lint/build and actual programmes pass. A real
|
||||
five-Tanks revival returns3 for4, then2 for2 after Recruits: one Axlotl pending2
|
||||
pays only at phase end, one normal Banker grant stays deferred. Earned Ecaz
|
||||
support is Bank2/personal0, fee-only3 Tanks, one rescued survivor and separate
|
||||
ally2 losses; its later Collection2 is not a support rebate. Native prices and
|
||||
single Mentat replacement retain their original recipients/custody.
|
||||
Eight natural composed tables reach turn3 or earlier wins; real390px normal
|
||||
four-module selection/training reaches Storm without errors/overflow.
|
||||
These are prototype changed-path proofs, not full combinations, strategic
|
||||
calibration, deployed or deferred assurance acceptance.
|
||||
|
||||
|
||||
## Paired E3 without Nexus — 17 October 2026
|
||||
|
||||
Fresh native Ecaz+Moritani/classics all14 Skills now uses its original setup
|
||||
without an artificial Nexus dependency, Basic/Advanced2–6, optionally independent
|
||||
cards (ecaz33/36) and no other modules. One physical-roster validator serves
|
||||
cards (ecaz33/36), original Tech3+ and/or Advanced Strongholds2+. One roster validator serves
|
||||
both original paired Nexus and plain Skills predicates. No Nexus hand is
|
||||
created; native6/6, Loyalty, ordinary assignment discs, mandatory Banker and
|
||||
native powers remain. Shared Duke never trains. Advanced Harkonnen, mixed
|
||||
|
||||
@@ -56,9 +56,9 @@ Harkonnen, the Ecaz pair and existing Homeworld occupation. Existing module
|
||||
profiles still govern Nexus, Discovery, Tech and Strongholds. Independent cards
|
||||
do not require an unrelated Nexus module or suppress assassination.
|
||||
With Skills, standalone or paired native Ecaz/Moritani/classic setups now work
|
||||
without Nexus, optionally the independent three-card variant and no other
|
||||
modules. Advanced Harkonnen, mixed families and other module envelopes retain
|
||||
their guards; see [the paired native composition](MORITANI_LEADER_SKILLS.md#paired-native-e3-skills--17-october-2026).
|
||||
without Nexus, optionally independent cards and original Tech3+/Advanced
|
||||
Strongholds2+. Advanced Harkonnen, mixed families and other module envelopes
|
||||
retain their guards; see [current phase composition](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026).
|
||||
Winner Suk rescue and Rihani choice finish first; the unspent assassination
|
||||
receipt waits, then opens its original private choice. Already completed
|
||||
casualties cannot replay after reveal/decline. A different eligible trained
|
||||
|
||||
@@ -5,11 +5,16 @@ Reviewed 2026-09-06. This supplements `MORITANI_TERROR_RULES.md`; it does not en
|
||||
## Paired Skills and independent-card entries — 17 October 2026
|
||||
|
||||
The existing serialized native queue now admits paired Ecaz/Moritani/classic
|
||||
all14 Skills without Nexus, optionally independent cards and no other modules.
|
||||
all14 Skills without Nexus, optionally independent cards and original Tech3+/Advanced Strongholds2+.
|
||||
Bare faction/card-only entries also retain it; selecting the printed independent
|
||||
variant does not require selecting Skills or suppress native reactions.
|
||||
This extends software capability, not the provisional storm-order policy below.
|
||||
Original in-flight control and effect-specific guards remain.
|
||||
Actual Basic Tech and Advanced combined-module entries also complete Duke6,
|
||||
Robbery and the original paid one-counter arrival/movement through this queue.
|
||||
[Current471/471 envelope and runtime proof](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026)
|
||||
does not change its selected priority or certify other effect-specific combinations.
|
||||
|
||||
|
||||
`tests/paired-e3-skills-arrival.test.ts` branches from genuine normal setup,
|
||||
uses actual native placements and one paid Guild arrival, then triggers
|
||||
|
||||
@@ -8,19 +8,23 @@ module guards remain, and complete rules/publication acceptance is unfinished.
|
||||
## Paired native E3 Skills — 17 October 2026
|
||||
|
||||
Both native factions now compose ordinary all14 Skills without Nexus,
|
||||
optionally independent cards only. Genuine native6/6 and Ecaz Loyalty precede
|
||||
optionally independent cards and original Tech3+/Advanced Strongholds2+. Native6/6 and Loyalty precede
|
||||
the actual battle. Moritani loses against Ecaz, reveals unused trained Ecaz4,
|
||||
returns its exact skill once and receives one physical replacement at Mentat.
|
||||
The existing fixture/runtime pair exposes `paired:true`; standalone defaults,
|
||||
Basic without assassination and original Advanced Harkonnen/Duke guards stay.
|
||||
[Current scope, controls and323/323 proof](LEADER_SKILLS_RUNTIME.md#paired-e3-without-nexus--17-october-2026).
|
||||
[Current phase composition and471/471 proof](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026)
|
||||
also exercises a plain paired no-card phase loss through winner Tech and one
|
||||
actual Mentat replacement, without inventing an extra module dependency.
|
||||
|
||||
|
||||
|
||||
## Independent Treachery composition — 17 October 2026
|
||||
|
||||
Fresh standalone Moritani/classic all14 Skills now retains the independent
|
||||
Recruits, Reinforcements and Harass & Withdraw variant, Basic/Advanced2–6,
|
||||
without other modules. Original36-card setup, five eligible assignment discs,
|
||||
with optional original Tech3+/Advanced Strongholds2+. Original36-card setup and five eligible assignment discs,
|
||||
private offers, Traitors and six-force placement remain. Advanced initializes
|
||||
the native assassination advantage; Basic does not. The original Advanced
|
||||
Harkonnen, shared-Duke and mixed-family Skills guards remain.
|
||||
|
||||
@@ -4,7 +4,19 @@
|
||||
Ecaz & Moritani Treachery Cards variant is included before genuine Basic or
|
||||
Advanced faction setup. All three physical cards are present exactly once.
|
||||
Recruits, bounded Reinforcements and Harass & Withdraw have connected controls,
|
||||
AI and saved continuation; full interactions and public starts remain gated.
|
||||
AI and saved continuation; full interactions and publication remain gated.
|
||||
|
||||
## Original Skills/phase composition — 17 October 2026
|
||||
|
||||
Supported classic, selected E1/E2 and standalone/paired E3 all14/card profiles
|
||||
retain original Tech3–6 Basic/Advanced and Advanced Strongholds2–6, without
|
||||
World/Nexus/Discovery. One genuine five-Tanks programme returns3 for4, then2
|
||||
for2 after Recruits, preserving prior payment and free/ordinary usage. The
|
||||
larger allowance cannot create a third counter when only two actual Tanks
|
||||
remain. Axlotl accrues once/pays at phase end; normal Banker keeps its actual
|
||||
Bank-payer threshold and deferred Mentat grant. Native prices/caps/recipients
|
||||
and provisional late-paid policy remain unchanged.
|
||||
[Current471/471 programme and limits](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026).
|
||||
|
||||
## Provisional late-paid activation — 8 October 2026
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ The separate Ecaz Treachery Cards variant shuffles one physical Reinforcements
|
||||
card independently of choosing the Ecaz faction. Its bounded handler supports
|
||||
Basic/Advanced classic-faction tables, clean selected Ix and/or CHOAM native/base
|
||||
rosters, and clean Ecaz/Moritani expansion tables. Classic and selected E1/E2
|
||||
Worlds use native sources below; native selected-family Worlds add no other
|
||||
modules. Classic base36 separately uses the
|
||||
[phase-module contract](ECAZ_TREACHERY_RULES.md#classic-world-phase-module-composition--16-october-2026).
|
||||
The selected Advanced Occupy profile separately composes co-present armies.
|
||||
Worlds retain native sources and their original Tech/Stronghold envelope;
|
||||
see the [World phase contract](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-october-2026).
|
||||
Supported native Basic/Advanced Occupy retains its own co-present armies;
|
||||
Basic mixed-marker and other unprofiled co-sides stay guarded.
|
||||
The holder may commit it in either Battle Plan card slot. It is neither a weapon
|
||||
nor a defense: Prescience of a category receives **None**, while the physical
|
||||
slot stays occupied and hidden until reveal. A named-card inspection still
|
||||
@@ -16,6 +16,15 @@ fixes the exact card. Harass & Withdraw may share the plan under the
|
||||
Original or copied Stone Burner composes in clean selected Ix/CHOAM families
|
||||
and the existing native Advanced Occupy envelope. Other profile boundaries remain.
|
||||
|
||||
## Original Skills/phase composition — 17 October 2026
|
||||
|
||||
Supported original all14/card profiles retain Tech3–6 Basic/Advanced and
|
||||
Strongholds2–6 Advanced without World/Nexus/Discovery. Original reserve-fee
|
||||
funding stays separate from earned support: Bank2/personal0 cannot finance
|
||||
three missing reserve counters. Suk rescues only actual committed losses,
|
||||
never the fee; native marker materialization and mandatory disposal remain.
|
||||
[Current471/471 composition and physical programmes](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026).
|
||||
|
||||
## Clean selected Ix/CHOAM families — 16 October 2026
|
||||
|
||||
Plain selected `ix`, `choam`, or both now admit Reinforcements with native/base
|
||||
|
||||
@@ -668,9 +668,9 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP
|
||||
| Recruits / classic Homeworlds | Publisher E3 pp11/16 gives low Fremen4 doubled but capped7. Under choose-recommended, compose original separate-request native population observations: current rate at acceptance, retained accepted group price/free classification, then later requests use changed population and recorded free usage. Bulk7 versus split1+5 is provisional timing, not a new FAQ/user answer. Fremen still cannot buy a seventh without Tleilaxu; typed native deposits and original special-force caps remain. Prior paid/ready returns retain payment and opportunity while phase4 stays open. Clean classic2–6 Basic/Advanced only; Reinforcements uses its separate native fee contract and other roster/overlay/public gates stay. | [Current-rate scope and proof](RECRUITS_RUNTIME.md#classic-homeworld-composition--16-october-2026) |
|
||||
| Reinforcements / classic Homeworlds | Under standing choose-recommended, seal exact native normal-first sources for the existing three-reserve/all-outcome fee. Ambiguous omission rejects; a unique source may derive. Native reserves on an own World are also its army, so that fee cannot fund the same physical dial/casualties; an off-World fee leaves the pool. Keep original pre-fee traits/printed face, including explosion loss choices across a threshold, then typed Tanks, Harass return and original casualties. This is explicit provisional physical-pool composition, not a new FAQ or own-World ban. Source-aware full/complying plan reviews and named independent-card conditions use canonical names and actual combat locations. Other rosters/overlays/public starts remain gated. | [Native sources and exact proof](REINFORCEMENTS_RUNTIME.md#classic-homeworld-reserve-fees--16-october-2026) |
|
||||
| Reinforcements / clean selected Ix and CHOAM | Admit original independent cards with clean selected native/base Basic/Advanced rosters and no optional modules/overlays. Reuse existing provisional normal-first/all-outcome payment, original No-Field materialization before the fee, then successful Harass return; physical Stone ignores +2. Native cyborg substitution uses its own casualty origins, not a duplicate reserve fee. This closes admission consistency, not a new numerical ruling or public expansion gate. | [Profile and bounded proof](REINFORCEMENTS_RUNTIME.md#clean-selected-ixchoam-families--16-october-2026) |
|
||||
| Independent Treachery / all14 Skills | Printed E3 faction-independent variant permission composes fresh classic, selected E1/E2 native/base, or standalone/paired Ecaz/Moritani/classic all14 Skills, Basic/Advanced2–6, without other modules. Original decks36/38/50, native setup and skill/card custody stay. E3 Advanced Harkonnen, shared-Duke, mixed-family Skills and other module envelopes remain guarded. Bare no-Skills card rosters retain every selected known family even without Ecaz seated; E3 World/phase modules and provisional fee/return timing are unchanged. | [Native physical sources](ECAZ_TREACHERY_RULES.md#native-e1e2-leader-skills-composition--17-october-2026), [paired E3](LEADER_SKILLS_RUNTIME.md#paired-e3-without-nexus--17-october-2026) |
|
||||
| Independent Treachery / all14 Skills | Printed E3 variant independence composes fresh classic, selected E1/E2 native/base or standalone/paired Ecaz/Moritani/classic all14, Basic/Advanced2–6, with original Tech3+ and/or Advanced Strongholds2+. Original36/38/50, cache, native setup and custody stay. Earned support is not personal income or reserve-fee funding; normal Banker and phase-end Tech remain separate. E3 Advanced Harkonnen, shared Duke, mixed E3, World/Nexus/Discovery and effect-specific guards remain. No new numeric rule or save retrofit. | [Current original phase composition and evidence](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026), [physical card sources](ECAZ_TREACHERY_RULES.md) |
|
||||
| Fresh native Ecaz/Moritani defaults | Fresh plain/card Ecaz constructors initialize printed coalition participation in Basic/Advanced. Supported native Advanced Moritani constructors initialize Assassinate Leaders without requiring Nexus; no-Skills selected native rosters include existing Harkonnen/Ecaz/native-family and Homeworld occupation compositions, while Skills retains its Advanced Harkonnen and standalone/pair profile boundaries. Basic never gains assassination. Existing game-long forfeiture and opportunity history are not invented in played saves. This repairs default initialization, not a new printed rule or complete-mode acceptance. | [Ecaz scope](ECAZ_OCCUPY_RULES.md#standalone-ecaz-leader-skills--17-october-2026), [Moritani authority and scope](MORITANI_ASSASSINATE_LEADERS.md#connected-scope-and-private-information) |
|
||||
| Independent Treachery / original phase modules | Printed E3 variant independence permits unused Tech3–6 Basic/Advanced and/or Strongholds2–6 Advanced with clean classic/selected Ix/CHOAM native/base rosters, including original Homeworlds. Original setup, delayed income and earned custody stay. R fee-before-Harass is provisional; returns are not free Revival/shipment and +2 is not supported force. Native exact fees, current rates/unlimited allowances, marker funding and aftercare retain their separate contracts. Skills, Nexus, Discovery, E3 mixtures and unrelated previews remain gated. | [Phase composition](ECAZ_TREACHERY_RULES.md#original-phase-module-composition--16-october-2026), [native World composition](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-october-2026) |
|
||||
| Independent Treachery / original phase modules | Printed variant independence permits original Tech3–6 Basic/Advanced and/or Strongholds2–6 Advanced with classic/selected E1/E2 card rosters including their Homeworlds. Without Worlds, supported all14/classic/E1/E2/standalone-paired E3 now composes the same original modules. Actual first-Storm/end-Mentat owners, separate delayed income and card funding/physical pools stay; fees/H returns are not revival/shipment and +2 is not supported force. No new priority ruling. Other World/Nexus/Discovery and mixed-E3 guards remain. | [Original phase contract](ECAZ_TREACHERY_RULES.md#original-phase-module-composition--16-october-2026), [current all14 phase composition](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026), [native World composition](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-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) |
|
||||
| Selected E1/E2 Worlds / independent cards | Use original selected38/50 native/base Basic/Advanced setup; original Tech/Advanced Strongholds follow the native World phase contract. Retain native typed/current-rate/unlimited allowances, native-only World declarations and every own-World H ban. R normal-first sources retain original face/traits and fee/dial disjointness. Native Richese validates3 AFTER projected marker materialization before a first seal, without moving it early. Only actual board Cyborg losses feed substitution; reserve fees are not duplicated as origins. Existing timing remains provisional, not a new FAQ. | [Source, scope and native funding](ECAZ_TREACHERY_RULES.md#native-selected-family-world-composition--17-october-2026), [phase modules](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-october-2026) |
|
||||
| Clean Ix Stone / Tooth / Mirror | Reuse existing provisional original Stone-first/uncopied-Tooth order; the earlier priority question stays unanswered. Printed Mirror copy-first and user-selected physical retention remain independent. Selected Ix/CHOAM now retains original Tech3+ and Advanced Strongholds2+, original47/independent50 plus separate cache, with genuine earned custody. No numerical ruling changes. Typed winner ambiguity, mixed E3 with these phase modules and other overlays stay guarded; existing Ecaz/Moritani combinations remain Advanced-only. | [Source and profile distinction](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-composition--16-october-2026), [phase-module proof](MIRROR_WEAPON_ENGINE_AUDIT.md#original-phase-module-composition--16-october-2026) |
|
||||
@@ -723,7 +723,7 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP
|
||||
| Classic Leader Skills / Tech Tokens | Fresh classic3..6 Basic/Advanced, base33/all14, canonical unused three tokens and original setup/first-Storm owners. The ordinary leader-skills entry preserves selected Tech and now includes mandatory normal Banker income; native/other overlays retain their existing admission. Tech collects at phase end, independent Banker waits for actual Mentat; neither currency store is a recursive payment producer. Smuggler changes actual priced count; original Suk/Sandmaster precede card cleanup and mandatory token transfer. One token auto-transfers, several require choice, none skips. This composes existing rules, not a new ruling or public activation. | [Scope and evidence](LEADER_SKILLS_RUNTIME.md#classic-tech-tokens-composition--4-october-2026) |
|
||||
| Native E1/E2 Leader Skills / Tech Tokens | Fresh supported Ixian/Tleilaxu/CHOAM/Richese3..6 Basic/Advanced keeps original native deck/roster limits, all14 skills, cache and printed Tech setup. Original typed payments/rescue/card cleanup precede mandatory winner token reward; Tleilaxu A.1–3 expressly preserves that reward before Face Dance. Actual Charity affects Production, not Axlotl; native CHOAM initial income is not poverty Charity. Separate Banker/Mentat bounds and unrelated guards remain. E3/other overlays, public starts and save conversion are excluded. This composes sourced rules, not a new interpretation. | [Scope, supplied physical21/38 and evidence](LEADER_SKILLS_RUNTIME.md#native-e1e2-tech-tokens-composition--4-october-2026) |
|
||||
| Advanced Leader Skills / Stronghold Cards | Fresh classic or supported E1/E2 native2..6 Advanced preserves all14 skills, six unused cards, original decks/cache/wallets and actual end-Mentat claims; optional Tech requires3..6. Arrakeen bank subsidy is separate from actual payer debit/Banker threshold and original CHOAM income. Physical defense/normal discipline, holder tie and Tabr/Tuek income compose original skill outcomes. Suk/native cleanup and mandatory Tech precede Face Dance, retaining original rewards and one killed-skill return. Copied-Diplomat/active skill-Stone and unrelated guards remain. Basic Strongholds, E3/other overlays, public starts and save conversion are excluded. | [Authorized physical21/38, existing effects and evidence](LEADER_SKILLS_RUNTIME.md#advanced-stronghold-composition--4-october-2026) |
|
||||
| Standalone E3 Skills / Tech / Stronghold Cards | Original Ecaz OR Moritani ecaz33/all14 rosters preserve Tech Basic/Advanced3..6 and Stronghold Advanced2..6, optionally both. Native Ecaz five-disc/temporary Duke/Advanced-Hark exclusion and Moritani skill-first assassination/normal-call forfeiture remain. Real claimed effects, original support/rescue, printed Duke6 or unused-disc bounty and one Mentat replacement compose native rewards without new ownership or income rules. E3 pairs, mixed families, allied Occupy skills, other overlays, public starts and save conversion remain excluded. | [Source contracts and bounded evidence](LEADER_SKILLS_RUNTIME.md#standalone-e3-module-composition--4-october-2026) |
|
||||
| Standalone/paired E3 Skills / Tech / Strongholds | Original Ecaz OR Moritani or both with classics retains ecaz33/all14, optional independent36, Tech3–6 Basic/Advanced and Advanced Strongholds2–6, optionally both. No artificial Nexus/card dependency remains. Actual earned selected-holder support, separate allied armies, committed-only Suk, skill-first assassination/normal-call forfeiture, printed bounty, winner Tech and one Mentat replacement reuse original handlers. Native five discs, untrained Duke and Advanced Harkonnen/mixed-family/other-envelope guards stay; no fake owner or new income/numeric ruling. | [Current source envelope and evidence](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026) |
|
||||
| Normal Mentat | The 11 October user-approved uniform private show step runs before posture/faction powers in new supported Leader Skills battles, without an extra opt-in. Matching and fallback preserve the same target-owned stage and held cards. Already-opened unversioned battles do not reopen; versioned receipts remain authoritative. Empty-hand interpretation and full/public acceptance remain separate. | [Approved normal flow](MENTAT_QUESTION.md#normal-activation-and-approved-ux--11-october-2026) |
|
||||
| Normal Spice Banker default | Fresh supported ordinary all14 Skills setups automatically initialize normal income, including their existing supported original modules and native overlays. The separate initializer/profile and fixture opt-ins are removed; no public start or old-game retrofit. Another payer's one actual final Bank payment ≥4 grants one additional deferred spice per physical card/turn/phase, unavailable until actual Mentat. Distinct payers and separate free-revival awards never pool into an invoice. Tleilaxu self-revival pays Bank; other-player revival pays Tleilaxu unless canceled. Existing same-payer diversion/rounding rulings remain. Earned-faction retention after death/capture/reassignment remains provisional, not a new answer to the pending custody question. | [Current setup and limits](SPICE_BANKER_RUNTIME.md#mandatory-ordinary-skills-default--17-october-2026), [payment contract](SPICE_BANKER_RUNTIME.md#normal-income-local-contract--2-october-2026), [skill authority](LEADER_SKILLS_RULES.md) |
|
||||
| Advanced Stronghold faction integration | Fresh `stronghold-factions` admits selected E1/E2 native families and standalone Ecaz OR Moritani with classic2..6-seat Advanced, canonical selected decks and six Stronghold Cards; optional unused Tech Tokens requires3..6, without Leader Skills. Standalone E3 uses ecaz33; Moritani excludes Harkonnen. Ecaz co-control holds the card at Ecaz, but only the selected actual holder-plan receives its effect; fixed Ecaz strength is free, and Arrakeen funds only actual variable support. Losing Moritani retains printed Tuek income before original assassination, printed bounty and one actual Mentat replacement; winner cleanup finishes once and normal Traitor reveal forfeits assassination for the game. Existing E1/E2 No-Field/Stone/FaceDance and retained winner rewards remain. E3 pairs/mixed families, mixed No-Field/full-plan, Stone/Poison Tooth and other overlays stay guarded. This composes existing contracts, not a new ruling. Save/privacy/custody assurance follows all rules. | [E1/E2 scope](STRONGHOLD_CARDS.md#expanded-e1e2-faction-rules), [standalone E3 scope and evidence](STRONGHOLD_CARDS.md#standalone-e3-stronghold-composition--4-october-2026), [native Tech composition](EXPANSION_FACTIONS_PROTOTYPE.md#native-tech-composition--4-october-2026) |
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
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.
|
||||
|
||||
## All14 Skills with independent cards — 17 October 2026
|
||||
|
||||
Supported original classic, selected E1/E2 and standalone/paired E3 all14
|
||||
profiles now retain independent cards with Strongholds2–6 Advanced, optionally
|
||||
Tech3–6, without World/Nexus/Discovery. Claims are actual end-Mentat play, not
|
||||
fixture owners. Earned Arrakeen funds only its selected holder-plan, not the
|
||||
ally, personal cash or a three-reserve card fee. Rescue/cleanup/winner Tech
|
||||
and native powers retain their original physical order.
|
||||
[Current471/471 scope and evidence](LEADER_SKILLS_RUNTIME.md#independent-cards-with-original-phase-modules--17-october-2026).
|
||||
|
||||
|
||||
## Independent Treachery composition — 16 October 2026
|
||||
|
||||
The fresh independent-card entry retains original unused Advanced Strongholds
|
||||
|
||||
+4
-2
@@ -10708,7 +10708,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
|
||||
const homeworlds = homeworldLeaderSkillsProfile(state);
|
||||
const independent = independentTreacheryLeaderSkillsProfile(state);
|
||||
if (state.ecazTreachery) {
|
||||
requireRule(independent, 'Independent Treachery with Leader Skills requires a fresh classic, selected Ix/CHOAM or standalone Ecaz/Moritani lobby without other modules.');
|
||||
requireRule(independent, 'Independent Treachery with Leader Skills requires a fresh supported classic or native lobby with only optional original Tech Tokens and Advanced Stronghold Cards.');
|
||||
requireFreshBaseRuntime(state);
|
||||
requireFreshFactionInventory(state);
|
||||
}
|
||||
@@ -10761,6 +10761,8 @@ function ecazHomeworldProfile(g: Game): boolean {
|
||||
}
|
||||
/** Independent cards compose original phase modules, not unrelated previews. */
|
||||
function independentCardModulesProfile(g: Game): boolean {
|
||||
if ((g.techTokens || g.strongholdCards) && g.leaderSkills && !g.sandtrout &&
|
||||
independentTreacheryLeaderSkillsProfile(g)) return true;
|
||||
if (!g.ecazTreachery || (!g.techTokens && !g.strongholdCards) || !independentCardRoster(g) || g.expansions.includes('ecaz') ||
|
||||
g.players.length < (g.techTokens ? 3 : 2) || g.players.length > 6 ||
|
||||
(g.strongholdCards && !g.advanced) ||
|
||||
@@ -13945,7 +13947,7 @@ function recruitsPlayBlock(g: Game, owner: Player, card: Card): string | null {
|
||||
if (!g.ecazTreachery || !ecazTreacheryDefinition(card) || card.id !== RECRUITS_CARD_ID)
|
||||
return 'Choose the physical Recruits card from the enabled Ecaz Treachery Cards variant.';
|
||||
if (!recruitsModeSupported(g))
|
||||
return 'Recruits requires a supported independent-card profile: faction-only, supported all14 Skills without other modules, or the existing native Homeworld and phase-module compositions.';
|
||||
return 'Recruits requires a supported independent-card profile: faction-only, all14 Skills with optional original phase modules, or the existing native Homeworld compositions.';
|
||||
if (g.status !== 'playing' || g.phase !== 4)
|
||||
return 'Play Recruits during Revival.';
|
||||
if (g.recruits?.turn === g.turn)
|
||||
|
||||
@@ -188,12 +188,13 @@ function pairedE3LeaderSkillRoster(game: LeaderSkillProfile): boolean {
|
||||
return ecaz && moritani;
|
||||
}
|
||||
|
||||
/** Both original E3 natives without an unrelated module; independent cards are optional. */
|
||||
/** Both original E3 natives with original phase modules; independent cards are optional. */
|
||||
export function pairedE3LeaderSkillsProfile(game: LeaderSkillProfile): boolean {
|
||||
if (game.expansions.length !== 1 || game.expansions[0] !== 'ecaz' ||
|
||||
typeof game.advanced !== 'boolean' || !game.players ||
|
||||
game.players.length < 2 || game.players.length > 6 ||
|
||||
(!noOtherLeaderSkillModules(game) && !independentTreacheryLeaderSkillsProfile(game)) ||
|
||||
game.homeworlds || game.nexusCards || game.discoveries || game.discoveryEnabled ||
|
||||
!e3LeaderSkillModulesSupported(game) ||
|
||||
game.discoveryStash || game.greatMaker || game.semutaPreview || game.advancedPreview ||
|
||||
(game.moritaniAssassinatePreview && !game.advanced) ||
|
||||
game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview ||
|
||||
@@ -278,6 +279,7 @@ function e3LeaderSkillModulesSupported(game: LeaderSkillProfile): boolean {
|
||||
|
||||
/** Classic three-to-six-seat Skills/Tech composition; native and other modules stay separate. */
|
||||
export function classicTechLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
|
||||
if (game.techTokens && !game.expansions.length && independentTreacheryLeaderSkillsProfile(game)) return true;
|
||||
const players = game.players;
|
||||
if (!game.techTokens || typeof game.advanced !== 'boolean' || game.expansions.length ||
|
||||
!players || players.length < 3 || players.length > 6 ||
|
||||
@@ -300,6 +302,7 @@ export function classicTechLeaderSkillsProfile(game: LeaderSkillProfile): boolea
|
||||
/** Original Advanced Strongholds, with optional Tech, for classic or supported native skills. */
|
||||
export function strongholdLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
|
||||
if (!game.strongholdCards || game.advanced !== true) return false;
|
||||
if (independentTreacheryLeaderSkillsProfile(game)) return true;
|
||||
if (game.expansions.length) return advancedNativeLeaderSkillsProfile(game) ||
|
||||
advancedMoritaniLeaderSkillsProfile(game) || nativeEcazLeaderSkillsProfile(game);
|
||||
const players = game.players;
|
||||
@@ -534,9 +537,10 @@ export function independentTreacheryLeaderSkillsProfile(game: LeaderSkillProfile
|
||||
const e3 = game.expansions.length === 1 && game.expansions[0] === 'ecaz';
|
||||
if (game.ecazTreachery !== true || game.expansions.length > 2 ||
|
||||
typeof game.advanced !== 'boolean' || !game.players ||
|
||||
game.players.length < 2 || game.players.length > 6 ||
|
||||
game.players.length < (game.techTokens ? 3 : 2) || game.players.length > 6 ||
|
||||
(game.strongholdCards && game.advanced !== true) ||
|
||||
game.homeworlds || game.nexusCards || game.discoveries || game.discoveryEnabled ||
|
||||
game.discoveryStash || game.greatMaker || game.techTokens || game.strongholdCards ||
|
||||
game.discoveryStash || game.greatMaker ||
|
||||
game.semutaPreview || game.advancedPreview ||
|
||||
game.kullPreview || game.nexusKullPreview || game.guildBetrayalPreview ||
|
||||
game.richeseBetrayalPreview || game.nexusIxianReplacementPreview ||
|
||||
|
||||
+13
-13
@@ -3236,7 +3236,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A Mentat replacement is an ordinary physical Traitor Card, including the original Ix Hero identity when present. After original owned initialization, retire the victim permanently. The replacement itself may later be legally exchanged, returned and drawn again; keep its old receipt unchanged and count one live card, not one per historical reference.',
|
||||
'The authorized revised Advanced PDF, physical page35, requires that you have not revealed your Traitor Card normally this game. A normal own-card reveal therefore forfeits Assassinate Leaders for the rest of this game, across turns and opponents. Existing state preserves that forfeiture; Karama has no effect against the ability. This is the authorized source, not a new GF9 erratum.',
|
||||
'The surviving actual played disc need not be native to its victorious controller. Native Ecaz/Duke6, Tleilaxu/foreign Guild5, CHOAM/Auditor2 and Harkonnen captives use the winner’s printed-faction targets. Captivity or Ghola control never rewrites native identity. Auditor has a matching Traitor outside CHOAM’s ordinary five; it can be a different native target for two but not the disc just played. Duke has no Traitor, and Cheap Hero is not a physical opposing disc. Exceptional captured/Ghola targets remain separate.',
|
||||
'Terror Assassination chooses a random leader when its token is triggered. It does not use this battle-loss condition, different-leader Traitor Card or once-per-faction allowance. Fresh supported plain/card Advanced Moritani starts initialize this printed advantage automatically; Basic never gains it. Independent cards also compose standalone or paired Ecaz/Moritani/classic all14 Skills without other modules, retaining Advanced Harkonnen and mixed-family Skills guards. Played saves do not gain invented prior-call history.',
|
||||
'Terror Assassination chooses a random leader when its token is triggered. It does not use this battle-loss condition, different-leader Traitor Card or once-per-faction allowance. Fresh supported plain/card Advanced Moritani starts initialize this printed advantage automatically; Basic never gains it. Standalone or paired Ecaz/Moritani/classic all14 Skills retains optional independent cards, original Tech3+ and Advanced Strongholds2+, with Advanced Harkonnen and mixed-family Skills guards. Played saves do not gain invented prior-call history.',
|
||||
],
|
||||
checklist: [
|
||||
{area:'Implementation',status:'Partial',detail:'Canonical played-disc/native target semantics remain. Moritani replacement accepts physical Hero separately; permanent victim cards join the shared removed census, while historical replacement IDs may move/recur legally. Exchange, Face Dancer, Betrayal and Rihani consumers bind their own source zone. Current authorization/census, revealed-card reservation and original parent remain.',evidence:['game/moritani-assassinate.ts','game/nexus-traitor-exchange.ts','game/nexus-face-dancers.ts','game/nexus-harkonnen-betrayal.ts','game/rihani-decipherer.ts','game/engine.ts']},
|
||||
@@ -3415,8 +3415,8 @@ 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.',
|
||||
'Fresh Advanced local development tables may combine all fourteen Leader Skills with Stronghold Cards in classic or supported Ixian, Tleilaxu, CHOAM and Richese skill rosters, two through six players; optional Tech Tokens needs three through six. Original deck/cache and end-Mentat claims remain. Arrakeen pays bank support, not personal income; only the remaining actual payer debit qualifies for normal Banker income, now mandatory in fresh supported Skills setups. Skill rescue preserves the original support charge, then card cleanup and Tech reward finish before Face Dance. Modified/copied Diplomat defenses and active skill/Stone interpretations stay guarded; public activation, E3/other overlays and save conversion remain separate.',
|
||||
'The original standalone Ecaz or Moritani skill roster additionally supports these Advanced combinations with the exact ecaz deck, original five-disc training and native exclusions. Normal Warmaster one on a temporary Duke composes Carthag Shield without granting trained three; normal winner Suk precedes unused-disc Moritani assassination, printed bounty, retained winner Tech and one private Mentat replacement. Standalone Ecaz also retains its selected-lead Arrakis coalition; unsupported E3 pairs and mixtures stay separate.',
|
||||
'Fresh Advanced classic or supported E1/E2 native all14 Skills retains original Strongholds2–6 and optional Tech3–6. Independent cards may also be selected within their No-World/Nexus/Discovery envelope. Original deck/cache and real end-Mentat claims remain. Arrakeen funds actual support, not personal income or reserve fees; only the final actual payer debit qualifies for mandatory normal Banker income. Rescue preserves the support charge, then card cleanup and Tech finish before Face Dance. Specific copied-defense/skill-Stone and unrelated module guards remain; no save conversion.',
|
||||
'Standalone or paired Ecaz/Moritani Skills retains these original Advanced phase modules, optionally independent cards, with exact ecaz33/36, native five-disc training and original roster exclusions. Normal Warmaster one on a temporary Duke never grants trained three. Winner Suk precedes unused-disc assassination, printed bounty, winner Tech and one Mentat replacement. Ecaz-held support belongs only to the selected Ecaz plan, not an allied Moritani plan; fixed and variable armies stay separate. Mixed E3 families and shared-Duke assignment remain guarded.',
|
||||
'Fresh independent Treachery previews compose classic or selected Ix/CHOAM native/base rosters, including original Homeworlds, with Advanced Strongholds2–6 and optional Tech3–6. Actual end-Mentat earns custody. Reinforcements adds no supported counters; Harass vacates control without transferring a retained card early. Exact native fee/return, original card effects and winner Tech order remain; other overlays stay guarded.',
|
||||
'The six effects, public ownership and pre-plan choice are integrated in development games. Enable Stronghold Cards in an Advanced preview lobby. Complete Advanced compliance and every expansion interaction remain under verification.',
|
||||
],
|
||||
@@ -3523,7 +3523,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Fremen reserve arrivals and Guild cross-planet or return shipments do not trigger Heighliners. A stopped shipment never arrives and cannot trigger it. Bene Gesserit accompanying forces do arrive from off planet and can trigger the token even after a Guild shipment.',
|
||||
'A battle winner takes one tech token from the defeated faction after casualties and played-card decisions. With several available tokens, the winner chooses. A winner still takes the token if no winning forces survive. Mutual destruction has no winner and transfers no token.',
|
||||
'Owning all three tokens counts as one additional stronghold at victory checks. An alliance cannot combine separate token holdings to qualify: one member must own the complete set. Multiple sides reaching the victory target can share the win.',
|
||||
'Token ownership and pending income are public and persist with the room. Fresh supported classic, E1/E2 native or standalone Ecaz/Moritani Leader Skills tables retain original three-through-six-seat Basic/Advanced setup and effects; Advanced may also retain Stronghold Cards. Skill rescue or board-spice awards finish before the mandatory original winner token reward and later Face Dance. Mandatory normal Banker income in fresh supported Skills setups waits for actual Mentat, independent of phase-end Tech income. CHOAM’s separate native opening payout is not ordinary poverty Charity. Two-player tokens, mixed E3/other combinations and full combined acceptance remain unfinished.',
|
||||
'Token ownership and pending income are public and persist. Fresh classic, E1/E2 native or standalone/paired Ecaz/Moritani all14 Skills retains original Tech3–6 Basic/Advanced; Advanced may add Strongholds2–6, with independent cards within the No-World/Nexus/Discovery envelope. Rescue or board-spice awards precede winner Tech and later Face Dance. Mandatory normal Banker waits for real Mentat, separate from phase-end Tech; fees and Harass are neither free revival nor off-planet shipment. Native CHOAM opening income is not poverty Charity. Two-player Tech, mixed E3 and full combined acceptance remain guarded.',
|
||||
'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 or selected Ix/CHOAM native/base independent-card previews, including Homeworlds, retain unused Tech3–6 Basic/Advanced and optional Advanced Strongholds2–6. 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; other overlays stay separate.',
|
||||
],
|
||||
@@ -4929,11 +4929,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
steps: [
|
||||
'Fresh all14 Leader Skills preserves original classic, supported E1/E2 or standalone Ecaz OR Moritani setup with Homeworlds/Discovery/Tech3+/Advanced Strongholds under each envelope. Explicit --nexus-cards additionally admits single/paired one-family E1/E2, BOTH selected Ix+CHOAM native families/union47 and standalone OR paired Ecaz/Moritani/ecaz33 with classics. Basic/Advanced2–6, original Ix pre-training offer, ordinary native discs and actual hands remain; Auditor/shared Duke never train. Winning normal/trained Suk uses source-labelled native, visitor or nested fixed-loss pools; native saved counters stay at that home and external Emperor returns allocate original homes. Original phase-end industry, held benefits, cleanup/Tech before Face Dance and native assassination remain; unreconciled bands, mixed E3/three-family, public starts and played-save conversion stay separate.',
|
||||
'Explicit guild-betrayal --leader-skills retains the original classic native-Guild Nexus profile without Homeworlds, Tech, Strongholds, Discovery, Semuta or other overlays. All14 training remains native. Ordinary skill quotes now admit the composition, restoring actual Mentat private questions before posture and Sandmaster ground collection rather than only enabling Bureaucrat. Native card custody, paid forces, original Betrayal Use/Pass and movement/plan continuation stay unchanged. Other combinations and complete-mode acceptance remain separate.',
|
||||
'Basic Moritani retains classic opponents including Harkonnen. Standalone or paired Ecaz/Moritani Skills without other modules, optionally independent cards, retains native setup and the Advanced non-Harkonnen assassination roster. Winner Suk rescue and Rihani choice finish first, then an eligible different trained disc may be assassinated with its exact skill return and printed bounty. No casualties repeat; original set-aside and one private Mentat replacement remain. Shared-Duke training and mixed-family Skills stay guarded.',
|
||||
'Basic Moritani retains classic opponents including Harkonnen. Standalone or paired Ecaz/Moritani Skills retains original Tech3+ and Advanced Strongholds2+, optionally independent cards, with the original Advanced non-Harkonnen assassination roster. Winner Suk and Rihani finish before an eligible different trained disc dies, returns one skill and pays printed bounty. No casualties repeat; original set-aside and one Mentat replacement remain. Shared-Duke training, mixed families and other module envelopes stay guarded.',
|
||||
'Fresh Advanced profiles compose native Ixians, Tleilaxu, CHOAM and/or Richese with classic opponents and distinct required family decks. All fourteen skills and native starts remain. Auditor stays unskilled; cyborg casualties, original CHOAM support and Richese capped marker revelation remain physical. Foreign revival creates no new skill. Supplied Advanced p34 excludes Richese auctions from Ixian Technology; own mixed No-Field, captured replacement, Advanced Atreides Suk/KH and unrelated overlays retain their guards.',
|
||||
'The independent Recruits, Reinforcements and Harass & Withdraw cards may be selected with all fourteen Skills in fresh classic, selected Ix/CHOAM native/base or standalone/paired Ecaz/Moritani/classic Basic/Advanced games, without other modules. Original36/38/50 decks, Richese cache, native setup, free-revival exceptions and paid Cyborg prices stay. Card fee/withdrawal/Suk and ordinary Smuggler, Planetologist and Sandmaster capabilities share admission. Ecaz retains selected-lead/separate armies; Advanced Moritani keeps skill-first assassination and one Mentat replacement. Advanced Harkonnen, mixed E3 families and other module guards remain.',
|
||||
'Fresh three-through-six-seat Basic/Advanced development tables may compose all fourteen Skills with original Tech Tokens: classic factions, supported E1/E2 native families, or standalone Ecaz/Moritani with the exact ecaz deck and original roster exclusions. Starting cards/cache, skill/Traitor choices and printed/first-Storm assignment stay genuine. Existing leader-skills/banker-income entry preserves unused selected tokens; mixed E3 families, other overlays, public activation and saved-game conversion remain separate.',
|
||||
'Fresh Advanced classic or supported native two-through-six-seat skill tables may retain six original Stronghold Cards, with Tech at three or more seats. Original end-Mentat/local held effects remain. Arrakeen changes actual payer cost, not rescue or Banker eligibility; native CHOAM income retains bank support. Physical defenses, normal disciplines, holder ties and original dial/Worthless earnings remain. Standalone Ecaz Duke/Arrakis coalition and Moritani assassination compose original rules; mixed E3, exceptional copied defenses, active skill/Stone and other guards stay separate.',
|
||||
'The independent Recruits, Reinforcements and Harass & Withdraw cards compose all14 Skills in fresh classic, selected Ix/CHOAM native/base or standalone/paired Ecaz/Moritani/classic Basic/Advanced games, optionally original Tech3+ and Advanced Strongholds2–6. Original36/38/50, Richese cache, native setup and revival prices stay. Card fee/returns/Suk and Smuggler, Planetologist and Sandmaster share admission. Earned support is not personal spice or reserve fee funding; phase-end Tech and actual-Mentat Banker remain separate. Advanced E3/Harkonnen, mixed E3, World/Nexus/Discovery and other guards remain.',
|
||||
'Fresh all14 Skills retains original Tech3–6 Basic/Advanced for classic factions, supported E1/E2 or standalone/paired Ecaz/Moritani with their actual selected decks. Cards are optional within the independent-card envelope. Starting hands/cache, skill/Traitor choices and printed/first-Storm owners remain genuine. No token owner, income or victory credit is assigned to simulate earned play. Existing World/Nexus envelopes and mixed E3 or unrelated guards stay separate; played games are not converted.',
|
||||
'Fresh Advanced supported classic/native all14 tables retain six original Stronghold Cards, optional Tech3+, and independent cards within their own envelope. Actual end-Mentat earns the holder; benefit stays local to the selected holder-plan. Arrakeen changes actual payer debit, not physical commitment, Suk entitlement or Banker threshold. Native E1/E2 aftercare and standalone/paired E3 coalition/assassination retain original order. Shared Duke never trains; mixed E3, unsupported copied defenses and active skill/Stone retain their guards.',
|
||||
'Fresh classic Leader Skills may explicitly add Nexus through local leader-skills --nexus-cards, Basic/Advanced two through six players, retaining selected original Tech at three or more seats and/or Advanced Strongholds. --discoveries additionally composes original Discovery7/8 in this classic profile. Saved hands/offers remain authoritative; the actual end-Spice closing draw follows Maker votes/typed rides, both Advanced piles and settled alliance. Free-return ledgers, bank purchase and Traitor Deck declarations stay distinct from training. Original phase-end industry, held payer support/physical defenses, physical Suk rescue and subsequent mandatory winner Tech rewards retain their own timing. Homeworlds, Banker/Mentat previews, borrowed Smuggler arithmetic and unresolved interactions remain separate.',
|
||||
'The same fresh entry also adds --nexus-cards --discoveries to paired Ixians+Tleilaxu, paired CHOAM+Richese or standalone Ecaz OR Moritani with classics, original all14/all12/Discovery7/8, Basic/Advanced two through six with optional Tech3+ and Advanced Strongholds. Original offers/hands/native components remain authoritative. E1 Suboid Cunning settles typed Suk rescue before one equal Cyborg Tank substitution, then original cleanup and mandatory winner Tech precede a matching Face Dance; E2 keeps its signed one-invoice pair; Ecaz keeps quiet living/unclaimed Duke then a real owner-labelled mandatory Occupy; Moritani keeps skill-first assassination and one private Mentat replacement. Mixed/E3 pairs, shared Duke, borrowed Smuggler/pair companions, native free returns, captured/foreign training, Basic odd Ecaz, Advanced Harkonnen and other pending rulings stay guarded.',
|
||||
'Both original paired Ixian/Tleilaxu or CHOAM/Richese factions plus classics may add Nexus through that fresh local skill entry, with one family deck/all14/all12, Basic/Advanced two through six players. Selected original Tech requires3+; Strongholds requires Advanced. Full unsupported Suboids retain Cyborg support and skill posture; physical Suk rescue precedes equal actual Cyborg Tank substitution. Original winner cleanup and mandatory Tech precede Face Dance, which returns a matching killed trainer skill once without another bounty. Held custody stays with the original winner. Richese Cunning retains its signed two-marker/one-invoice operation and native phase-end industry; ordinary skill proofs and companion exclusions stay separate. CHOAM fuel precedes ordinary Planetologist movement without a second card effect or artificial combat Special.',
|
||||
@@ -4985,7 +4985,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Clean classic2–6 Basic/Advanced Homeworlds compose through the fresh independent36-card setup. Double current native free rates including low-population bonuses, within the seven-force total. Provisionally recheck population and recorded free use on each separate request: low Fremen may take7 together, but1 crossing high leaves5 further free and cannot buy a seventh without Tleilaxu. Original typed destinations and special-force limits stay. Other overlays/rosters, public activation and whole-mode acceptance remain unfinished.',
|
||||
'Classic or selected Ix/CHOAM native/base rosters, including Homeworlds, may retain original unused Tech Tokens3–6 and/or Stronghold Cards2–6 Advanced. Setup, first-Storm Tech and actual end-Mentat custody remain original. Recruits changes current native free rates, not the once-per-phase Axlotl trigger/delayed payout. Harass returns are not free Revival.',
|
||||
'Selected Ix/CHOAM native/base Basic/Advanced Worlds use original38/50 independent-card setup with optional original phase modules. Current native bonuses, typed deposits and accepted paid/free classifications stay. Ix keeps its multiple-Cyborg allowance; Tleilaxu and CHOAM remain unlimited rather than being reduced to seven. Other card/overlay/public-start gates remain.',
|
||||
'Fresh classic or selected Ix/CHOAM games may combine these three cards with all fourteen Leader Skills, without other modules. The current native free allowance is still doubled after prior recorded use. An Ix revival keeps its paid Cyborg surcharge and any Tleilaxu recipient; native CHOAM/Tleilaxu limits are not reduced to seven. No separate skill or native revival ledger is invented.',
|
||||
'Fresh supported classic, selected Ix/CHOAM or standalone/paired Ecaz/Moritani all14 Skills may retain these cards with original Tech3+ and Advanced Strongholds2+. Current native free allowance still doubles after recorded use; paid Cyborg surcharges and any Tleilaxu recipient remain, and native unlimited allowances are not reduced to seven. Axlotl accrues once and waits for phase end; mandatory normal Banker uses the actual final Bank payer and waits for Mentat. No revival ledger, refund, token owner or spendable premature income is invented.',
|
||||
],
|
||||
related: ['revival', 'homeworlds', 'ecaz-modules', 'expansion-faction-games', 'implementation-checklist'],
|
||||
checklist: [
|
||||
@@ -5007,12 +5007,12 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'In the independent Ecaz Treachery Cards variant, place your held Reinforcements card in either battle-card slot; it is neither a weapon nor a defense. A category Prescience answer is None, not the name of this card. One physical card cannot fill both slots.',
|
||||
'Have three own forces in reserves when sealing. Classic/paired Ecaz/Moritani Basic/Advanced, clean selected Ix/CHOAM native/base rosters and explicitly selected native Advanced Occupy prototypes spend ordinary reserves first and then elite reserves, moving exactly three to corresponding Tanks subpools at resolution. These are the actual card holder’s reserves, not the variable ally’s pool; no extra forces arrive.',
|
||||
'The normal-outcome score gains 2 without adding to the physical dial, spice support or on-board casualties. The card and its reserve cost are consumed even on a Traitor victory, a mutual Traitor result or a Lasgun/Shield explosion; successful Traitor calls still decide the outcome directly.',
|
||||
'All-outcome payment, normal-first selection and reserve transfer counting as battle losses remain provisional interpretations, not publisher rulings. Same-plan Harass pays the original three-reserve cost before returns; returned reserves cannot fund or change it. In native Advanced Occupy, original or copied Stone ignores +2 normal score but still charges the reserve cost. Basic/unprofiled co-side and other profile/module limits remain guarded.',
|
||||
'All-outcome payment, normal-first selection and reserve transfer counting as battle losses remain provisional, not publisher rulings. Same-plan Harass pays the original three-reserve fee before returns; returned reserves cannot fund it. In native Advanced Occupy, original/copied Stone ignores +2 normal score but still charges the fee. Basic mixed-marker co-sides, unprofiled armies and other card-specific limits remain guarded.',
|
||||
'With your own concealed No-Field, the provisional native order materializes its reserve-limited counters before this cost. At least three own reserves must remain after that reveal, before any future Harass return; otherwise Reinforcements is unavailable before the first plan seals. This checks only the actual card user, never its ally’s marker or reserves.',
|
||||
'Classic and selected Ix/CHOAM Basic/Advanced Worlds retain exact native normal-first sources, sealed when ambiguous; unique sources may derive. Own-World fees cannot also fund dial/casualties; original face/support stays, including explosion thresholds. Original phase modules preserve their own prerequisites and earned custody. Native-only World Traitors, typed Tanks and fee-before-Harass stay provisional composition, not a new card FAQ.',
|
||||
'One canonical physical-name registry now includes the independent cards for current facts and named battle conditions. Naming Reinforcements in a weapon slot does not make it a weapon category. Native questions use actual combat-location labels. Source-aware legal completion, private full-plan/read-only review and the fill button retain exact native sources.',
|
||||
'Selected Ix/CHOAM Worlds retain original38/50 setup and native stock, optionally with original Tech3+ and Advanced Strongholds2+. Cyborg substitution and Face Dance use their actual physical pools, never a duplicate fee origin. Native Richese must retain three World reserves AFTER its own projected No-Field reveal before R seals; rejection moves no counter or marker. Original phase rewards and earned held-card support stay; admitted Stone compositions retain their separate guards.',
|
||||
'With supported native Ix/CHOAM all-fourteen Skills, the same physical fee and score rules apply. Trained Suk may rescue the actual committed battle casualties, never the three reserve counters spent on this card. A native Richese marker must still leave the full fee after its own materialization; future Harass returns cannot fund a refused seal.',
|
||||
'Supported all14 Skills profiles, including original phase modules, keep the same physical fee and score. Trained Suk rescues only actual committed casualties, never reserve fee counters. Earned Arrakeen pays eligible support, not the fee or personal spice. A native Richese marker must leave the full fee after materialization; future Harass cannot fund a refused seal. Fixed Ecaz and variable ally ownership, mandatory disposal and original winner rewards stay separate.',
|
||||
],
|
||||
related: ['battle-cards', 'battle', 'homeworlds', 'card-recruits', 'card-harass-withdraw', 'card-truthtrance', 'implementation-checklist'],
|
||||
checklist: [
|
||||
@@ -5036,12 +5036,12 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Return your undialed ordinary and elite forces to reserves. Your leader can still be killed. An opponent’s successful Traitor call cancels withdrawal, including mutual calls. A Face Dancer cannot replace the forces already returned.',
|
||||
'Discard Harass after use, including a win by your own Traitor call. Reveal-before-explosion return and card-specific discard precedence are explicit implementation inferences; neither is a dedicated combined FAQ ruling.',
|
||||
'For multiple legal physical allocations, choose all undialed ordinary and elite counters by sector after both plans reveal and before traitor declarations. Unique returns are automatic. The selected return is public; it is not an extra sealed plan element. Stone Burner provisionally compares the undialed forces remaining after successful withdrawal, including same-plan, opposing and copied Stone. Additional optional modules remain guarded. The printed card forbids use on your own Homeworld.',
|
||||
'In explicitly selected Advanced Occupy, only the actual card user’s undialed forces return. An Ecaz user preserves its original mandatory fixed count at free full strength; an allied user uses its native variable commitment, while canceled Occupy uses its selected lead’s ordinary own pool. Returned reserves remain outside losses and Face Dance. Basic/unprofiled co-side, public starts and full acceptance remain gated.',
|
||||
'In supported native Basic/Advanced Occupy, only the actual card user’s undialed forces return. An Ecaz user preserves the original mandatory fixed count at free full strength; an allied user uses its native variable commitment, while canceled Occupy uses only the selected lead’s own pool. Returned reserves stay outside losses and Face Dance. Basic mixed markers, unprofiled co-sides and complete-mode acceptance remain guarded.',
|
||||
'In supported same-plan Reinforcements, the provisional order pays three original own reserves before Harass returns undialed counters. Future returns cannot fund or change that reserve quote; neither the +2 score bonus nor that cost adds to the physical commitment. Both cards discard after use. An opposing successful Traitor cancels Harass but not the provisional Reinforcements cost.',
|
||||
'With an own concealed No-Field, this prototype provisionally uses the original materialization-first order. Before sealing, privately forecast its actual ordinary counters at the original marker sector; do not move them early. Both plans seal before native revelation, then any return allocation uses actual counters. Marker0 remains legal with a leader/cards, reserve shortage caps materialization, and withdrawal does not return/reset the token. Opposing successful Traitor cancels Harass, not that native reveal.',
|
||||
'Classic and selected Ix/CHOAM Basic/Advanced Worlds may choose the independent cards before original setup. Harass works on Arrakis or a foreign World, never an own World. Return actual undialed board/visitor types through native custody: normal/special to the original primary World except Advanced Emperor normal to Kaitain and Sardaukar to Salusa. Original commitment stays across thresholds; native-only World Traitors/Face Dancers, late defense/explosion and disposal stay.',
|
||||
'Classic or selected Ix/CHOAM native/base rosters, including Homeworlds, compose original Tech3+ and/or Advanced Strongholds2+. An earned held Stronghold affects only its original eligible plan/actual support; +2 R adds no supported counters. Harass deposits are not off-planet shipment/free Revival. Original winner card cleanup/Tech reward and native queues retain their order.',
|
||||
'Supported native Ix/CHOAM all-fourteen Skills keep the same typed undialed complement and original selected-trainer effects. Ix may return both Suboids and Cyborgs, then resolve trained rescue only on the remaining committed casualties. A capped Richese marker may legally play Harass when Reinforcements is unfunded; unambiguous single-sector returns stay automatic.',
|
||||
'Supported all14 Skills with original Tech/Advanced Strongholds keeps the same typed undialed complement and selected-trainer effects. Ix may return Suboids/Cyborgs, then rescue only committed losses. Native E3 keeps fixed/variable owners separate. Earned held support belongs only to its original eligible selected plan; it creates no reserves or fee funding. A capped Richese marker may legally use Harass when Reinforcements is unfunded; unique single-sector returns stay automatic.',
|
||||
],
|
||||
related: ['battle-cards', 'battle', 'card-recruits', 'homeworlds', 'ecaz-modules', 'implementation-checklist'],
|
||||
checklist: [
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { BattleResolutionQuote } from '../game/battle-resolution-quote';
|
||||
import { quoteIndependentSkillsBattle } from './fixture-independent-skills-battle';
|
||||
import {
|
||||
assertChoamIndependentSkillsCustody, choamIndependentSkillsPlayer,
|
||||
createChoamIndependentSkillsFixture,
|
||||
createChoamIndependentSkillsFixture, createChoamIndependentSkillsHeldSupportFixture,
|
||||
finishChoamIndependentSkillsBattle, openChoamIndependentSkillsBattle,
|
||||
sealChoamIndependentSkillsBoth, type ChoamIndependentSkillsFixture,
|
||||
} from './fixture-choam-independent-skills';
|
||||
@@ -159,3 +159,54 @@ void test('The independent CHOAM/Richese setup keeps the separate cache and each
|
||||
(rival(fixture, fixture.beforePlans).forces[fixture.location] ?? 0),
|
||||
'The defeated garrison, not armies elsewhere, reached the Tanks');
|
||||
});
|
||||
|
||||
void test('the first real Richese Mentat claim supports a funded second marker without paying or refunding its reserve fee', () => {
|
||||
const f = createChoamIndependentSkillsHeldSupportFixture({ tech: true, strongholds: true });
|
||||
const { first } = f;
|
||||
assert.equal(first.offered.richeseCache!.length, 10);
|
||||
assert.notEqual(first.trainers[first.choam], 'choam-auditor');
|
||||
assert.equal(f.firstAfterBattle.strongholdCards!.owners.arrakeen, null);
|
||||
assert.equal(owner(first, f.firstAfterBattle).forces[first.location], 2,
|
||||
'The first machine winner really keeps the two undialed marker counters');
|
||||
assert.equal(f.afterFirstMentat.strongholdCards!.claimedTurn, 1);
|
||||
assert.equal(f.afterFirstMentat.strongholdCards!.owners.arrakeen, first.richese);
|
||||
assert.equal(owner(first, f.afterFirstMentat).forces[first.location], 2);
|
||||
const shipped = owner(first, f.afterMarkerShipment), beforeShipment = owner(first, f.beforeMarkerShipment);
|
||||
assert.equal(shipped.reserves, beforeShipment.reserves,
|
||||
'The different concealed token does not borrow physical counters before reveal');
|
||||
assert.notEqual(shipped.noField!.lastShipped, owner(first, first.afterShipment).noField!.lastShipped,
|
||||
'The original same-token consecutive-shipment limit remains in force');
|
||||
assert.equal(owner(first, f.beforePlans).forces[first.location], 2);
|
||||
assert.equal(rival(first, f.beforePlans).reserves, rival(first, f.beforeEnemyShipment).reserves - 4);
|
||||
assert.equal(rival(first, f.beforePlans).spice, rival(first, f.beforeEnemyShipment).spice - 4,
|
||||
'The returning enemy army paid its real four-force stronghold invoice');
|
||||
const preview = viewGame(f.beforePlans, first.richese).battle!;
|
||||
assert.equal(preview.strongholdEffects![first.richese], 'arrakeen');
|
||||
assert.equal(preview.reinforcements!.noField!.materializes, 5);
|
||||
assert.equal(preview.reinforcements!.noField!.reservesAfterReveal, 9);
|
||||
assert.equal(preview.reinforcements!.normal, 3);
|
||||
assert.equal(owner(first, f.revealed).forces[first.location], 7);
|
||||
assert.equal(owner(first, f.revealed).reserves, 9);
|
||||
const quote = quoteIndependentSkillsBattle(f.revealed, { returns: f.returns });
|
||||
const payment = quote.payments.find(entry => entry.player === first.richese)!;
|
||||
assert.equal(payment.bankSupport, 2);
|
||||
assert.equal(payment.ownPayment, 0);
|
||||
assert.equal(payment.cost, 0);
|
||||
assert.deepEqual(bonusOf(f.revealed, quote, first.richese),
|
||||
{ bonus: 2, applied: [{ skill: 'mentat', amount: 2, mode: 'skilled' }] });
|
||||
assert.deepEqual(quote.reinforcements,
|
||||
[{ player: first.richese, card: REINFORCEMENTS_CARD, normal: 3, elite: 0 }]);
|
||||
assert.equal(quote.auditor, null, 'The native Auditor never becomes an ordinary trained battle leader');
|
||||
const after = owner(first, f.afterBattle), revealed = owner(first, f.revealed);
|
||||
assert.equal(f.afterBattle.lastBattleContext!.winner, first.richese);
|
||||
assert.equal(after.spice, revealed.spice, 'Held bank support is a discount, never personal income');
|
||||
assert.equal(after.reserves, revealed.reserves - 3 + 5,
|
||||
'The three-reserve card fee is still paid before the five real undialed forces return');
|
||||
assert.equal(after.tanks, revealed.tanks + 3 + 2,
|
||||
'Free support neither funds extra reserve counters nor rescues the fee from Tanks');
|
||||
assert.equal(after.forces[first.location] ?? 0, 0);
|
||||
assert.equal(rival(first, f.afterBattle).tanks, rival(first, f.revealed).tanks + 4);
|
||||
for (const id of [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD])
|
||||
assert.equal(f.afterBattle.discard.filter(card => card.id === id).length, 1);
|
||||
assertChoamIndependentSkillsCustody(first, f.afterBattle);
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { treacheryDeck } from '../game/cards';
|
||||
import { ecazTreacheryCards } from '../game/ecaz-cards';
|
||||
import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
|
||||
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
|
||||
import { ownedTech } from '../game/tech-tokens';
|
||||
import {
|
||||
assertEcazIndependentSkillsCustody,
|
||||
createEcazIndependentSkillsFixture, ecazIndependentSkillsPlayer as player,
|
||||
@@ -45,8 +46,8 @@ function disposedLeadCards(f: EcazIndependentSkillsFixture, end: Game): void {
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) {
|
||||
void test(`original independent cards with all14 ${advanced ? 'Advanced' : 'Basic'} reach the printed lead choice, and the trained Ecaz Suk rescues only its own committed losses`, () => {
|
||||
const f = createEcazIndependentSkillsFixture({ advanced, lead: 'ecaz' });
|
||||
void test(`original independent cards with all14 ${advanced ? 'Advanced' : 'Basic Tech'} reach the printed lead choice, and the trained Ecaz Suk rescues only its own committed losses`, () => {
|
||||
const f = createEcazIndependentSkillsFixture({ advanced, lead: 'ecaz', tech: !advanced });
|
||||
physicalSource(f.initial, 36);
|
||||
physicalSource(f.game, 36);
|
||||
assert.equal(f.beforeLead.decision?.kind, 'ecazBattleLead');
|
||||
@@ -110,6 +111,13 @@ for (const advanced of [false, true]) {
|
||||
assert.equal(change(player(f.game, f.opponent).tanks, player(end, f.opponent).tanks), 2);
|
||||
assert.equal(at(end, f.opponent, f.location), 0);
|
||||
assert.equal(change(player(f.game, f.opponent).reserves, player(end, f.opponent).reserves), 0);
|
||||
if (!advanced) {
|
||||
const tokens = ownedTech(f.game.techTokens, f.opponent);
|
||||
assert.equal(tokens.length, 1, 'the actual defeated Guild carries one original token');
|
||||
assert.equal(end.techTokens![tokens[0]].owner, f.ecaz,
|
||||
'original winner Tech cleanup credits the selected Ecaz plan, after its own card fee and rescue');
|
||||
disposedLeadCards(f, end);
|
||||
}
|
||||
assertEcazIndependentSkillsCustody(end);
|
||||
});
|
||||
}
|
||||
@@ -333,8 +341,8 @@ void test('native paired Ecaz lead pays its own fee, returns its undialed fixed
|
||||
assertEcazIndependentSkillsCustody(end);
|
||||
});
|
||||
|
||||
void test('native paired Basic Moritani lead pays and withdraws its normal variable army without merging Ecaz fixed losses', () => {
|
||||
const f = createEcazIndependentSkillsFixture({ paired: true, advanced: false, lead: 'ally' });
|
||||
void test('native paired Basic Tech Moritani lead pays and withdraws its normal variable army without merging Ecaz fixed losses', () => {
|
||||
const f = createEcazIndependentSkillsFixture({ paired: true, advanced: false, lead: 'ally', tech: true });
|
||||
const quote = quoteEcazIndependentSkillsBattle(f);
|
||||
assert.deepEqual(quote.reinforcements,
|
||||
[{ player: f.ally, card: REINFORCEMENTS_CARD, normal: 3, elite: 0 }]);
|
||||
@@ -354,8 +362,51 @@ void test('native paired Basic Moritani lead pays and withdraws its normal varia
|
||||
assert.equal(at(end, f.ecaz, f.location), 2);
|
||||
assert.equal(player(end, f.ecaz).leaders.find(leader => leader.id === f.trainer)!.dead, false);
|
||||
assert.equal(change(player(f.game, f.opponent).tanks, player(end, f.opponent).tanks), 2);
|
||||
const tokens = ownedTech(f.game.techTokens, f.opponent);
|
||||
assert.equal(tokens.length, 1);
|
||||
assert.equal(end.techTokens![tokens[0]].owner, f.ally,
|
||||
'the selected Moritani winner, not its Ecaz coalition partner, inherits the defeated Guild token');
|
||||
disposedLeadCards(f, end);
|
||||
pairedArmies(f, end);
|
||||
physicalSource(end, 36);
|
||||
assertEcazIndependentSkillsCustody(end);
|
||||
});
|
||||
|
||||
for (const lead of ['ecaz', 'ally'] as const) {
|
||||
void test(`earned Arrakeen supports only its Ecaz holder's selected plan: paired ${lead === 'ecaz' ? 'holder' : 'Moritani'} lead keeps card fees, losses and Tech separate`, () => {
|
||||
const f = createEcazIndependentSkillsFixture({ paired: true, tech: true, strongholds: true, lead });
|
||||
const claim = f.afterFirstMentat!;
|
||||
assert.equal(claim.strongholdCards!.claimedTurn, 1);
|
||||
assert.equal(claim.strongholdCards!.owners.arrakeen, f.ecaz);
|
||||
assert.equal(at(claim, f.ecaz, f.location), 4);
|
||||
assert.equal(f.actions.filter(step => step.actor === f.ecaz && step.action.type === 'ship').length, 1,
|
||||
'the same four paid turn-one counters remain through the claim and real worm alliance');
|
||||
const quote = quoteEcazIndependentSkillsBattle(f);
|
||||
const payment = quote.payments.find(payment => payment.player === f.cardUser)!;
|
||||
assert.equal(payment.bankSupport, lead === 'ecaz' ? 2 : 0);
|
||||
assert.equal(payment.ownPayment, lead === 'ecaz' ? 0 : 2,
|
||||
'Moritani does not inherit the Ecaz-held card when Ecaz selects its ally to lead');
|
||||
assert.deepEqual(quote.reinforcements,
|
||||
[{ player: f.cardUser, card: REINFORCEMENTS_CARD, normal: 3, elite: 0 }]);
|
||||
assert.deepEqual(quote.harassWithdraw![0].returned, { normal: 2, elite: 0 });
|
||||
assert.deepEqual(quote.fixedLosses, [{ owner: f.ecaz, normal: 2, elite: 0 }]);
|
||||
assert.equal(quote.casualties?.owner, f.ally);
|
||||
const paid = settleEcazIndependentSkillsBattle(f, { stop: 'rescue' });
|
||||
assert.equal(player(paid, f.cardUser).spice, player(f.game, f.cardUser).spice - payment.ownPayment,
|
||||
'support settles before the Ecaz rescue and the separate Collection income');
|
||||
const end = settleEcazIndependentSkillsBattle(f, { state: paid });
|
||||
assert.equal(end.phase, 7);
|
||||
assert.equal(end.lastBattleContext?.winner, f.cardUser);
|
||||
assert.equal(change(player(f.game, f.ecaz).tanks, player(end, f.ecaz).tanks), lead === 'ecaz' ? 3 : 2);
|
||||
assert.equal(change(player(f.game, f.ally).tanks, player(end, f.ally).tanks), lead === 'ecaz' ? 2 : 5);
|
||||
assert.equal(end.lastBattleContext?.sukRescue?.completed, lead === 'ecaz' ? true : undefined);
|
||||
const lostTokens = ownedTech(f.game.techTokens, f.opponent);
|
||||
assert.equal(lostTokens.length, 1);
|
||||
assert.equal(end.techTokens![lostTokens[0]].owner, f.cardUser);
|
||||
disposedLeadCards(f, end);
|
||||
pairedArmies(f, end);
|
||||
physicalSource(end, 36);
|
||||
assert.deepEqual(end.ecazLoyalty, f.afterSetup.ecazLoyalty);
|
||||
assertEcazIndependentSkillsCustody(end);
|
||||
});
|
||||
}
|
||||
@@ -32,6 +32,8 @@ export type ChoamIndependentSkillsOptions = {
|
||||
/** Continue only the original fresh undealt lobby or its own post-start setup. */
|
||||
initial?: Game;
|
||||
advanced?: boolean;
|
||||
tech?: boolean;
|
||||
strongholds?: boolean;
|
||||
route?: ChoamIndependentSkillsRoute;
|
||||
marker?: NoFieldValue;
|
||||
/** Own physical reserves the Basic 'tuning' route must leave before shipping. */
|
||||
@@ -251,6 +253,13 @@ export function createChoamIndependentSkillsSetup(
|
||||
assert.ok(!game.lobbyOptions && !game.deck.length && !game.richeseCache &&
|
||||
game.players.every((player) => !player.hand.length),
|
||||
'Only the original fresh undealt lobby accepts the two module selections.');
|
||||
for (const [type, enabled] of [
|
||||
['techTokens', options.tech ?? false], ['strongholdCards', options.strongholds ?? false],
|
||||
] as const) if (enabled) {
|
||||
const action: Action = { type, enabled };
|
||||
actions.push({ actor: game.host, action });
|
||||
game = applyAction(game, game.host, action);
|
||||
}
|
||||
for (const option of ['leaderSkills', 'ecazTreachery'] as const) {
|
||||
const action: Action = { type: 'moduleOption', module: option, enabled: true };
|
||||
actions.push({ actor: game.host, action });
|
||||
@@ -329,6 +338,8 @@ export function nextChoamIndependentSkillsStep(
|
||||
): ChoamIndependentSkillsStep | null {
|
||||
if (!game.response && !game.phaseOpening && game.decision) {
|
||||
const decision = game.decision;
|
||||
if (decision.kind === 'techToken')
|
||||
return { actor: decision.player, action: { type: 'decision', token: decision.choices[0] } };
|
||||
if (decision.kind === 'leaderSkillVisibility')
|
||||
return { actor: decision.player, action: { type: 'leaderSkillVisibility', event: decision.event,
|
||||
hide: policy.hide?.[decision.player] ?? false } };
|
||||
@@ -629,3 +640,95 @@ export function finishChoamIndependentSkillsBattle(
|
||||
}
|
||||
throw Error('The original independent-card battle aftermath did not reach Collection');
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the original turn-one machine winner's two undialed forces in Arrakeen,
|
||||
* earn its card at the first real Mentat, then ship a different native marker
|
||||
* into that held city before the original Atreides seat pays to return there.
|
||||
* No owner, army, wallet, marker or phase receipt is assigned.
|
||||
*/
|
||||
export function createChoamIndependentSkillsHeldSupportFixture(
|
||||
options: Pick<ChoamIndependentSkillsOptions, 'tech' | 'strongholds'> = {},
|
||||
) {
|
||||
assert.equal(options.strongholds, true, 'The held-support programme needs original Stronghold Cards');
|
||||
const first = createChoamIndependentSkillsFixture({
|
||||
...options, advanced: true, route: 'immediate', stopBeforePlans: true,
|
||||
});
|
||||
assert.equal(first.location, 'arrakeen:10');
|
||||
const actions: ChoamIndependentSkillsStep[] = [];
|
||||
const firstAfterBattle = finishChoamIndependentSkillsBattle(
|
||||
sealChoamIndependentSkillsBoth(first, 'reinforcements'), { actions });
|
||||
let game = firstAfterBattle;
|
||||
const act = (step: ChoamIndependentSkillsStep) => {
|
||||
actions.push(structuredClone(step));
|
||||
game = applyAction(game, step.actor, step.action);
|
||||
};
|
||||
const advance = (until: (state: Game) => boolean) => {
|
||||
for (let count = 0; count < 1600; count++) {
|
||||
if (until(game)) return;
|
||||
const step = nextChoamIndependentSkillsStep(game, { hide: first.hide });
|
||||
assert.ok(step, 'A real native continuation must reach the next held-city action');
|
||||
act(step);
|
||||
}
|
||||
throw Error('Held Arrakeen programme did not reach its next native action');
|
||||
};
|
||||
advance(state => state.turn === 2 && state.phase === 0 && choamIndependentSkillsClean(state));
|
||||
const afterFirstMentat = structuredClone(game);
|
||||
let beforeMarkerShipment: Game | undefined, afterMarkerShipment: Game | undefined;
|
||||
let beforeEnemyShipment: Game | undefined, markerShipment: ChoamIndependentSkillsStep | undefined;
|
||||
const enemyShipment: ChoamIndependentSkillsStep = { actor: first.opponent, action: {
|
||||
type: 'ship', territory: 'arrakeen', sector: 10, amount: 4,
|
||||
} };
|
||||
const waiting = new Set([first.richese, first.opponent]);
|
||||
while (waiting.size) {
|
||||
advance(state => state.turn === 2 && state.phase === 5 && !!state.active &&
|
||||
waiting.has(state.active) && choamIndependentSkillsClean(state));
|
||||
const actor = game.active!;
|
||||
if (actor === first.richese) {
|
||||
beforeMarkerShipment = structuredClone(game);
|
||||
const richese = choamIndependentSkillsPlayer(game, actor);
|
||||
const marker = richese.noField!.tokens.find(token => token.value === 5)!;
|
||||
markerShipment = { actor, action: { type: 'ship', noField: marker.id,
|
||||
event: richese.noFieldEvent, territory: 'arrakeen', sector: 10 } };
|
||||
act(markerShipment);
|
||||
} else {
|
||||
beforeEnemyShipment = structuredClone(game);
|
||||
act(enemyShipment);
|
||||
}
|
||||
advance(state => state.active === actor && choamIndependentSkillsClean(state) && !state.pendingShipment);
|
||||
if (actor === first.richese) afterMarkerShipment = structuredClone(game);
|
||||
act({ actor, action: { type: 'endMovement' } });
|
||||
waiting.delete(actor);
|
||||
}
|
||||
assert.ok(beforeMarkerShipment && afterMarkerShipment && beforeEnemyShipment && markerShipment);
|
||||
advance(state => state.turn === 2 && state.phase === 6 && choamIndependentSkillsClean(state));
|
||||
const staging = [...first.staging];
|
||||
positionCard(game, first.richese, REINFORCEMENTS_CARD, staging);
|
||||
const beforeBattle = structuredClone(game);
|
||||
const choice = viewGame(game, first.richese).battleChoices.find(candidate => candidate.territory === 'arrakeen');
|
||||
assert.ok(choice);
|
||||
const battleStep: ChoamIndependentSkillsStep = { actor: choice.chooser, action: {
|
||||
type: 'chooseBattle', territory: 'arrakeen',
|
||||
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker,
|
||||
} };
|
||||
act(battleStep);
|
||||
advance(state => !!state.battle && !state.battle.revealed && choamIndependentSkillsClean(state) &&
|
||||
!!state.battle.preLeader?.closed && !state.battle.preparation);
|
||||
const beforePlans = structuredClone(game);
|
||||
const plans: ChoamIndependentSkillsStep[] = [
|
||||
{ actor: first.richese, action: { type: 'battlePlan', dial: 2, support: 2,
|
||||
leader: first.trainers[first.richese], weapon: REINFORCEMENTS_CARD, defense: HARASS_WITHDRAW_CARD } },
|
||||
{ actor: first.opponent, action: { type: 'battlePlan', dial: 0, support: 0,
|
||||
leader: availableLeaders(game, first.opponent, 'arrakeen')[0].id } },
|
||||
];
|
||||
for (const plan of plans) {
|
||||
actions.push(structuredClone(plan));
|
||||
game = sealChoamIndependentSkillsPlan(game, plan, { hide: first.hide });
|
||||
}
|
||||
const revealed = structuredClone(game);
|
||||
const returns: HarassWithdrawSelection = { [first.location]: { normal: 5, elite: 0 } };
|
||||
const afterBattle = finishChoamIndependentSkillsBattle(game, { return: returns, actions });
|
||||
return { first, firstAfterBattle, afterFirstMentat, beforeMarkerShipment, markerShipment,
|
||||
afterMarkerShipment, beforeEnemyShipment, enemyShipment, beforeBattle, battleStep, beforePlans,
|
||||
plans, revealed, returns, afterBattle, actions, staging };
|
||||
}
|
||||
@@ -32,6 +32,9 @@ export type EcazIndependentSkillsOptions = {
|
||||
skills?: boolean;
|
||||
/** The independent physical three-card variant; `false` is the printed no-card control. */
|
||||
independent?: boolean;
|
||||
/** Original optional modules, both off by default; Strongholds require Advanced. */
|
||||
tech?: boolean;
|
||||
strongholds?: boolean;
|
||||
/** Real native Battle Plan cards in the lead's own plan. */
|
||||
cards?: readonly EcazIndependentSkillsCard[];
|
||||
lead?: 'ecaz' | 'ally';
|
||||
@@ -111,6 +114,8 @@ export function nextEcazIndependentSkillsStep(game: Game,
|
||||
return { actor: decision.player, action: { type: 'decision', event: decision.event, draw: false } };
|
||||
if (decision?.kind === 'faceDance')
|
||||
return { actor: decision.player, action: { type: 'decision', reveal: false } };
|
||||
if (decision?.kind === 'techToken')
|
||||
return { actor: decision.player, action: { type: 'decision', token: decision.choices[0] } };
|
||||
if (['sukRescue', 'harassWithdraw', 'battleLosses'].includes(decision?.kind ?? '')) return null;
|
||||
}
|
||||
return nextStrongholdFactionsNativeStep(game);
|
||||
@@ -167,6 +172,8 @@ export function assertEcazIndependentSkillsCustody(game: Game): void {
|
||||
export type EcazIndependentSkillsFixture = {
|
||||
/** Original fresh lobby/setup snapshots taken by the shared genuine programme. */
|
||||
initial: Game; afterSetup: Game;
|
||||
/** Real turn-one Mentat acquisition, before the worm alliance and battle. */
|
||||
afterFirstMentat?: Game; strongholdTerritory?: string;
|
||||
/** Original before-lead window and the recorded native lead choice. */
|
||||
beforeLead: Game; leadStep: EcazIndependentSkillsStep; afterLead: Game;
|
||||
/** Clean before-plan window (both plans still unsealed) and the real plans. */
|
||||
@@ -257,6 +264,7 @@ export function createEcazIndependentSkillsFixture(
|
||||
const draws = skills ? permutationDraws(LEADER_SKILL_CARDS.map(card => card.id), order) : [];
|
||||
const shared = entropy(() => createHomeworldEcazOccupyFixture({
|
||||
advanced, skills, independent, homeworlds: false,
|
||||
tech: options.tech ?? false, strongholds: options.strongholds ?? false,
|
||||
...(options.paired ? { allyFaction: 'moritani' as const } : {}),
|
||||
...(skills ? { finishSetup: (initial: Game) => trainEcazSeat(initial, picks) } : {}),
|
||||
}), draws);
|
||||
@@ -330,7 +338,9 @@ export function createEcazIndependentSkillsFixture(
|
||||
returns = defaultHarassWithdrawAllocation(offer.context, offer.dial, offer.support);
|
||||
}
|
||||
const fixture: EcazIndependentSkillsFixture = {
|
||||
initial: shared.initial, afterSetup: shared.afterSetup, beforeLead, leadStep, afterLead,
|
||||
initial: shared.initial, afterSetup: shared.afterSetup,
|
||||
afterFirstMentat: shared.afterFirstMentat, strongholdTerritory: shared.strongholdTerritory,
|
||||
beforeLead, leadStep, afterLead,
|
||||
beforePlans, plans, game, returns, ecaz: shared.ecaz, ally: shared.ally, opponent: shared.opponent,
|
||||
territory: shared.territory, location: shared.location, fixedEcazDial: profile.fixedEcazDial,
|
||||
planDial: profile.fixedEcazDial + variable, support, cardUser, cardIds, cardKinds, trainer,
|
||||
@@ -355,6 +365,7 @@ export function quoteEcazIndependentSkillsBattle(f: EcazIndependentSkillsFixture
|
||||
return { id: actor, faction: player.faction, spice: player.spice, hand: player.hand, plan,
|
||||
leader: player.leaders.find(leader => leader.id === plan.leader),
|
||||
forces: own.ownForces!,
|
||||
stronghold: own.strongholdEffects[actor],
|
||||
leaderSkills: (game.leaderSkills?.assignments ?? []).filter(a => a.owner === actor)
|
||||
.map(a => ({ skill: a.skill, leader: a.leader,
|
||||
faceUp: battle.leaderSkillHidden?.[actor] !== true, captured: false })),
|
||||
|
||||
@@ -5,7 +5,9 @@ import { territory } from '../game/board';
|
||||
import { finishEcazOccupySetup, type EcazOccupyFixture } from './fixture-ecaz-occupy';
|
||||
|
||||
export type HomeworldEcazStep = { actor: string; action: Action };
|
||||
export type HomeworldEcazOccupyFixture = EcazOccupyFixture & { actions: HomeworldEcazStep[] };
|
||||
export type HomeworldEcazOccupyFixture = EcazOccupyFixture & {
|
||||
actions: HomeworldEcazStep[]; afterFirstMentat?: Game; strongholdTerritory?: string;
|
||||
};
|
||||
export const homeworldEcazClean = (game: Game) =>
|
||||
!game.phaseOpening && !game.response && !game.decision && !game.pendingTreacheryDiscard;
|
||||
|
||||
@@ -39,6 +41,7 @@ export function nextHomeworldEcazStep(game: Game): HomeworldEcazStep | null {
|
||||
export function createHomeworldEcazOccupyFixture(options: {
|
||||
advanced?: boolean; skills?: boolean; independent?: boolean;
|
||||
allyFaction?: 'emperor' | 'moritani';
|
||||
tech?: boolean; strongholds?: boolean;
|
||||
/** No-World control uses the same genuine alliance/Arrakis route. */
|
||||
homeworlds?: boolean;
|
||||
/** Select only actual offered setup choices; the captured initial stays untouched. */
|
||||
@@ -56,6 +59,8 @@ export function createHomeworldEcazOccupyFixture(options: {
|
||||
game = applyAction(game, actor, action);
|
||||
};
|
||||
if (homeworlds) act('ecaz', { type: 'homeworlds', enabled: true });
|
||||
if (options.tech) act('ecaz', { type: 'techTokens', enabled: true });
|
||||
if (options.strongholds) act('ecaz', { type: 'strongholdCards', enabled: true });
|
||||
if (options.independent) act('ecaz', { type: 'moduleOption', module: 'ecazTreachery', enabled: true });
|
||||
if (options.skills) act('ecaz', { type: 'moduleOption', module: 'leaderSkills', enabled: true });
|
||||
for (const player of game.players) act(player.id, { type: 'ready' });
|
||||
@@ -82,10 +87,19 @@ export function createHomeworldEcazOccupyFixture(options: {
|
||||
orderBlow(false, 0);
|
||||
if (advanced) orderBlow(false, 1);
|
||||
reach(g => g.phase === 5 && homeworldEcazClean(g));
|
||||
const strongholdTerritory = options.strongholds ? 'arrakeen' : undefined;
|
||||
while (game.phase === 5) {
|
||||
if (homeworldEcazClean(game)) act(game.active!, { type: 'endMovement' }); else step();
|
||||
if (!homeworldEcazClean(game)) { step(); continue; }
|
||||
const actor = game.active!;
|
||||
if (strongholdTerritory && actor === 'ecaz' && !game.players.find(player => player.id === actor)!.shipped) {
|
||||
act(actor, { type: 'ship', territory: strongholdTerritory,
|
||||
sector: territory(strongholdTerritory).sectors[0], amount: 4, elite: 0, allyPayment: 0, smuggler: false });
|
||||
reach(homeworldEcazClean);
|
||||
}
|
||||
act(actor, { type: 'endMovement' });
|
||||
}
|
||||
reach(g => g.turn === 2 && g.phase === 0 && !g.phaseOpening);
|
||||
const afterFirstMentat = options.tech || options.strongholds ? structuredClone(game) : undefined;
|
||||
orderBlow(true, 0);
|
||||
orderBlow(false, 1);
|
||||
if (advanced) orderBlow(false, 2);
|
||||
@@ -93,7 +107,7 @@ export function createHomeworldEcazOccupyFixture(options: {
|
||||
act('ecaz', { type: 'alliance', target: ally });
|
||||
act(ally, { type: 'alliance', target: 'ecaz' });
|
||||
reach(g => g.phase === 5 && homeworldEcazClean(g));
|
||||
const battleTerritory = ['arrakeen', 'sietch_tabr', 'habbanya_ridge_sietch']
|
||||
const battleTerritory = strongholdTerritory ?? ['arrakeen', 'sietch_tabr', 'habbanya_ridge_sietch']
|
||||
.find(id => territory(id).sectors[0] !== game.storm &&
|
||||
!game.players.some(player => Object.entries(player.forces).some(([key, amount]) =>
|
||||
amount > 0 && key.startsWith(`${id}:`))))!;
|
||||
@@ -103,7 +117,7 @@ export function createHomeworldEcazOccupyFixture(options: {
|
||||
while (game.phase === 5) {
|
||||
if (!homeworldEcazClean(game)) { step(); continue; }
|
||||
const actor = game.active!;
|
||||
if (!shipped.has(actor)) {
|
||||
if (!shipped.has(actor) && !(strongholdTerritory && actor === 'ecaz')) {
|
||||
act(actor, { type: 'ship', territory: battleTerritory, sector,
|
||||
amount: actor === 'guild' ? 2 : 4,
|
||||
elite: actor === ally && ally === 'emperor' && (advanced || homeworlds) ? 2 : 0, allyPayment: 0 });
|
||||
@@ -118,6 +132,7 @@ export function createHomeworldEcazOccupyFixture(options: {
|
||||
act(choice.chooser, { type: 'chooseBattle', territory: battleTerritory,
|
||||
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
|
||||
if (!options.skills) assert.equal(game.decision?.kind, 'ecazBattleLead');
|
||||
return { game, initial, afterSetup, actions, ecaz: 'ecaz', ally, opponent: 'guild',
|
||||
return { game, initial, afterSetup, actions, afterFirstMentat, strongholdTerritory,
|
||||
ecaz: 'ecaz', ally, opponent: 'guild',
|
||||
territory: battleTerritory, location, ecazForces: 4 };
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAction, createGame, initializeEcazTreacheryGameForAudit, initializeFactionExpansionsGameForAudit,
|
||||
initializeStrongholdFactionsGameForAudit, joinGame, newPlayer, type Action, type Game,
|
||||
initializeLeaderSkillsGameForAudit, initializeStrongholdFactionsGameForAudit, joinGame, newPlayer,
|
||||
viewGame, type Action, type Game,
|
||||
} from '../game/engine';
|
||||
import type { FactionId } from '../game/catalog';
|
||||
import { createTechTokens } from '../game/tech-tokens';
|
||||
import { createStrongholdCards } from '../game/stronghold-cards';
|
||||
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
|
||||
import { LEADER_SKILL_CARDS, type LeaderSkillId } from '../game/leader-skill-cards';
|
||||
|
||||
export type IndependentCardModulesStep = { actor: string; action: Action };
|
||||
export type IndependentCardModulesOptions = {
|
||||
@@ -15,13 +17,37 @@ export type IndependentCardModulesOptions = {
|
||||
expansions?: Game['expansions'];
|
||||
tech?: boolean;
|
||||
strongholds?: boolean;
|
||||
skills?: boolean;
|
||||
training?: Partial<Record<FactionId, LeaderSkillId>>;
|
||||
/** False keeps the original native deck for revealed-card comparisons. */
|
||||
independent?: boolean;
|
||||
homeworlds?: boolean;
|
||||
};
|
||||
|
||||
/** Select original modules and independent cards before the undealt native setup.
|
||||
* No armies, wallets, module owners, phase or history are assigned afterward. */
|
||||
/** Select only the original undealt fourteen-card lottery, never assignments. */
|
||||
function skillOfferRolls(game: Game, training: IndependentCardModulesOptions['training']): number[] {
|
||||
const source = LEADER_SKILL_CARDS.map(card => card.id), selected: LeaderSkillId[] = [];
|
||||
for (const player of game.players) {
|
||||
const skill = training?.[player.faction] ?? source.find(card => !selected.includes(card))!;
|
||||
assert.ok(source.includes(skill) && !selected.includes(skill), 'Use distinct original skill offers');
|
||||
selected.push(skill);
|
||||
}
|
||||
const rest = source.filter(card => !selected.includes(card));
|
||||
const desired = selected.flatMap(card => [card, rest.shift()!]).concat(rest);
|
||||
const working = [...source], rolls: number[] = [];
|
||||
for (let index = working.length - 1; index > 0; index--) {
|
||||
const from = working.indexOf(desired[index]);
|
||||
assert.ok(from >= 0 && from <= index);
|
||||
rolls.push(Math.floor((from + 0.5) * 0x100000000 / (index + 1)));
|
||||
[working[index], working[from]] = [working[from], working[index]];
|
||||
}
|
||||
return rolls;
|
||||
}
|
||||
|
||||
/** Original unused modules and independent cards are selected before the real
|
||||
* development initializer. Keep the undealt `initial` input for existing native
|
||||
* constructors; `offered` is the actual post-initializer setup. No armies,
|
||||
* wallets, owners, phases or history are assigned afterward. */
|
||||
export function createIndependentCardModulesFixture(options: IndependentCardModulesOptions = {}) {
|
||||
const factions = options.factions ?? ['emperor', 'atreides', 'guild'];
|
||||
const advanced = options.advanced ?? false;
|
||||
@@ -38,22 +64,44 @@ export function createIndependentCardModulesFixture(options: IndependentCardModu
|
||||
steps.push(step); game = applyAction(game, step.actor, step.action);
|
||||
}
|
||||
const initial = structuredClone(game);
|
||||
const rolls = options.skills ? skillOfferRolls(game, options.training) : [];
|
||||
let cursor = 0;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
|
||||
crypto.getRandomValues = <T extends ArrayBufferView | null>(array: T): T => {
|
||||
assert.ok(array);
|
||||
new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(255);
|
||||
if (array instanceof Uint32Array && array.length === 1 && cursor < rolls.length) array[0] = rolls[cursor++];
|
||||
else new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(255);
|
||||
return array;
|
||||
};
|
||||
try {
|
||||
game = options.independent !== false ? initializeEcazTreacheryGameForAudit(game) :
|
||||
if (options.skills) {
|
||||
if (options.independent !== false) game.ecazTreachery = true;
|
||||
game = initializeLeaderSkillsGameForAudit(game);
|
||||
} else game = options.independent !== false ? initializeEcazTreacheryGameForAudit(game) :
|
||||
options.strongholds ? initializeStrongholdFactionsGameForAudit(game) : initializeFactionExpansionsGameForAudit(game);
|
||||
}
|
||||
finally {
|
||||
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
|
||||
else Reflect.deleteProperty(crypto, 'getRandomValues');
|
||||
}
|
||||
game = advanceIndependentCardModules(game, state => state.status === 'playing', steps);
|
||||
return { initial, game, steps };
|
||||
const offered = structuredClone(game);
|
||||
for (let count = 0; game.status === 'setup' && count < 300; count++) {
|
||||
let step: IndependentCardModulesStep;
|
||||
if (game.setupStage === 'leaderSkills') {
|
||||
const actor = Object.keys(game.leaderSkills!.offers)[0], offer = game.leaderSkills!.offers[actor];
|
||||
const own = viewGame(game, actor).leaderSkills!, player = game.players.find(seat => seat.id === actor)!;
|
||||
const skill = options.training?.[player.faction] ?? offer.cards.find(card => !own.unavailableSkills?.[card]);
|
||||
const leader = own.eligibleLeaders[0];
|
||||
assert.ok(skill && offer.cards.includes(skill) && !own.unavailableSkills?.[skill] && leader);
|
||||
step = { actor, action: { type: 'leaderSkill', event: offer.event, skill, leader: leader.id } };
|
||||
} else {
|
||||
const next = nextIndependentCardModulesStep(game); assert.ok(next, 'Complete original native setup');
|
||||
step = next;
|
||||
}
|
||||
steps.push(structuredClone(step)); game = applyAction(game, step.actor, step.action);
|
||||
}
|
||||
assert.equal(game.status, 'playing');
|
||||
return { initial, offered, game, steps };
|
||||
}
|
||||
|
||||
/** Reuse original native choices, including all phase/module windows. */
|
||||
|
||||
@@ -16,6 +16,8 @@ import { traitorDeck } from '../game/traitors';
|
||||
import { quoteBattleResolution, type ResolutionCombatant } from '../game/battle-resolution-quote';
|
||||
import { usesNativeBattleSpecial } from '../game/battle-card-slots';
|
||||
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
|
||||
import { createTechTokens } from '../game/tech-tokens';
|
||||
import { createStrongholdCards, strongholdBenefit } from '../game/stronghold-cards';
|
||||
|
||||
export type IndependentSkillsBattleStep = { actor: string; action: Action };
|
||||
/** 'same-plan' trains the opposing Warmaster; 'traitor-cancel' adds its real
|
||||
@@ -30,6 +32,9 @@ export type IndependentSkillsBattleOptions = {
|
||||
/** Continue only the original fresh classic lobby built by this fixture. */
|
||||
initial?: Game;
|
||||
advanced?: boolean;
|
||||
/** Select original unused phase modules before admission; both default off. */
|
||||
tech?: boolean;
|
||||
strongholds?: boolean;
|
||||
seatIds?: readonly string[];
|
||||
case?: IndependentSkillsBattleCase;
|
||||
/** Construction hook: request exact original skill cards for one seat. */
|
||||
@@ -53,6 +58,8 @@ export type IndependentSkillsBattleFixture = IndependentSkillsBattleSetup & {
|
||||
location: string;
|
||||
first: string;
|
||||
second: string;
|
||||
/** Present only when a genuine turn-one occupation earned the Arrakeen card. */
|
||||
afterFirstMentat?: Game;
|
||||
beforeShipment: Game;
|
||||
shipment: IndependentSkillsBattleStep;
|
||||
afterShipment: Game;
|
||||
@@ -61,6 +68,7 @@ export type IndependentSkillsBattleFixture = IndependentSkillsBattleSetup & {
|
||||
opponentMove: IndependentSkillsBattleStep;
|
||||
beforeBattle: Game;
|
||||
battleStep: IndependentSkillsBattleStep;
|
||||
beforePlans: Game;
|
||||
/** Revealed window: both original plans sealed, postures committed. */
|
||||
game: Game;
|
||||
plans: IndependentSkillsBattleStep[];
|
||||
@@ -162,8 +170,11 @@ export function createIndependentSkillsBattleSetup(
|
||||
joinGame(game, newPlayer(ids[index], ids[index], factions[index]));
|
||||
assert.equal(game.advanced, options.advanced ?? game.advanced);
|
||||
assert.deepEqual(game.expansions, []);
|
||||
assert.ok(!game.leaderSkills && !game.homeworlds && !game.nexusCards && !game.discoveryEnabled &&
|
||||
!game.techTokens && !game.strongholdCards, 'Only the original classic composition is accepted.');
|
||||
assert.ok(!game.leaderSkills && !game.homeworlds && !game.nexusCards && !game.discoveryEnabled,
|
||||
'Only the original classic phase modules are accepted.');
|
||||
assert.ok(!options.strongholds || game.advanced, 'Original Stronghold Cards require Advanced');
|
||||
if (options.tech && !game.techTokens) game.techTokens = createTechTokens();
|
||||
if (options.strongholds && !game.strongholdCards) game.strongholdCards = createStrongholdCards();
|
||||
assert.equal(game.status, 'lobby', 'Continue only an original fresh classic lobby.');
|
||||
const staging: string[] = [];
|
||||
const actions: IndependentSkillsBattleStep[] = [];
|
||||
@@ -295,8 +306,10 @@ function strongestOther(game: Game, actor: string, excluded: string): string {
|
||||
return leader.id;
|
||||
}
|
||||
|
||||
/** Real training, a paid native shipment, real within-territory movement and both
|
||||
* original Battle Plans. Only card and Traitor custody is staged. */
|
||||
/** Real training, a paid native shipment and both original Battle Plans. With
|
||||
* Strongholds, that shipment earns Arrakeen at genuine first END Mentat and a
|
||||
* paid turn-two Guild shipment challenges it; otherwise the original Pasty Mesa
|
||||
* split movement remains unchanged. Only card and Traitor custody is staged. */
|
||||
export function createIndependentSkillsBattleFixture(
|
||||
options: IndependentSkillsBattleOptions = {},
|
||||
): IndependentSkillsBattleFixture {
|
||||
@@ -306,55 +319,79 @@ export function createIndependentSkillsBattleFixture(
|
||||
const staging = [...setup.staging];
|
||||
const actions = [...setup.actions];
|
||||
const { owner, opponent, trainers } = setup;
|
||||
const location = 'pasty_mesa', first = 'pasty_mesa:5', second = 'pasty_mesa:6';
|
||||
assert.deepEqual(territory(location).sectors, [5, 6, 7, 8]);
|
||||
const strongholds = !!setup.afterSetup.strongholdCards;
|
||||
const location = strongholds ? 'arrakeen' : 'pasty_mesa';
|
||||
const first = `${location}:${strongholds ? 10 : 5}`;
|
||||
const second = strongholds ? first : 'pasty_mesa:6';
|
||||
if (!strongholds) assert.deepEqual(territory(location).sectors, [5, 6, 7, 8]);
|
||||
let game = advanceIndependentSkillsBattle(setup.afterSetup,
|
||||
(state) => state.phase === 5 && state.active === owner && independentSkillsBattleClean(state), actions);
|
||||
const beforeShipment = structuredClone(game);
|
||||
assert.equal(independentSkillsBattlePlayer(game, owner).reserves, 20);
|
||||
assert.equal(independentSkillsBattlePlayer(game, owner).spice, 10);
|
||||
const shipment: IndependentSkillsBattleStep = { actor: owner, action: {
|
||||
type: 'ship', territory: location, sector: 5, amount: 4, elite: 0, allyPayment: 0 } };
|
||||
type: 'ship', territory: location, sector: strongholds ? 10 : 5, amount: 4, elite: 0, allyPayment: 0 } };
|
||||
actions.push(structuredClone(shipment));
|
||||
game = applyAction(game, owner, shipment.action);
|
||||
game = advanceIndependentSkillsBattle(game, (state) => independentSkillsBattleClean(state) &&
|
||||
!state.pendingShipment && state.phase === 5 && state.active === owner, actions);
|
||||
const afterShipment = structuredClone(game);
|
||||
assert.equal(independentSkillsBattlePlayer(game, owner).spice, 2);
|
||||
assert.equal(independentSkillsBattlePlayer(game, owner).reserves, 16);
|
||||
assert.equal(independentSkillsBattlePlayer(game, owner).forces[first], 4);
|
||||
assert.equal(independentSkillsBattlePlayer(game, opponent).spice, 5 + 8);
|
||||
staging.push('The Emperor paid the original eight-spice desert shipment of four reserves into Pasty Mesa; that payment reached the Guild as printed shipping income.');
|
||||
const move: IndependentSkillsBattleStep = { actor: owner, action: {
|
||||
type: 'move', from: first, amount: 2, territory: location, sector: 6 } };
|
||||
staging.push(`The Emperor paid its original ${strongholds ? 'four-spice stronghold' : 'eight-spice desert'} shipment of four reserves; the real payment reached the Guild.`);
|
||||
const move: IndependentSkillsBattleStep = { actor: owner, action: strongholds
|
||||
? { type: 'endMovement' }
|
||||
: { type: 'move', from: first, amount: 2, territory: location, sector: 6 } };
|
||||
actions.push(structuredClone(move));
|
||||
game = applyAction(game, owner, move.action);
|
||||
let afterFirstMentat: Game | undefined;
|
||||
if (strongholds) {
|
||||
game = advanceIndependentSkillsBattle(game, state => state.turn === 2 && state.phase === 0, actions);
|
||||
afterFirstMentat = structuredClone(game);
|
||||
assert.equal(game.strongholdCards!.claimedTurn, 1);
|
||||
assert.equal(game.strongholdCards!.owners.arrakeen, owner);
|
||||
staging.push('The four originally shipped Emperor counters held empty Arrakeen through genuine first END Mentat and earned its card; no owner or army was assigned.');
|
||||
game = advanceIndependentSkillsBattle(game, state => state.turn === 2 && state.phase === 5 &&
|
||||
state.active === owner && independentSkillsBattleClean(state), actions);
|
||||
} else {
|
||||
game = advanceIndependentSkillsBattle(game, (state) => independentSkillsBattleClean(state) &&
|
||||
state.phase === 5 && state.active === owner, actions);
|
||||
assert.deepEqual(independentSkillsBattlePlayer(game, owner).forces, { [first]: 2, [second]: 2 });
|
||||
staging.push('The Emperor moved two originally shipped counters to sector 6, leaving two physical Harass return locations.');
|
||||
}
|
||||
const afterMove = structuredClone(game);
|
||||
staging.push('The Emperor then moved two of those four counters from sector 5 to sector 6 in the same movement turn, so the Harass & Withdraw return allocation is genuinely ambiguous between two physical sectors.');
|
||||
const endMove: IndependentSkillsBattleStep = { actor: owner, action: { type: 'endMovement' } };
|
||||
actions.push(structuredClone(endMove));
|
||||
game = applyAction(game, owner, endMove.action);
|
||||
game = advanceIndependentSkillsBattle(game, (state) => independentSkillsBattleClean(state) &&
|
||||
state.phase === 5 && state.active === opponent, actions);
|
||||
assert.equal(independentSkillsBattlePlayer(game, opponent).forces['tueks_sietch:5'], 5);
|
||||
const opponentMove: IndependentSkillsBattleStep = { actor: opponent, action: {
|
||||
type: 'move', from: 'tueks_sietch:5', amount: 5, territory: location, sector: 6 } };
|
||||
const opponentMove: IndependentSkillsBattleStep = { actor: opponent, action: strongholds
|
||||
? { type: 'ship', territory: location, sector: 10, amount: 5, elite: 0 }
|
||||
: { type: 'move', from: 'tueks_sietch:5', amount: 5, territory: location, sector: 6 } };
|
||||
actions.push(structuredClone(opponentMove));
|
||||
game = applyAction(game, opponent, opponentMove.action);
|
||||
game = advanceIndependentSkillsBattle(game, (state) => independentSkillsBattleClean(state) &&
|
||||
state.phase === 5 && state.active === opponent, actions);
|
||||
assert.deepEqual(independentSkillsBattlePlayer(game, opponent).forces, { [second]: 5 });
|
||||
staging.push('The Guild declined the first Advanced movement slot (a real legal Guild choice) and moved its whole army from Tuek’s Sietch into Pasty Mesa sector 6 during its own movement turn.');
|
||||
!state.pendingShipment && state.phase === 5 && state.active === opponent, actions);
|
||||
assert.equal(independentSkillsBattlePlayer(game, opponent).forces[second], 5);
|
||||
staging.push(`The Guild declined its first Advanced movement slot and ${strongholds ? 'paid to ship five original reserves into the held Arrakeen' : 'marched its original five-counter army into Pasty Mesa'}.`);
|
||||
const endOpponent: IndependentSkillsBattleStep = { actor: opponent, action: { type: 'endMovement' } };
|
||||
actions.push(structuredClone(endOpponent));
|
||||
game = applyAction(game, opponent, endOpponent.action);
|
||||
game = advanceIndependentSkillsBattle(game, (state) => state.phase === 6 && independentSkillsBattleClean(state), actions);
|
||||
if (strongholds) {
|
||||
const amount = independentSkillsBattlePlayer(game, owner).spice;
|
||||
if (amount) {
|
||||
const bribe: IndependentSkillsBattleStep = { actor: owner,
|
||||
action: { type: 'bribe', target: setup.observer, amount } };
|
||||
actions.push(structuredClone(bribe));
|
||||
game = applyAction(game, owner, bribe.action);
|
||||
}
|
||||
staging.push('The Emperor actually bribed its entire remaining wallet to the observer before plans; the held Arrakeen contribution is support budget, never personal spice.');
|
||||
}
|
||||
for (const player of game.players) game.deck.push(...player.hand.splice(0));
|
||||
staging.push('At the real Battle window every original hand returned to the undealt deck before the physical Reinforcements and Harass & Withdraw cards were conserved to the Emperor.');
|
||||
hold(game, owner, (card) => card.id === REINFORCEMENTS_CARD, staging);
|
||||
hold(game, owner, (card) => card.id === HARASS_WITHDRAW_CARD, staging);
|
||||
const worthless = caseKind === 'suk' ? null : hold(game, opponent, (card) => card.kind === 'worthless', staging);
|
||||
const worthless = caseKind === 'suk' && !strongholds ? null : hold(game, opponent, (card) => card.kind === 'worthless', staging);
|
||||
const hide: Record<string, boolean> = { [owner]: true, [opponent]: caseKind !== 'suk' };
|
||||
// The trained leader must be committed (so Warmaster/Suk take effect) exactly
|
||||
// when the owner's posture decision hides it behind the shield.
|
||||
@@ -369,15 +406,20 @@ export function createIndependentSkillsBattleFixture(
|
||||
const beforeBattle = structuredClone(game);
|
||||
const choice = viewGame(game, game.active!).battleChoices.find((candidate) => candidate.territory === location);
|
||||
assert.ok(choice);
|
||||
if (!strongholds) {
|
||||
assert.equal(choice.attacker, owner);
|
||||
assert.equal(choice.chooser, owner);
|
||||
}
|
||||
assert.ok(choice.chooser);
|
||||
const battleStep: IndependentSkillsBattleStep = { actor: choice.chooser, action: {
|
||||
type: 'chooseBattle', territory: location, target: choice.defender } };
|
||||
type: 'chooseBattle', territory: location,
|
||||
target: choice.chooser === choice.attacker ? choice.defender : choice.attacker } };
|
||||
actions.push(structuredClone(battleStep));
|
||||
game = applyAction(game, choice.chooser, battleStep.action);
|
||||
game = advanceIndependentSkillsBattle(game,
|
||||
(state) => !!state.battle && !state.battle.revealed && independentSkillsBattleClean(state),
|
||||
actions, { hide });
|
||||
const beforePlans = structuredClone(game);
|
||||
const support = advanced ? 2 : 0;
|
||||
const plans: IndependentSkillsBattleStep[] = [
|
||||
{ actor: owner, action: { type: 'battlePlan', dial: 2, support, leader: ownerLeader,
|
||||
@@ -391,10 +433,12 @@ export function createIndependentSkillsBattleFixture(
|
||||
}
|
||||
assert.ok(game.battle?.revealed);
|
||||
staging.push(`Both combatants made their real posture decisions before sealing: the Emperor hid its skilled leader behind the shield, the Guild ${caseKind === 'suk' ? 'kept its skilled leader face up and therefore committed another leader' : 'hid its skilled leader so that the committed Warmaster could resolve'}.`);
|
||||
const returns: HarassWithdrawSelection = { [first]: { normal: 1, elite: 0 }, [second]: { normal: 1, elite: 0 } };
|
||||
const returns: HarassWithdrawSelection = strongholds
|
||||
? { [first]: { normal: 2, elite: 0 } }
|
||||
: { [first]: { normal: 1, elite: 0 }, [second]: { normal: 1, elite: 0 } };
|
||||
const fixture: IndependentSkillsBattleFixture = { ...setup, case: caseKind, advanced, location, first, second,
|
||||
beforeShipment, shipment, afterShipment, move, afterMove, opponentMove, beforeBattle, battleStep,
|
||||
game, plans, returns, hide, traitor, actions, staging };
|
||||
afterFirstMentat, beforeShipment, shipment, afterShipment, move, afterMove, opponentMove, beforeBattle, battleStep,
|
||||
beforePlans, game, plans, returns, hide, traitor, actions, staging };
|
||||
assertIndependentSkillsBattleCustody(game);
|
||||
return fixture;
|
||||
}
|
||||
@@ -409,6 +453,7 @@ export function quoteIndependentSkillsBattle(game: Game,
|
||||
const player = independentSkillsBattlePlayer(game, actor);
|
||||
const view = viewGame(game, actor).battle!;
|
||||
return { id: actor, faction: player.faction, spice: player.spice, hand: player.hand,
|
||||
stronghold: game.advanced ? strongholdBenefit(game.strongholdCards, actor, battle.territory, battle.strongholdCopy) : null,
|
||||
plan: battle.plans[actor],
|
||||
leader: player.leaders.find((leader) => leader.id === battle.plans[actor].leader),
|
||||
forces: view.ownForces!,
|
||||
|
||||
@@ -9,6 +9,8 @@ import { RECRUITS_CARD_ID } from '../game/recruits';
|
||||
import { forceRevivalQuote } from '../game/revival';
|
||||
import { ecazTreacheryCards } from '../game/ecaz-cards';
|
||||
import { TERRITORIES } from '../game/board';
|
||||
import { createTechTokens } from '../game/tech-tokens';
|
||||
import { createStrongholdCards } from '../game/stronghold-cards';
|
||||
import type { Card } from '../game/cards';
|
||||
import {
|
||||
nextIndependentCardModulesStep, type IndependentCardModulesStep,
|
||||
@@ -18,6 +20,9 @@ export type IndependentSkillsRevivalStep = IndependentCardModulesStep;
|
||||
export type IndependentSkillsRevivalOptions = {
|
||||
/** Original Basic or Advanced rules; the independent composition is the same. */
|
||||
advanced?: boolean;
|
||||
/** Select original unused phase modules before admission; both default off. */
|
||||
tech?: boolean;
|
||||
strongholds?: boolean;
|
||||
seatIds?: readonly string[];
|
||||
};
|
||||
|
||||
@@ -63,6 +68,8 @@ export type IndependentSkillsRevivalFixture = {
|
||||
beforeMainRevival: Game;
|
||||
/** After the payer's real revival: Bank income is deferred and unpaid. */
|
||||
afterRevival: Game;
|
||||
/** Original Shipment boundary after Revival pays and resets its Tech income. */
|
||||
afterPhaseEnd: Game;
|
||||
/** Turn-two Spice Collection settled, immediately before the Mentat settlement. */
|
||||
atMentat2: Game;
|
||||
/** Turn-two Mentat Pause after the deferred income was credited exactly once. */
|
||||
@@ -224,6 +231,7 @@ export function createIndependentSkillsRevivalFixture(
|
||||
options: IndependentSkillsRevivalOptions = {},
|
||||
): IndependentSkillsRevivalFixture {
|
||||
const advanced = options.advanced ?? false;
|
||||
assert.ok(!options.strongholds || advanced, 'Original Stronghold Cards require Advanced');
|
||||
const ids = options.seatIds ?? ['p0', 'p1', 'p2'];
|
||||
assert.equal(ids.length, 3, 'The revival programme uses the Banker, payer and battle opponent seats');
|
||||
const [owner, payer, opponent] = ids;
|
||||
@@ -241,6 +249,8 @@ export function createIndependentSkillsRevivalFixture(
|
||||
let game = createGame('INDEPENDENTSKILLSREVIVAL', newPlayer(owner, 'Banker', BANKER), advanced);
|
||||
joinGame(game, newPlayer(payer, 'Payer', PAYER));
|
||||
joinGame(game, newPlayer(opponent, 'Opponent', OPPONENT));
|
||||
if (options.tech) game.techTokens = createTechTokens();
|
||||
if (options.strongholds) game.strongholdCards = createStrongholdCards();
|
||||
for (const player of game.players) game = record(game, { actor: player.id, action: { type: 'ready' } });
|
||||
// The marker is set after every lobby action, before the ordinary initializer.
|
||||
game.ecazTreachery = true;
|
||||
@@ -495,13 +505,14 @@ export function createIndependentSkillsRevivalFixture(
|
||||
assert.deepEqual(viewGame(entry.game, owner).spiceBankerIncome?.deferred, [],
|
||||
`${entry.label} must not earn Bank income`);
|
||||
|
||||
const atMentat2 = advanceUntil(game, state => state.turn === 2 && state.phase === 7 && clean(state));
|
||||
const afterPhaseEnd = advanceUntil(game, state => state.turn === 2 && state.phase === 5 && clean(state));
|
||||
const atMentat2 = advanceUntil(afterPhaseEnd, state => state.turn === 2 && state.phase === 7 && clean(state));
|
||||
assert.deepEqual(viewGame(atMentat2, owner).spiceBankerIncome?.deferred, [{ owner, amount: 1 }],
|
||||
'The deferred Bank income survives until the actual Mentat settlement');
|
||||
const afterMentat = advanceUntil(atMentat2, state => state.turn === 2 && state.phase === 8 && clean(state));
|
||||
return {
|
||||
initial, offered, afterSetup, afterBattle, afterBribe, atMentat1, atRevival,
|
||||
beforeRecruits, beforeMainRevival, afterRevival, atMentat2, afterMentat,
|
||||
beforeRecruits, beforeMainRevival, afterRevival, afterPhaseEnd, atMentat2, afterMentat,
|
||||
owner, payer, opponent, bankerLeader, card: RECRUITS_CARD_ID,
|
||||
territory: { id: TERRITORY_ID, sector: TERRITORY_SECTOR, type: 'sand' },
|
||||
battle: {
|
||||
|
||||
@@ -228,9 +228,16 @@ export function createIxChoamSkillsTechFixture(options: IxChoamSkillsTechOptions
|
||||
for (let i = 0; (game.response || game.decision || game.pendingShipment) && i < 200; i++) game = step(game, actions);
|
||||
// Actual ten-spice desert invoice exhausts Emperor's original bank wallet.
|
||||
// Any native Heighliners payout can add at most one; no Collection at this pile.
|
||||
const counter = game.deck.findIndex(card => card.effect === 'karama');
|
||||
assert.ok(counter >= 0);
|
||||
ixChoamSkillsTechPlayer(game, actor).hand.push(game.deck.splice(counter, 1)[0]);
|
||||
const hand = ixChoamSkillsTechPlayer(game, actor).hand;
|
||||
if (!hand.some(card => card.effect === 'karama')) {
|
||||
// A real starting deal can put both printed counters in hands, not the deck.
|
||||
const source = [game.deck, game.discard,
|
||||
...game.players.filter(player => player.id !== actor).map(player => player.hand)]
|
||||
.find(zone => zone.some(card => card.effect === 'karama'));
|
||||
assert.ok(source, 'Conserve an original printed Karama from its actual physical zone');
|
||||
const counter = source.findIndex(card => card.effect === 'karama');
|
||||
hand.push(source.splice(counter, 1)[0]);
|
||||
}
|
||||
let beforeCharityIncome: Game | undefined;
|
||||
let afterCharityIncome: Game | undefined;
|
||||
for (let i = 0; (game.turn !== 2 || game.phase !== 2 || game.phaseOpening || game.response || game.decision)
|
||||
|
||||
@@ -18,7 +18,7 @@ import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions'
|
||||
import { quoteIndependentSkillsBattle } from './fixture-independent-skills-battle';
|
||||
|
||||
export type IxIndependentSkillsStep = { actor: string; action: Action };
|
||||
export type IxIndependentSkillsOptions = { advanced?: boolean };
|
||||
export type IxIndependentSkillsOptions = { advanced?: boolean; tech?: boolean; strongholds?: boolean };
|
||||
export const ixIndependentSkillsPlayer = (game: Game, actor: string): Player => {
|
||||
const player = game.players.find(p => p.id === actor); assert.ok(player); return player;
|
||||
};
|
||||
@@ -67,6 +67,8 @@ export function ixIndependentSkillsPolicyActions(game: Game, actor: string, diff
|
||||
export function nextIxIndependentSkillsStep(game: Game): IxIndependentSkillsStep | null {
|
||||
if (!game.phaseOpening && !game.response) {
|
||||
const d = game.decision;
|
||||
if (d?.kind === 'techToken') return { actor: d.player,
|
||||
action: { type: 'decision', token: d.choices[0] } };
|
||||
if (d?.kind === 'leaderSkillVisibility') return { actor: d.player,
|
||||
action: { type: 'leaderSkillVisibility', event: d.event, hide: d.player === 'ixians' } };
|
||||
if (d?.kind === 'guildTiming') return { actor: d.player, action: { type: 'decision', take: false } };
|
||||
@@ -153,6 +155,9 @@ export function createIxIndependentSkillsFixture(options: IxIndependentSkillsOpt
|
||||
actions.push(structuredClone(step)); game = stepIxIndependentSkills(game, step);
|
||||
return game;
|
||||
};
|
||||
for (const [type, enabled] of [
|
||||
['techTokens', options.tech ?? false], ['strongholdCards', options.strongholds ?? false],
|
||||
] as const) if (enabled) record({ actor: game.host, action: { type, enabled } });
|
||||
for (const option of ['leaderSkills', 'ecazTreachery'] as const)
|
||||
record({ actor: game.host, action: { type: 'moduleOption', module: option, enabled: true } });
|
||||
for (const p of game.players) record({ actor: p.id, action: { type: 'ready' } });
|
||||
|
||||
@@ -31,6 +31,9 @@ export type MoritaniIndependentSkillsOptions = {
|
||||
skills?: boolean;
|
||||
/** The independent three-card Ecaz Treachery variant of printed p. 18. */
|
||||
cards?: boolean;
|
||||
/** Original optional modules, both off by default; Strongholds require Advanced. */
|
||||
tech?: boolean;
|
||||
strongholds?: boolean;
|
||||
opponent?: MoritaniIndependentSkillsOpponent;
|
||||
/** Native Ecaz replaces the classic opponent; both factions keep printed setup.
|
||||
* `opponent` remains the standalone-only Guild/Harkonnen selector. */
|
||||
@@ -172,6 +175,8 @@ export function nextMoritaniIndependentSkillsStep(
|
||||
if (decision?.kind === 'battleCards')
|
||||
return { actor: decision.player, action: { type: 'decision',
|
||||
discard: policy.winnerDiscard === 'none' ? [] : [...decision.cards] } };
|
||||
if (decision?.kind === 'techToken')
|
||||
return { actor: decision.player, action: { type: 'decision', token: decision.choices[0] } };
|
||||
if (decision?.kind === 'captureOffer')
|
||||
return { actor: decision.player,
|
||||
action: { type: 'decision', accept: policy.capture?.accept ?? false } };
|
||||
@@ -287,9 +292,12 @@ export function createMoritaniIndependentSkillsFixture(
|
||||
assert.equal(game.advanced, advanced);
|
||||
assert.deepEqual(game.expansions, ['ecaz']);
|
||||
assert.equal(game.status, 'lobby', 'Continue only a fresh lobby of this exact composition.');
|
||||
assert.ok(!game.homeworlds && !game.nexusCards && !game.discoveryEnabled &&
|
||||
!game.techTokens && !game.strongholdCards,
|
||||
'Only the standalone E3/classic composition without other modules is accepted.');
|
||||
assert.ok(!game.homeworlds && !game.nexusCards && !game.discoveryEnabled,
|
||||
'Only the original E3/classic composition and optional Tech/Strongholds are accepted.');
|
||||
assert.ok(!game.techTokens || options.tech,
|
||||
'A preselected original Tech module needs tech:true.');
|
||||
assert.ok(!game.strongholdCards || options.strongholds,
|
||||
'A preselected original Stronghold module needs strongholds:true.');
|
||||
const actions: MoritaniIndependentSkillsStep[] = [], staging: string[] = [];
|
||||
const act = (actor: string, action: Action) => {
|
||||
actions.push({ actor, action: structuredClone(action) });
|
||||
@@ -299,6 +307,8 @@ export function createMoritaniIndependentSkillsFixture(
|
||||
const initial = structuredClone(game);
|
||||
if (skills) act('moritani', { type: 'moduleOption', module: 'leaderSkills', enabled: true });
|
||||
if (cards) act('moritani', { type: 'moduleOption', module: 'ecazTreachery', enabled: true });
|
||||
if (options.tech && !game.techTokens) act('moritani', { type: 'techTokens', enabled: true });
|
||||
if (options.strongholds && !game.strongholdCards) act('moritani', { type: 'strongholdCards', enabled: true });
|
||||
for (const player of game.players) if (!player.ready) act(player.id, { type: 'ready' });
|
||||
const start: MoritaniIndependentSkillsStep = { actor: 'moritani',
|
||||
action: { type: 'start', ...(advanced ? { advancedPreview: true as const } : {}) } };
|
||||
|
||||
@@ -156,3 +156,54 @@ void test('normal Bureaucrat remains available in independent all14 play and div
|
||||
assert.equal(independentSkillsBattlePlayer(game, f.opponent).bribes, 3);
|
||||
assert.equal(viewGame(game, f.observer).bureaucrat!.usedThisPhase, true);
|
||||
});
|
||||
|
||||
void test('earned Advanced Arrakeen funds support without personal spice, changing neither the fee, the real Suk commitment nor mandatory disposal', () => {
|
||||
const f = createIndependentSkillsBattleFixture({ advanced: true, tech: true, strongholds: true, case: 'suk' });
|
||||
assert.equal(f.afterFirstMentat!.strongholdCards!.claimedTurn, 1);
|
||||
assert.equal(f.afterFirstMentat!.strongholdCards!.owners.arrakeen, f.owner);
|
||||
assert.equal(owner(f, f.afterFirstMentat!).forces[f.first], 4, 'The original paid shipment earned and held the card');
|
||||
assert.equal(owner(f, f.beforePlans).spice, 0, 'A real bribe emptied the wallet before support was sealed');
|
||||
assert.equal(f.plans[0].action.support, 2);
|
||||
assert.throws(() => applyAction(structuredClone(f.beforePlans), f.owner,
|
||||
{ ...f.plans[0].action, dial: 3, support: 3 }),
|
||||
'The funded two-spice support budget cannot become a third personal spice');
|
||||
const quote = quoteIndependentSkillsBattle(f.game, { returns: f.returns });
|
||||
assert.deepEqual(quote.payments.find(payment => payment.player === f.owner),
|
||||
{ player: f.owner, cost: 0, ownPayment: 0, allyPayment: 0, donor: null, bankSupport: 2, freeByTraitor: false });
|
||||
assert.deepEqual(quote.reinforcements,
|
||||
[{ player: f.owner, card: REINFORCEMENTS_CARD, normal: 3, elite: 0 }]);
|
||||
assert.deepEqual(quote.harassWithdraw?.[0].returned, { normal: 2, elite: 0 });
|
||||
const fee = finishIndependentSkillsBattle(f.game,
|
||||
{ return: f.returns, until: state => state.decision?.kind === 'sukRescue' });
|
||||
assert.equal(owner(f, fee).spice, 0, 'The Bank paid support directly, never to the wallet');
|
||||
assert.equal(owner(f, fee).reserves, 15);
|
||||
assert.equal(owner(f, fee).tanks, 3, 'The three physical Reinforcements fee counters are not rescue income');
|
||||
assert.deepEqual(owner(f, fee).forces, { [f.first]: 2 }, 'Only the two dialed casualties remain after Harass');
|
||||
assert.equal(fee.techTokens!.axlotl.spice, 0, 'A physical fee and Harass return are not a Revival-phase income trigger');
|
||||
assert.equal(fee.decision?.kind, 'sukRescue');
|
||||
const rescue = fee.decision!;
|
||||
const options = rescue.kind === 'sukRescue' ? rescue.options : [];
|
||||
assert.equal(Math.max(...options.map(option => option.normal + option.elite)), 2,
|
||||
'Suk can rescue the committed casualties, never the three-counter fee');
|
||||
const done = finishIndependentSkillsBattle(fee, { return: f.returns });
|
||||
assert.equal(done.turn, 2);
|
||||
assert.equal(done.phase, 7);
|
||||
assert.equal(done.lastBattleContext?.winner, f.owner);
|
||||
assert.equal(done.lastBattleContext?.sukRescue?.completed, true);
|
||||
assert.equal(done.pendingSukRescue, null);
|
||||
assert.equal(owner(f, done).reserves, 16);
|
||||
assert.equal(owner(f, done).tanks, 3);
|
||||
assert.deepEqual(owner(f, done).forces, { [f.first]: 1 });
|
||||
assert.equal(owner(f, done).spice, 2, 'Only the original Collection income arrives after the battle');
|
||||
assert.equal(opponent(f, done).tanks, 5);
|
||||
assert.equal(opponent(f, done).reserves, 10);
|
||||
assert.deepEqual(opponent(f, done).forces, { 'tueks_sietch:5': 5 }, 'The native army was not replaced to build this battle');
|
||||
const worthless = f.plans[1].action.weapon;
|
||||
assert.ok(typeof worthless === 'string');
|
||||
assert.deepEqual(done.lastBattleContext?.mandatoryDiscard?.entries, [{ card: worthless, owner: f.opponent }]);
|
||||
for (const card of [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD, worthless]) {
|
||||
assert.equal(held(done, card).length, 1);
|
||||
assert.equal(done.discard.filter(entry => entry.id === card).length, 1);
|
||||
}
|
||||
assertIndependentSkillsBattleCustody(done);
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { forceRevivalQuote } from '../game/revival';
|
||||
import { ownedTech } from '../game/tech-tokens';
|
||||
import {
|
||||
assertIndependentSkillsRevivalInventory, createIndependentSkillsRevivalFixture,
|
||||
independentSkillsRevivalPlayer,
|
||||
@@ -100,3 +101,55 @@ void test('the Advanced programme preserves real paid revival and deferred Banke
|
||||
assert.deepEqual(viewGame(advanced.afterMentat, advanced.owner).spiceBankerIncome?.deferred, []);
|
||||
assertIndependentSkillsRevivalInventory(advanced.afterMentat);
|
||||
});
|
||||
|
||||
void test('Basic Tech Recruits reopens exhausted genuine Tanks without repeating payment, Axlotl accrual or normal Banker income', () => {
|
||||
const f = createIndependentSkillsRevivalFixture({ tech: true });
|
||||
let game = structuredClone(f.atRevival);
|
||||
const tokenOwner = game.techTokens!.axlotl.owner!;
|
||||
const accrual = ownedTech(game.techTokens, tokenOwner).length;
|
||||
const payerSpice = spiceOf(game, f.payer);
|
||||
const tokenSpice = spiceOf(game, tokenOwner);
|
||||
const bankerSpice = spiceOf(game, f.owner);
|
||||
assert.ok(tokenOwner && accrual > 0);
|
||||
// These are the same five genuine battle losses, not a supplied Tanks pool.
|
||||
assert.equal(independentSkillsRevivalPlayer(game, f.payer).tanks, 5);
|
||||
game = applyAction(game, f.payer, { type: 'revive', amount: 3 });
|
||||
assert.equal(spiceOf(game, f.payer), payerSpice - 4);
|
||||
assert.equal(viewGame(game, f.payer).revival.forcesRemaining, 0);
|
||||
assert.equal(game.techTokens!.axlotl.spice, accrual);
|
||||
assert.equal(spiceOf(game, tokenOwner), tokenSpice - (tokenOwner === f.payer ? 4 : 0),
|
||||
'Axlotl is accrued, not spendable, regardless of the actual original custodian');
|
||||
game = applyAction(game, f.payer, f.playAction.action);
|
||||
assert.equal(independentSkillsRevivalPlayer(game, f.payer).revived, 3);
|
||||
assert.equal(independentSkillsRevivalPlayer(game, f.payer).freeForcesRevived, 1);
|
||||
assert.equal(spiceOf(game, f.payer), payerSpice - 4, 'Activation does not replay the previous charge');
|
||||
const activated = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, f.payer, { type: 'revive', amount: 3 }),
|
||||
'Only two actual Tanks remain, even though the increased ordinary allowance is larger');
|
||||
assert.deepEqual(game, activated);
|
||||
game = applyAction(game, f.payer, { type: 'revive', amount: 2 });
|
||||
assert.equal(spiceOf(game, f.payer), payerSpice - 6, 'Only the new two-spice quote is charged');
|
||||
const returned = independentSkillsRevivalPlayer(game, f.payer);
|
||||
assert.equal(returned.tanks, 0);
|
||||
assert.equal(returned.reserves, 20);
|
||||
assert.equal(returned.revived, 5);
|
||||
assert.equal(returned.freeForcesRevived, 2);
|
||||
assert.equal(game.techTokens!.axlotl.spice, accrual, 'The second real revival cannot accrue again');
|
||||
assert.equal(game.techTokens!.axlotl.triggeredTurn, 2);
|
||||
assert.equal(spiceOf(game, tokenOwner), tokenSpice - (tokenOwner === f.payer ? 6 : 0));
|
||||
const grants = game.spiceBankerIncome!.sources.filter(source => source.grant);
|
||||
assert.equal(grants.length, 1, 'Normal Banker keeps its four-spice threshold; the second two-spice payment does not qualify');
|
||||
assert.equal(grants[0].source.kind, 'force-revival');
|
||||
assert.deepEqual(grants[0].source.bankLegs, [{ payer: f.payer, amount: 4 }]);
|
||||
assert.deepEqual(viewGame(game, f.owner).spiceBankerIncome?.deferred, [{ owner: f.owner, amount: 1 }]);
|
||||
assert.equal(spiceOf(game, f.owner), bankerSpice);
|
||||
const shipment = f.advanceUntil(game, state => state.turn === 2 && state.phase === 5 &&
|
||||
!state.phaseOpening && !state.response && !state.decision);
|
||||
assert.equal(shipment.techTokens!.axlotl.spice, 0);
|
||||
assert.equal(spiceOf(shipment, tokenOwner), tokenSpice - (tokenOwner === f.payer ? 6 : 0) + accrual,
|
||||
'The original phase end alone pays Axlotl once');
|
||||
assert.equal(spiceOf(shipment, f.owner), bankerSpice + (tokenOwner === f.owner ? accrual : 0),
|
||||
'Only actual Tech custody pays now; Banker remains deferred until Mentat');
|
||||
assert.equal(shipment.discard.filter(card => card.id === f.card).length, 1);
|
||||
assertIndependentSkillsRevivalInventory(shipment);
|
||||
});
|
||||
@@ -145,3 +145,41 @@ void test('all four minimal policies can seal native paid plans and execute proj
|
||||
assertIxIndependentSkillsCustody(done);
|
||||
}
|
||||
});
|
||||
|
||||
void test('original native Tech and Stronghold phases keep the typed Suboid fee outside Suk and carry the winner into paid Recruits', () => {
|
||||
const f = createIxIndependentSkillsRevivalFixture({ advanced: true, tech: true, strongholds: true });
|
||||
const revealed = revealIxIndependentSkillsBattle(f);
|
||||
const quote = quoteIxIndependentSkillsBattle(f, revealed);
|
||||
assert.deepEqual(quote.reinforcements,
|
||||
[{ player: f.ixians, card: REINFORCEMENTS_CARD, normal: 3, elite: 0 }]);
|
||||
assert.deepEqual(quote.harassWithdraw![0].locations, f.returns);
|
||||
const rescue = finishIxIndependentSkillsBattle(f, { state: revealed, stop: 'rescue' });
|
||||
const initial = player(f.game, f.ixians), charged = player(rescue, f.ixians);
|
||||
assert.equal(charged.reserves, initial.reserves - 3 + 2,
|
||||
'Three actual Suboids pay before the physical Suboid and Cyborg return');
|
||||
assert.equal(charged.elites!.reserves, initial.elites!.reserves + 1);
|
||||
assert.equal(charged.tanks, 3);
|
||||
assert.equal(charged.elites!.tanks, 0);
|
||||
const saved = finishIxIndependentSkillsBattle(f, { state: rescue });
|
||||
assert.equal(saved.lastBattleContext!.sukRescue!.completed, true);
|
||||
assert.equal(player(saved, f.ixians).tanks, 3, 'Suk cannot rescue the reserve fee');
|
||||
assert.equal(player(saved, f.ixians).elites!.tanks, 0);
|
||||
assert.equal(f.afterBattle.techTokens!.production.owner, f.ixians,
|
||||
'The real winner takes the original unowned token, not a staged claim');
|
||||
assert.equal(f.beforeRevival.strongholdCards!.claimedTurn, 1);
|
||||
assert.equal(f.beforeRevival.strongholdCards!.owners.hidden_mobile_stronghold, f.ixians);
|
||||
assert.deepEqual(f.price, { free: 1, normalCost: 5, cost: 5 });
|
||||
const before = player(f.beforeRevival, f.ixians), after = player(f.afterRevival, f.ixians);
|
||||
assert.equal(after.spice, before.spice - 5);
|
||||
assert.equal(player(f.afterRevival, f.tleilaxu).spice, player(f.beforeRevival, f.tleilaxu).spice + 5,
|
||||
'Native paid Cyborg/Suboid revival still pays the real Tleilaxu recipient');
|
||||
assert.equal(after.reserves, before.reserves + 3);
|
||||
assert.equal(after.elites!.reserves, before.elites!.reserves + 1);
|
||||
assert.equal(after.tanks, before.tanks - 3);
|
||||
assert.equal(after.elites!.tanks, 0);
|
||||
assert.equal(viewGame(f.afterRevival, f.ixians).revival.freeRemaining, 0);
|
||||
for (const id of [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD, RECRUITS_CARD_ID])
|
||||
assert.equal(f.afterRevival.discard.filter(card => card.id === id).length, 1);
|
||||
assertIxIndependentSkillsCustody(saved);
|
||||
assertIxIndependentSkillsCustody(f.afterRevival);
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import test from 'node:test';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
|
||||
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
|
||||
import { ownedTech } from '../game/tech-tokens';
|
||||
import {
|
||||
advanceMoritaniIndependentSkills,
|
||||
assertMoritaniIndependentSkillsCustody,
|
||||
@@ -342,8 +343,10 @@ void test('a real normal Traitor call forfeits the printed power for the game an
|
||||
});
|
||||
|
||||
|
||||
void test('Advanced paired Skills/cards loss returns selected sectors, then assassinates one unused native Ecaz disc before Mentat replacement', () => {
|
||||
const f = createMoritaniIndependentSkillsFixture({ paired: true, rules: 'advanced', skills: true, cards: true });
|
||||
void test('Advanced paired Skills/cards with original Tech/Strongholds return selected sectors, then assassinate one unused native Ecaz disc before Mentat replacement', () => {
|
||||
const f = createMoritaniIndependentSkillsFixture({
|
||||
paired: true, rules: 'advanced', skills: true, cards: true, tech: true, strongholds: true,
|
||||
});
|
||||
const loyal = f.afterSetup.ecazLoyalty!.card!;
|
||||
assert.notEqual(f.traitorTarget, loyal);
|
||||
assert.notEqual(f.traitorTarget, f.winnerLeader);
|
||||
@@ -401,6 +404,12 @@ void test('Advanced paired Skills/cards loss returns selected sectors, then assa
|
||||
const beforeMentat = finishMoritaniIndependentSkillsBattle(spent, { winnerDiscard: 'played' },
|
||||
(game) => game.phase === 7 && moritaniIndependentSkillsClean(game));
|
||||
assert.equal(beforeMentat.discard.filter((card) => card.id === f.cards.winnerWeapon).length, 1);
|
||||
const lostTokens = ownedTech(f.game.techTokens, f.moritani);
|
||||
assert.equal(lostTokens.length, 1, 'the actual losing Moritani army entered with one original token');
|
||||
assert.equal(beforeMentat.techTokens![lostTokens[0]].owner, f.opponent,
|
||||
'Assassinate preserves the original winner cleanup and its automatic single-token transfer');
|
||||
assert.equal(beforeMentat.strongholdCards!.claimedTurn, 0,
|
||||
'no Stronghold Card is fabricated before the first genuine Mentat acquisition');
|
||||
assert.equal(beforeMentat.moritaniAssassinate!.opportunities[0].stage, 'revealed');
|
||||
assert.deepEqual(player(beforeMentat, f.moritani).traitors, [f.traitorTarget]);
|
||||
const settled = finishMoritaniIndependentSkillsBattle(beforeMentat);
|
||||
@@ -436,9 +445,11 @@ void test('Basic paired Skills/cards loss leaves the unused trained Ecaz disc an
|
||||
assertMoritaniIndependentSkillsCustody(settled);
|
||||
});
|
||||
|
||||
void test('paired Advanced Skills without independent cards still pays the printed Ecaz assassination bounty', () => {
|
||||
const f = createMoritaniIndependentSkillsFixture({ paired: true, rules: 'advanced', skills: true });
|
||||
void test('paired Advanced Skills with original phase modules pays the printed Ecaz bounty and winner Tech without independent cards', () => {
|
||||
const f = createMoritaniIndependentSkillsFixture({ paired: true, rules: 'advanced', skills: true,
|
||||
tech: true, strongholds: true });
|
||||
assert.equal(f.pending.lastBattleContext!.winner, f.opponent);
|
||||
assert.equal(f.initial.nexusCards, undefined);
|
||||
assert.equal(f.returns, null);
|
||||
assert.equal(f.cards.reinforcements, null);
|
||||
assert.equal(f.cards.harass, null);
|
||||
@@ -447,10 +458,21 @@ void test('paired Advanced Skills without independent cards still pays the print
|
||||
const physical = [...f.pending.deck, ...f.pending.discard, ...f.pending.players.flatMap((seat) => seat.hand)];
|
||||
assert.equal(physical.length, 33);
|
||||
assert.equal(physical.some((card) => card.id === REINFORCEMENTS_CARD || card.id === HARASS_WITHDRAW_CARD), false);
|
||||
const defeatedTokens = Object.entries(f.pending.techTokens!).filter(([, token]) =>
|
||||
token.owner === f.moritani).map(([id]) => id);
|
||||
const spent = chooseMoritaniIndependentSkillsAssassination(f.pending, f.traitorTarget);
|
||||
assert.equal(owned(f, spent, f.traitorTarget).dead, true);
|
||||
assert.equal(spent.moritaniAssassinate!.opportunities[0].bounty, 4);
|
||||
assert.equal(player(spent, f.moritani).spice, player(f.pending, f.moritani).spice + 4);
|
||||
assert.equal(spent.leaderSkills!.assignments.some((entry) => entry.leader === f.traitorTarget), false);
|
||||
assertMoritaniIndependentSkillsCustody(spent);
|
||||
const settled = finishMoritaniIndependentSkillsBattle(spent, { winnerDiscard: 'played' });
|
||||
assert.equal(settled.phase, 8, 'plain paired Skills keeps the original phase envelope without a card-module dependency');
|
||||
assert.equal(settled.discard.filter(card => card.id === f.cards.winnerWeapon).length, 1);
|
||||
assert.equal(settled.moritaniAssassinate!.opportunities.length, 1);
|
||||
assert.equal(settled.moritaniAssassinate!.opportunities[0].stage, 'replaced');
|
||||
assert.equal(settled.traitorReserve!.length, f.pending.traitorReserve!.length - 1);
|
||||
assert.equal(Object.entries(settled.techTokens!).filter(([id, token]) =>
|
||||
defeatedTokens.includes(id) && token.owner === f.opponent).length, 1);
|
||||
assertMoritaniIndependentSkillsCustody(settled);
|
||||
});
|
||||
Reference in new issue
Block a user