From f50dda607809e6f977b4f534f105a24c388a7ffd Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:00:45 +1100 Subject: [PATCH] Admit clean Ix Stone and Mirror with existing revealed priorities --- components/game-table.tsx | 16 ++++ docs/CURRENT_STATUS.md | 55 +++++------ docs/ECAZ_OCCUPY_RULES.md | 5 +- docs/HARASS_WITHDRAW_RUNTIME.md | 5 +- docs/IMPLEMENTATION_STATUS.md | 32 +++++++ docs/MIRROR_WEAPON_ENGINE_AUDIT.md | 64 +++++++++++++ docs/RULE_DECISIONS.md | 3 +- docs/STONE_BURNER_ENGINE_AUDIT.md | 20 +++- game/card-presentation.ts | 4 +- game/engine.ts | 5 +- game/mirror-weapon-mode.ts | 9 +- game/reference.ts | 29 +++++- tests/fixture-ix-revealed-weapons.ts | 65 +++++++++++++ tests/fixture-stone-harass.ts | 15 ++- tests/ix-revealed-weapons.test.ts | 131 +++++++++++++++++++++++++++ tests/stone-burner-review.test.ts | 7 +- 16 files changed, 411 insertions(+), 54 deletions(-) create mode 100644 tests/fixture-ix-revealed-weapons.ts create mode 100644 tests/ix-revealed-weapons.test.ts diff --git a/components/game-table.tsx b/components/game-table.tsx index 85f5b011..07042ef5 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -3096,6 +3096,11 @@ export function GameTable({ tokens or change Stone Burner’s comparison.

)} + {g.expansions.includes('ix') &&

+ Provisional original Stone/Tooth order: Stone chooses first, + then uncopied Tooth. A Mirror copy still chooses before its + original, as printed. Traitor decisions follow all choices. +

} Read Stone Burner rules @@ -3135,6 +3140,11 @@ export function GameTable({ leaders, including yours. Chemistry as a defense protects; Snoopers do not. Traitor decisions follow.

+ {g.expansions.includes('ix') &&

+ A Mirror copy chooses before the original Tooth. + With uncopied Stone and Tooth, this prototype provisionally + chooses Stone first. Both answers precede Traitor decisions. +

} {actionButton('Activate Poison Tooth', { type: 'decision', event: g.decision.event, @@ -5368,6 +5378,12 @@ export function GameTable({ Discard it after this battle, even if you win.

)} + {g.expansions.includes('ix') && + (selectedSpecialWeapon || selectedBattleWeapon?.kind === 'poisonTooth') &&

+ Provisional original Stone/Tooth order: Stone chooses + first, then uncopied Tooth. Mirror copies still choose + before their originals, as printed. +

} {selectedSpecialWeapon && (

diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 2be17c7c..3b36b3d3 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -29,38 +29,41 @@ optional skill bands, combinations and public expansion starts remain open. Rules implementation continues; comprehensive assurance and administration remain deferred until all rules are implemented. -**Independent Ecaz battle-card interaction wave — 16 October.** -- Richese-family Harass/Stone uses visibly provisional post-withdrawal counts, - including own/opposing/copied Stone and Traitor-canceled returns. -- Same-plan Harass/Reinforcements pays the original typed reserve quote before - any return; future returns cannot fund or change it. -- In the existing native Advanced Occupy envelope, Reinforcements composes - with original or Mirror-copied Stone. Its +2 normal score adds no physical - undialed tokens, but the original own-reserve cost still pays. +**Selected Richese weapon/card interaction wave — 16 October.** +- Clean selected CHOAM/Ix profiles now admit Stone/Mirror with native + Ixians/Tleilaxu in Basic/Advanced; Ecaz/Moritani stays Advanced-only. + Original Stone-first then uncopied Tooth reuses existing CHOAM priority + under explicitly provisional choose-recommended interpretation. Printed + Mirror copy-first and the user's physical-Mirror retention stay independent. +- Richese-family Harass/Stone uses provisional post-withdrawal counts. + Same-plan Harass/Reinforcements pays its original reserve quote before returns. + Native Advanced Occupy Reinforcements/Stone adds no physical +2 tokens. -Original fixed Ecaz commitment, variable ally casualties, support, Traitor -precedence and mandatory utility-card disposal stay separate. The earlier -questions remain unanswered; explicit product interpretations are not publisher -rulings. No state field, new profile or public start is added. +Original fixed Ecaz commitment, native variable counters, support, Traitor +precedence, reserve cost and physical-card disposal/retention remain native. +Earlier questions remain unanswered; no new publisher ruling, state field, +public start or played-game conversion is implied. New Ix Tech/Stronghold, +other module bands and winner-changing allocation boundaries remain guarded. -Latest affected sixteen-file167/167, types/lint and build pass. Eight fresh -Node programs/all four profiles cover both leads/original or Mirror attacks -with36 accepted continuation actions. The corrected390px native-control journey -starts within hand capacity: physical5-vs4 wins, Ecaz reserves15→12/Tanks6, -Guild Tanks1, original support9→8, Ecaz2/Guild3 remain. Reinforcements discards -once; Stone retention clears battle/decision into collection, without overflow -or browser/application error. An overfull initial fixture was corrected by -returning unselected cards to the conserved deck; that initial hand is not -legal-hand evidence. +Latest affected thirteen-file99/99, TypeScript, lint and build pass. Eight fresh +Node native programs/all four policies accept42 continuation actions; two +original47/10-cache programs also complete without independent Ecaz cards. +Two real390px control journeys prove Advanced copied Tooth activation versus +unused original (both deaths, actual cyborg losses, physical Mirror kept, +spice34) and Basic Stone-before-Tooth (four-vs-zero, army/Stone kept, spice39). +Both clear battle/decision into the original Ix Collection opening, with legal +hands and no overflow/browser/application error. These use genuine setup with +explicit conserved battle staging, not unstaged complete-game acceptance. -Current parse/composition anchor and23 topic paths resolve; all ten relevant -facets remain Partial. Exact policies and earlier bounded human/policy proof: +Actual generated guide import, policy anchor and23 Stone/Mirror evidence paths +resolve; all ten facets stay Partial. Exact policies/earlier bounded proof: +[clean Ix](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-composition--16-october-2026), [Harass/Stone](HARASS_WITHDRAW_RUNTIME.md#provisional-stone-burner-timing--16-october-2026), [Harass/Reinforcements](REINFORCEMENTS_RUNTIME.md#combined-utility-plan--16-october-2026), [Reinforcements/Stone](REINFORCEMENTS_RUNTIME.md#native-stone-composition--16-october-2026). -Ix/Poison Tooth, own concealed No-Field Harass and other profile/module limits -remain. No human server, saved room, D1, deployment or new assurance campaign -changed; final assurance follows all rules. +Own concealed No-Field Harass and other profile/module limits remain. No human +server, saved room, D1, deployment or new assurance campaign changed; final +assurance follows all rules. **Ixian Nexus purchased-card interaction wave — 16 October.** Purchased-card replacement now composes: diff --git a/docs/ECAZ_OCCUPY_RULES.md b/docs/ECAZ_OCCUPY_RULES.md index c7f1d3f1..c6960e9e 100644 --- a/docs/ECAZ_OCCUPY_RULES.md +++ b/docs/ECAZ_OCCUPY_RULES.md @@ -303,8 +303,9 @@ active Occupy, or the **selected lead's own pool only** after prevention. Its total dial offset is removed before native typed casualty enumeration. Mirror copies use the same comparison and original physical-card retention. Public opposing No-Field preflight enumerates possible native pools, never the -actual secret denomination. Ix Stone/Poison Tooth timing and genuinely -winner-changing typed allocations retain their existing guards. +actual secret denomination. Clean selected CHOAM/Ix now uses the +[existing provisional original/copy priority](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-composition--16-october-2026); +winner-changing allocations and Ix combined modules retain their guards. Harass/Stone now uses the explicitly [provisional post-withdrawal policy](HARASS_WITHDRAW_RUNTIME.md#provisional-stone-burner-timing--16-october-2026): remove only the actual card user's undialed pool, never recompute original diff --git a/docs/HARASS_WITHDRAW_RUNTIME.md b/docs/HARASS_WITHDRAW_RUNTIME.md index 6ef3c9f3..b27589b9 100644 --- a/docs/HARASS_WITHDRAW_RUNTIME.md +++ b/docs/HARASS_WITHDRAW_RUNTIME.md @@ -101,8 +101,9 @@ This is a legal strategy path, not strength calibration. battles are already implemented for this card. - 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 and separate Ix/Poison Tooth timing - boundary remain. No public expansion start or played-game retrofit is added. + The ordinary invariant-allocation guard remains. Clean selected Ix follows + the [separate priority contract](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-composition--16-october-2026); + other profile/module limits, public starts and played-game retrofit stay gated. A card user's own still-concealed No-Field remains guarded until its original reveal; this wave does not certify Harass on a concealed native marker. - Co-present armies remain guarded in Basic or without the supported Advanced diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 0775b3ce..36ba8a28 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,37 @@ # Dune implementation status +## 16 October 2026 — Clean selected Ix Stone and physical Mirror + +Replaced the blanket Ix guard only for clean selected CHOAM/Ix profiles, +including native Ixians/Tleilaxu Basic/Advanced and Advanced Ecaz combinations. +Reuse existing CHOAM original Stone-first/uncopied-Tooth priority; standing +choose-recommended selects it provisionally while the earlier question remains +unanswered. Printed Mirror copy-first and the user's28 September physical +retention are unchanged. Public typed-allocation and new Ix Tech/Stronghold/ +other module boundaries stay; no state field or public start added. + +Thirteen-file99/99, TypeScript, lint and build pass with the existing +module-register/large-chunk/Unknown-route warnings. Natural Storm can change +tie priority: the native Occupy fixture now picks a public-safe original Stone +commitment, rather than relying on an already sealed zero dial or weakening +the guard. Six new cases include fixed/variable reserve-cost composition. + +Eight fresh Node native programs/all four policies accept42 continuation actions. +Two original47/10-cache programs additionally complete without independent Ecaz +cards. Two actual390px journeys pass: Advanced copied Tooth activates before +the unused original, kills both leaders once, loses two actual cyborgs and keeps +winning physical Mirror at spice34; Basic mixed Ix Stone chooses kill before +unused Tooth, wins physical4-vs0, retains army/Stone and reaches spice39. +Both original battles/decisions clear into the unchanged Ix Collection opening. +No overflow/browser/application error or over-capacity hand. These are original +native setups with explicit conserved battle staging, not unstaged full games. + +Actual generated guide import/policy anchor and23 topic evidence paths resolve; +all ten facets stay Partial. Obsolete blanket-Ix rejection removed, not re-pinned. +No human server, D1, saved room, deployment/public start or broad/HTTP/recovery +assurance campaign changed. Owned hosts/scripts removed. +[Source, profile and scoped proof](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-composition--16-october-2026). + ## 16 October 2026 — Native Occupy Stone and Reinforcements Removed the same-plan/opposing Stone rejection while keeping existing mode diff --git a/docs/MIRROR_WEAPON_ENGINE_AUDIT.md b/docs/MIRROR_WEAPON_ENGINE_AUDIT.md index 168f8b6e..2938870f 100644 --- a/docs/MIRROR_WEAPON_ENGINE_AUDIT.md +++ b/docs/MIRROR_WEAPON_ENGINE_AUDIT.md @@ -4,6 +4,70 @@ Audited 6 September 2026 as a proposed contract. The 28 September bounded runtim **28 September ruling and runtime:** the user selected ordinary winner retention for the physical Mirror even when its copy activates Tooth or Artillery. This is an explicit product interpretation, not a located GF9 clarification. `game/effective-weapons.ts` keeps physical identity apart from copied attacks; the engine now connects copied Tooth/Stone choices, Stone comparison, custody, battle controls and legal AI in the bounded configuration. +## Clean selected Ix composition — 16 October 2026 + +Clean CHOAM/Ix profiles now admit original Stone and physical Mirror with +native Ixians/Tleilaxu and classics in Basic or Advanced. Existing native +Ecaz/Moritani combinations remain Advanced-only. This preserves the original +selected47-card deck plus separate Richese10; choosing the independent Ecaz +cards instead adds its genuine three cards for50, never a trimmed substitute. +Fresh/public start gates and actual physical card ownership remain unchanged. + +**Existing policy, not a newly invented queue:** CHOAM's ordinary deck already +contains Tooth without Ix. Before this cutover, an actual original CHOAM setup +proved uncopied Stone chooses first, then Tooth. The standing 14 October +choose-recommended instruction extends that existing prototype policy to clean +Ix. The earlier priority question remains unanswered; Stone-before-Tooth is +visibly provisional, not a new publisher ruling or a user answer. Mirror +copy-first is separately printed on its physical face and remains unchanged. + +The public profile and physical allocation guards never inspect an opposing +hand or sealed plan. Winner-changing typed allocation remains blocked, even +when the actual hidden plan would happen to make it finishable. New clean-Ix +admission excludes Homeworlds, Nexus, Leader Skills, Discoveries, active +Sandtrout, Tech Tokens and Stronghold Cards. The existing non-Ix Tech/Stronghold +band is intentionally unchanged. Other combinations remain separate. + +Physical Mirror retention still follows the user's28 September interpretation; +the original Tooth/Artillery retain their actual mandatory-discard rules. +Original fixed Ecaz commitment, native variable counters and same-plan reserve +fees stay separate in Advanced Occupy. +### Scoped clean-Ix proof + +The affected thirteen-file offline union passes **99/99**. TypeScript, lint and +production build pass; build retains module-register, large-chunk and +Unknown-route warnings. Six new behavioral cases cover both natives/modes/ +physical slots, independent copied answers, original priority, retention, +public allocation ambiguity and native fixed/variable Occupy reserve cost. +Natural Storm tie priority required a public-safe original Stone fixture plan; +the allocation guard was preserved, not bypassed using the sealed zero dial. + +Eight fresh Node programs/all four profiles use genuine native setup with +explicit conserved rule staging and42 accepted continuation actions. Two +additional fresh original47-card/10-cache programs, without the independent +Ecaz variant, complete copied Stone in Basic and Advanced. These are native +setup plus staged battle evidence, not unstaged complete-game certification. + +Two actual390px journeys start before commitment with legal hands: +- Advanced Mirror activates copied Tooth first; original Tooth independently + stays unused. Both leaders die once, Ix loses four actual counters including + two cyborgs, ordinary dial5/support5 wins, bounty9 leaves spice34, and the + physical Mirror is retained. +- Basic mixed Ix Stone compares four undialed counters with zero, chooses kill + before the original unused Tooth, keeps its four-counter army and physical + Stone, and receives bounty9/spice39. Neither typed strength nor leader value + replaces the physical comparison. + +Both original battles/decisions clear into the unchanged Ix Spice Collection +opening. Provisional priority is visible to Stone, Mirror and original Tooth +before commitment and in revealed controls. No root overflow or browser/ +application error; all hands remain within capacity. Actual generated Mirror +and Stone guidance imports, the policy anchor and23 evidence paths resolve; +all ten facets stay Partial. No human server, saved room, D1, deployment, +public start or new broad/HTTP/recovery/privacy/custody assurance campaign +changed. Owned hosts/scripts are removed; private artifacts stay outside Git. + + ## Primary evidence and a retention correction The actual face was visually reread at `/tmp/dune-rules/choam-mirror-reading.png`, alongside Stone Burner at `choam-stone-reading.png`. Both derive from the [physical-component photograph](https://cdn.anyfinder.eu/assets/QsUJtVktEC87xwK08oLlwnKyaozUWfuaLzG0LFEn67ENEEVgNrDGoITy3608Dqe7) recorded with its hash and limitations in `RICHESE_COMPONENTS.md`. This is publisher text visible on a physical component; the photographer, publication date and complete printing history remain unverified. No photograph is imported into the application. diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index e33abb2e..a0ce6311 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -658,6 +658,7 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP | Ecaz Loyalty | Before initial Traitor dealing, randomly set aside one native Ecaz card face up permanently. No Karama effect; no change to its leader disc. Resolving after existing pre-Traitor setup is an explicit sequencing composition; native ordinary-disc Skills now precede this draw through public assignment. Older dealt saves are not retrofitted; wider Skills/assassination combinations remain gated. | [Official sources and runtime](ECAZ_LOYALTY.md), [native skill composition](LEADER_SKILLS_RUNTIME.md#native-ecaz-ordinary-disc-composition--4-october-2026) | | Sapho battle order | First/last reorders remaining Battle-phase choosers at clean boundaries. Phase priority persists through aftermath; actual legal pairs determine eligibility. Separating the chooser from physical attacker/tie advantage is an explicit implementation interpretation of the card's distinct alternatives, not a dedicated FAQ ruling. Last does not prevent others from choosing the holder as an opponent. | [Runtime scope and source interpretation](SAPHO_BATTLE_ORDER.md) | | Harass & Withdraw | Either slot but neither category; private Prescience answers null. Return uniquely determined own undialed physical forces before weapon losses, canceled by an opposing successful Traitor. Mandatory after-use discard overrides winner retention. Explosion ordering and own-Traitor disposal are explicit timing/specificity inferences, consistent with the recorded Planetologist precedent. | [Runtime and sources](HARASS_WITHDRAW_RUNTIME.md) | +| Clean Ix Stone / Tooth / Mirror | Reuse the existing CHOAM prototype order: original Stone first, then uncopied Tooth. The earlier priority question is unanswered; standing choose-recommended extends that order provisionally, not as a publisher ruling or user answer. Printed Mirror copy-first and user-selected physical retention stay independent. Clean selected Ix/Richese supports native Ixians/Tleilaxu in both modes; Ecaz/Moritani stays Advanced-only. Typed winner ambiguity, new Ix Tech/Stronghold and other module bands remain guarded. | [Source and profile distinction](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-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) | | Spice Banker battle spending | Secret own-spice commitment is separate from support; only the surviving selected disc gains strength. Core Battle Plan payment rules spend it on death/loss and waive it for a sole traitor-call winner. This payment composition is explicit; upper deferred-income custody remains pending. | [Source contract and prototype](SPICE_BANKER_RUNTIME.md) | @@ -759,7 +760,7 @@ These are unresolved in the cited records. Do not infer an answer from elapsed t | Boundary | Still unresolved | Existing record | | --- | --- | --- | | Reinforcements cost | The earlier all-outcome versus sole-Traitor-winner question remains unanswered. The bounded prototype provisionally charges on every revealed outcome, chooses normal reserves first and counts the transfer as battle losses. Same-plan Harass pays the original quote before returns; future returns cannot fund or change it. Native Advanced Occupy Stone/Mirror composition keeps the fee but ignores +2 normal score in physical-token comparison. These are product interpretations, not a new publisher ruling or user answer. Other profile/module gates remain; eligibility never depends on concealed Traitors. | [Source review](ECAZ_TREACHERY_RULES.md#14-september-reinforcements-cost-review) · [Combined policy](REINFORCEMENTS_RUNTIME.md#combined-utility-plan--16-october-2026) · [Stone composition](REINFORCEMENTS_RUNTIME.md#native-stone-composition--16-october-2026) | -| Harass / Stone Burner | The earlier question (keep gated or compare after withdrawal) remains unanswered. Under the standing 14 October choose-recommended instruction, the development prototype now provisionally compares undialed counters remaining after successful withdrawal. Opposing Traitor cancels the return; original fixed Occupy commitment stays. Public preflight considers possible fixed Ecaz withdrawal without reading hidden custody. Allocation ambiguity, Ix/Poison Tooth and other module boundaries remain. This is an implementation interpretation, not a publisher ruling or a user answer. | [Provisional timing](HARASS_WITHDRAW_RUNTIME.md#provisional-stone-burner-timing--16-october-2026) | +| Harass / Stone Burner | The earlier keep-gated versus compare-after-withdrawal question is unanswered. Standing choose-recommended provisionally compares counters left after successful withdrawal. Opposing Traitor cancels the return; original fixed Occupy commitment stays. Public preflight considers possible fixed Ecaz withdrawal without hidden custody. Clean Ix priority follows its separate explicitly provisional contract; allocation ambiguity and other profile/module limits remain. This is implementation interpretation, not a new publisher ruling or user answer. | [Withdrawal](HARASS_WITHDRAW_RUNTIME.md#provisional-stone-burner-timing--16-october-2026) · [Clean Ix priority](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-composition--16-october-2026) | | Recruits late accounting and rates | The earlier one/two-further-free question remains unanswered. The content-first prototype now preserves paid spice, deducts recorded actual free usage from the doubled rate and all returns from the total cap: two-free/one-paid Atreides can take two more free. This is visibly provisional, not a numerical user ruling. Unknown ledgers, pending repricing, late rate changes and same-turn replay remain guarded. | [Source alternatives](RECRUITS_RULES.md), [provisional runtime](RECRUITS_RUNTIME.md) | | Sapho remaining timing | First versus unspent Advanced Guild priority, partly completed combined turns, cyclic auction duration and later battle timing stay guarded. The existing pre-plan opportunity supports [early aggressor](SAPHO_AGGRESSOR.md). Prior questions remain unanswered. | [Source audit](JUICE_OF_SAPHO_SOURCE_UPDATE.md), [battle review](JUICE_OF_SAPHO_BATTLE_REVIEW.md) | | Semuta Drug | Full-hand exchange is user-approved; neutral all-seat acknowledgements remain the explicit application policy. Other discard producers, nested continuation coverage and full-game acceptance remain unfinished. | [Source update and selected exchange](SEMUTA_SOURCE_UPDATE_20260907.md), [privacy review](SEMUTA_PRIVACY_REVIEW.md) | diff --git a/docs/STONE_BURNER_ENGINE_AUDIT.md b/docs/STONE_BURNER_ENGINE_AUDIT.md index 3377564d..9c799975 100644 --- a/docs/STONE_BURNER_ENGINE_AUDIT.md +++ b/docs/STONE_BURNER_ENGINE_AUDIT.md @@ -29,8 +29,10 @@ checks possible fixed Ecaz withdrawal without inspecting hidden cards. The earlier question remains unanswered. This is an interpretation selected under the standing choose-recommended priority, not newly retrieved publisher -authority. Ix/Poison Tooth, own concealed No-Field Harass and other profile/ -module boundaries remain separate. The 6 September evidence above +authority. Clean Ix now follows the separate +[selected-deck priority contract](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-composition--16-october-2026); +own concealed No-Field Harass and other profile/module boundaries remain. +The 6 September evidence above is historical; the current checkpoint has its own bounded proof. @@ -44,6 +46,20 @@ undialed tokens or support. Original fixed Ecaz commitment, variable ally losses mode choices, Traitor precedence and physical Stone/Mirror retention stay. Other profile/module gates and the explicit reserve-cost interpretation remain. +## Clean Ix priority cutover — 16 October 2026 + +The blanket Ix timing guard is replaced only for the clean selected CHOAM/Ix +profile. It reuses the already connected original Stone-first/uncopied-Tooth +order, while printed Mirror copy-first remains independent. The earlier +priority question is unanswered; standing choose-recommended selects the +existing prototype order provisionally, not a new publisher ruling. +See the [exact profile and source distinction](MIRROR_WEAPON_ENGINE_AUDIT.md#clean-selected-ix-composition--16-october-2026). + +Public precommit still quantifies over every feasible opposing force/dial/ +support possibility, never a hidden already-sealed plan. Original typed force, +support, death, bounty, substitution and retention paths remain native. +New Ix Tech/Stronghold and other combined-module bands remain guarded. + ## Primary evidence and exact effect The physical publisher face was visually read at `/tmp/dune-rules/choam-stone-reading.png`, enlarged from the [component photograph](https://cdn.anyfinder.eu/assets/QsUJtVktEC87xwK08oLlwnKyaozUWfuaLzG0LFEn67ENEEVgNrDGoITy3608Dqe7) linked by the [product gallery](https://www.tabletopfinder.eu/en/boardgame/32692/dune-choam-richese). Photograph provenance/hash are recorded in `RICHESE_ACQUISITION_RULES.md`. The visible publisher wording is the evidence; uploader printing/date remains unverified. diff --git a/game/card-presentation.ts b/game/card-presentation.ts index 57e323ee..94a5f6cb 100644 --- a/game/card-presentation.ts +++ b/game/card-presentation.ts @@ -94,9 +94,9 @@ export function cardPresentation(card: VisibleCard): CardPresentation { : richese.card.effect === 'semutaDrug' ? 'The explicit Semuta development preview offers neutral responses after clean public ordinary-card discards, paid Box disposal, retired Ornithopter movement (including a following Bene Gesserit Intrusion), Distrans, ordinary and queued definite Truthtrance answers, four Sapho scopes, unready pre-leader Residual Poison, printed Karama cancelling CHOAM Charity/Inflation/Bene Gesserit Charity, native capture/Auditor/battle-income/Ix-substitution/Basic allied-retention powers, or settling a normal-auction purchase or winning bid, a completed CHOAM sale, clean Kulon, proactive or reactive La La La/Baliset, proactive/final-Mentat Trip to Gamont with an ordinary/elite force or concealed No-Field, accepted Jubba Cloak before storm protection, Ix-deck Thumper/Amal, three battle discard stages (including before original Tech award, native Advanced capture, Face Dance, quoted CHOAM income or Auditor) and private normal-auction or Black Market Ixian ally replacements. Acquiring a printed cancellation cost never restores its canceled effect. Tech requires a supported fresh 3–6-seat CHOAM/optional-Ix profile. Native Ixian casualty substitution keeps its original before-Tooth order. Native Basic Moritani cleanup offers only unkept played cards after the original choice and Karama response, preserving its kept card and unchanged original retention pool. The Basic Richese/Moritani CHOAM/Ecaz entry also adds private Sabotage and Robbery overflow after clean shipment/movement arrivals, preserving the original victim/draw and arrival suffix. Commitment is owner-only; the approved full-hand exchange spends Semuta without increasing your completed hand. Other pending rewards, producers, nested responses and normal expansion starts remain unfinished.' : richese.card.effect === 'mirrorWeapon' - ? 'Mirror Weapon works in bounded CHOAM/Richese-deck classic-faction battles; copied Tooth and Stone choices resolve separately, and a winning physical Mirror may be kept even after copying Tooth or Artillery. This winner-retention treatment is a user-selected interpretation, not an official clarification. Combined modes and full Richese starts remain gated.' + ? 'Mirror Weapon works in supported CHOAM/Richese-deck tables, including clean selected Ix profiles and Advanced native Ecaz/Moritani. Copied Tooth or Stone chooses before its original; a winning physical Mirror may be kept after copied Tooth or Artillery under the user-selected retention interpretation. Original Stone/Tooth uses the visibly provisional Stone-first order. Other profile/module and full-start gates remain.' : richese.card.effect === 'stoneBurner' - ? 'Stone Burner has development battle controls. Combined allocation timing and Ix timing remain guarded; full expansion starts stay disabled.' + ? 'Stone Burner has development controls, including clean selected CHOAM/Ix profiles. Original Stone provisionally chooses before uncopied Tooth; Mirror copies still choose first as printed. Winner-changing physical allocations and other profile/module boundaries remain guarded; full expansion starts stay disabled.' : 'Richese effect integration and verification are incomplete. Karama, Distrans, Nullentropy Box, Ornithopter, Residual Poison, Portable Snooper, Stone Burner and Mirror Weapon have bounded card handlers; full expansion starts remain disabled.', topics: RULE_TOPICS.filter( (topic) => diff --git a/game/engine.ts b/game/engine.ts index b6b1fde9..eccd3786 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -21041,8 +21041,9 @@ function stonePublicPools( : [pool]; } function stoneTimingBlock(g: Game): string | null { - return g.expansions.includes('ix') - ? 'Stone Burner with the Ix expansion is awaiting a ruling on its simultaneous revealed choice with Poison Tooth. This implementation boundary applies before either hidden plan is submitted.' + if (!g.expansions.includes('ix')) return null; + return mirrorWeaponModeBlock(g) + ? 'Stone Burner with Ix needs the clean selected CHOAM/Ix profile without combined optional modules; the public allocation guard still applies.' : null; } function stoneOccupyContext(g: Game): StoneBurnerContext | undefined { diff --git a/game/mirror-weapon-mode.ts b/game/mirror-weapon-mode.ts index 391e65cf..e85c8dd8 100644 --- a/game/mirror-weapon-mode.ts +++ b/game/mirror-weapon-mode.ts @@ -3,6 +3,7 @@ import type { FactionId } from './catalog'; const supported = new Set([ 'atreides', 'harkonnen', 'emperor', 'guild', 'beneGesserit', 'fremen', 'choam', 'richese', 'ecaz', 'moritani', + 'ixians', 'tleilaxu', ]); /** Public configuration only: never inspect an opponent's hand or sealed plan. */ @@ -15,12 +16,16 @@ export function mirrorWeaponModeBlock(g: { leaderSkills?: unknown; discoveries?: unknown; sandtrout?: boolean; + techTokens?: unknown; + strongholdCards?: unknown; }): string | null { if (!g.expansions.includes('choam') || - g.expansions.some(id => id !== 'choam' && id !== 'ecaz') || + g.expansions.some(id => id !== 'choam' && id !== 'ecaz' && id !== 'ix') || (g.expansions.includes('ecaz') && g.advanced !== true) || g.players.some((p) => !supported.has(p.faction))) - return 'Mirror Weapon currently needs CHOAM/Richese with classic or native Ecaz/Moritani factions; Ix timing remains unresolved.'; + return 'Mirror Weapon needs selected CHOAM/Richese with supported native factions; Ecaz/Moritani combinations require Advanced.'; + if (g.expansions.includes('ix') && (g.techTokens || g.strongholdCards)) + return 'Ix-deck Stone/Mirror with Tech Tokens or Stronghold Cards remains outside this clean profile.'; if (g.homeworlds || g.nexusCards || g.leaderSkills || g.discoveries || g.sandtrout) return 'Mirror Weapon with combined optional modules is still being integrated.'; return null; diff --git a/game/reference.ts b/game/reference.ts index 90df5c4d..a75c88d3 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -1108,7 +1108,8 @@ export const RULE_TOPICS: RuleTopic[] = [ 'Compare the number of physical force tokens not dialed into battle. The aggressor wins a tie. Dialed losses and spice support still follow their normal rules; elite strength is not an extra physical token.', 'With Harass & Withdraw, this prototype provisionally compares undialed tokens remaining after successful withdrawal. An opposing successful Traitor cancels the return and decides the outcome instead. In Occupy, only the card user’s pool returns; original fixed Ecaz commitment and the other ally’s undialed counters remain. This timing is an explicit implementation interpretation, not a publisher ruling.', 'In the existing native Advanced Occupy prototype, Reinforcements still pays its original own-reserve cost with same-plan, opposing or Mirror-copied Stone. Its +2 normal-score bonus adds no physical undialed tokens, dial, support or on-board casualties. Original mode choices, Traitor precedence and physical Stone/Mirror retention remain unchanged; other profile/module boundaries stay.', - 'The plan editor checks the chosen dial and support against every projected public opposing force possibility. If legal force allocations could change the winner, commitment is blocked until that combined timing is supported. Ix expansion timing involving Poison Tooth also remains guarded pending its ordering ruling. Opposing hidden No-Field values are not inspected.', + 'The plan editor checks dial/support against every public opposing force possibility, never a hidden plan or No-Field value. Winner-changing physical allocations remain guarded. Clean selected CHOAM/Ix profiles now reuse the existing provisional original Stone-first, then uncopied Tooth order; Mirror copies still choose before their originals as printed. The earlier priority question remains unanswered.', + 'Clean Ix admission excludes Homeworlds, Nexus, Leader Skills, Discoveries, active Sandtrout, Tech Tokens and Stronghold Cards. Native Ecaz/Moritani combinations remain Advanced-only. Other non-Ix profile behavior and full expansion/public-start gates are unchanged.', 'Traitor and Lasgun–shield explosion precedence remain intact. Ordinary bounty applies unless another effect suppresses it. Stone Burner uses the general winning battle-card retention rule: a winner may keep it, and a losing played card is discarded. Normal Moritani ally retention may apply.', 'The post-reveal choice is bound to the current battle event and cannot be repeated after refresh. Review both revealed plans and select one mode; there is no separate victim or death confirmation.', ], @@ -1158,6 +1159,8 @@ export const RULE_TOPICS: RuleTopic[] = [ 'tests/stone-harass-runtime.test.ts', 'tests/ecaz-occupy-harass-quote.test.ts', 'tests/stone-reinforcements.test.ts', + 'tests/ix-revealed-weapons.test.ts', + 'tests/fixture-ix-revealed-weapons.ts', ], }, ], @@ -1184,9 +1187,9 @@ export const RULE_TOPICS: RuleTopic[] = [ : definition.card.effect === 'residualPoison' ? 'Use the Residual Poison panel before either combatant commits a leader. A shared preparation step provides an opportunity regardless of hand contents; advanced Harkonnen tables remain guarded pending the secret-captive ruling. In the explicit Semuta preview, a clean death with no readiness or inspection commitments pauses after public disposal for the neutral offer, then resumes the same preparation.' : definition.card.effect === 'stoneBurner' - ? 'Use Stone Burner in the battle weapon selector and review the chosen dial/support guard before sealing. Choose the leader effect after revelation; combined timing remains guarded.' + ? 'Use Stone Burner in the weapon selector and review public dial/support feasibility before sealing. Clean selected CHOAM/Ix now uses visibly provisional original Stone-first then uncopied Tooth order; copy-first and physical allocation guards remain.' : definition.card.effect === 'mirrorWeapon' - ? 'Choose Mirror Weapon in the battle weapon slot in a supported CHOAM/Richese-deck table. Its attack copies the opposing revealed weapon; copied Tooth or Stone choices resolve first and independently. Under the user-selected interpretation, a winning physical Mirror may be retained even after a copied Tooth or Artillery attack. Full Richese starts and combined modules remain guarded.' + ? 'Choose physical Mirror in a supported CHOAM/Richese-deck table, including clean selected Ix and Advanced Ecaz/Moritani. Copied Tooth or Stone chooses before its original. The user-selected winning physical-Mirror retention still applies after copied Tooth or Artillery. Original Stone/Tooth provisionally uses Stone-first order. Other profile/module and public-start gates remain.' : definition.card.effect === 'portableSnooper' ? 'Choose Portable Snooper as your ordinary poison defense, or use the late-defense panel after reveal before your own traitor decision. It uses ordinary winner retention and cannot stop Poison Tooth.' : definition.card.effect === 'semutaDrug' @@ -1321,7 +1324,25 @@ export const RULE_TOPICS: RuleTopic[] = [ }, ], } - : {}), + : definition.card.effect === 'mirrorWeapon' + ? { checklist: [ + { area: 'Implementation' as const, status: 'Partial' as const, + detail: 'Canonical physical Mirror, copy-first answers and physical retention compose with clean selected Ixian/Tleilaxu pools and original Advanced Occupy. Public feasibility and other profile/module boundaries remain.', + evidence: ['game/mirror-weapon-mode.ts', 'game/effective-weapons.ts', 'game/engine.ts'] }, + { area: 'Player controls' as const, status: 'Partial' as const, + detail: 'Actual weapon selectors, independent copied choices and original retention are usable. Ix original Stone/Tooth priority is visibly provisional before commitment and during revelation.', + evidence: ['components/battle-preparation.tsx', 'components/game-table.tsx'] }, + { area: 'AI' as const, status: 'Partial' as const, + detail: 'All four profiles have legal clean-Ix candidate and original/copy continuation paths. This is not strategy or difficulty calibration.', + evidence: ['game/bots.ts', 'tests/ix-revealed-weapons.test.ts'] }, + { area: 'Documentation' as const, status: 'Partial' as const, + detail: 'Printed copy-first, user-selected physical retention and provisional original Stone/Tooth priority are distinguished. Combined modules and full-mode acceptance remain separate.', + evidence: ['docs/MIRROR_WEAPON_ENGINE_AUDIT.md', 'docs/STONE_BURNER_ENGINE_AUDIT.md'] }, + { area: 'Verification' as const, status: 'Partial' as const, + detail: 'Original Matrix/engine proof plus genuine native setup with explicit conserved battle staging covers Basic/Advanced, both native force pools, original/copy modes, public ambiguity and Advanced fixed/variable Occupy. Mobile and policy proof is bounded, not unstaged complete-game or final assurance certification.', + evidence: ['tests/ix-revealed-weapons.test.ts', 'tests/fixture-ix-revealed-weapons.ts', 'tests/mirror-weapon-engine.test.ts'] }, + ] } + : {}), related: [ 'richese-cards', 'choam-modules', diff --git a/tests/fixture-ix-revealed-weapons.ts b/tests/fixture-ix-revealed-weapons.ts new file mode 100644 index 00000000..8dc709fe --- /dev/null +++ b/tests/fixture-ix-revealed-weapons.ts @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import {applyAction, viewGame, type Action, type Game} from '../game/engine'; +import {harassWithdrawGame} from './fixture-harass-withdraw'; +import {stoneHarassCustody, takeStoneHarassCard, STONE_CARD, MIRROR_CARD} from './fixture-stone-harass'; +import {finishOccupyCardsAftermath} from './fixture-ecaz-occupy-cards'; + +export const IX_TOOTH = 'ix-poison-tooth'; +export type IxRevealedCase = { + game: Game; + native: 'ixians' | 'tleilaxu'; + holder: 'a' | 'd'; + other: 'a' | 'd'; + kind: 'mirrorStone' | 'mirrorTooth' | 'stoneTooth'; + plans: {actor: string; action: Action}[]; +}; +/** Original native Ix/Richese setup and exact deck, then the upstream fixture's + * explicit phase/board staging. Actual chooseBattle owns every battle control; + * this is a conserved rule fixture, not an unstaged complete-game claim. */ +export function ixRevealedWeaponFixture({advanced = false, native = 'ixians', holder = 'a', + kind = 'mirrorStone'}: {advanced?: boolean; native?: 'ixians' | 'tleilaxu'; + holder?: 'a' | 'd'; kind?: IxRevealedCase['kind']} = {}): IxRevealedCase { + const game = harassWithdrawGame({advanced, factions: [native, 'atreides', 'richese'], + normal: native === 'ixians' ? 2 : 4, elite: native === 'ixians' ? 2 : 0, + territory: 'shield_wall', sector: 8}); + for (const player of game.players) { + game.deck.push(...player.hand); + player.hand = []; + } + const other = holder === 'a' ? 'd' : 'a'; + const holderCard = kind === 'stoneTooth' ? STONE_CARD : MIRROR_CARD; + const sourceCard = kind === 'mirrorStone' ? STONE_CARD : IX_TOOTH; + takeStoneHarassCard(game, holder, holderCard); + takeStoneHarassCard(game, other, sourceCard); + const plans = (['a', 'd'] as const).map(actor => ({actor, action: { + // Full ordinary commitment remains preflight-safe against every opposing + // mixed Ix allocation; a hidden already-sealed zero dial is not a shortcut. + type: 'battlePlan', dial: actor === 'a' ? 0 : 5, + support: advanced && actor === 'd' ? 5 : 0, + leader: game.players.find(player => player.id === actor)!.leaders[0].id, + weapon: actor === holder ? holderCard : sourceCard, + } satisfies Action})); + stoneHarassCustody(game); + return {game, native, holder, other, kind, plans}; +} +export function revealIxWeapons(source: IxRevealedCase): Game { + let game = source.game; + for (const {actor, action} of source.plans) game = applyAction(game, actor, action); + assert.equal(game.battle?.revealed, true); + return game; +} +export function finishIxWeaponBattle(state: Game): Game { + let game = state; + assert.equal(game.decision, null, 'All native reveal choices must precede traitor calls.'); + for (const id of viewGame(game, 'a').battle!.traitorVoters) + game = applyAction(game, id, {type: 'traitorCall', call: false}); + game = finishOccupyCardsAftermath(game, {stopAtFaceDance: true}); + if (game.decision?.kind === 'faceDance') { + game = applyAction(game, game.decision.player, {type: 'decision', reveal: false}); + game = finishOccupyCardsAftermath(game); + } + assert.equal(game.battle, null); + assert.equal(game.decision, null); + stoneHarassCustody(game); + return game; +} diff --git a/tests/fixture-stone-harass.ts b/tests/fixture-stone-harass.ts index 8c98d588..d8042f52 100644 --- a/tests/fixture-stone-harass.ts +++ b/tests/fixture-stone-harass.ts @@ -78,7 +78,7 @@ export function stoneHarassCustody(game: Game): void { ...(player.faceDancers ?? []).map(card => card.leader)]), ].sort(), traitorDeck(game.players, game.expansions.includes('ix')).sort()); for (const player of game.players) { - // Both native fixture rosters use four-card hands. + // All admitted native fixture rosters use four-card hands. assert.ok(player.hand.length <= 4, 'Conserved card staging must also respect native hand capacity.'); assert.equal(player.reserves + player.tanks + Object.values(player.forces).reduce((sum, amount) => sum + amount, 0), 20); @@ -273,6 +273,7 @@ export function finishStoneHarassBattle(state: Game, options: { export type StoneHarassOccupyOptions = { lead?: 'ecaz' | 'ally'; + allyFaction?: 'guild' | 'ixians' | 'tleilaxu'; /** Stage Harass in the selected lead's plan; false keeps the same card in hand. */ harass?: boolean; stone?: 'opponent' | 'harass'; @@ -281,6 +282,7 @@ export type StoneHarassOccupyOptions = { support?: number; ecazForces?: number; allyForces?: number; + allyElite?: number; opponentForces?: number; opponentElite?: number; opponentDial?: number; @@ -301,17 +303,20 @@ export function stoneHarassOccupyFixture( options: StoneHarassOccupyOptions = {}, ): StoneHarassOccupyCase { const lead: 'ecaz' | 'ally' = options.lead ?? 'ecaz'; + const allyFaction = options.allyFaction ?? 'guild'; + const expansions: Game['expansions'] = ['ecaz', 'choam', ...(allyFaction === 'guild' ? [] : ['ix'] as const)]; // The original four-seat advanced lobby supplies Ecaz, its real ally, a // classic opponent and Richese's own card cache; every later step is native. - let initial = createGame('STONEOCCUPY', newPlayer('ecaz', 'ecaz', 'ecaz'), true, ['ecaz', 'choam']); - for (const faction of ['guild', 'emperor', 'richese'] as const) + let initial = createGame('STONEOCCUPY', newPlayer('ecaz', 'ecaz', 'ecaz'), true, expansions); + for (const faction of [allyFaction, 'emperor', 'richese'] as const) joinGame(initial, newPlayer(faction, faction, faction)); for (const player of initial.players) initial = applyAction(initial, player.id, { type: 'ready' }); initial.ecazTreachery = true; initial = initializeEcazOccupyGameForAudit(initial); - const occupy = ecazOccupyFixture({ initial, expansions: ['ecaz', 'choam'], ecazTreachery: true, - allyFaction: 'guild', opponentFaction: 'emperor', + const occupy = ecazOccupyFixture({ initial, expansions, ecazTreachery: true, + allyFaction, opponentFaction: 'emperor', ecazForces: options.ecazForces ?? 5, allyForces: options.allyForces ?? 4, + ...(options.allyElite === undefined ? {} : { allyElite: options.allyElite }), ...(options.opponentForces === undefined ? {} : { opponentForces: options.opponentForces }), ...(options.opponentElite === undefined ? {} : { opponentElite: options.opponentElite }) }); // Isolate selected battle cards after genuine setup/auctions, before the diff --git a/tests/ix-revealed-weapons.test.ts b/tests/ix-revealed-weapons.test.ts new file mode 100644 index 00000000..13188352 --- /dev/null +++ b/tests/ix-revealed-weapons.test.ts @@ -0,0 +1,131 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import {applyAction, viewGame} from '../game/engine'; +import {mirrorWeaponModeBlock} from '../game/mirror-weapon-mode'; +import {STONE_CARD, MIRROR_CARD, stoneHarassOccupyFixture, takeStoneHarassCard, + sealStoneHarassPlans, finishStoneHarassBattle} from './fixture-stone-harass'; +import {IX_TOOTH, ixRevealedWeaponFixture, revealIxWeapons, + finishIxWeaponBattle} from './fixture-ix-revealed-weapons'; + +void test('native Basic and Advanced Ix or Tleilaxu copied Stone keeps copy-first choices and physical custody in either slot', () => { + for (const advanced of [false, true]) for (const native of ['ixians', 'tleilaxu'] as const) + for (const holder of ['a', 'd'] as const) { + const source = ixRevealedWeaponFixture({advanced, native, holder, kind: 'mirrorStone'}); + let game = revealIxWeapons(source); + assert.equal(game.decision?.kind, 'stoneBurner'); + assert.equal(game.decision?.player, holder); + assert.equal(game.decision?.physicalId, MIRROR_CARD); + assert.equal(game.decision?.copiedFrom, STONE_CARD); + game = applyAction(game, holder, {type: 'decision', event: game.decision!.event, mode: 'ignore'}); + assert.equal(game.decision?.kind, 'stoneBurner'); + assert.equal(game.decision?.player, source.other); + game = applyAction(game, source.other, {type: 'decision', event: game.decision!.event, mode: 'kill'}); + game = finishIxWeaponBattle(game); + assert.equal(game.lastBattleContext?.winner, 'a', 'four native undialed counters beat zero ordinary counters'); + assert.equal(game.players.find(player => player.id === holder)!.hand.some(card => card.id === MIRROR_CARD), holder === 'a'); + assert.equal(game.discard.filter(card => card.id === MIRROR_CARD).length, holder === 'a' ? 0 : 1); + for (const actor of ['a', 'd']) { + const selected = source.plans.find(step => step.actor === actor)!.action.leader; + assert.equal(game.players.find(player => player.id === actor)!.leaders.find(leader => leader.id === selected)!.dead, true); + } + } +}); + +void test('native Ix-deck Mirror activates copied Tooth first without converting physical Mirror into a mandatory discard', () => { + for (const advanced of [false, true]) for (const native of ['ixians', 'tleilaxu'] as const) + for (const holder of ['a', 'd'] as const) { + const source = ixRevealedWeaponFixture({advanced, native, holder, kind: 'mirrorTooth'}); + let game = revealIxWeapons(source); + assert.equal(game.decision?.kind, 'poisonTooth'); + assert.equal(game.decision?.player, holder); + assert.equal(game.decision?.physicalId, MIRROR_CARD); + game = applyAction(game, holder, {type: 'decision', event: game.decision!.event, activate: true}); + assert.ok(game.decision?.kind === 'poisonTooth'); + assert.equal(game.decision?.player, source.other); + assert.equal(game.decision?.physicalId, IX_TOOTH); + game = applyAction(game, source.other, {type: 'decision', event: game.decision!.event, activate: false}); + assert.equal(game.battle!.poisonTooth![holder], true); + assert.equal(game.battle!.poisonTooth![source.other], false); + game = finishIxWeaponBattle(game); + assert.equal(game.lastBattleContext?.winner, 'd', 'after both leaders die the original ordinary dial5 beats native zero'); + assert.equal(game.players.find(player => player.id === holder)!.hand.some(card => card.id === MIRROR_CARD), holder === 'd'); + assert.equal(game.players.find(player => player.id === source.other)!.hand.some(card => card.id === IX_TOOTH), source.other === 'd'); + for (const actor of ['a', 'd']) { + const selected = source.plans.find(step => step.actor === actor)!.action.leader; + assert.equal(game.players.find(player => player.id === actor)!.leaders.find(leader => leader.id === selected)!.dead, true); + } + } +}); + +void test('original Ix-deck Stone precedes uncopied Tooth under the existing provisional policy, without forcing Tooth activation', () => { + for (const advanced of [false, true]) for (const native of ['ixians', 'tleilaxu'] as const) + for (const holder of ['a', 'd'] as const) { + const source = ixRevealedWeaponFixture({advanced, native, holder, kind: 'stoneTooth'}); + let game = revealIxWeapons(source); + assert.equal(game.decision?.kind, 'stoneBurner'); + assert.equal(game.decision?.player, holder); + game = applyAction(game, holder, {type: 'decision', event: game.decision!.event, mode: 'kill'}); + assert.equal(game.decision?.kind, 'poisonTooth'); + assert.equal(game.decision?.player, source.other); + game = applyAction(game, source.other, {type: 'decision', event: game.decision!.event, activate: false}); + game = finishIxWeaponBattle(game); + assert.equal(game.lastBattleContext?.winner, 'a'); + assert.equal(game.players.find(player => player.id === source.other)!.hand.some(card => card.id === IX_TOOTH), source.other === 'a'); + assert.equal(game.discard.filter(card => card.id === IX_TOOTH).length, source.other === 'a' ? 0 : 1); + } +}); + +void test('Ix public precommit still rejects winner-changing cyborg allocations independently of the hidden opposing plan', () => { + const source = ixRevealedWeaponFixture({advanced: true, kind: 'stoneTooth'}); + const own = {...source.plans[0].action, dial: 2, support: 0}; + const messages: string[] = []; + for (const dial of [0, 5]) { + const game = applyAction(source.game, 'd', {...source.plans[1].action, dial, support: dial}); + const before = structuredClone(game); + assert.throws(() => applyAction(game, 'a', own), error => { + assert.match(String(error), /change the winner/); + messages.push(String(error)); + return true; + }); + assert.deepEqual(game, before); + } + assert.equal(messages[0], messages[1]); +}); + +void test('new Ix admission leaves optional-module and Basic Ecaz limits closed while retaining the existing non-Ix band', () => { + const source = ixRevealedWeaponFixture().game; + assert.equal(mirrorWeaponModeBlock(source), null); + for (const moduleName of ['homeworlds', 'nexusCards', 'leaderSkills', 'discoveries', 'techTokens', 'strongholdCards'] as const) + assert.notEqual(mirrorWeaponModeBlock({...source, [moduleName]: {}}), null); + assert.notEqual(mirrorWeaponModeBlock({...source, sandtrout: true}), null); + assert.notEqual(mirrorWeaponModeBlock({...source, expansions: ['ix']}), null); + assert.notEqual(mirrorWeaponModeBlock({...source, expansions: ['ix', 'choam', 'ecaz'], advanced: false}), null); + assert.equal(mirrorWeaponModeBlock({expansions: ['choam'], advanced: true, + players: [{faction: 'emperor'}, {faction: 'richese'}, {faction: 'guild'}], techTokens: {}, strongholdCards: {}}), null); +}); + +void test('native Ixian or Tleilaxu Occupy keeps fixed remainder, copied Stone queue and actual card-user reserve cost separate', () => { + for (const allyFaction of ['ixians', 'tleilaxu'] as const) for (const lead of ['ecaz', 'ally'] as const) { + const source = stoneHarassOccupyFixture({allyFaction, lead, harass: false, stone: 'opponent', mirror: true, + variableDial: 0, support: 0, allyForces: 4, ...(allyFaction === 'ixians' ? {allyElite: 2} : {}), opponentForces: 5}); + takeStoneHarassCard(source.game, source.lead, 'ecaz-reinforcements'); + source.plans.find(step => step.actor === source.lead)!.action.defense = 'ecaz-reinforcements'; + const originalReserves = source.game.players.find(player => player.id === source.lead)!.reserves; + // Natural Storm changes public tie priority. Make the original Stone + // finishable for every mixed allocation, never just the sealed zero dial. + const opponentPlan = source.plans.find(step => step.actor === source.opponent)!.action; + const fullCommitment = viewGame(source.game, source.opponent).battle!.tieWinner !== source.opponent; + opponentPlan.dial = fullCommitment ? 5 : 0; + opponentPlan.support = fullCommitment ? 5 : 0; + let game = sealStoneHarassPlans(source); + assert.equal(game.decision?.player, source.lead, 'the selected lead physically owns Mirror and chooses the copy first'); + game = finishStoneHarassBattle(game, {modes: {[source.lead]: 'ignore', [source.opponent]: 'ignore'}}); + assert.equal(game.lastBattleContext?.winner, source.lead, 'the public-safe original plan leaves fewer undialed counters than the coalition'); + assert.equal(game.players.find(player => player.id === source.lead)!.hand.some(card => card.id === MIRROR_CARD), true); + assert.equal(game.players.find(player => player.id === source.ecaz)!.tanks, lead === 'ecaz' ? 6 : 3); + assert.equal(game.players.find(player => player.id === source.ally)!.tanks, lead === 'ally' ? 3 : 0); + assert.equal(game.players.find(player => player.id === source.lead)!.reserves, originalReserves - 3); + assert.equal(game.discard.filter(card => card.id === 'ecaz-reinforcements').length, 1); + assert.equal(viewGame(game, source.lead).battle, null); + } +}); diff --git a/tests/stone-burner-review.test.ts b/tests/stone-burner-review.test.ts index c753ee10..f445a3db 100644 --- a/tests/stone-burner-review.test.ts +++ b/tests/stone-burner-review.test.ts @@ -128,7 +128,7 @@ const inventory = (g: Game) => .map((c) => c.id) .sort(); -void test('forged Stone role and public Ix timing reject before any hidden plan commitment', () => { +void test('forged Stone roles reject before any hidden plan commitment', () => { for (const patch of [ { id: 'forged-stone' }, { name: 'Other Stone' }, @@ -145,11 +145,6 @@ void test('forged Stone role and public Ix timing reject before any hidden plan ); assert.deepEqual(g, before); } - let ix = fixture(); - ix.expansions.push('ix'); - ix = prepare(start(ix)); - assert.throws(() => plan(ix, 'r'), /Ix expansion/); - assert.deepEqual(ix.battle!.plans, {}); }); void test('Advanced Emperor outcome-changing allocation is rejected identically before or after an unrelated hidden opposing plan', () => {