Compose native World cards with original phase modules

This commit is contained in:
Quintine committed 2026-10-17 10:58:43 +11:00
1 parent cda9338f13
commit d6d71c7c02
18 files changed
+714 -39

No files matched your search

+19
View File
@@ -29,6 +29,25 @@ optional skill bands, combinations and public expansion starts remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Native World phase modules — 17 October.** Selected E1/E2 native/base Worlds
now retain original Tech3+ Basic/Advanced and/or Advanced Strongholds2+ through
the independent-card entry. Original38/50 decks, native fees/custody, current
rates/unlimited allowances, marker funding and own-World H bans stay.
Fifteen genuine module setups enter. Three390px journeys/23 actions exercise
paid Cyborg reopening/thresholds, unlimited12-force CHOAM with its actual ending
market, one delayed Axlotl payout and genuinely earned Arrakeen bank2/personal1
support. Arrakeen applies only in its original territory, not on a foreign World.
Native fee3N precedes return3N; three actual Cyborgs alone become board casualties.
An original conserved optional Face Dance holds the invoice before Collection;
no initiative, army, wallet, owner or phase is assigned. Other native own/foreign
fees and board-only aftercare retain their paths. Parent removed a nondeterministic
second-conflict fixture and log-wording assertions rather than freezing later Storms.
Four policies finish16 useful Revivals/112 actions and12 native battles/82 actions.
Conserved cards/identities remain explicit staging. Related18-file union225/225,
types/lint, build and actual prototype help pass; existing build warnings remain.
Skills, Nexus, Discovery, E3 and other overlays stay gated; full-mode and
deployed acceptance remain open.
**Native E1/E2 World independent cards — 17 October.** Clean selected Ix/CHOAM
native/base Basic/Advanced Worlds retain original38/50 setup without phase
modules; classic base36 Tech/Strongholds stays separate. Shared roster checks
+50 -9
View File
@@ -158,10 +158,10 @@ the original Richese cache remains separate.
Use original setup and native ownership: remaining Tech after first Storm,
Stronghold Cards only at actual end-Mentat. No owned module receipt, bank spice,
forces, phase or history is injected. Classic Homeworlds separately compose the
same original phase modules below. Expansion Worlds, Nexus, Skills, Discovery,
Sandtrout, mixed E3 and unrelated previews, played-game conversion and public
starts stay gated. Stone/Mirror retains its separate selected-family contract.
forces, phase or history is injected. Classic and selected native Homeworlds
compose the same original phase modules in the contracts below. Nexus, Skills,
Discovery, Sandtrout, mixed E3 and unrelated previews, played-game conversion and
public starts stay gated. Stone/Mirror retains its separate selected-family contract.
Recruits changes current free rates/ordinary allowance, not the original Axlotl
once-per-phase trigger or delayed income. Eligible actual free revival triggers
@@ -196,8 +196,8 @@ calibration, broad assurance or deployed acceptance is claimed.
The fresh classic independent36-card Homeworld entry now retains original unused
Tech3–6 Basic/Advanced and/or Strongholds2–6 Advanced; both requires Advanced3–6.
This composes the existing native-pool and phase-module handlers, not new printed
cost/rate arithmetic or a new public start. Expansion Worlds, Skills, Nexus,
Discovery, Sandtrout and unrelated overlays stay separate.
cost/rate arithmetic or a new public start. Selected native Worlds follow the
later composition below; Skills, Nexus, Discovery, Sandtrout and other overlays stay separate.
Original setup creates native typed custody; first Storm assigns remaining Tech
and actual end-Mentat earns held cards. Axlotl/Heighliners retain once-per-phase
@@ -227,9 +227,9 @@ staging. These are not complete modes, calibration, broad assurance or deploymen
Clean selected Ix and/or CHOAM native/base Basic/Advanced2–6 Worlds now compose
the independent cards before original setup: selected38/50, with Richese's
ten-card cache separate when present. Original HMS/seven Cyborgs, Face Dancers,
Auditor, marker identities and native typed custody remain. These selected-family
Worlds do not add phase modules, Skills, Nexus, Discovery, Sandtrout, E3 or other
previews; classic base36 World Tech/Strongholds remains its separate profile.
Auditor, marker identities and native typed custody remain. Original Tech3+
and Advanced Strongholds2+ follow the later native phase contract below.
Skills, Nexus, Discovery, Sandtrout, E3 and other previews remain gated.
Public starts and played-game conversion stay gated.
One shared selected-family roster rule replaces duplicate checks. The renamed
@@ -258,3 +258,44 @@ H-only seals. Four-policy programmes retain useful conserved-card staging,
not strategy/calibration. Existing build warnings, complete modes and deployed
acceptance remain open; no saved-game/database assurance campaign was run.
## Native World phase modules — 17 October 2026
The selected Ix/CHOAM native/base World independent-card entry now retains
original unused Tech Tokens3–6 Basic/Advanced and/or Stronghold Cards2–6
Advanced; both requires Advanced3–6. This removes the remaining composition
barrier, not a new rate, payment or card-effect ruling. Original38/50 inventory,
Richese cache, HMS/Cyborgs, native-only World declarations and custody stay.
Keep exact source-bearing normal-first fees, own-World fee/dial disjointness,
printed traits/face, fee-before-Harass and every own-World Harass prohibition.
Richese must still fund R after projected original marker materialization.
First Storm assigns remaining tokens; actual end-Mentat earns held cards.
Axlotl keeps its once-per-phase trigger and delayed income. Recruits does not
refund accepted paid returns or reduce native unlimited allowances. Harass
deposits and reserve fees are neither free revival nor off-planet shipment.
Held Arrakeen funds only actual support in Arrakeen, never a foreign World,
personal spice or the score-only +2. A real Arrakeen plan may still pay its
reserve fee from native Ix. Original winner cleanup/rewards and aftercare remain.
Skills, Nexus, Discovery, Sandtrout, E3, unrelated previews, public starts and
played-save conversion remain guarded.
Bounded proof:15 fresh native module setups and three390px journeys/23 actions.
Ix pays3/0/5 across prior-paid reopening, current population threshold and native
Cyborg deposits; native CHOAM returns12 without imposing seven, finishes its own
market and then original Guild timing. Axlotl accrues once and pays only when
Revival really ends. Real first-turn paid3N3E Arrakeen placement earns its card
at actual end-Mentat; next-turn support3 costs bank2/personal1. Native fee3N
precedes Harass3N; three board Cyborgs die separately. An original conserved
Face Dancer offers its real continuation before Collection can mask the invoice.
Its decline reaches Collection without changing initiative or freezing later
Storms. Original own-World fee/dial/H restrictions, mixed fees/typed aftercare
and marker-short rejection/H fallback also compose.
Behavioral evidence: `tests/native-world-phase-recruits.test.ts`,
`tests/native-world-phase-battle.test.ts`, `tests/native-world-phase-marker.test.ts`,
`tests/fixture-native-world-phase-battle.ts` and the extended canonical native
fixtures. Four policies finish16 useful Revival programmes/112 actions and12
native battle programmes/82 actions, not strategic calibration. Related18-file
union225/225, types/lint, build and actual prototype help pass with existing
warnings. No full-mode, deployed, HTTP/database or broad assurance claim.
+4 -3
View File
@@ -145,7 +145,8 @@ physical cards before original classic2–6 Basic/Advanced setup: base33+3.
Harass composes on Arrakis and foreign Homeworlds. The printed own-World
prohibition remains, including both Advanced Emperor Worlds. Original
Tech/Advanced Strongholds now follow the [classic World phase contract](ECAZ_TREACHERY_RULES.md#classic-world-phase-module-composition--16-october-2026).
Selected E1/E2 Worlds separately use the [native no-phase-module contract](ECAZ_TREACHERY_RULES.md#native-selected-family-world-composition--17-october-2026);
Selected E1/E2 Worlds use the [native setup contract](ECAZ_TREACHERY_RULES.md#native-selected-family-world-composition--17-october-2026)
and [original phase-module composition](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-october-2026);
other rosters/overlays, public starts and save conversion remain closed.
Recruits has a separate
[native current-rate composition](RECRUITS_RUNTIME.md#classic-homeworld-composition--16-october-2026);
@@ -224,8 +225,8 @@ route warnings remain; no HTTP or database boundary changed.
after reveal. Unique returns are automatic; previously valid legacy battles
retain unique-only derivation. See the [allocation contract](HARASS_ALLOCATION.md).
- Clean classic Homeworlds use the [native reserve-routing contract](#classic-homeworld-reserve-routing--16-october-2026).
Classic World rosters or selected Ix/CHOAM without Worlds may retain
[original Tech3+ and Advanced Strongholds2+](ECAZ_TREACHERY_RULES.md#classic-world-phase-module-composition--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.
- Richese card-family configurations now admit Harass under the
+43
View File
@@ -1,5 +1,48 @@
# Dune implementation status
## 17 October 2026 — Native World independent cards with phase modules
Original independent-card variant authority permits composition; fresh selected
Ix/CHOAM native/base Worlds still rejected original Tech/Advanced Strongholds.
Remove that single roster/module barrier while preserving Tech3–6 Basic/
Advanced and Strongholds2–6 Advanced, all other overlays/public/save gates and
original38/50 inventory/cache. Native rates, unlimited allowances, exact fee
sources, own-World disjointness/H ban, marker funding and aftercare remain.
Two disjoint contributors supplied native Revival and earned battle programmes.
Fifteen module setups enter. Original first Storm assigns tokens; actual
end-Mentat earns Arrakeen. No live owner/army/wallet/phase/receipt is assigned.
Ix prior-paid reopening keeps prices3/0/5 and current population observations;
native CHOAM retains unlimited twelve returns at prices7/0/3. One Axlotl
accrual stays unspendable until the real phase end, including CHOAM's ending
market. All later original Shipment continuations retain their timing.
Real turn-one paid6=3N3E Arrakeen placement earns the local card before a paid
turn-two Guild attack. Support0/1/3 costs bank0/1/2 and personal0/0/1. Native
Ix pays3N before Harass returns3N; three original board Cyborgs die separately.
Printed Stronghold benefits are territory-local, not foreign-World subsidies.
Parent replaced a nondeterministic extra-conflict fixture with an original
conserved optional Face Dance before Collection; its decline retains actual
cleanup and phase opening. Native initiative/later Storms are not overwritten.
Log-wording and redundant setup-forwarding assertions were removed.
Three actual390px journeys/23 actions verify typed reopening, unlimited CHOAM
with original market/Guild timing and earned bank2/personal1 plus native fees.
Final surfaces have no root overflow or app alert. Isolated host setup initially
needed real Vinext image/environment/middleware configuration; its selector and
polling were corrected. Stale-node browser observations were read afresh without
replaying accepted controls. Owned host/tab/scaffolds are removed; actual rooms,
databases, human server and deployment were untouched.
Four legal policies finish16 useful Revivals/112 actions and12 native battles/
82 actions. Conserved useful-card/identity staging is explicit, not production
acquisition or AI strategy/calibration. Node support0/1/3 and marker-budget smoke
reach actual original continuations. Related18-file union225/225, types/lint,
build and actual prototype help pass; existing build warnings remain. Initial
QA-only lint/type errors disappeared with the removed throwaway surface.
Private artifacts: `/tmp/dune-native-world-phase-*-proof.json`. Complete modes,
other combinations and deployed/broad assurance acceptance remain open.
## 17 October 2026 — Selected native E1/E2 World independent cards
Actual ready Ix/Tleilaxu/CHOAM/Richese World lobbies rejected the classic-only
+2 -1
View File
@@ -38,7 +38,8 @@ activation or played-save conversion is added. Reinforcements has a separate
[native-source fee composition](REINFORCEMENTS_RUNTIME.md#classic-homeworld-reserve-fees--16-october-2026);
original Tech/Advanced Strongholds now follow the separate
[classic World phase contract](ECAZ_TREACHERY_RULES.md#classic-world-phase-module-composition--16-october-2026).
Selected E1/E2 Worlds now use a [separate native no-phase-module contract](ECAZ_TREACHERY_RULES.md#native-selected-family-world-composition--17-october-2026);
Selected E1/E2 Worlds use the [native setup contract](ECAZ_TREACHERY_RULES.md#native-selected-family-world-composition--17-october-2026)
and [original phase-module composition](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-october-2026);
other rosters and overlays remain separate.
Publisher Ecaz & Moritani printed pp11/16 specifically gives low Fremen4,
+2 -1
View File
@@ -207,7 +207,8 @@ and all three independent physical cards (base33+3). Reinforcements may be
used on Arrakis, a foreign World **or an own World**; it does not inherit
Harass's printed own-World ban. Original Tech/Advanced Strongholds now follow
the [classic World phase contract](ECAZ_TREACHERY_RULES.md#classic-world-phase-module-composition--16-october-2026).
Selected native E1/E2 Worlds now use the [separate no-phase-module contract](ECAZ_TREACHERY_RULES.md#native-selected-family-world-composition--17-october-2026);
Selected native E1/E2 Worlds use the [native setup contract](ECAZ_TREACHERY_RULES.md#native-selected-family-world-composition--17-october-2026)
and [original phase-module composition](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-october-2026);
other rosters/overlays, public starts and save conversion remain gated.
### Sources, physical pool and original quote
+2 -2
View File
@@ -666,9 +666,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 / 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. Classic base36 Worlds separately compose them; selected E1/E2 World phase modules remain gated. 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. Skills, Nexus, Discovery, E3 mixtures and unrelated previews remain gated. | [Phase composition](ECAZ_TREACHERY_RULES.md#original-phase-module-composition--16-october-2026), [classic World composition](ECAZ_TREACHERY_RULES.md#classic-world-phase-module-composition--16-october-2026) |
| 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) |
| 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 without other modules; classic base36 phase modules stay separate. 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) |
| 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) |
| Expansion-faction setup | Selecting Ecaz/Moritani factions does not select their independent three-card variant. Ordinary decks contain 33/35/47 cards; Richese's ten-card cache stays separate. Ecaz allocates six total forces within Imperial Basin sectors 9–11, retaining fourteen reserves. Flexible sector allocation is the literal printed territory constraint; Fremen→Ecaz→Advanced BG order is an implementation inference, while Moritani-last is printed. | [Source and profile contract](EXPANSION_FACTIONS_PROTOTYPE.md) |
| Ixian Technology on Richese lots | Superseded 8 October by the authorized supplied Advanced physical p34, which expressly excludes Richese auctions from Ixian Technology. Richese cache/Black Market lots now open directly; the once-per-round Technology use remains for ordinary lots. The temporary per-lot decline scaffolding and its tests were deleted. | [Auction source boundary](RICHESE_AUCTION_RULES.md#13-september-saved-per-lot-technology-decline--superseded-8-october) |
+3 -3
View File
@@ -6,14 +6,14 @@ Historical development module checkpoint, 7 September 2026: the six printed effe
The fresh independent-card entry retains original unused Advanced Strongholds
at2–6 seats and optional Tech3–6, with clean classic/selected Ix/CHOAM native/base
rosters. Classic Homeworlds now compose the same original phase modules;
selected native families remain without Worlds. Actual end-Mentat earns
rosters. Classic and selected native Homeworlds compose the same original phase
modules. Actual end-Mentat earns
ownership; no card is assigned by a fixture. Arrakeen funds actual support (bank2/personal1
for support3); native Ix earns Tabr before a real next-turn attack and mixed
Reinforcements fee/Harass return. Original token rewards/held effects stay.
Stone/Mirror uses its separate selected-family contract. Other overlays, public
starts and save conversion remain gated. See the [shared phase contract](ECAZ_TREACHERY_RULES.md#original-phase-module-composition--16-october-2026)
and [classic World composition](ECAZ_TREACHERY_RULES.md#classic-world-phase-module-composition--16-october-2026).
and [World composition](ECAZ_TREACHERY_RULES.md#native-world-phase-modules--17-october-2026).
## Semuta composition — 11 October 2026
+3 -3
View File
@@ -118,10 +118,10 @@ export function cardPresentation(card: VisibleCard): CardPresentation {
guidance: ecaz.summary,
gameplay: ecaz.gameplay,
availability: ecaz.card.effect === 'recruits'
? 'Recruits has Revival controls in classic and selected Ix/CHOAM Basic/Advanced Worlds. Selected-family Worlds retain original native setup without other modules; base-deck classic Worlds may also retain Tech3+ and Advanced Strongholds2+. Double current native free rates within the ordinary seven-force total; native unlimited allowances, typed deposits and special-force caps stay. Prior paid returns keep payment under the provisional no-refund policy. Pending transactions, unknown ledgers, later Fremen grants and replay remain guarded.'
? 'Recruits has Revival controls in classic and selected Ix/CHOAM Basic/Advanced Worlds, optionally retaining original Tech3+ and Advanced Strongholds2+. Double current native free rates within the ordinary seven-force total; native unlimited allowances, typed deposits and special-force caps stay. Axlotl keeps its original once-per-phase trigger and delayed payout. Prior paid returns keep payment under the provisional no-refund policy. Pending transactions, unknown ledgers, later Fremen grants and replay remain guarded.'
: ecaz.card.effect === 'harassWithdraw'
? 'Harass & Withdraw uses either battle-card slot and exact physical allocation after reveal, including selected Ix/CHOAM Worlds without other modules. Base-deck classic Worlds separately retain Tech3+ and Advanced Strongholds2+. Return actual native types: Advanced Emperor normal to Kaitain/Sardaukar to Salusa; other natives to their original primary World. Never withdraw on any own World. Original commitment/support stays. Own No-Field provisionally materializes first; Stone keeps its separate profile/allocation guards.'
: 'Reinforcements uses either slot, including selected Ix/CHOAM Basic/Advanced Worlds without other modules. Classic base36 Worlds separately retain Tech3+ and Advanced Strongholds2+. Pay three original own reserves normal-first for +2 normal score, not support or physical tokens. Exact native sources seal when ambiguous; an own-World fee cannot also fund its dial, while original traits/face stay. Own No-Field must leave three before payment, including native World reserves; future Harass cannot fund it. Remaining profile guards and cost/order interpretations stay provisional.',
? 'Harass & Withdraw uses either battle-card slot and exact physical allocation after reveal, including classic and selected Ix/CHOAM Worlds with original Tech3+ and Advanced Strongholds2+. Return actual native types: Advanced Emperor normal to Kaitain/Sardaukar to Salusa; other natives to their original primary World. Never withdraw on any own World. Original commitment/support stays. Returns are not free revival or shipment. Own No-Field provisionally materializes first; Stone keeps its separate profile/allocation guards.'
: 'Reinforcements uses either slot, including classic and selected Ix/CHOAM Basic/Advanced Worlds with original Tech3+ and Advanced Strongholds2+. Pay three original own reserves normal-first for +2 normal score, not support or physical tokens. Exact native sources seal when ambiguous; an own-World fee cannot also fund its dial, while original traits/face stay. Own No-Field must leave three before payment, including native World reserves; future Harass cannot fund it. Earned bank subsidy pays only actual support. Remaining profile guards and cost/order interpretations stay provisional.',
topics: RULE_TOPICS.filter((topic) => topic.id === (ecaz.card.effect === 'harassWithdraw' ? 'card-harass-withdraw' : ecaz.card.effect === 'reinforcements' ? 'card-reinforcements' : 'card-recruits')),
};
const topicIds =
+2 -2
View File
@@ -10723,7 +10723,7 @@ function independentCardModulesProfile(g: Game): boolean {
if (!g.ecazTreachery || (!g.techTokens && !g.strongholdCards) || !independentCardRoster(g) ||
g.players.length < (g.techTokens ? 3 : 2) || g.players.length > 6 ||
(g.strongholdCards && !g.advanced) ||
(g.homeworlds && g.expansions.length !== 0) || g.nexusCards || g.leaderSkills ||
g.nexusCards || g.leaderSkills ||
g.discoveryEnabled || g.discoveries || g.discoveryStash || g.greatMaker || g.sandtrout ||
g.ecazOccupyPreview || g.semutaPreview || g.kullPreview || g.nexusKullPreview ||
g.richeseBetrayalPreview || g.guildBetrayalPreview || g.moritaniAssassinatePreview ||
@@ -10738,7 +10738,7 @@ export function initializeEcazTreacheryGameForAudit(state: Game): Game {
g.ecazTreachery = true;
const homeworlds = !!g.homeworlds;
requireRule(!homeworlds || ecazHomeworldProfile(g),
'Independent cards with Homeworlds need a fresh classic or selected Ix/CHOAM roster; phase modules remain classic-only.');
'Independent cards with Homeworlds need a fresh classic or selected Ix/CHOAM roster with only original Tech or Advanced Strongholds.');
return initializeSetupGameForAudit(g, homeworlds, false, false, false, false, false, true, true);
}
function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = false, ix = false, discovery = false, leaderSkills = false, choam = false, factions = false, ecazTreachery = false, strongholdFactions = false): Game {
+8 -8
View File
@@ -3417,7 +3417,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'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 separate normal Banker income. 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. E3 pairs, mixtures and allied Occupy skills stay separate.',
'Fresh independent Treachery previews compose classic rosters including original Homeworlds, or selected Ix/CHOAM native/base rosters without Worlds, 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.',
'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.',
],
example:
@@ -3525,7 +3525,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'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. Banker remains a separate source-to-Mentat opt-in. CHOAM’s separate native opening payout is not ordinary poverty Charity. Two-player tokens, mixed E3/other combinations and full combined acceptance remain unfinished.',
'Without Skills, fresh native E1/E2 families or standalone Ecaz OR Moritani with classic opponents may preserve unused original Tech Tokens through the local factions entry, Basic/Advanced three through six players; Advanced stronghold-factions adds the original six Stronghold Cards. Native payments, typed casualties, selected Ecaz lead rewards and Moritani original assassination remain separate. Split allied holdings never qualify as a complete set. Basic odd Occupy, Richese cache/No-Field rulings, unrelated overlays, public starts and saved-game conversion remain guarded.',
'Classic independent-card previews including Homeworlds, or selected Ix/CHOAM native/base rosters without Worlds, 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.',
'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.',
],
example:
'You own Heighliners and Axlotl Tanks. When Emperor ships from reserves, place two spice on Heighliners. A later shipment adds nothing. Collect the two spice when Shipping and Movement ends.',
@@ -4982,8 +4982,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'Atreides rises from two free forces to four; Fremen from three to six; Guild from one to two. The ordinary force limit becomes seven. Native unlimited CHOAM and Tleilaxu allowances remain unlimited, and their free rates become zero and four. Paid prices and separate elite limits still apply.',
'Free prevention still prevents free returns. Prior paid ordinary returns now have a visibly provisional policy: preserve payment, deduct only recorded free returns from the doubled free allowance and all returns from the total cap. Two free plus one paid Atreides may take two more free without refund; this is not a numerical user ruling. Missing/inconsistent ledgers, later Fremen grants, pending transactions and same-turn replay remain guarded.',
'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 rosters including Homeworlds, or selected Ix/CHOAM native/base rosters without Worlds, 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 now use original38/50 independent-card setup without other 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.',
'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.',
],
related: ['revival', 'homeworlds', 'ecaz-modules', 'expansion-faction-games', 'implementation-checklist'],
checklist: [
@@ -5007,9 +5007,9 @@ export const RULE_TOPICS: RuleTopic[] = [
'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.',
'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. Phase modules remain classic base36 only. Native-only World Traitors, typed Tanks and fee-before-Harass stay provisional composition, not a new card FAQ.',
'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 add no other modules; original38/50 setup and native stock remain. 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. Without Worlds, original phase-module and admitted Stone compositions remain separate.',
'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.',
],
related: ['battle-cards', 'battle', 'homeworlds', 'card-recruits', 'card-harass-withdraw', 'card-truthtrance', 'implementation-checklist'],
checklist: [
@@ -5036,8 +5036,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'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 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. Native Worlds add no phase modules.',
'Classic rosters including Homeworlds, or selected Ix/CHOAM native/base rosters without Worlds, 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.',
'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.',
],
related: ['battle-cards', 'battle', 'card-recruits', 'homeworlds', 'ecaz-modules', 'implementation-checklist'],
checklist: [
+6 -2
View File
@@ -18,6 +18,9 @@ export { REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD };
export type NativeWorldBattleStep = HarassHomeworldStep;
export type NativeWorldBattleOptions = {
advanced?: boolean;
/** Original unused phase modules selected before native setup. */
tech?: boolean;
strongholds?: boolean;
route?: 'ix-visitor' | 'ix-native' | 'choam-native';
/** Actual first-turn export, not a reserve assignment. */
exportNormal?: number;
@@ -76,7 +79,7 @@ function conserveCard(game: Game, actor: string, id: string, staging: string[]):
owner.hand.push(game.deck.splice(at, 1)[0]);
staging.push(`Conserved original ${id}: undealt deck → ${actor}; not a purchase.`);
}
function conserveDancer(game: Game, identity: string, staging: string[]): void {
export function conserveDancer(game: Game, identity: string, staging: string[]): void {
const tleilaxu = nativeWorldBattlePlayer(game, 'tleilaxu');
if (tleilaxu.faceDancers!.some(d => d.leader === identity)) return;
const dancer = tleilaxu.faceDancers!.find(d => !d.revealed); assert.ok(dancer);
@@ -96,7 +99,8 @@ function conserveDancer(game: Game, identity: string, staging: string[]): void {
export function createNativeWorldBattleFixture(options: NativeWorldBattleOptions = {}): NativeWorldBattleFixture {
const route = options.route ?? 'ix-visitor';
const advanced = options.advanced ?? false;
const setup = createIndependentCardModulesFixture({ advanced, homeworlds: true, tech: false, strongholds: false,
const setup = createIndependentCardModulesFixture({ advanced, homeworlds: true,
tech: options.tech ?? false, strongholds: options.strongholds ?? false,
factions: route === 'choam-native' ? ['choam', 'guild', 'tleilaxu', 'ixians'] : ['ixians', 'tleilaxu', 'guild'],
expansions: route === 'choam-native' ? ['ix', 'choam'] : ['ix'] });
let game = setup.game;
+177
View File
@@ -0,0 +1,177 @@
import assert from 'node:assert/strict';
import { applyAction, viewGame, type Game } from '../game/engine';
import { combatArmy } from '../game/combat-location';
import { nativeShipmentSources } from '../game/homeworld-options';
import type { NativeReserveSelections } from '../game/homeworld-native-reserves';
import type { HomeworldForces } from '../game/homeworld-custody';
import { createIndependentCardModulesFixture } from './fixture-independent-card-modules';
import { actWorldPhaseBattle, advanceWorldPhaseBattle } from './fixture-world-phase-battle';
import { harassHomeworldClean, nextHarassHomeworldStep } from './fixture-harass-homeworld';
import { takeHarassCard } from './fixture-harass-withdraw';
import {
advanceNativeWorldBattle, assertNativeWorldBattleInventory, conserveDancer, createNativeWorldBattleFixture,
HARASS_WITHDRAW_CARD, nativeWorldBattleFinished, nativeWorldBattlePlayer,
REINFORCEMENTS_CARD, type NativeWorldBattleFixture, type NativeWorldBattleStep,
} from './fixture-native-world-battle';
export type NativeWorldPhaseBattleOptions = {
advanced?: boolean;
tech?: boolean;
strongholds?: boolean;
/** Actual Cyborg support, never support for Reinforcements' score-only +2. */
support?: 0 | 1 | 3;
};
export type NativeWorldPhaseBattleFixture = {
initial: Game;
afterSetup: Game;
beforeFirstMentat: Game;
firstMentatStep: NativeWorldBattleStep;
afterFirstMentat: Game;
shipments: { before: Game; step: NativeWorldBattleStep; after: Game }[];
beforeBattle: Game;
battleStep: NativeWorldBattleStep;
/** Original prepared battle, no sealed plans. */
game: Game;
owner: string;
opponent: string;
observer: string;
location: string;
planActions: [NativeWorldBattleStep, NativeWorldBattleStep];
sources: NativeReserveSelections;
returns: Record<string, HomeworldForces>;
nativeSteps: NativeWorldBattleStep[];
staging: string[];
};
/** Arrakeen's held card applies only in Arrakeen, never on a foreign World.
* Ix therefore really pays to place 3 Suboids + 3 Cyborgs there on turn one,
* earns the card at original END Mentat, then meets a paid Guild attack on
* turn two. The three-force R fee still comes from original native Ix custody.
* A conserved matching Face Dancer offers its original optional continuation,
* exposing the settled support invoice before Collection. No army/wallet/phase/
* owner or module receipt is assigned.
*/
export function createNativeWorldPhaseBattleFixture(
options: NativeWorldPhaseBattleOptions = {},
): NativeWorldPhaseBattleFixture {
const advanced = options.advanced ?? true;
const setup = createIndependentCardModulesFixture({ advanced, homeworlds: true,
tech: options.tech ?? true, strongholds: options.strongholds ?? advanced,
factions: ['ixians', 'tleilaxu', 'guild'], expansions: ['ix'] });
let game = setup.game;
const afterSetup = structuredClone(game), nativeSteps = [...setup.steps];
const owner = 'ixians', opponent = 'guild', observer = 'tleilaxu', location = 'arrakeen:10';
const staging = [
'Original setup and first-Storm entropy only are fixed; later Storm Cards and movement stay original.',
'Position eight original unopened non-worm Spice Cards for the first two native Spice Blows; no replacement face or phase assignment.',
];
for (let position = 0; position < 8; position++) {
const at = game.spiceDeck.findIndex((card, index) => index >= position && 'territory' in card && !card.discovery);
assert.ok(at >= position); game.spiceDeck.splice(position, 0, game.spiceDeck.splice(at, 1)[0]);
}
const act = (step: NativeWorldBattleStep) => {
game = actWorldPhaseBattle(game, step, nativeSteps); return game;
};
const reach = (until: (g: Game) => boolean) => { game = advanceWorldPhaseBattle(game, until, nativeSteps); };
const shipments: NativeWorldPhaseBattleFixture['shipments'] = [];
const ship = (actor: string, amount: number, elite: number) => {
const before = structuredClone(game), view = viewGame(game, actor);
const sources = nativeShipmentSources(view, amount, elite); assert.ok(sources);
const step: NativeWorldBattleStep = { actor, action: {
type: 'ship', territory: 'arrakeen', sector: 10, amount, elite, homeworldSources: sources, allyPayment: 0,
} };
act(step);
reach(g => harassHomeworldClean(g) && !g.pendingShipment && !g.pendingHomeworldShipment);
shipments.push({ before, step, after: structuredClone(game) });
};
reach(g => g.turn === 1 && g.phase === 5 && g.active === owner && harassHomeworldClean(g));
ship(owner, 6, 3);
assert.deepEqual(combatArmy(game, owner, 'arrakeen'), { normal: 3, elite: 3 });
reach(g => g.turn === 1 && g.phase === 8 && harassHomeworldClean(g));
let beforeFirstMentat: Game | undefined, firstMentatStep: NativeWorldBattleStep | undefined;
while (game.turn === 1) {
const step = nextHarassHomeworldStep(game); assert.ok(step);
const before = structuredClone(game), after = act(step);
if (after.turn === 2) { beforeFirstMentat = before; firstMentatStep = step; }
}
assert.ok(beforeFirstMentat && firstMentatStep);
const afterFirstMentat = structuredClone(game);
reach(g => g.turn === 2 && g.phase === 5 && harassHomeworldClean(g));
while (game.phase === 5) {
const actor = game.active!;
if (harassHomeworldClean(game) && !nativeWorldBattlePlayer(game, actor).shipped && actor === opponent)
ship(actor, 3, 0);
else {
const step = nextHarassHomeworldStep(game); assert.ok(step); act(step);
}
}
reach(g => g.phase === 6 && !!g.active && harassHomeworldClean(g));
for (const player of game.players) game.deck.push(...player.hand.splice(0));
takeHarassCard(game, owner, REINFORCEMENTS_CARD);
takeHarassCard(game, owner, HARASS_WITHDRAW_CARD);
staging.push('Return all original hands, including Ix setup stock, to undealt stock; conserve original R/H into Ix’s two real slots. This is not a purchase.');
const beforeBattle = structuredClone(game);
const choice = viewGame(game, game.active!).battleChoices.find(c => c.territory === 'arrakeen'); assert.ok(choice);
const battleStep: NativeWorldBattleStep = { actor: choice.chooser, action: { type: 'chooseBattle', territory: 'arrakeen',
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker } };
act(battleStep);
reach(g => !!g.battle && harassHomeworldClean(g) && !g.battle.preparation && g.battle.preLeader?.closed !== false);
const living = (actor: string) => nativeWorldBattlePlayer(game, actor).leaders.filter(l => !l.dead && !l.usedAt)
.sort((a, b) => a.strength - b.strength || a.id.localeCompare(b.id));
const ownLeader = living(owner).at(-1)!, otherLeader = living(opponent)[0];
assert.ok(ownLeader && otherLeader);
conserveDancer(game, ownLeader.id, staging);
const support = advanced ? options.support ?? 3 : 0;
const sources = { 'homeworld:ixians': { normal: 3, elite: 0 } };
// Exactly three Cyborgs are committed in every programme. At half support
// they dial three; each actually supported Cyborg adds one, not another token.
const planActions: NativeWorldPhaseBattleFixture['planActions'] = [
{ actor: owner, action: { type: 'battlePlan', dial: advanced ? 3 + support : 6, support,
leader: ownLeader.id, weapon: REINFORCEMENTS_CARD, defense: HARASS_WITHDRAW_CARD, reinforcementsSources: sources } },
{ actor: opponent, action: { type: 'battlePlan', dial: 0, support: 0, leader: otherLeader.id } },
];
const returns = { [location]: { normal: 3, elite: 0 } };
assert.deepEqual(game.battle!.plans, {});
assertNativeWorldBattleInventory(game, afterSetup);
return { initial: setup.initial, afterSetup, beforeFirstMentat, firstMentatStep, afterFirstMentat, shipments,
beforeBattle, battleStep, game, owner, opponent, observer, location, planActions, sources, returns, nativeSteps, staging };
}
export function revealNativeWorldPhaseBattle(fixture: NativeWorldPhaseBattleFixture): Game {
let game = structuredClone(fixture.game);
for (const step of fixture.planActions) {
game = applyAction(game, step.actor, step.action);
game = advanceNativeWorldBattle(game, g => !!g.battle && harassHomeworldClean(g) &&
!g.battle.preparation && g.battle.preLeader?.closed !== false, [], { returns: fixture.returns });
}
assert.ok(game.battle?.revealed); return game;
}
export function settleNativeWorldPhaseBattle(game: Game, fixture: NativeWorldPhaseBattleFixture): Game {
return advanceNativeWorldBattle(game, g => g.decision?.kind === 'faceDance', [],
{ returns: fixture.returns, casualties: { normal: 0, elite: 3 } });
}
export function finishNativeWorldPhaseBattle(game: Game, fixture: NativeWorldPhaseBattleFixture,
steps: NativeWorldBattleStep[] = []): Game {
if (game.decision?.kind === 'faceDance' && !game.response && !game.phaseOpening) {
const step: NativeWorldBattleStep = { actor: game.decision.player, action: { type: 'decision', reveal: false } };
steps.push(step); game = applyAction(game, step.actor, step.action);
}
return advanceNativeWorldBattle(game, nativeWorldBattleFinished, steps,
{ returns: fixture.returns, casualties: { normal: 0, elite: 3 } });
}
/** Original native programmes with unused modules, not invented module benefits.
* The visitor variant retains exact mixed fee custody and optional native
* Cyborg/Face Dance aftercare; own Ix retains its original H prohibition.
*/
export function createNativeWorldPhaseForeignBattleFixture(options: {
advanced?: boolean; typedAftercare?: boolean;
} = {}): NativeWorldBattleFixture {
const advanced = options.advanced ?? true;
return createNativeWorldBattleFixture({ advanced, tech: true, strongholds: advanced,
...(options.typedAftercare ? { exportNormal: 9, exportElite: 1, ownerDial: advanced ? 1 : 2,
harass: false, outcome: 'win' as const, faceDancer: true } : {}) });
}
export function createNativeWorldPhaseOwnBattleFixture(advanced = true): NativeWorldBattleFixture {
return createNativeWorldBattleFixture({ advanced, tech: true, strongholds: advanced, route: 'ix-native' });
}
+15 -4
View File
@@ -88,10 +88,15 @@ export function createNativeWorldRecruitsFixture(options: {
advanced?: boolean;
faction?: NativeWorldRecruitsFaction;
imperialCasualties?: boolean;
tech?: boolean;
strongholds?: boolean;
} = {}): NativeWorldRecruitsFixture {
const native = options.faction ?? 'ixians';
const setup = createIndependentCardModulesFixture({ advanced: options.advanced ?? false,
homeworlds: true, tech: false, strongholds: false, factions: ['emperor', native, 'guild'],
const advanced = options.advanced ?? false;
const tech = options.tech ?? false, strongholds = options.strongholds ?? false;
assert.ok(!strongholds || advanced, 'Original Stronghold Cards require Advanced');
const setup = createIndependentCardModulesFixture({ advanced,
homeworlds: true, tech, strongholds, factions: ['emperor', native, 'guild'],
expansions: native === 'ixians' || native === 'tleilaxu' ? ['ix'] : ['choam'] });
let game = setup.game;
const afterSetup = structuredClone(game), steps = [...setup.steps];
@@ -99,8 +104,12 @@ export function createNativeWorldRecruitsFixture(options: {
assert.ok(setup.initial.homeworlds, 'The undealt ready lobby must have Homeworlds selected');
assert.deepEqual(game.expansions, native === 'ixians' || native === 'tleilaxu' ? ['ix'] : ['choam']);
assert.equal(game.status, 'playing');
for (const [name, value] of [['Tech Tokens', game.techTokens], ['Stronghold Cards', game.strongholdCards],
['Nexus Cards', game.nexusCards], ['Leader Skills', game.leaderSkills],
if (game.strongholdCards) {
assert.equal(game.strongholdCards.claimedTurn, 0);
assert.ok(Object.values(game.strongholdCards.owners).every(owner => owner === null),
'Only original end-Mentat may earn Stronghold Cards');
}
for (const [name, value] of [['Nexus Cards', game.nexusCards], ['Leader Skills', game.leaderSkills],
['Discovery', game.discoveryEnabled]] as const)
assert.equal(value, undefined, `${name} must stay outside a native World Recruits programme`);
const visitor = 'emperor', owner = visitor;
@@ -110,6 +119,8 @@ export function createNativeWorldRecruitsFixture(options: {
assert.ok(home);
const location = home.id;
const staging = ['Return original setup hands to the unopened Treachery deck; no weapon or defense is used.'];
if (tech || strongholds)
staging.push('Select requested unused original Tech/Advanced Strongholds before readiness; native first Storm assigns tokens and actual end-Mentat earns cards. No owner or income receipt is assigned.');
for (const player of game.players) { game.deck.push(...player.hand); player.hand = []; }
for (let i = 0; i < (game.advanced ? 4 : 2); i++) {
const at = game.spiceDeck.findIndex((card, index) => index >= i && 'territory' in card && !card.discovery);
+150
View File
@@ -0,0 +1,150 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, RuleError, viewGame } from '../game/engine';
import { combatArmy } from '../game/combat-location';
import {
advanceNativeWorldBattle, assertNativeWorldBattleInventory, HARASS_WITHDRAW_CARD,
nativeWorldBattleFinished, nativeWorldBattlePlayer as player, nativeWorldBattlePool as pool,
REINFORCEMENTS_CARD, revealNativeWorldBattle, type NativeWorldBattleStep,
} from './fixture-native-world-battle';
import {
createNativeWorldPhaseBattleFixture, createNativeWorldPhaseForeignBattleFixture,
createNativeWorldPhaseOwnBattleFixture, finishNativeWorldPhaseBattle, revealNativeWorldPhaseBattle,
settleNativeWorldPhaseBattle,
} from './fixture-native-world-phase-battle';
for (const scenario of [
{ advanced: true, tech: true, support: 0 },
{ advanced: true, tech: true, support: 1 },
{ advanced: true, tech: true, support: 3 },
{ advanced: true, tech: false, support: 3 },
{ advanced: false, tech: true, support: 0 },
] as const) {
void test(`native Ix ${scenario.advanced ? 'Advanced earned Arrakeen' : 'Basic'} / Tech ${scenario.tech}: actual support ${scenario.support}, native fee, typed Harass`, () => {
const f = createNativeWorldPhaseBattleFixture(scenario);
const ownShipment = f.shipments[0];
assert.equal(ownShipment.step.actor, f.owner);
assert.equal(player(ownShipment.before, f.owner).spice - player(ownShipment.after, f.owner).spice, 6);
assert.deepEqual(combatArmy(ownShipment.after, f.owner, 'arrakeen'), { normal: 3, elite: 3 });
assert.deepEqual(pool(f.game, f.owner, 'homeworld:ixians'), { normal: 7, elite: 1 });
assert.deepEqual(combatArmy(f.game, f.owner, 'arrakeen'), { normal: 3, elite: 3 });
if (scenario.advanced) {
assert.equal(f.beforeFirstMentat.strongholdCards!.owners.arrakeen, null);
assert.deepEqual(combatArmy(f.beforeFirstMentat, f.owner, 'arrakeen'), { normal: 3, elite: 3 });
assert.equal(f.afterFirstMentat.strongholdCards!.owners.arrakeen, f.owner);
assert.equal(f.afterFirstMentat.strongholdCards!.claimedTurn, 1);
assert.equal(viewGame(f.game, f.owner).battle!.strongholdEffects[f.owner], 'arrakeen');
} else assert.equal(viewGame(f.game, f.owner).strongholdCards, null);
assert.deepEqual(f.game.battle!.plans, {}, 'Both original controls start at the unsealed plan boundary');
const before = structuredClone(f.game);
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action, dial: 9.5 }), RuleError,
'R adds score, not physical Suboids/Cyborgs available to dial');
if (scenario.advanced)
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action, support: 4 }), RuleError,
'Only three actual Cyborgs can be supported; neither R +2 nor bank2 adds counters');
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action,
reinforcementsSources: { 'homeworld:guild': { normal: 3, elite: 0 } } }), RuleError);
assert.deepEqual(f.game, before);
const revealed = revealNativeWorldPhaseBattle(f);
assert.equal(player(revealed, f.owner).reserves, 8, 'Sealing/reveal never spends a forecast fee');
assert.equal(player(revealed, f.owner).spice, player(f.game, f.owner).spice,
'Holding Arrakeen never credits two personal spice at the plan boundary');
const settled = settleNativeWorldPhaseBattle(revealed, f);
assert.equal(player(settled, f.owner).spice,
player(f.game, f.owner).spice - Math.max(0, scenario.support - 2));
const done = finishNativeWorldPhaseBattle(settled, f);
assert.equal(done.turn, 2); assert.equal(done.phase, 7);
assert.equal(done.lastBattleContext!.winner, f.owner);
assert.equal(done.lastBattleContext!.result, 'normal');
assert.equal(player(done, f.owner).tanks, 6, 'Exactly native fee3N and the three physically committed Cyborgs');
assert.equal(player(done, f.owner).elites!.tanks, 3, 'No Cyborg is silently used instead of available normal fee counters');
assert.equal(player(done, f.owner).battleLosses, 6);
assert.deepEqual(pool(done, f.owner, 'homeworld:ixians'), { normal: 7, elite: 1 },
'Ix7N1E pays original3N, then all three undialed Suboids return to native Ix');
assert.equal(player(done, f.owner).reserves, 8);
assert.deepEqual(combatArmy(done, f.owner, 'arrakeen'), { normal: 0, elite: 0 });
assert.equal(player(done, f.owner).forces['hidden_mobile_stronghold:0'], 6);
assert.equal(player(done, f.owner).elites!.forces['hidden_mobile_stronghold:0'], 3);
assert.equal(player(done, f.opponent).tanks, 3);
for (const card of [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD])
assert.equal(done.discard.filter(face => face.id === card).length, 1);
if (scenario.tech) {
assert.equal(f.game.techTokens!.heighliners.spice, 0, 'Original phase-end shipment income has already settled');
assert.ok(done.techTokens, 'Original winner reward/cleanup preserves the actual Tech module');
} else assert.equal(done.techTokens, undefined);
assertNativeWorldBattleInventory(done, f.afterSetup);
});
}
for (const advanced of [false, true]) {
void test(`${advanced ? 'Advanced Tech+Strongholds' : 'Basic Tech'} foreign World: exact original mixed fee cannot borrow future Harass returns`, () => {
const f = createNativeWorldPhaseForeignBattleFixture({ advanced });
assert.ok(f.game.techTokens);
assert.equal(viewGame(f.game, f.owner).battle!.strongholdEffects[f.owner], null,
'A Stronghold Card never grants its Arrakis-local benefit on a foreign World');
assert.deepEqual(pool(f.game, f.owner, 'homeworld:ixians'), { normal: 1, elite: 4 });
assert.deepEqual(f.sources, { 'homeworld:ixians': { normal: 1, elite: 2 } });
const before = structuredClone(f.game);
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action,
reinforcementsSources: { 'homeworld:ixians': { normal: 3, elite: 0 } } }), RuleError);
assert.deepEqual(f.game, before);
const done = advanceNativeWorldBattle(revealNativeWorldBattle(f), nativeWorldBattleFinished, [], { returns: f.returns });
assert.equal(done.lastBattleContext!.winner, f.opponent);
assert.deepEqual(pool(done, f.owner, 'homeworld:ixians'), { normal: 7, elite: 2 });
assert.equal(player(done, f.owner).tanks, 5);
assert.equal(player(done, f.owner).elites!.tanks, 2,
'The future seven normal returns cannot repay the original two-Cyborg fee');
assert.deepEqual(combatArmy(done, f.owner, f.location), { normal: 0, elite: 0 });
assertNativeWorldBattleInventory(done, f.afterSetup);
});
void test(`${advanced ? 'Advanced Tech+Strongholds' : 'Basic Tech'} own Ix: Harass stays forbidden and local native fee is disjoint from dial`, () => {
const f = createNativeWorldPhaseOwnBattleFixture(advanced), before = structuredClone(f.game);
assert.deepEqual(pool(f.game, f.owner, f.location), { normal: 1, elite: 4 });
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action, defense: HARASS_WITHDRAW_CARD }), RuleError);
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action, dial: 4.5 }), RuleError,
'After local1N2E fee, only two real Cyborgs remain eligible for the dial');
assert.deepEqual(f.game, before);
const revealed = revealNativeWorldBattle(f), forces = viewGame(revealed, f.owner).battle!.ownForces!;
assert.equal(forces.normal, 0); assert.equal(forces.elite, 2);
assert.equal(forces.normalFixedHalf, true); assert.equal(forces.eliteStrength, 2);
assert.equal(forces.freeSupport, !advanced);
const done = advanceNativeWorldBattle(revealed, nativeWorldBattleFinished);
assert.equal(done.lastBattleContext!.winner, f.owner);
assert.equal(player(done, f.owner).tanks, advanced ? 5 : 4);
assert.equal(player(done, f.owner).elites!.tanks, advanced ? 4 : 3);
assert.ok(player(done, f.owner).hand.some(card => card.id === HARASS_WITHDRAW_CARD));
assertNativeWorldBattleInventory(done, f.afterSetup);
});
}
void test('Advanced original phase modules: native board-only Cyborg substitution resumes real Face Dance without recovering reserve fees', () => {
const f = createNativeWorldPhaseForeignBattleFixture({ typedAftercare: true });
const steps: NativeWorldBattleStep[] = [];
let game = advanceNativeWorldBattle(revealNativeWorldBattle(f), g => g.decision?.kind === 'ixSubstitution', steps,
{ casualties: { normal: 0, elite: 1 } });
assert.equal(player(game, f.owner).tanks, 4);
assert.equal(player(game, f.owner).elites!.tanks, 3);
assert.deepEqual(combatArmy(game, f.owner, f.location), { normal: 9, elite: 0 });
const before = structuredClone(game);
assert.throws(() => applyAction(game, f.owner, { type: 'decision', sources: { [f.location]: 3 },
recover: { [f.location]: 3 } }), RuleError, 'Two native fee Cyborgs are not board casualty origins');
assert.deepEqual(game, before);
game = applyAction(game, f.owner, { type: 'decision', sources: { [f.location]: 1 }, recover: { [f.location]: 1 } });
game = advanceNativeWorldBattle(game, g => g.decision?.kind === 'faceDance', steps);
assert.deepEqual(combatArmy(game, f.owner, f.location), { normal: 8, elite: 1 });
assert.equal(player(game, f.owner).elites!.tanks, 2);
assert.equal(game.decision?.player, f.opponent);
assert.equal(game.decision?.kind === 'faceDance' && game.decision.territory, f.location);
assert.equal(viewGame(game, f.opponent).decision?.kind, 'faceDance');
game = applyAction(game, f.opponent, { type: 'decision', reveal: true, sources: {} });
game = advanceNativeWorldBattle(game, nativeWorldBattleFinished, steps);
assert.deepEqual(combatArmy(game, f.owner, f.location), { normal: 0, elite: 0 });
assert.deepEqual(pool(game, f.owner, 'homeworld:ixians'), { normal: 8, elite: 2 });
assert.equal(player(game, f.owner).tanks, 4); assert.equal(player(game, f.owner).elites!.tanks, 2);
assert.equal(player(game, f.owner).leaders.find(l => l.id === f.planActions[0].action.leader)!.dead, true);
assert.ok(steps.some(step => step.action.type === 'decision' && step.action.choice !== undefined),
'The genuine native typed casualty decision precedes the separate substitution and Face Dance queues');
assertNativeWorldBattleInventory(game, f.afterSetup);
});
+51
View File
@@ -0,0 +1,51 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, RuleError, viewGame } from '../game/engine';
import { createIndependentCardModulesFixture } from './fixture-independent-card-modules';
import {
createRicheseReinforcementsFixture, finishRicheseReinforcementsBattle,
prepareRicheseReinforcementsPlan, RICHESE_REINFORCEMENTS,
richeseReinforcementsPlayer, sealRicheseReinforcementsPlan,
} from './fixture-richese-reinforcements';
function markerFixture(advanced: boolean) {
const setup = createIndependentCardModulesFixture({ advanced, homeworlds: true,
tech: true, strongholds: advanced, expansions: ['choam'], factions: ['richese', 'guild', 'atreides'] });
return createRicheseReinforcementsFixture({ advanced, family: 'choam', marker: 3,
...(advanced ? {} : { reserves: 5 }), initial: setup.initial });
}
void test('Basic native World Tech income cannot fund a marker fee from future withdrawal counters', () => {
const f = markerFixture(false);
const before = structuredClone(f.game);
const preview = viewGame(f.game, f.richese).battle!.reinforcements!;
assert.deepEqual(preview.noField, { materializes: 3, reservesAfterReveal: 2 });
assert.ok(preview.blocked);
const raw = { ...f.plans.reinforcements.action };
delete raw.reinforcementsSources;
assert.throws(() => applyAction(f.game, f.richese, raw), RuleError);
assert.deepEqual(f.game, before);
const first = prepareRicheseReinforcementsPlan(f.game, f.plans.harass);
const done = finishRicheseReinforcementsBattle(sealRicheseReinforcementsPlan(first, f.plans.opponent));
const owner = richeseReinforcementsPlayer(done, f.richese);
assert.equal(owner.reserves, 3);
assert.equal(owner.tanks, 2);
assert.ok(owner.hand.some(card => card.id === RICHESE_REINFORCEMENTS));
});
void test('Advanced native World Tech and Strongholds keep marker materialization and the later reserve fee disjoint', () => {
const f = markerFixture(true);
const first = prepareRicheseReinforcementsPlan(f.game, f.plans.reinforcements);
assert.equal(richeseReinforcementsPlayer(first, f.richese).reserves, 20);
assert.ok(richeseReinforcementsPlayer(first, f.richese).noField!.deployed);
const revealed = sealRicheseReinforcementsPlan(first, f.plans.opponent);
assert.equal(richeseReinforcementsPlayer(revealed, f.richese).reserves, 17);
assert.equal(richeseReinforcementsPlayer(revealed, f.richese).tanks, 0);
const done = finishRicheseReinforcementsBattle(revealed);
const owner = richeseReinforcementsPlayer(done, f.richese);
assert.equal(owner.reserves, 14);
assert.equal(owner.tanks, 6);
const home = viewGame(done, f.richese).homeworlds?.worlds?.find(world => world.native === f.richese);
assert.deepEqual(home?.forces[f.richese], { normal: 14, elite: 0 });
assert.equal(done.discard.filter(card => card.id === RICHESE_REINFORCEMENTS).length, 1);
});
+176
View File
@@ -0,0 +1,176 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, RuleError, viewGame, type Action, type Game } from '../game/engine';
import { ownedTech } from '../game/tech-tokens';
import { RECRUITS_CARD_ID } from '../game/recruits';
import {
advanceNativeWorldRecruits, assertNativeWorldRecruitsInventory, createNativeWorldRecruitsFixture,
nativeWorldRecruitsClean, nativeWorldRecruitsPlayer as player, nextNativeWorldRecruitsStep,
requestNativeWorldRecruits, type NativeWorldRecruitsStep,
} from './fixture-native-world-recruits';
const play = { type: 'card', card: RECRUITS_CARD_ID } as const;
const wallets = (game: Game) => Object.fromEntries(game.players.map(p => [p.id, p.spice]));
function rejectsUnchanged(game: Game, actor: string, action: Action): void {
const before = JSON.stringify(game);
assert.throws(() => applyAction(game, actor, action), RuleError);
assert.equal(JSON.stringify(game), before);
}
function axlotlProgramme(start: Game, native: string) {
const token = start.techTokens!.axlotl;
const owner = token.owner;
assert.ok(owner, 'The actual first Storm must have assigned Axlotl');
assert.notEqual(owner, native, 'The invader took any original native token in the real battle');
assert.equal(token.spice, 0);
assert.notEqual(token.triggeredTurn, start.turn);
assert.ok(Object.values(start.techTokens!).every(t => t.owner !== null));
const income = ownedTech(start.techTokens, owner).length;
assert.ok(income > 0);
const ownerWallet = player(start, owner).spice;
const accrued = (game: Game) => {
assert.equal(game.techTokens!.axlotl.owner, owner);
assert.equal(game.techTokens!.axlotl.triggeredTurn, start.turn);
assert.equal(game.techTokens!.axlotl.spice, income,
'Further eligible free requests must not accrue another original phase payment');
assert.equal(player(game, owner).spice, ownerWallet,
'Accrued Axlotl income is not spendable personal spice before the phase ends');
assertNativeWorldRecruitsInventory(game);
};
const finish = (game: Game) => {
accrued(game);
const bribes = viewGame(game, owner).bribeOptions;
assert.equal(bribes.blocked, null);
assert.equal(bribes.available, ownerWallet);
rejectsUnchanged(game, owner, { type: 'bribe', target: native, amount: ownerWallet + income });
let beforePhaseEnd: Game | undefined;
let phaseEndStep: NativeWorldRecruitsStep | undefined;
for (let count = 0; count < 100 && game.phase === 4; count++) {
const step = nextNativeWorldRecruitsStep(game);
assert.ok(step);
const before = structuredClone(game);
game = applyAction(game, step.actor, step.action);
if (game.phase !== 4) { beforePhaseEnd = before; phaseEndStep = step; }
else accrued(game);
}
assert.ok(beforePhaseEnd && phaseEndStep, 'The actual final ready must end Revival');
accrued(beforePhaseEnd);
assert.equal(game.turn, 2);
assert.equal(game.phase, 5);
const afterPhaseEnd = structuredClone(game);
const expectedWallets = wallets(beforePhaseEnd);
expectedWallets[owner] += income;
assert.deepEqual(wallets(afterPhaseEnd), expectedWallets);
assert.equal(afterPhaseEnd.techTokens!.axlotl.spice, 0);
assert.equal(afterPhaseEnd.techTokens!.axlotl.triggeredTurn, 2);
assert.equal(player(afterPhaseEnd, native).revived, player(beforePhaseEnd, native).revived);
assert.equal(player(afterPhaseEnd, native).freeForcesRevived,
player(beforePhaseEnd, native).freeForcesRevived);
const shipmentSteps: { actor: string; action: Action }[] = [];
const afterShipment = advanceNativeWorldRecruits(afterPhaseEnd,
g => g.turn === 2 && g.phase === 6 && nativeWorldRecruitsClean(g), shipmentSteps);
assert.ok(shipmentSteps.some(step => step.action.type === 'endMovement'),
'Finish the actual next Shipment, not just its opening boundary');
assert.deepEqual(wallets(afterShipment), expectedWallets,
'Original Shipment continuation cannot pay Axlotl a second time');
assert.equal(afterShipment.techTokens!.axlotl.spice, 0);
assert.equal(afterShipment.techTokens!.axlotl.triggeredTurn, 2);
for (const boundary of [beforePhaseEnd, afterPhaseEnd, afterShipment])
assertNativeWorldRecruitsInventory(boundary);
return { beforePhaseEnd, phaseEndStep, afterPhaseEnd, afterShipment };
};
return { owner, income, accrued, finish };
}
for (const options of [
{ name: 'Basic native Ix + Tech', advanced: false, strongholds: false },
{ name: 'Advanced native Ix + Tech + original Strongholds', advanced: true, strongholds: true },
]) {
void test(`${options.name}: current typed rates, paid ledger and delayed once-only Axlotl survive Recruits`, () => {
const f = createNativeWorldRecruitsFixture({ ...options, tech: true, faction: 'ixians' });
const native = f.native;
const programme = axlotlProgramme(f.game, native);
assert.deepEqual({ free: viewGame(f.game, native).revival.freeRemaining,
bonus: viewGame(f.game, native).revival.homeworldBonus }, { free: 2, bonus: 1 });
const paid = requestNativeWorldRecruits(f.game, native, 3, 1, 0);
assert.equal(paid.price, 3, 'Two free suboids leave the original three-spice paid cyborg');
assert.equal(paid.walletDelta, 3);
assert.equal(paid.free, 2);
assert.deepEqual(paid.nativeAfter, { normal: 2, elite: 1 });
programme.accrued(paid.game);
let game = applyAction(paid.game, native, { type: 'ready' });
assert.ok(game.ready.includes(native));
const beforePlayWallets = wallets(game);
game = applyAction(game, f.owner, play);
assert.deepEqual(wallets(game), beforePlayWallets, 'Reopening never refunds the prior paid cyborg');
assert.equal(player(game, native).revived, 3);
assert.equal(player(game, native).freeForcesRevived, 2);
assert.equal(viewGame(game, native).revival.freeRemaining, 2);
const crossing = requestNativeWorldRecruits(game, native, 2, 2, 2);
assert.equal(crossing.price, 0);
assert.equal(crossing.walletDelta, 0);
assert.equal(crossing.free, 2);
assert.deepEqual(crossing.nativeAfter, { normal: 2, elite: 3 });
programme.accrued(crossing.game);
const rate = viewGame(crossing.game, native).revival;
assert.equal(rate.homeworldBonus, 0);
assert.equal(rate.freeRemaining, 0, 'A separate request reads the current population-five World');
assert.equal(rate.eliteRemaining, 1, 'The native multi-cyborg allowance remains in both modes');
rejectsUnchanged(crossing.game, native, { type: 'revive', amount: 2, elite: 2 });
const last = requestNativeWorldRecruits(crossing.game, native, 2, 1, 0);
assert.equal(last.price, 5);
assert.equal(last.walletDelta, 5);
assert.equal(last.free, 0);
assert.deepEqual(last.nativeAfter, { normal: 3, elite: 4 });
assert.equal(player(last.game, native).spice, player(f.game, native).spice - 8);
assert.equal(player(last.game, native).revived, 7);
assert.equal(player(last.game, native).freeForcesRevived, 4);
assert.equal(last.game.discard.filter(card => card.id === RECRUITS_CARD_ID).length, 1);
rejectsUnchanged(last.game, native, { type: 'revive', amount: 1, elite: 0 });
programme.accrued(last.game);
const completed = programme.finish(last.game);
assert.equal(player(completed.afterPhaseEnd, native).spice, player(f.game, native).spice - 8);
if (options.strongholds) {
assert.equal(f.afterSetup.strongholdCards!.claimedTurn, 0);
assert.ok(Object.values(f.afterSetup.strongholdCards!.owners).every(owner => owner === null));
assert.equal(f.game.strongholdCards!.claimedTurn, 1, 'Actual first end-Mentat earns original custody');
assert.deepEqual(completed.afterShipment.strongholdCards, f.game.strongholdCards,
'No held card is fabricated or reassigned by native Revival or the following Shipment');
} else assert.equal(f.game.strongholdCards, undefined);
});
}
void test('Advanced native CHOAM + Tech: unlimited paid/free returns retain their ledgers and one delayed Axlotl payout', () => {
const f = createNativeWorldRecruitsFixture({ advanced: true, faction: 'choam', tech: true });
const native = f.native;
const programme = axlotlProgramme(f.game, native);
const first = requestNativeWorldRecruits(f.game, native, 8);
assert.equal(first.price, 7);
assert.equal(first.walletDelta, 7);
assert.equal(first.free, 1);
programme.accrued(first.game);
const beforePlayWallets = wallets(first.game);
let game = applyAction(first.game, f.owner, play);
assert.deepEqual(wallets(game), beforePlayWallets, 'Seven paid returns stay paid when Recruits doubles free use');
assert.equal(viewGame(game, native).revival.limit, 20, 'Recruits never reduces native unlimited to seven');
assert.equal(viewGame(game, native).revival.forcesRemaining, 12);
assert.equal(viewGame(game, native).revival.freeRemaining, 1);
const free = requestNativeWorldRecruits(game, native, 1);
assert.equal(free.price, 0);
assert.equal(free.walletDelta, 0);
assert.equal(free.free, 1);
programme.accrued(free.game);
const paid = requestNativeWorldRecruits(free.game, native, 3);
assert.equal(paid.price, 3, 'Native CHOAM keeps its original one-spice price after free use');
assert.equal(paid.walletDelta, 3);
assert.equal(paid.free, 0);
game = paid.game;
assert.deepEqual(paid.nativeAfter, { normal: 12, elite: 0 });
assert.equal(player(game, native).revived, 12);
assert.equal(player(game, native).freeForcesRevived, 2);
assert.equal(player(game, native).spice, player(f.game, native).spice - 10);
assert.equal(viewGame(game, native).revival.forcesRemaining, 8);
assert.equal(viewGame(game, native).revival.homeworldBonus, 0);
programme.accrued(game);
const completed = programme.finish(game);
assert.equal(player(completed.afterPhaseEnd, native).spice, player(f.game, native).spice - 10);
});
+1 -1
View File
@@ -37,7 +37,7 @@ async function main() {
'\n--profile homeworld-occupation --leader-skills composes original all14 Leader Skills with the classic2–6-seat occupied Homeworld income preview. Native setup, reserve custody, occupation history and owner allocation remain original. Bureaucrat diverts two from gross before the occupied percentage split while preserving the full original payer shares. Nexus Cards, expansions, Tech, Strongholds, Discovery, other overlays and conversion of played games remain excluded.',
'\n--profile guild-betrayal --leader-skills composes the bounded classic native-Guild Nexus preview with original all14 skills. Original payment/physical shipment and once-per-phase Bureaucrat receipt remain; ordinary Skills use their shared quotes. Homeworlds, expansions, Tech, Strongholds, Discovery, Semuta, other overlays and conversion of played games remain excluded.',
'\n--profile ecaz-treachery --homeworlds selects independent36-card classic2–6 Basic/Advanced Homeworld setup, retaining original unused Tech3+ and/or Advanced Strongholds2+; both needs Advanced3+. Native custody, first-Storm Tech and earned end-Mentat cards remain. Recruits doubles current native rates within seven total, provisionally rechecking separate-request thresholds/prior free use and original Axlotl timing. Harass returns exact native counters, never on an own World. Reinforcements seals exact normal-first sources when ambiguous, pays before Harass and excludes an on-World fee from physical dial/casualties while preserving original face/support. Earned bank subsidy funds only real support. No expansions, Skills, Nexus, Discovery, other overlays, public start or played-save conversion.',
'\nThe same --profile ecaz-treachery --homeworlds entry admits clean selected Ix and/or CHOAM native/base Basic/Advanced2–6 rosters without other modules, using original38/50 plus separate Richese cache when seated. Native HMS/Cyborgs, Face Dancers, Auditor and concealed markers remain original. Native rates/unlimited caps and exact fee/return custody stay; own-World Harass is forbidden. Native Richese must leave three actual World reserves after No-Field materialization before R seals. No Tech/Strongholds/Skills/Nexus/Discovery/Sandtrout, E3, other previews, public starts or played-game conversion opens for these native World profiles.',
'\nThe same --profile ecaz-treachery --homeworlds entry admits clean selected Ix and/or CHOAM native/base Basic/Advanced2–6 rosters, using original38/50 plus separate Richese cache when seated, with original unused Tech3+ and/or Advanced Strongholds2+; both needs Advanced3+. Native HMS/Cyborgs, Face Dancers, Auditor and concealed markers remain original. Native rates/unlimited caps and exact fee/return custody stay; own-World Harass is forbidden. Native Richese must leave three actual World reserves after No-Field materialization before R seals. First-Storm token ownership, delayed phase income and earned end-Mentat cards retain original mechanics. Skills/Nexus/Discovery/Sandtrout, E3, other previews, public starts and played-game conversion remain gated.',
'\nWithout Homeworlds, ecaz-treachery supports clean classic or selected Ix and/or CHOAM native/base Basic/Advanced rosters with original unused Tech3+ and/or Advanced Strongholds2+; both requires Advanced3+. Original selected33/35/47 decks gain the three independent cards; Richese cache stays separate. First-Storm Tech owners/income and actual end-Mentat Stronghold ownership remain original. Recruits keeps original Axlotl triggers; Reinforcements/Harass keep physical costs/returns and earned held-card effects. Original Stone/Mirror allocation/profile guards stay. Nexus, Skills, Discovery, Sandtrout, mixed E3 and unrelated overlays remain outside this entry.',
'\nSelected Ix/CHOAM original Stone and physical Mirror now also retain original Tech/Advanced Strongholds, in ordinary47 or independently selected50-card setups with a separate cache. Earned Habbanya decides physical ties; original Stone-before-Tooth remains provisional, printed copy-first and user-selected physical Mirror retention stay. Public winner-changing allocations, mixed E3 phase modules and other overlay/public-start guards remain.',
'\nEcaz-occupy (--profile ecaz-occupy) opts a fresh ready2–6 native Ecaz/classic or selected Advanced native-family lobby. Basic uses exact ecaz33 and provisionally follows the main paragraph for odd/even counts: ceil contribution/loss and floor survivors; the conflicting FAQ is recorded, not adjudicated. Advanced retains original selected ecaz/ix/choam decks and native offers/HMS/Auditor/Face Dancers/cache. Explicit --ecaz-treachery independently adds three physical cards in Advanced. Native Richese ordinary, marker-only and mixed pools compose with selected leads under the provisional additive interpretation. Ordinary dial Prescience remains barred for the physical No-Field army; distinct Advanced special Karama uses the explicit complete private-plan interpretation without early token revelation. Other profile/module and public-start gates remain.' +