diff --git a/components/game-table.tsx b/components/game-table.tsx index be5ed9c0..85f5b011 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -550,9 +550,7 @@ export function GameTable({ selectedDefenseId === 'ecaz-reinforcements'; const reinforcementsPlanReason = !reinforcementsSelected ? null : !g.battle?.reinforcements ? 'Reinforcements is unavailable in this battle.' : - g.battle.reinforcements.blocked ?? - ((!!selectedBattleWeapon && isStoneBurner(selectedBattleWeapon)) - ? 'Reinforcements cannot share this plan with Stone Burner.' : null); + g.battle.reinforcements.blocked; const selectedBattleKwisatz = kwisatz && !!me.kwisatz?.active && !me.kwisatz.dead && !g.battle?.kwisatzBlocked && (!me.kwisatz.usedAt || me.kwisatz.usedAt === g.battle?.territory); @@ -3091,6 +3089,13 @@ export function GameTable({ forces remaining after a successful withdrawal. An opposing successful Traitor cancels that withdrawal.
+ {g.battle?.cards.some(card => card.id === 'ecaz-reinforcements') && ( ++ Reinforcements still pays its original three-reserve cost. + Its +2 normal-score bonus does not add undialed physical + tokens or change Stone Burner’s comparison. +
+ )} Read Stone Burner rules @@ -5370,6 +5375,13 @@ export function GameTable({ ? 'Mirror Weapon copies the opposing revealed weapon attack, not its physical card. If the copied attack is Stone Burner, it uses undialed physical tokens; any copied post-reveal choice is yours. A winning Mirror remains physical Mirror, even if it copies activated Poison Tooth or Artillery.' : 'Stone Burner compares undialed physical tokens, not dialed strength. Its leader effect is chosen after both plans are revealed.'} + {reinforcementsSelected && ( ++ Reinforcements still spends three original reserves. + Its +2 normal-score bonus cannot increase the undialed + physical tokens compared by original or copied Stone Burner. +
+ )}diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index ddb97e0c..2be17c7c 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -29,46 +29,38 @@ optional skill bands, combinations and public expansion starts remain open. Rules implementation continues; comprehensive assurance and administration remain deferred until all rules are implemented. -**Same-plan Harass / Reinforcements — 16 October.** Both slot orders now follow -the existing original-reserve-cost-before-return order under explicit -choose-recommended interpretation. Future returns cannot fund deficient reserves -or replace the original normal/elite quote. Original score-only +2, fixed Ecaz -commitment, variable ally losses, all-outcome reserve cost and Traitor-canceled -Harass remain separate; both physical cards discard once. +**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. -Affected eleven-file134/134, types/lint and build pass. Six new behavioral -regressions, eight fresh Node policy programs/all four profiles with16 accepted -continuation actions and a real390px typed-cost journey pass. Original2-normal/ -1-elite reserve payment survives the later4-normal return; owner finishes -reserves4/Tanks16/elite Tanks5/spice29, both cards discarded and collection open. -No root overflow or browser/application error. Current parse/policy anchor and38 -topic evidence paths resolve; all ten facets remain Partial. -[Policy and scoped proof](REINFORCEMENTS_RUNTIME.md#combined-utility-plan--16-october-2026). -Stone/Reinforcements, other profile/module limits and final assurance remain -separate. No human server, saved room, D1, deployment or public gate changed. +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. +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. -**Harass / Stone Burner interaction — 16 October.** Richese-family Harass now -uses visibly provisional post-withdrawal physical comparison under the standing -choose-recommended priority; the earlier source question remains unanswered. -Same-plan, opposing and Mirror-resolved Stone preserve Traitor precedence, -original support and mandatory disposal. Active Occupy removes only its -actual card user's fixed or variable undialed pool, preserving the original -mandatory Ecaz commitment. Public preflight considers possible fixed withdrawal -without reading hidden cards. No persisted game field is added. - -Affected thirteen-file161/161, types/lint and build pass. Eight same-plan -policy programs/all four profiles accept24 continuation actions. Two actual -390px native-control journeys prove Basic winner reversal and Advanced typed -return/support/leader-death/bounty through retention and collection. No root -overflow or application/browser error in the corrected host's journeys. -Original Richese lot and shared-preparation fixture drivers were corrected; -incidental collection-wallet expectations were removed. Current guide parse, -policy anchor and33 evidence paths resolve; both topics' five facets stay -Partial. [Policy and scoped proof](HARASS_WITHDRAW_RUNTIME.md#provisional-stone-burner-timing--16-october-2026). -Ix/Poison Tooth, Reinforcements/Stone and own -concealed No-Field Harass and other module gates remain. No human server, -saved room, D1, deployment or public gate changed; final assurance stays due. +Current parse/composition anchor and23 topic paths resolve; all ten relevant +facets remain Partial. Exact policies and earlier bounded human/policy proof: +[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. **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 f38badb0..c7f1d3f1 100644 --- a/docs/ECAZ_OCCUPY_RULES.md +++ b/docs/ECAZ_OCCUPY_RULES.md @@ -252,8 +252,11 @@ is unchanged and remains explicitly not a publisher ruling. Both slots and opposing holders work. Same-plan Harass/Reinforcements now follows the [provisional original-reserve-cost-first policy](REINFORCEMENTS_RUNTIME.md#combined-utility-plan--16-october-2026), preserving actual card-user ownership and original fixed losses. -Reinforcements with Stone/optional modules and Basic/unprofiled co-side -card battles remain guarded. +Original or copied Stone now composes with same-plan/opposing Reinforcements +under the [physical-token contract](REINFORCEMENTS_RUNTIME.md#native-stone-composition--16-october-2026). +Its +2 normal score adds no undialed counters; original reserve cost and fixed +versus variable on-board losses stay separate. Other optional modules and +Basic/unprofiled co-side card battles remain guarded. Native/pure67 cases, relevant284-case batch and27 actual native-program cases pass, including all four legal policies. Two natural captured games diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 650048f5..0775b3ce 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,33 @@ # Dune implementation status +## 16 October 2026 — Native Occupy Stone and Reinforcements + +Removed the same-plan/opposing Stone rejection while keeping existing mode +eligibility. Original/copy Stone compares physical undialed counters, not +2 +normal score; the reserve fee still pays its original own typed quote under the +existing provisional all-outcome policy. Fixed Ecaz and variable ally losses, +mode choices, Traitor precedence and physical Stone/Mirror retention stay. +No profile/state-field/public-start widening. + +Sixteen-file167/167, types/lint and build pass (module-register/large-chunk/ +Unknown-route warnings). Pure/native cases cover physical victory despite +2, +same/opposing costs, typed reserves, both leads, copied kill and Traitor override. +Eight fresh Node programs/all four profiles accept36 continuation actions. + +The first native fixture held five cards after adding Reinforcements. Corrected +by returning unselected physical hands to the conserved deck before lead choice, +then enforcing four-card capacity. Repeated actual390px controls start at three +cards, seal dial4/support1, choose ignore and decline all three native votes: +physical5-vs4 wins; Ecaz reserves15→12/Tanks6/support9→8, Guild Tanks1, +Ecaz2/Guild3 remain. Reinforcements discards once, Stone retains, battle/decision +clear into collection. No corrected-journey overflow/browser/application error; +the overfull initial hand is not accepted legal-hand evidence. + +Current parse/anchor and23 topic paths resolve; all ten facets remain Partial. +Owned hosts/scripts removed. No human server, D1, saved room, deployment or +broad/HTTP/recovery assurance campaign changed. +[Composition and scoped proof](REINFORCEMENTS_RUNTIME.md#native-stone-composition--16-october-2026). + ## 16 October 2026 — Same-plan Harass and Reinforcements Connected both physical slot orders without adding a new settlement mechanism diff --git a/docs/REINFORCEMENTS_RUNTIME.md b/docs/REINFORCEMENTS_RUNTIME.md index 4c69f32f..4b80a973 100644 --- a/docs/REINFORCEMENTS_RUNTIME.md +++ b/docs/REINFORCEMENTS_RUNTIME.md @@ -11,7 +11,8 @@ nor a defense: Prescience of a category receives **None**, while the physical slot stays occupied and hidden until reveal. A named-card inspection still fixes the exact card. Harass & Withdraw may share the plan under the [provisional original-cost-first policy](#combined-utility-plan--16-october-2026). -Stone Burner, Homeworld and other optional-module combinations remain gated. +Original or copied Stone Burner composes in the existing native Advanced Occupy +envelope; Homeworld, other profile and optional-module boundaries remain. The [combined-army follow-up](ECAZ_OCCUPY_RULES.md#independent-cards-with-combined-armies--2-october-2026) retains own-holder reserve cost and selected lead card/payment ownership. @@ -45,7 +46,8 @@ provisional all-outcome policy. Both slot orders compose in the existing Basic/Advanced and selected Advanced Occupy envelopes. Only the card user's reserves and undialed forces change; original fixed Ecaz commitment and the other ally's pool stay separate. -Stone/Reinforcements, Basic/unprofiled co-side and other module gates stay. +Basic/unprofiled co-side and other module gates stay; Stone uses the +[physical-token composition below](#native-stone-composition--16-october-2026). No persisted state or fresh/public profile is added. ### Scoped proof @@ -74,3 +76,53 @@ HTTP, recovery/privacy/custody assurance campaign ran. Private proof artifacts remain outside Git; the owned source host and throwaway scripts were removed. +## Native Stone composition — 16 October 2026 + +Reinforcements now composes with same-plan or opposing Stone Burner, including +a physical Mirror Weapon copying Stone. This uses the existing fresh native +Advanced Occupy envelope with the independent Ecaz cards and actual Richese +cache. No new profile, Basic Richese eligibility, public start or optional +module is enabled. + +Stone still compares only physical undialed counters, including original +fixed Ecaz and native variable ally contributions. Reinforcements adds no +physical tokens, extra dial, support or on-board casualties; its +2 normal-score +bonus is inactive for Stone comparison. Its original own-reserve cost still +pays once under the existing provisional all-outcome policy. Original/copy +kill-or-ignore modes and Traitor precedence remain unchanged. Reinforcements +discards once; a winning physical Stone or Mirror keeps ordinary retention. + +The printed physical-token instruction and the existing score-only reserve +card implementation are composed here. The reserve-payment question remains +unanswered; no new publisher ruling or user answer is implied. + +### Scoped native Stone proof + +The affected sixteen-file offline union passes **167/167**, with types/lint +and production build passing (module-register, large-chunk and Unknown-route +warnings). New pure/native cases cover physical-token victory despite +2, +same-plan/opposing cost, original elite reserves, fixed/variable card users, +copied kill mode, Traitor precedence and physical retention. + +Eight fresh Node programs cover both lead roles and all four legal profiles, +with original/Mirror attacks and36 accepted continuation actions. The native +fixture initially exceeded hand capacity when a third selected card was added; +it now returns unselected real hands to the conserved deck before lead choice +and checks four-card limits. The actual390px journey was repeated on that +corrected fixture, starting with only three held cards. + +Original total dial4/support1 compares five undialed coalition counters against +four, never seven. All three native voters decline Traitor through actual +controls. Ecaz pays reserves15→12 plus original fixed3 losses (Tanks6); +Guild loses only variable1, leaving Ecaz2/Guild3 in the territory. Support +costs Ecaz9→8. Reinforcements discards once; Stone is retained through its +original choice, and battle/decision clear into collection. No root overflow, +browser/application error or over-capacity hand occurs in the corrected journey. +The earlier overfull fixture is not accepted evidence of a legal hand. + +Current reference parses; the composition anchor and23 evidence paths across +both topics resolve, with all ten facets Partial. No human server, saved room, +D1, deployment, public profile or new broad/HTTP/recovery/privacy/custody +assurance campaign changed. Owned hosts/scripts are removed; private proof +artifacts stay outside Git. + diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 7dc66480..e33abb2e 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -758,7 +758,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. Under standing choose-recommended, same-plan Harass composes by paying that original reserve quote before returns; future returns cannot fund or change it. Those are product interpretations, not publisher rulings or a numerical user answer. Stone/Reinforcements and other module gates remain. Eligibility never depends on concealed matching Traitors. | [Source review](ECAZ_TREACHERY_RULES.md#14-september-reinforcements-cost-review) · [Combined policy](REINFORCEMENTS_RUNTIME.md#combined-utility-plan--16-october-2026) | +| 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) | | 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) | diff --git a/docs/STONE_BURNER_ENGINE_AUDIT.md b/docs/STONE_BURNER_ENGINE_AUDIT.md index 40c39715..3377564d 100644 --- a/docs/STONE_BURNER_ENGINE_AUDIT.md +++ b/docs/STONE_BURNER_ENGINE_AUDIT.md @@ -29,11 +29,21 @@ 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, Reinforcements/Stone, own concealed No-Field Harass -and other module boundaries remain separate. The 6 September evidence above +authority. Ix/Poison Tooth, own concealed No-Field Harass and other profile/ +module boundaries remain separate. The 6 September evidence above is historical; the current checkpoint has its own bounded proof. +## Reinforcements composition — 16 October 2026 + +Within the existing native Advanced Occupy envelope, same-plan/opposing +Reinforcements and original/Mirror-copied Stone now share the +[physical-token composition](REINFORCEMENTS_RUNTIME.md#native-stone-composition--16-october-2026). +The original three-own-reserve fee pays once; +2 normal score adds no physical +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. + ## 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/battle-resolution-quote.ts b/game/battle-resolution-quote.ts index 8673cfd4..091cba7e 100644 --- a/game/battle-resolution-quote.ts +++ b/game/battle-resolution-quote.ts @@ -410,9 +410,8 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote { requireQuote(validBattleSlotPair(slots[0], slots[1]) && !(side === d && reinforcements.length), 'Reinforcements needs a unique physical card and valid weapon and defense slot pair.'); - requireQuote(!input.homeworld && !(side.leaderSkills?.length || (side === a ? d : a).leaderSkills?.length) && - !hasStone, - 'Reinforcements cannot combine with Homeworld, leader skills or Stone Burner.'); + requireQuote(!input.homeworld && !(side.leaderSkills?.length || (side === a ? d : a).leaderSkills?.length), + 'Reinforcements cannot combine with Homeworld or leader skills.'); const reserves = side.reinforcementsReserves!; requireQuote(!!reserves && typeof reserves === 'object' && !Array.isArray(reserves), 'Reinforcements needs own typed reserves.'); @@ -686,7 +685,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote { attacker: a.plan.dial - attackerPenalty + (a.ecazSpecialKarama ? ecazSpecialKaramaBonus(strengths.attacker, strengths.defender) : 0) + - (reinforcements[0]?.player === a.id ? 2 : 0) + + (!stone && reinforcements[0]?.player === a.id ? 2 : 0) + (a.id === homeworld?.native ? homeworld.strength : 0) + (deaths.attacker || effects.stunned ? 0 @@ -696,7 +695,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote { defender: d.plan.dial - defenderPenalty + (d.ecazSpecialKarama ? ecazSpecialKaramaBonus(strengths.defender, strengths.attacker) : 0) + - (reinforcements[0]?.player === d.id ? 2 : 0) + + (!stone && reinforcements[0]?.player === d.id ? 2 : 0) + (d.id === homeworld?.native ? homeworld.strength : 0) + (deaths.defender || effects.stunned ? 0 diff --git a/game/bots.ts b/game/bots.ts index 3a705ed2..b665b051 100644 --- a/game/bots.ts +++ b/game/bots.ts @@ -823,7 +823,6 @@ function plans(g: GameView): Action[] { if ((!leader && (weapon || defense)) || !validBattleSlotPair(w, d, specialForLeader(w,leader))) continue; const reinforcements = weapon === 'ecaz-reinforcements' || defense === 'ecaz-reinforcements'; - if (reinforcements && isStoneBurner(w)) continue; if (voice) { const used = playedVoiceMatch( diff --git a/game/card-presentation.ts b/game/card-presentation.ts index e0bffb71..57e323ee 100644 --- a/game/card-presentation.ts +++ b/game/card-presentation.ts @@ -121,7 +121,7 @@ export function cardPresentation(card: VisibleCard): CardPresentation { ? 'Recruits has development controls during Revival. Earlier paid revivals, pending transactions, later free-rate changes and repeated use this turn remain guarded pending their rulings.' : ecaz.card.effect === 'harassWithdraw' ? 'Harass & Withdraw has development controls in either battle-card slot, with physical allocation choices after reveal. Stone Burner provisionally compares undialed forces remaining after successful withdrawal; additional optional modules remain guarded.' - : 'Reinforcements has development controls in either battle-card slot, including the explicit Advanced Occupy card variant. It spends three of the card user’s own reserve forces and adds +2 battle score without extra spice support. All-outcome payment and paying its original reserve cost before same-plan Harass returns are provisional. Returned forces cannot fund or change that cost. Stone Burner and other optional modules remain guarded.', + : 'Reinforcements has development controls in either battle-card slot, including the explicit Advanced Occupy card variant. It spends three of the card user’s own reserve forces and adds +2 normal battle score without extra support or physical tokens. Original or copied Stone Burner ignores that score bonus but still charges the reserve cost. All-outcome payment and paying before same-plan Harass returns are provisional; returned forces cannot fund or change the cost. Other profile and optional-module limits remain guarded.', topics: RULE_TOPICS.filter((topic) => topic.id === (ecaz.card.effect === 'harassWithdraw' ? 'card-harass-withdraw' : ecaz.card.effect === 'reinforcements' ? 'card-reinforcements' : 'card-recruits')), }; const topicIds = diff --git a/game/engine.ts b/game/engine.ts index f58e0cdb..b6b1fde9 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -21251,11 +21251,7 @@ function validatePlan( validBattleSlotPair(w, d, planetologistWeapon), 'Chemistry as a weapon needs another defense; Weirding Way as a defense needs another weapon.', ); - if ([w, d].some(isReinforcements)) { - requireRule(![w, d].some(isStoneBurner), - 'Reinforcements cannot share this bounded plan with Stone Burner.'); - currentReinforcementsCost(g, p); - } + if ([w, d].some(isReinforcements)) currentReinforcementsCost(g, p); if ([w, d].some(isHarassWithdraw)) currentHarassWithdrawQuote(g, p, plan); if (isMirrorWeapon(w)) { const blocked = mirrorWeaponModeBlock(g) ?? stonePlanBlock(g, p, plan); diff --git a/game/reference.ts b/game/reference.ts index 0bbac68f..90df5c4d 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -1107,6 +1107,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'After both plans reveal, choose to kill both leaders or ignore the strength of leaders who otherwise survive. Neither leader strength nor Kwisatz Haderach strength determines this battle. The choice affects deaths and bounty, not which undialed-force comparison is used. Ignoring strength supplies no protection against another weapon.', '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.', '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.', @@ -1156,6 +1157,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'tests/stone-harass-quote.test.ts', 'tests/stone-harass-runtime.test.ts', 'tests/ecaz-occupy-harass-quote.test.ts', + 'tests/stone-reinforcements.test.ts', ], }, ], @@ -4950,7 +4952,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'In the independent Ecaz Treachery Cards variant, place your held Reinforcements card in either battle-card slot; it is neither a weapon nor a defense. A category Prescience answer is None, not the name of this card. One physical card cannot fill both slots.', 'Have three own forces in reserves when sealing. Classic/paired Ecaz/Moritani Basic/Advanced and explicitly selected native Advanced Occupy prototypes spend ordinary reserves first and then elite reserves, moving exactly three to corresponding Tanks subpools at resolution. These are the actual card holder’s reserves, not the variable ally’s pool; no extra forces arrive.', 'The normal-outcome score gains 2 without adding to the physical dial, spice support or on-board casualties. The card and its reserve cost are consumed even on a Traitor victory, a mutual Traitor result or a Lasgun/Shield explosion; successful Traitor calls still decide the outcome directly.', - 'All-outcome payment, normal-first selection and reserve transfer counting as battle losses remain provisional interpretations, not publisher rulings. Same-plan Harass & Withdraw provisionally pays the original three-reserve cost before undialed forces return; returned reserves cannot fund or change that quote. Basic/unprofiled co-side, Stone Burner and other optional modules remain guarded.', + '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.', ], related: ['battle-cards', 'battle', 'card-recruits', 'card-harass-withdraw', 'implementation-checklist'], checklist: [ @@ -4958,7 +4960,7 @@ export const RULE_TOPICS: RuleTopic[] = [ { area: 'Player controls', status: 'Partial', detail: 'Owner-only availability, exact cost guidance and both battle selectors support the card; private category answers remain None.', evidence: ['components/battle-preparation.tsx', 'components/game-table.tsx'] }, { area: 'AI', status: 'Partial', detail: 'Four profiles consider legal physical slot candidates and the normal score bonus; strategy and calibration remain unfinished.', evidence: ['game/bots.ts'] }, { area: 'Documentation', status: 'Partial', detail: 'The printed effect and unresolved Traitor, explosion, type-selection and accounting interpretations are distinguished from this temporary prototype.', evidence: ['docs/ECAZ_TREACHERY_RULES.md', 'docs/REINFORCEMENTS_RUNTIME.md'] }, - { area: 'Verification', status: 'Partial', detail: 'Original rule/control proof and same-plan Harass regressions cover both slots, unfunded original reserves, typed cost-before-return, Traitor/explosion and fixed/variable Occupy. Actual mobile control and legal four-profile smoke are bounded evidence; complete modes, calibration and final assurance remain open.', evidence: ['tests/harass-reinforcements.test.ts', 'tests/reinforcements.test.ts', 'tests/reinforcements-engine.test.ts', 'tests/ecaz-occupy-cards-runtime.test.ts', 'docs/ECAZ_OCCUPY_RULES.md'] }, + { area: 'Verification', status: 'Partial', detail: 'Original proof plus same-plan Harass and native Stone/Mirror cases cover original typed costs, physical-token victory, fixed/variable Occupy, Traitor/explosion and retention. Actual mobile controls and legal four-profile smoke are bounded evidence; complete modes, calibration and final assurance remain open.', evidence: ['tests/stone-reinforcements.test.ts', 'tests/harass-reinforcements.test.ts', 'tests/reinforcements.test.ts', 'tests/reinforcements-engine.test.ts', 'tests/ecaz-occupy-cards-runtime.test.ts', 'docs/ECAZ_OCCUPY_RULES.md'] }, ], }, { diff --git a/tests/fixture-stone-harass.ts b/tests/fixture-stone-harass.ts index 89e5555a..8c98d588 100644 --- a/tests/fixture-stone-harass.ts +++ b/tests/fixture-stone-harass.ts @@ -78,6 +78,8 @@ 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. + 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); assert.ok([player.reserves, player.tanks, ...Object.values(player.forces)] @@ -312,6 +314,13 @@ export function stoneHarassOccupyFixture( ecazForces: options.ecazForces ?? 5, allyForces: options.allyForces ?? 4, ...(options.opponentForces === undefined ? {} : { opponentForces: options.opponentForces }), ...(options.opponentElite === undefined ? {} : { opponentElite: options.opponentElite }) }); + // Isolate selected battle cards after genuine setup/auctions, before the + // lead choice reserves any plan. Return real unselected hands to the deck; + // adding Stone, Harass and a paired reserve card must not create a fifth card. + for (const player of occupy.game.players) { + occupy.game.deck.push(...player.hand); + player.hand = []; + } const owner = lead === 'ecaz' ? occupy.ecaz : occupy.ally; const harassOwner = options.harass === false ? null : owner; const stoneOwner = options.stone === 'harass' ? owner : occupy.opponent; diff --git a/tests/reinforcements.test.ts b/tests/reinforcements.test.ts index 500204e0..5ec1b1fd 100644 --- a/tests/reinforcements.test.ts +++ b/tests/reinforcements.test.ts @@ -121,7 +121,7 @@ void test('the losing card cannot enter a Moritani Nexus retention window', () = assert.deepEqual(q.discarded, [{ player: 'a', card: REINFORCEMENTS_CARD }]); }); -void test('selected card requires own funded typed reserves and rejects malformed cards, pairs and unsupported combinations', () => { +void test('selected card requires own funded typed reserves and rejects malformed or duplicate commitments', () => { const missing = fixture(); delete missing.attacker.reinforcementsReserves; assert.throws(() => quoteBattleResolution(missing), BattleResolutionQuoteError); @@ -139,7 +139,17 @@ void test('selected card requires own funded typed reserves and rejects malforme const duplicate = fixture(); duplicate.attacker.plan.defense = reinforcement.id; assert.throws(() => quoteBattleResolution(duplicate), BattleResolutionQuoteError); - const stone = fixture(); - putCard(stone, 'defender', 'weapon', 'stoneBurner'); - assert.throws(() => quoteBattleResolution(stone), BattleResolutionQuoteError); +}); + +void test('Stone ignores the Reinforcements score bonus but still charges its original typed reserve cost', () => { + const g = fixture(); + putCard(g, 'defender', 'weapon', 'stoneBurner'); + g.defender.stoneMode = 'ignore'; + g.defender.plan.dial = 1; + g.defender.plan.support = 1; + const q = quoteBattleResolution(g); + assert.equal(q.winner, 'd', 'three undialed attacker counters lose to four; +2 is not a physical token bonus'); + assert.deepEqual(q.stone, {winner: 'defender', attacker: [3], defender: [4]}); + assert.deepEqual(q.reinforcements, [{player: 'a', card: REINFORCEMENTS_CARD, normal: 1, elite: 2}]); + assert.deepEqual(q.discarded, [{player: 'a', card: REINFORCEMENTS_CARD}]); }); diff --git a/tests/stone-reinforcements.test.ts b/tests/stone-reinforcements.test.ts new file mode 100644 index 00000000..0b87c65e --- /dev/null +++ b/tests/stone-reinforcements.test.ts @@ -0,0 +1,103 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import {viewGame, type Game} from '../game/engine'; +import {holdOccupyCardsTraitor} from './fixture-ecaz-occupy-cards'; +import { + STONE_CARD, MIRROR_CARD, stoneHarassOccupyFixture, stoneHarassPlayer, + takeStoneHarassCard, sealStoneHarassPlans, finishStoneHarassBattle, traitorIdentities, +} from './fixture-stone-harass'; + +const reinforcement = 'ecaz-reinforcements'; +function prepare({lead = 'ecaz', holder = 'lead', mirror = false, opponentForces = 6, + variableDial = 1}: {lead?: 'ecaz' | 'ally'; holder?: 'lead' | 'opponent'; mirror?: boolean; + opponentForces?: number; variableDial?: number} = {}) { + const source = stoneHarassOccupyFixture({lead, harass: false, + stone: mirror ? 'opponent' : 'harass', mirror, variableDial, + support: variableDial, ecazForces: 5, allyForces: 4, opponentForces}); + const owner = holder === 'lead' ? source.lead : source.opponent; + takeStoneHarassCard(source.game, owner, reinforcement); + const plan = source.plans.find(step => step.actor === owner)!.action; + if (plan.weapon) plan.defense = reinforcement; + else plan.weapon = reinforcement; + return {source, owner}; +} +const count = (game: Game, id: string) => game.discard.filter(card => card.id === id).length; +const has = (game: Game, owner: string, id: string) => stoneHarassPlayer(game, owner).hand.some(card => card.id === id); + +void test('same-plan Stone and Reinforcements pay only the actual fixed or variable card user without adding physical strength', () => { + for (const lead of ['ecaz', 'ally'] as const) for (const winning of [false, true]) { + const {source, owner} = prepare({lead, opponentForces: winning ? 4 : 6}); + const before = structuredClone(source.game); + const done = finishStoneHarassBattle(sealStoneHarassPlans(source), {mode: 'ignore'}); + assert.equal(done.lastBattleContext?.winner, winning ? source.lead : source.opponent, + 'the five undialed coalition counters beat four but lose to six; Reinforcements adds no tokens'); + const delta = (id: string, field: 'reserves' | 'tanks') => + stoneHarassPlayer(done, id)[field] - stoneHarassPlayer(before, id)[field]; + assert.equal(delta(owner, 'reserves'), -3); + assert.equal(delta(owner === source.ecaz ? source.ally : source.ecaz, 'reserves'), 0); + assert.equal(delta(source.ecaz, 'tanks'), winning ? lead === 'ecaz' ? 6 : 3 : lead === 'ecaz' ? 8 : 5); + assert.equal(delta(source.ally, 'tanks'), winning ? lead === 'ally' ? 4 : 1 : lead === 'ally' ? 7 : 4); + assert.equal(stoneHarassPlayer(done, source.ecaz).forces[source.location] ?? 0, winning ? 2 : 0); + assert.equal(stoneHarassPlayer(done, source.ally).forces[source.location] ?? 0, winning ? 3 : 0); + assert.equal(count(done, reinforcement), 1); + assert.equal(has(done, source.lead, STONE_CARD), winning); + assert.equal(count(done, STONE_CARD), winning ? 0 : 1); + assert.equal(done.battle, null); + } +}); + +void test('opposing Reinforcements cannot turn a five-token army into seven and still pays its original elite reserve cost', () => { + for (const lead of ['ecaz', 'ally'] as const) { + const {source, owner} = prepare({lead, holder: 'opponent', variableDial: 0, opponentForces: 5}); + const player = stoneHarassPlayer(source.game, owner); + player.reserves = 3; player.tanks = 12; + player.elites!.reserves = 1; player.elites!.tanks = 4; + const cost = viewGame(source.game, owner).battle!.reinforcements!; + assert.equal(cost.normal, 2); assert.equal(cost.elite, 1); + const done = finishStoneHarassBattle(sealStoneHarassPlans(source), {mode: 'ignore'}); + assert.equal(done.lastBattleContext?.winner, source.lead, 'six physical undialed counters beat five'); + assert.equal(stoneHarassPlayer(done, owner).reserves, 0); + assert.equal(stoneHarassPlayer(done, owner).tanks, 20); + assert.equal(stoneHarassPlayer(done, owner).elites!.tanks, 5); + assert.equal(count(done, reinforcement), 1); + assert.equal(has(done, source.lead, STONE_CARD), true); + } +}); + +void test('the independent copied Stone mode preserves Mirror custody and the reserve fee', () => { + for (const lead of ['ecaz', 'ally'] as const) { + const {source, owner} = prepare({lead, mirror: true, variableDial: 0, opponentForces: 5}); + const beforeReserves = stoneHarassPlayer(source.game, owner).reserves; + const done = finishStoneHarassBattle(sealStoneHarassPlans(source), + {modes: {[source.lead]: 'kill', [source.opponent]: 'ignore'}}); + assert.equal(done.lastBattleContext?.winner, source.lead); + assert.equal(stoneHarassPlayer(done, owner).reserves, beforeReserves - 3); + assert.equal(has(done, source.lead, MIRROR_CARD), true); + assert.equal(count(done, MIRROR_CARD), 0); + assert.equal(count(done, STONE_CARD), 1); + assert.equal(count(done, reinforcement), 1); + for (const actor of [source.lead, source.opponent]) + assert.equal(stoneHarassPlayer(done, actor).leaders.find(leader => leader.id === source.leaders[actor])!.dead, true); + } +}); + +void test('opposing successful Traitor overrides Stone kill but does not waive the Reinforcements reserve cost', () => { + for (const lead of ['ecaz', 'ally'] as const) { + const {source, owner} = prepare({lead, opponentForces: 4}); + const physical = traitorIdentities(source.game); + const leader = stoneHarassPlayer(source.game, source.lead).leaders.find(leader => + !leader.dead && !leader.usedAt && physical.has(leader.id))!; + assert.ok(leader); + source.plans.find(step => step.actor === source.lead)!.action.leader = leader.id; + holdOccupyCardsTraitor(source.game, source.opponent, leader.id); + const before = stoneHarassPlayer(source.game, owner).reserves; + const done = finishStoneHarassBattle(sealStoneHarassPlans(source), {mode: 'kill', traitors: [source.opponent]}); + assert.equal(done.lastBattleContext?.result, 'traitor'); + assert.equal(done.lastBattleContext?.winner, source.opponent); + assert.equal(stoneHarassPlayer(done, owner).reserves, before - 3); + assert.equal(stoneHarassPlayer(done, source.opponent).leaders.find(candidate => + candidate.id === source.leaders[source.opponent])!.dead, false, 'the defeated Stone attack cannot kill the winning caller’s leader'); + assert.equal(count(done, reinforcement), 1); + assert.equal(count(done, STONE_CARD), 1); + } +});