Compose same-plan Harass and Reinforcements with original reserve costs
This commit is contained in:
1 parent
feb2b03251
commit
be0644c0e9
15 files changed
+288
-30
No files matched your search
@@ -551,10 +551,8 @@ export function GameTable({
|
||||
const reinforcementsPlanReason = !reinforcementsSelected ? null :
|
||||
!g.battle?.reinforcements ? 'Reinforcements is unavailable in this battle.' :
|
||||
g.battle.reinforcements.blocked ??
|
||||
((selectedBattleWeapon?.id === 'ecaz-harass-withdraw' ||
|
||||
selectedBattleDefense?.id === 'ecaz-harass-withdraw' ||
|
||||
(!!selectedBattleWeapon && isStoneBurner(selectedBattleWeapon)))
|
||||
? 'Reinforcements cannot share this plan with Harass & Withdraw or Stone Burner.' : null);
|
||||
((!!selectedBattleWeapon && isStoneBurner(selectedBattleWeapon))
|
||||
? 'Reinforcements cannot share this plan with Stone Burner.' : null);
|
||||
const selectedBattleKwisatz = kwisatz && !!me.kwisatz?.active &&
|
||||
!me.kwisatz.dead && !g.battle?.kwisatzBlocked &&
|
||||
(!me.kwisatz.usedAt || me.kwisatz.usedAt === g.battle?.territory);
|
||||
@@ -5347,6 +5345,15 @@ export function GameTable({
|
||||
{g.battle.reinforcements.blocked && ` ${g.battle.reinforcements.blocked}`}
|
||||
</p>
|
||||
)}
|
||||
{reinforcementsSelected &&
|
||||
(selectedWeaponId === 'ecaz-harass-withdraw' || selectedDefenseId === 'ecaz-harass-withdraw') && (
|
||||
<p className="fine">
|
||||
Provisional combined timing: pay the original three-reserve
|
||||
Reinforcements cost before Harass & Withdraw returns any
|
||||
undialed forces. Returned reserves cannot fund or change that
|
||||
cost. Both physical cards are discarded after use.
|
||||
</p>
|
||||
)}
|
||||
{reinforcementsPlanReason && <p role="status" className="notice">{reinforcementsPlanReason}</p>}
|
||||
{!battlePairValid && <p role="status" className="notice">Choose two compatible battle cards. A single physical card cannot fill both slots.</p>}
|
||||
{selectedBattleWeapon && planetologistSpecial(selectedBattleWeapon) && (
|
||||
|
||||
+20
-1
@@ -29,6 +29,25 @@ optional skill bands, combinations and public expansion starts remain open.
|
||||
Rules implementation continues; comprehensive assurance and administration
|
||||
remain deferred until all rules are implemented.
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
**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.
|
||||
@@ -47,7 +66,7 @@ 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, same-plan Harass/Reinforcements, Reinforcements/Stone, own
|
||||
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.
|
||||
|
||||
|
||||
@@ -249,9 +249,11 @@ Reinforcements uses the card user's own three normal-first reserve counters
|
||||
and existing +2 score-only modifier, not ally reserves, extra physical dial
|
||||
losses or support. Existing provisional all-revealed-outcome cost/disposal
|
||||
is unchanged and remains explicitly not a publisher ruling. Both slots and
|
||||
opposing holders work; own same-plan Harass/Reinforcements and Reinforcements
|
||||
with Stone/optional modules remain guarded. Basic/unprofiled co-side card
|
||||
battles remain rejected.
|
||||
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.
|
||||
|
||||
Native/pure67 cases, relevant284-case batch and27 actual native-program
|
||||
cases pass, including all four legal policies. Two natural captured games
|
||||
|
||||
@@ -110,11 +110,12 @@ This is a legal strategy path, not strength calibration.
|
||||
returns only actual card-user forces: original fixed Ecaz commitment or the
|
||||
selected ally's native variable commitment. It never returns the other ally
|
||||
or recomputes mandatory fixed losses from the diminished board.
|
||||
- [Bounded Reinforcements](REINFORCEMENTS_RUNTIME.md) can appear in the
|
||||
opposing Battle Plan in classic-faction Basic/Advanced games. The two cards
|
||||
cannot share one player's plan; their relative same-plan timing remains
|
||||
unresolved. Both prototypes retain their separate provisional cost and
|
||||
outcome interpretations.
|
||||
- [Bounded Reinforcements](REINFORCEMENTS_RUNTIME.md) may appear in the opposing
|
||||
or the same supported Battle Plan. Same-plan use provisionally pays the
|
||||
original reserve quote before returning undialed counters; a future return
|
||||
cannot fund or change that cost. Both prototypes retain their separate
|
||||
provisional outcome interpretations. See the
|
||||
[combined policy](REINFORCEMENTS_RUNTIME.md#combined-utility-plan--16-october-2026).
|
||||
|
||||
## Scoped interaction proof — 16 October 2026
|
||||
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 16 October 2026 — Same-plan Harass and Reinforcements
|
||||
|
||||
Connected both physical slot orders without adding a new settlement mechanism
|
||||
or state field. Existing native order pays the original three-reserve quote
|
||||
before Harass returns undialed fighters. Future returns cannot fund deficient
|
||||
reserves or change quoted elite cost. This is explicitly provisional under
|
||||
choose-recommended, not a new source ruling; original all-outcome fee and
|
||||
score-only +2 remain. Own fixed/variable Occupy pools and Traitor cancellation
|
||||
stay separate. Both physical cards discard once.
|
||||
|
||||
Eleven-file134/134, types/lint and production build pass with the existing
|
||||
module-register/large-chunk/Unknown-route warnings. Six new behavioral cases
|
||||
cover original typed reserve cost, eligibility, both slots, Traitor/explosion
|
||||
and fixed/variable Occupy. Eight fresh Node programs exercise all four legal
|
||||
policies/both modes and16 accepted continuation actions.
|
||||
|
||||
Actual390px controls seal original2-normal/1-elite payment and dial1/support1.
|
||||
The +2 decisively beats opposing leader4; four ordinary forces return, but
|
||||
the original elite cost still pays. Final reserves4/Tanks16/elite Tanks5,
|
||||
spice29, both cards discarded and collection open. No root overflow or
|
||||
browser/application error. Current parse/anchor and38 topic paths resolve;
|
||||
all ten facets stay Partial. Obsolete pair rejection and absent-card pin
|
||||
removed. Other gates remain; no human server, D1, saved room, deployment or
|
||||
broad/HTTP/recovery assurance campaign changed.
|
||||
[Combined policy and proof](REINFORCEMENTS_RUNTIME.md#combined-utility-plan--16-october-2026).
|
||||
|
||||
## 16 October 2026 — Provisional post-Harass Stone comparison
|
||||
|
||||
Removed only the public Richese-family Harass guard. Under standing
|
||||
|
||||
@@ -9,8 +9,9 @@ families; Basic/unprofiled co-side use stays guarded. The
|
||||
holder may commit it in either Battle Plan card slot. It is neither a weapon
|
||||
nor a defense: Prescience of a category receives **None**, while the physical
|
||||
slot stays occupied and hidden until reveal. A named-card inspection still
|
||||
fixes the exact card. Harass & Withdraw and Stone Burner cannot share this
|
||||
plan; Homeworld and other optional-module combinations remain gated.
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -23,3 +24,53 @@ The holder must have three own reserve counters when sealing the plan. The owner
|
||||
The owner receives a card-slot explanation and disabled reason when fewer than three reserve counters remain or the bounded mode is unsupported. All four AI profiles generate legal candidates in the classic and Ecaz/Moritani profiles and can select either slot, including an Advanced plan. The current scoring is a minimal legal participation heuristic, not difficulty calibration. The prototype does not certify the whole three-card variant, other expansion combinations, optional modules, historical-save compatibility or public mode starts.
|
||||
|
||||
Focused rule, engine, controls and in-memory SQLite tests cover both slots, Basic/Advanced payment, elite subpool conservation, traitor/explosion disposal, private sealed views, JSON restoration, rejected-action immutability and competing final room commits, now including a paired Ecaz/Moritani saved battle. The test results and actual browser/UI evidence for the current checkpoint are recorded in [Implementation status](IMPLEMENTATION_STATUS.md); test coverage is not a publisher ruling.
|
||||
|
||||
## Combined utility plan — 16 October 2026
|
||||
|
||||
The standing choose-recommended priority selects the existing native settlement
|
||||
order for the two physical specials in one plan: quote and debit three
|
||||
**original own reserve** counters before Harass returns any undialed counters.
|
||||
This is an implementation interpretation, not a newly retrieved publisher FAQ
|
||||
or an answer to the earlier reserve-payment question. The source audit's printed
|
||||
effects and original provisional all-outcome/normal-first cost are unchanged.
|
||||
|
||||
Three reserves must exist at commitment. A future return cannot fund a deficient
|
||||
plan or change its quoted normal/elite mix. For example, two original ordinary
|
||||
and one elite reserve are spent even when Harass subsequently returns four
|
||||
ordinary counters. Both cards discard once. The +2 remains normal-score-only,
|
||||
not extra physical dial, support or winner casualties. Opposing successful
|
||||
Traitor cancels only Harass; Reinforcements still pays under its existing
|
||||
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.
|
||||
No persisted state or fresh/public profile is added.
|
||||
|
||||
### Scoped proof
|
||||
|
||||
The affected eleven-file offline union passes **134/134**, with types/lint and
|
||||
production build passing (module-register, large-chunk and Unknown-route
|
||||
warnings). Six new behavioral regressions cover both slots, original typed
|
||||
costs, deficient original reserves, all Traitor outcomes, explosion and both
|
||||
active Occupy card-user roles. The test battle uses rock so automatic
|
||||
collection income cannot obscure support accounting.
|
||||
|
||||
Fresh Node programs exercise eight Basic/Advanced cases across all four
|
||||
profiles, both slot orders and16 accepted continuation actions. A real styled
|
||||
390px journey starts at three reserves (two normal, one elite), selects both
|
||||
cards and shows the explicit provisional order. Original dial1/support1 with
|
||||
leader2 plus bonus2 beats opposing leader4. Four ordinary fighters return;
|
||||
the original elite reserve is spent, leaving reserves4/Tanks16/elite Tanks5,
|
||||
spice29 and no army, battle or decision. Both cards discard once; native
|
||||
collection opens. No root overflow or browser/application error occurred.
|
||||
|
||||
Current guide parse, combined-policy anchor and38 evidence paths across the
|
||||
two topics resolve; all ten checklist facets remain Partial. Removed the
|
||||
obsolete pair rejection and an incidental absent-card candidate pin. No
|
||||
human server, saved room, D1, deployment or public gate changed; no new broad,
|
||||
HTTP, recovery/privacy/custody assurance campaign ran. Private proof artifacts
|
||||
remain outside Git; the owned source host and throwaway scripts were removed.
|
||||
|
||||
|
||||
@@ -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 user question still asks whether to keep the card disabled, always transfer three reserve forces on every outcome, or waive only a sole traitor-call winner's cost; no answer is assumed. The bounded classic and Ecaz/Moritani prototype provisionally charges on all revealed outcomes, chooses normal reserves before elite and counts the transfer as battle losses. Those are product interpretations, not a ruling. Same-plan Harass/Stone Burner remain gated; opposing Harass is supported. Eligibility never depends on concealed matching Traitors. | [Source cost review](ECAZ_TREACHERY_RULES.md#14-september-reinforcements-cost-review) · [Bounded runtime](REINFORCEMENTS_RUNTIME.md) |
|
||||
| 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) |
|
||||
| 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) |
|
||||
|
||||
@@ -411,9 +411,8 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
|
||||
!(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) &&
|
||||
!slots.some(isHarassWithdraw) &&
|
||||
!hasStone,
|
||||
'Reinforcements cannot combine with Homeworld, leader skills, own Harass & Withdraw or Stone Burner.');
|
||||
'Reinforcements cannot combine with Homeworld, leader skills or Stone Burner.');
|
||||
const reserves = side.reinforcementsReserves!;
|
||||
requireQuote(!!reserves && typeof reserves === 'object' && !Array.isArray(reserves),
|
||||
'Reinforcements needs own typed reserves.');
|
||||
|
||||
+1
-2
@@ -823,8 +823,7 @@ 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 && (weapon === 'ecaz-harass-withdraw' ||
|
||||
defense === 'ecaz-harass-withdraw' || isStoneBurner(w))) continue;
|
||||
if (reinforcements && isStoneBurner(w)) continue;
|
||||
if (voice) {
|
||||
const used =
|
||||
playedVoiceMatch(
|
||||
|
||||
@@ -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. Charging that cost on every revealed outcome is provisional; same-plan Harass & Withdraw, 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 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.',
|
||||
topics: RULE_TOPICS.filter((topic) => topic.id === (ecaz.card.effect === 'harassWithdraw' ? 'card-harass-withdraw' : ecaz.card.effect === 'reinforcements' ? 'card-reinforcements' : 'card-recruits')),
|
||||
};
|
||||
const topicIds =
|
||||
|
||||
+2
-2
@@ -21252,8 +21252,8 @@ function validatePlan(
|
||||
'Chemistry as a weapon needs another defense; Weirding Way as a defense needs another weapon.',
|
||||
);
|
||||
if ([w, d].some(isReinforcements)) {
|
||||
requireRule(![w, d].some(card => isHarassWithdraw(card) || isStoneBurner(card)),
|
||||
'Reinforcements cannot share this bounded plan with Harass & Withdraw or Stone Burner.');
|
||||
requireRule(![w, d].some(isStoneBurner),
|
||||
'Reinforcements cannot share this bounded plan with Stone Burner.');
|
||||
currentReinforcementsCost(g, p);
|
||||
}
|
||||
if ([w, d].some(isHarassWithdraw)) currentHarassWithdrawQuote(g, p, plan);
|
||||
|
||||
+4
-3
@@ -4950,15 +4950,15 @@ 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. The explicit Advanced Occupy/independent-card profile is verified through native and human play; Basic/unprofiled co-side, own same-plan Harass/Reinforcements, 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 & 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.',
|
||||
],
|
||||
related: ['battle-cards', 'battle', 'card-recruits', 'card-harass-withdraw', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Physical card, both slots, own typed reserve cost, score-only bonus and single mandatory discard connect in classic/paired modes and explicit native Advanced Occupy. Selected lead cards/payment and variable ally casualties stay separate.', evidence: ['game/reinforcements.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Physical card, both slots, own typed reserve cost, score-only bonus and mandatory discard connect in supported profiles. Same-plan Harass preserves the original reserve quote before return; selected lead cards/payment and variable ally casualties stay separate.', evidence: ['game/reinforcements.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
|
||||
{ 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: 'Existing rule/control proofs plus combined-card67 cases, actual27-case program, four policies, natural combined games and390px decisive equal-leader Reinforcements cover the bounded profile. Full faction/optional-module acceptance remains open; comprehensive assurance follows all rules.', evidence: ['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 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'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -4975,6 +4975,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Discard Harass after use, including a win by your own Traitor call. Reveal-before-explosion return and card-specific discard precedence are explicit implementation inferences; neither is a dedicated combined FAQ ruling.',
|
||||
'For multiple legal physical allocations, choose all undialed ordinary and elite counters by sector after both plans reveal and before traitor declarations. Unique returns are automatic. The selected return is public; it is not an extra sealed plan element. Stone Burner provisionally compares the undialed forces remaining after successful withdrawal, including same-plan, opposing and copied Stone. Additional optional modules remain guarded. The printed card forbids use on your own Homeworld.',
|
||||
'In explicitly selected Advanced Occupy, only the actual card user’s undialed forces return. An Ecaz user preserves its original mandatory fixed count at free full strength; an allied user uses its native variable commitment, while canceled Occupy uses its selected lead’s ordinary own pool. Returned reserves remain outside losses and Face Dance. Basic/unprofiled co-side, public starts and full acceptance remain gated.',
|
||||
'In supported same-plan Reinforcements, the provisional order pays three original own reserves before Harass returns undialed counters. Future returns cannot fund or change that reserve quote; neither the +2 score bonus nor that cost adds to the physical commitment. Both cards discard after use. An opposing successful Traitor cancels Harass but not the provisional Reinforcements cost.',
|
||||
],
|
||||
related: ['battle-cards', 'battle', 'card-recruits', 'ecaz-modules', 'implementation-checklist'],
|
||||
checklist: [
|
||||
|
||||
@@ -30,7 +30,6 @@ void test('each AI profile can commit Harass in either slot through genuine Basi
|
||||
assert.equal(sealed.battle!.plans.a[slot], 'ecaz-harass-withdraw');
|
||||
harassCustody(sealed);
|
||||
}
|
||||
assert.ok(candidates.every(action => action.weapon !== 'ecaz-reinforcements' && action.defense !== 'ecaz-reinforcements'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {applyAction, viewGame, type Action, type Game} from '../game/engine';
|
||||
import {finishHarassPreparation, harassCustody, harassWithdrawGame, takeHarassCard} from './fixture-harass-withdraw';
|
||||
import {chooseEcazOccupyLead, ecazOccupyFixture, openEcazOccupyPlans} from './fixture-ecaz-occupy';
|
||||
import {finishOccupyCardsAftermath, holdOccupyCardsTraitor} from './fixture-ecaz-occupy-cards';
|
||||
|
||||
const harass = 'ecaz-harass-withdraw';
|
||||
const reinforcements = 'ecaz-reinforcements';
|
||||
function fixture(advanced = false) {
|
||||
// Rock has no collection income to obscure original battle support payments.
|
||||
const game = harassWithdrawGame({advanced, factions: ['emperor', 'atreides', 'guild'],
|
||||
territory: 'shield_wall', sector: 8});
|
||||
takeHarassCard(game, 'a', reinforcements);
|
||||
return game;
|
||||
}
|
||||
function ownPlan(game: Game, slot: 'weapon' | 'defense' = 'weapon'): Action {
|
||||
return {type: 'battlePlan', dial: 1, support: game.advanced ? 1 : 0,
|
||||
leader: 'emperor-4', weapon: slot === 'weapon' ? reinforcements : harass,
|
||||
defense: slot === 'weapon' ? harass : reinforcements};
|
||||
}
|
||||
function seal(game: Game, slot: 'weapon' | 'defense' = 'weapon', other: Partial<Action> = {}) {
|
||||
game = applyAction(game, 'a', ownPlan(game, slot));
|
||||
return applyAction(game, 'd', {type: 'battlePlan', dial: 0, support: 0,
|
||||
leader: 'atreides-2', ...other});
|
||||
}
|
||||
function finish(game: Game, calls: readonly string[] = []) {
|
||||
for (const player of ['a', 'd']) game = applyAction(game, player,
|
||||
{type: 'traitorCall', call: calls.includes(player)});
|
||||
game = finishOccupyCardsAftermath(game);
|
||||
harassCustody(game);
|
||||
return game;
|
||||
}
|
||||
function assertDiscarded(game: Game) {
|
||||
for (const id of [harass, reinforcements]) {
|
||||
assert.equal(game.discard.filter(card => card.id === id).length, 1);
|
||||
assert.ok(game.players.every(player => !player.hand.some(card => card.id === id)));
|
||||
}
|
||||
}
|
||||
|
||||
void test('both same-plan slot orders add the decisive bonus without changing Harass commitment, support or disposal', () => {
|
||||
for (const advanced of [false, true]) for (const slot of ['weapon', 'defense'] as const) {
|
||||
const game = fixture(advanced);
|
||||
const done = finish(seal(game, slot));
|
||||
assert.equal(done.lastBattleContext?.winner, 'a', 'dial1 + leader2 + bonus2 beats leader4');
|
||||
assert.equal(done.players[0].reserves, 16, 'fifteen original reserves minus cost3 plus four returned');
|
||||
assert.equal(done.players[0].tanks, 4, 'three reserve counters and only the one original dialed counter');
|
||||
assert.deepEqual(done.players[0].forces, {});
|
||||
assert.equal(done.players[0].spice, advanced ? 29 : 30);
|
||||
assert.equal(done.players[0].leaders.find(leader => leader.id === 'emperor-4')!.dead, false);
|
||||
assertDiscarded(done);
|
||||
assert.equal(done.battle, null);
|
||||
assert.equal(done.decision, null);
|
||||
}
|
||||
});
|
||||
|
||||
void test('returned ordinary counters cannot replace the original elite reserve cost', () => {
|
||||
const game = fixture(true), owner = game.players[0];
|
||||
owner.reserves = 3;
|
||||
owner.tanks = 12;
|
||||
owner.elites!.reserves = 1;
|
||||
owner.elites!.tanks = 4;
|
||||
harassCustody(game);
|
||||
const preview = viewGame(game, 'a').battle!.reinforcements!;
|
||||
assert.equal(preview.normal, 2);
|
||||
assert.equal(preview.elite, 1);
|
||||
const done = finish(seal(game));
|
||||
assert.equal(done.players[0].reserves, 4);
|
||||
assert.equal(done.players[0].elites!.reserves, 0);
|
||||
assert.equal(done.players[0].elites!.tanks, 5, 'the original elite reserve was spent before ordinary returns');
|
||||
assert.equal(done.players[0].tanks, 16);
|
||||
assert.equal(done.players[0].spice, 29);
|
||||
assertDiscarded(done);
|
||||
});
|
||||
|
||||
void test('a future Harass return cannot fund an underfunded same-plan Reinforcements commitment', () => {
|
||||
const game = fixture(true), owner = game.players[0];
|
||||
owner.reserves = 2;
|
||||
owner.tanks = 13;
|
||||
owner.elites!.reserves = 1;
|
||||
owner.elites!.tanks = 4;
|
||||
harassCustody(game);
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, 'a', ownPlan(game)), /three own forces in reserves/);
|
||||
assert.deepEqual(game, before);
|
||||
});
|
||||
|
||||
void test('own, opposing and mutual Traitors preserve the original Reinforcements cost and cancel only opposing Harass', () => {
|
||||
for (const advanced of [false, true]) for (const calls of [['a'], ['d'], ['a', 'd']]) {
|
||||
const game = fixture(advanced);
|
||||
if (calls.includes('a')) holdOccupyCardsTraitor(game, 'a', 'atreides-2');
|
||||
if (calls.includes('d')) holdOccupyCardsTraitor(game, 'd', 'emperor-4');
|
||||
const done = finish(seal(game), calls);
|
||||
const canceled = calls.includes('d');
|
||||
assert.equal(done.lastBattleContext?.result, calls.length === 2 ? 'mutualTraitors' : 'traitor');
|
||||
assert.equal(done.lastBattleContext?.winner, calls.length === 2 ? null : calls[0]);
|
||||
assert.equal(done.players[0].reserves, canceled ? 12 : 16);
|
||||
assert.equal(done.players[0].tanks, canceled ? 8 : 3);
|
||||
assert.deepEqual(done.players[0].forces, canceled ? {} : {'shield_wall:8': 1});
|
||||
const bounty = !canceled ? game.players[1].leaders.find(leader => leader.id === 'atreides-2')!.strength : 0;
|
||||
assert.equal(done.players[0].spice, game.players[0].spice - (advanced && canceled ? 1 : 0) + bounty);
|
||||
assertDiscarded(done);
|
||||
}
|
||||
});
|
||||
|
||||
void test('same-plan utility cards both pay and discard while Harass returns precede a Lasgun Shield explosion', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const game = fixture(advanced);
|
||||
const laser = game.deck.find(card => card.kind === 'lasgun')!;
|
||||
const shield = game.deck.find(card => card.kind === 'shield')!;
|
||||
takeHarassCard(game, 'd', laser.id);
|
||||
takeHarassCard(game, 'd', shield.id);
|
||||
const done = finish(seal(game, 'weapon', {weapon: laser.id, defense: shield.id}));
|
||||
assert.equal(done.lastBattleContext?.result, 'explosion');
|
||||
assert.equal(done.lastBattleContext?.winner, null);
|
||||
assert.equal(done.players[0].reserves, 16);
|
||||
assert.equal(done.players[0].tanks, 4);
|
||||
assert.deepEqual(done.players[0].forces, {});
|
||||
assert.equal(done.players[1].tanks, 5);
|
||||
assertDiscarded(done);
|
||||
}
|
||||
});
|
||||
|
||||
void test('active Occupy same-plan utilities pay only the actual card user and retain original fixed versus variable losses', () => {
|
||||
for (const lead of ['ecaz', 'ally'] as const) {
|
||||
const source = ecazOccupyFixture({ecazTreachery: true, allyFaction: 'guild', opponentFaction: 'emperor',
|
||||
ecazForces: 5, allyForces: 4});
|
||||
const owner = lead === 'ecaz' ? source.ecaz : source.ally;
|
||||
takeHarassCard(source.game, owner, harass);
|
||||
takeHarassCard(source.game, owner, reinforcements);
|
||||
let game = finishHarassPreparation(openEcazOccupyPlans(chooseEcazOccupyLead(source, lead)));
|
||||
const before = structuredClone(game);
|
||||
const profile = viewGame(game, owner).battle!.ecazOccupy!.profile!;
|
||||
const player = game.players.find(player => player.id === owner)!;
|
||||
const other = game.players.find(player => player.id === source.opponent)!;
|
||||
game = applyAction(game, owner, {type: 'battlePlan', dial: profile.fixedEcazDial + 1, support: 1,
|
||||
leader: player.leaders.find(leader => !leader.dead && !leader.usedAt)!.id,
|
||||
weapon: reinforcements, defense: harass});
|
||||
game = applyAction(game, source.opponent, {type: 'battlePlan', dial: 0, support: 0,
|
||||
leader: [...other.leaders].sort((a,b) => a.strength - b.strength)[0].id});
|
||||
for (const voter of viewGame(game, owner).battle!.traitorVoters)
|
||||
game = applyAction(game, voter, {type: 'traitorCall', call: false});
|
||||
game = finishOccupyCardsAftermath(game);
|
||||
assert.equal(game.lastBattleContext?.winner, owner);
|
||||
const delta = (id: string, field: 'reserves' | 'tanks') =>
|
||||
game.players.find(player => player.id === id)![field] - before.players.find(player => player.id === id)![field];
|
||||
assert.equal(delta(owner, 'reserves'), lead === 'ecaz' ? -1 : 0);
|
||||
assert.equal(delta(source.ecaz, 'tanks'), lead === 'ecaz' ? 6 : 3);
|
||||
assert.equal(delta(source.ally, 'tanks'), lead === 'ally' ? 4 : 1);
|
||||
assert.equal(game.players.find(player => player.id === source.ecaz)!.forces[source.location] ?? 0,
|
||||
lead === 'ecaz' ? 0 : 2);
|
||||
assert.equal(game.players.find(player => player.id === source.ally)!.forces[source.location] ?? 0,
|
||||
lead === 'ally' ? 0 : 3);
|
||||
assertDiscarded(game);
|
||||
}
|
||||
});
|
||||
@@ -143,7 +143,7 @@ void test('Reinforcements normal-first cost preserves elite subpool and is paid
|
||||
assert.equal(done.discard.filter(c => c.id === cardId).length, 1);
|
||||
});
|
||||
|
||||
void test('insufficient reserves, unsupported module, and a paired special reject without mutating the original game', () => {
|
||||
void test('insufficient reserves and unsupported modules reject without mutating the original game', () => {
|
||||
const g = fixture(true);
|
||||
const p = g.players[0];
|
||||
p.reserves = 2;
|
||||
@@ -157,8 +157,6 @@ void test('insufficient reserves, unsupported module, and a paired special rejec
|
||||
p.tanks = 0;
|
||||
p.elites!.reserves = 5;
|
||||
p.elites!.tanks = 0;
|
||||
reject(g, 'a', { type: 'battlePlan', dial: 1, leader: 'emperor-0', weapon: cardId,
|
||||
defense: 'ecaz-harass-withdraw' }, /cannot share/);
|
||||
g.sandtrout = {} as Game['sandtrout'];
|
||||
assert.throws(() => applyAction(g, 'a',
|
||||
{ type: 'battlePlan', dial: 1, leader: 'emperor-0', weapon: cardId }), RuleError);
|
||||
|
||||
Reference in new issue
Block a user