From 05c4c8c7fbfdf3e6ace18ef31eb02bf4afd0ac75 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:34:48 +1100 Subject: [PATCH] Compose Recruits with current classic Homeworld revival rates --- components/game-table.tsx | 7 ++ components/recruits.tsx | 1 + docs/CURRENT_STATUS.md | 27 ++++- docs/HARASS_WITHDRAW_RUNTIME.md | 5 +- docs/IMPLEMENTATION_STATUS.md | 55 +++++++++++ docs/RECRUITS_RULES.md | 8 ++ docs/RECRUITS_RUNTIME.md | 60 +++++++++++- docs/RULE_DECISIONS.md | 5 +- game/card-presentation.ts | 2 +- game/engine.ts | 6 +- game/reference.ts | 12 +-- tests/fixture-recruits-homeworld.ts | 146 ++++++++++++++++++++++++++++ tests/recruits-homeworld.test.ts | 126 ++++++++++++++++++++++++ tools/start-prototype.ts | 2 +- 14 files changed, 443 insertions(+), 19 deletions(-) create mode 100644 tests/fixture-recruits-homeworld.ts create mode 100644 tests/recruits-homeworld.test.ts diff --git a/components/game-table.tsx b/components/game-table.tsx index c5fdbd74..6d6a85cf 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -4373,9 +4373,16 @@ export function GameTable({ }, g.revival.prevented || !me.tanks || + eliteAmount > g.revival.eliteRemaining || amount > g.revival.forcesRemaining || !!homeworldRevivalActionBlock(g, { type: 'revive', amount, elite: eliteAmount }), )} + {eliteAmount > g.revival.eliteRemaining && ( +

+ {g.revival.eliteRemaining} special-force revivals remain this turn. + Reduce the selected elite count. +

+ )} {homeworldRevivalActionBlock(g, { type: 'revive', amount, elite: eliteAmount }) && (

{homeworldRevivalActionBlock(g, { type: 'revive', amount, elite: eliteAmount })} diff --git a/components/recruits.tsx b/components/recruits.tsx index c68f95b9..aae98083 100644 --- a/components/recruits.tsx +++ b/components/recruits.tsx @@ -24,6 +24,7 @@ export function Recruits({ game, act, busy }: {

Already completed revivals still count. Paid forces retain their normal prices, and special-force revival limits still apply.

Provisional late-play accounting: prior paid revivals keep their payments and count toward the total force limit, but only recorded free returns consume the doubled free allowance. No refund or retroactive reclassification. This is a first-version policy, not a publisher clarification or numerical user ruling.

+ {game.homeworlds &&

Provisional current-rate composition: double the native free rate including its low-population bonus, capped by the seven-force total. Later separate requests recheck population and recorded free use; accepted returns keep their price. Normal and special counters retain native destinations and special-force limits.

} diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index d920c6e2..f94491db 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -29,11 +29,34 @@ optional skill bands, combinations and public expansion starts remain open. Rules implementation continues; comprehensive assurance and administration remain deferred until all rules are implemented. +**Current-rate Homeworld Recruits — 16 October.** Clean classic2–6 Basic and +Advanced independent36-card setup now admits native Revival. Reuse current +free rates/low bonuses, recorded usage and typed deposits, not a new saved +snapshot. Publisher E3 pp11/16 gives low Fremen4 doubled but capped7. +Separate-request population timing is explicitly provisional: bulk7 is free; +1 first crossing native2→3 leaves5 further free, total6, with no paid seventh +without Tleilaxu. Accepted prices/classifications remain. Special-force caps, +already-ready opportunity and no-refund prior paid accounting stay native. + +Four actual mobile journeys prove bulk7, typed1+5, ready/paid Atreides and +mixed Emperor return. A stale elite1/remaining0 selection now disables the +invalid repeat with a limit notice; ordinary counters remain selectable. +16 fresh programmes/four minimal policies accept104 card/Revival/readiness +actions into phase5. Ten new regressions pass; related18-file167/167, +types/lint and build pass. The test-only nullable-world narrowing is followed +by a passing10-case rerun. CLI help and207 guide evidence paths/source anchor +pass. Initial QA middleware ordering and eight Vite dependency aborts are +recorded, not called zero diagnostics. No overflow/application exception. +Owned hosts/tab/scaffolds removed; no games/DBs/human services touched. +Reinforcements retains its explicit Homeworld gate. Other overlays/rosters, +public starts, whole-mode/final assurance and deployment remain gated. + **Classic Homeworld Harass — 16 October.** Fresh classic2–6 Basic/Advanced Homeworlds can compose the original independent36-card variant. Harass works on Arrakis or a foreign World, never any own World, including both Emperor -Worlds in Advanced. Other two variant cards retain their World guards; no other -overlays, expansion rosters, public starts or played-save conversion open. +Worlds in Advanced. Recruits uses the separate native-rate composition above; +Reinforcements keeps its World guard. No other overlays, expansion rosters, +public starts or played-save conversion open. Actual undialed counters return through one native custody quote: Basic original primary retains typed elites; Advanced normal→Kaitain/elite→Salusa. diff --git a/docs/HARASS_WITHDRAW_RUNTIME.md b/docs/HARASS_WITHDRAW_RUNTIME.md index 94081a10..77e56b66 100644 --- a/docs/HARASS_WITHDRAW_RUNTIME.md +++ b/docs/HARASS_WITHDRAW_RUNTIME.md @@ -145,8 +145,9 @@ physical cards before original classic2–6 Basic/Advanced setup: base33+3. Harass composes on Arrakis and foreign Homeworlds. The printed own-World prohibition remains, including both Advanced Emperor Worlds. Expansion rosters, other overlays, public starts and played-game conversion stay closed. -Recruits/Reinforcements retain their existing Homeworld module guards; this -is not certification of the entire three-card variant. +Reinforcements retains its Homeworld guard; Recruits has a separate +[native current-rate composition](RECRUITS_RUNTIME.md#classic-homeworld-composition--16-october-2026). +This is not certification of the entire three-card variant. Arrakis returns remove exact ordinary/elite board counters and use the existing native reserve deposit quote. Foreign-World returns atomically diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 5e424f88..94f04ace 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,60 @@ # Dune implementation status +## 16 October 2026 — Recruits with current native Homeworld rates + +Final types/lint pass after narrowing the test helper's nullable World view; +the corrected native10-case file also passes. Related18-file167/167 and build +proof below used unchanged production behavior; no redundant broad rerun. + +Before-state a genuine classic Homeworld Revival with the physical Recruits +owned rejects at its blanket module gate. Reuse the clean classic2–6 +Basic/Advanced independent36-card profile. The shared revival calculation +already doubles current native free rates, retains accepted group prices and +deposits actual typed counters. Admit that path; keep an explicit independent +Reinforcements World guard so relaxing the shared prerequisite does not open +its unimplemented native fee/source behavior. + +Publisher E3 pp11/16 directly gives low Fremen4 doubled but capped7. The +standing choose-recommended instruction selects existing separate-request +population observations as visibly provisional timing: one group7 is free; +one first crossing native2→3 leaves5 more, total6. No Tleilaxu means no paid +seventh. This is not a new publisher timing answer or phase-wide snapshot. +Original special limits, native destinations/deployment eligibility, unknown +ledger/pending/late-Fremen-grant/replay and other overlay/roster guards remain. + +Two new fixture/test files exercise10 meaningful native cases in both modes. +Original setup/physical shipment and two zero-dial losses create Fremen18 and +Atreides10 dead, not assigned Tanks/populations/wallets/phases. Physical hands +and unopened first-Blow cards are positioned; an existing Recruits is moved to +its explicit holder, not claimed as an unstaged draw. Actual Emperor mixed +losses use the prior native Harass programme. Basic preserves star identity; +Advanced keeps one-per-turn special limits and Salusa deposits. Already-ready +Atreides' actual3 prior returns retain2 paid spice and allow2 further free, +total5/free4; no refund or readiness bypass is invented. + +Four actual390px GameTable paths observe Basic bulk7, Advanced typed1+5, +ready/paid Atreides and mixed Emperor return. Actual first Fedaykin revival +revealed an enabled stale elite1 selection despite remaining0. The button now +disables that over-cap selection and displays the remaining limit; fresh AX +and screenshot prove the disabled action, then normal5 returns complete. +Server caps remain unchanged. All four surfaces preserve native custody, +once-only discard and original prices, without overflow/application exception. + +Sixteen fresh programmes/four policies accept104 actual card/Revival/readiness +actions into original phase5, including useful owned activation while already +ready. Related18-file offline union167/167 and build pass. A new test helper +needed explicit nullable-world view narrowing; no production rule was changed +for that compiler finding. Actual CLI help executes; guide import checks207 +unique evidence paths and the new source anchor. Both coverages remain Partial. +Existing build register/large-chunk/route-classification warnings remain. + +Eight old Vite dependency requests abort during startup. The initial owned +host had a QA-only middleware ordering error, corrected before successful +journeys; stale browser node handles were re-observed without replaying accepted +actions. No zero-diagnostic claim. Proof artifacts remain outside Git; owned +hosts/tab/scaffolds removed. Existing games/DBs/human services, public starts, +deployment and recovery/privacy/custody certification are unchanged. + ## 16 October 2026 — Native Homeworld Harass return custody Before-state fresh classic Homeworlds plus independent cards rejected before diff --git a/docs/RECRUITS_RULES.md b/docs/RECRUITS_RULES.md index 546f816f..1156d7d5 100644 --- a/docs/RECRUITS_RULES.md +++ b/docs/RECRUITS_RULES.md @@ -8,6 +8,14 @@ returns without refund. This is an explicitly provisional implementation choice, not an answer to the publisher/user question below. See [the current runtime contract](RECRUITS_RUNTIME.md#provisional-late-paid-activation--8-october-2026). +**Native Homeworld follow-up — 16 October 2026:** the clean classic prototype +composes the original separate-request threshold calculation with doubled +current rates. One accepted low-Fremen group may take7;1 first reaching high +leaves5 further free. Prior accepted prices/free classifications remain; no +phase-wide snapshot or paid Fremen permission is added. This timing is visibly +provisional, not a new FAQ or resolution of all later-rate questions. See the +[native contract](RECRUITS_RUNTIME.md#classic-homeworld-composition--16-october-2026). + ## Primary evidence diff --git a/docs/RECRUITS_RUNTIME.md b/docs/RECRUITS_RUNTIME.md index 4c2b9c12..ad2b2d0d 100644 --- a/docs/RECRUITS_RUNTIME.md +++ b/docs/RECRUITS_RUNTIME.md @@ -28,6 +28,62 @@ controls in Chromium produce the same available2/remaining4 quote and preserve spice18/total3/free2 at activation. No broad suite, recovery or deployed proof. +## Classic Homeworld composition — 16 October 2026 + +The fresh `ecaz-treachery --homeworlds` entry now admits Recruits during +original classic2–6 Basic/Advanced Revival, with its original36-card inventory. +Reuse native current free rates and typed revival deposits. No rate snapshot, +new saved field, seven extra counters, automatic refund, generic module +activation or played-save conversion is added. Reinforcements keeps its own +explicit Homeworld gate; other optional overlays and expansion rosters stay +outside this clean composition. + +Publisher Ecaz & Moritani printed pp11/16 specifically gives low Fremen4, +doubled but capped at7. Under the standing choose-recommended instruction, +compose the existing native separate-request population rule: quote against +current population when each request is accepted, retaining that group's +accepted free/paid classification. Later separate requests observe the +resulting population and actual recorded free usage. This changing-threshold +timing is visibly provisional, not a new publisher worked example or answer +to the earlier late-rate question. + +Thus low Fremen at2 native counters can accept7 free together. If it accepts1 +first, reaching high3, the next native rate is3→6 and only5 free remain. With +Tleilaxu absent it cannot buy a seventh after those six. The total card cap +is still7, not an independent paid-revival permission. Original special-force +limits remain: Advanced one Fedaykin/Sardaukar per turn; Basic preserves +physical star identities without adding the Advanced one-per-turn rule. +Original destination/deployment eligibility remains native, not guaranteed +merely because Recruits increased a rate. + +Atreides with actual3 prior returns (2 free/1 paid) can accept2 further free +after play, even if already ready while phase4 remains open. Its payment +stays2 spice, total5/free4. A native mixed Emperor return deposits normal on +Kaitain and Sardaukar on Salusa in Advanced; Basic retains its primary World. +The new panel names the provisional current-rate policy. An actual Advanced +Fedaykin return exposed an enabled repeat with stale selected elite1 against +remaining0: the button now disables that over-cap selection and explains the +remaining special limit, without altering authoritative revival rules. + +Four actual390px GameTable journeys cover Basic bulk7, Advanced typed1+5, +already-ready paid Atreides and Advanced mixed Emperor revival. Native low→high +quotes, physical custody, no refund and exact once-only discard are observed. +Sixteen fresh programmes/four minimal policies accept104 card/Revival/readiness +actions through original phase5, including useful activation by an already-ready +Atreides. Real setup/shipments/zero-dial losses produce the dead counters; +only physical hands/unopened first-Blow cards are positioned, with the existing +Recruits moved to the selected holder explicitly. These are not unstaged +full-game card draws. Eight startup Vite dependency aborts and a corrected +QA-only middleware ordering issue precede successful surfaces; no overflow +or application exception. Owned hosts/tab/scaffolds removed; proof outside Git. + +Final checks:10 new cases pass, related18 offline files167/167, types/lint and +app build pass. A test-only nullable-view narrowing is followed by a passing +10-case rerun; production behavior is unchanged. CLI help executes and guide +import resolves207 unique evidence paths/source anchor, with both coverages +Partial. Existing build register/large-chunk/route-classification warnings +remain; no HTTP/DB boundary changed or new recovery assurance ran. + ## Rules contract [The source audit](RECRUITS_RULES.md) records publisher Ecaz & Moritani pages 11 @@ -42,8 +98,8 @@ source review. No newly retrieved late-paid ruling is claimed. The scope permits activation before returns or after consistently recorded free/paid normal returns, once priority windows and transactions finish. Unknown legacy usage, later Fremen grants, same-turn replay after recovery -and additional optional modules remain guarded. These are unfinished cases, -not new printed timing rules. +and other optional modules remain guarded, apart from the clean classic +Homeworld composition above. These are boundaries, not printed timing rules. Already-ready players retain their Revival opportunity while phase four is open. Atreides doubles two to four, Fremen three to six, and Guild one to two. Native diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 5975a930..241ecc81 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -662,7 +662,8 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP | Mixed No-Field Occupy | Extend the existing provisional additive ordinary-plus-reserve-capped-marker pool into selected native Advanced coalition battles, as ally or opponent. Original fixed Ecaz strength, support payer/counter owners, Traitor/actual reveal and history stay; prevention restores selected-lead-only pool. Selected Ecaz plan controls the ally's private variable quote, not its own ordinary maximum; rivals get no concealed increment. The FAQ mixed-cap ambiguity remains unresolved, not newly adjudicated. Basic mixed-co-side and other profile/module gates remain. | [Exact composition](RICHESE_NO_FIELD_RULES.md#mixed-native-occupy-pool--16-october-2026) | | No-Field / Atreides special plan | Supplied physical p27 C bars ordinary dial Prescience; distinct p27 F grants an entire-plan special Karama and p36 B names Prescience, not separately F. Under standing choose-recommended, provisionally allow F's actual complete private plan read, retaining C's dial bar. Original target-first commitment is not public No-Field revelation; both seals still materialize once, including zero/caps, then original costs/losses/history run. No token inventory or automatic allied inspection entitlement is granted. This is explicit textual/timing composition, not a new combined FAQ/user answer or mode opening. | [Exact source and scope](ATREIDES_FULL_PLAN_TIMING.md#supplied-source-no-field-composition--16-october-2026) | | No-Field / current Truthtrance fact | Broad game-related yes/no-fact authority supports the distinct current concealed printed-value predicate0/3/5. No marker/revealed history is No even for0; capped/exhausted5 remains printed5. Read the respondent's own model, never an ally or planned shipment. Publish only aggregate Yes/No after the private expected answer; information disclosure is not physical revelation or future-use obligation. This is explicit composition, not a dedicated No-Field FAQ. Existing physical-counter exclusion, native disposal/parent and mode gates remain. | [Source and current-fact scope](RICHESE_NO_FIELD_RULES.md#current-concealed-value-truthtrance--16-october-2026) | -| Harass / classic Homeworlds | Fresh classic2–6 Basic/Advanced independent36-card/Homeworld composition admits Arrakis and foreign-World returns, never an own World. Return exact typed board/visitor counters through native custody: Basic primary; Advanced Emperor normal→Kaitain/elite→Salusa. This destination/timing composition is explicit, not a new card FAQ or separate Nexus ruling. Original sealed commitment/support remains; subsequent population observations cannot rescore it. Homeworld loss receipts retain fully-committed Harass complements; native-only Traitors, late defense/explosion and after-use discard stay. Other cards, faction/overlay and public gates remain. | [Source and native routing](HARASS_WITHDRAW_RUNTIME.md#classic-homeworld-reserve-routing--16-october-2026) | +| Harass / classic Homeworlds | Fresh classic2–6 Basic/Advanced independent36-card/Homeworld composition admits Arrakis and foreign-World returns, never an own World. Return exact typed board/visitor counters through native custody: Basic primary; Advanced Emperor normal→Kaitain/elite→Salusa. This destination/timing composition is explicit, not a new card FAQ or separate Nexus ruling. Original sealed commitment/support remains; subsequent population observations cannot rescore it. Homeworld loss receipts retain fully-committed Harass complements; native-only Traitors, late defense/explosion and after-use discard stay. Recruits has the separate current-rate composition below; Reinforcements/faction/overlay/public gates remain. | [Source and native routing](HARASS_WITHDRAW_RUNTIME.md#classic-homeworld-reserve-routing--16-october-2026) | +| Recruits / classic Homeworlds | Publisher E3 pp11/16 gives low Fremen4 doubled but capped7. Under choose-recommended, compose original separate-request native population observations: current rate at acceptance, retained accepted group price/free classification, then later requests use changed population and recorded free usage. Bulk7 versus split1+5 is provisional timing, not a new FAQ/user answer. Fremen still cannot buy a seventh without Tleilaxu; typed native deposits and original special-force caps remain. Prior paid/ready returns retain payment and opportunity while phase4 stays open. Clean classic2–6 Basic/Advanced only; Reinforcements and other roster/overlay/public gates stay. | [Current-rate scope and proof](RECRUITS_RUNTIME.md#classic-homeworld-composition--16-october-2026) | | 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) | @@ -766,7 +767,7 @@ These are unresolved in the cited records. Do not infer an answer from elapsed t | --- | --- | --- | | 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 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) | +| Recruits late accounting and rates | The earlier one/two-further-free question remains unanswered. The prototype preserves paid spice, deducts recorded free usage from the doubled current rate and all returns from the total cap: two-free/one-paid Atreides can take two more free. Clean classic Homeworlds now provisionally recheck native thresholds on separate accepted requests, retaining completed prices/classifications; this is not a numerical user ruling or newly found timing FAQ. Unknown ledgers, pending repricing, later Fremen grants and same-turn replay remain guarded. | [Source alternatives](RECRUITS_RULES.md), [current native runtime](RECRUITS_RUNTIME.md#classic-homeworld-composition--16-october-2026) | | 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) | | Leader Skills edge cases | Capture/replacement eligibility, Banker escrow custody, Smuggler modifiers, Mentat empty-hand fallback, late/mirror/numeric Diplomat defenses and simultaneous optional timing remain source gaps. Owned multi-pile Smuggler/Sandmaster allocation and printed alternate/hybrid defense roles now have connected, explicitly interpreted paths. The bounded Diplomat retreat prototype chooses printed/copied strength and surviving-loser retreat before casualties; this is not an official priority clarification. | [Material boundaries and retreat](LEADER_SKILLS_RULES.md#bounded-diplomat-retreat-interpretation), [Defense limits](DIPLOMAT_DEFENSE.md#evidence-and-remaining-scope) | diff --git a/game/card-presentation.ts b/game/card-presentation.ts index 322d246e..bcdbd4cd 100644 --- a/game/card-presentation.ts +++ b/game/card-presentation.ts @@ -118,7 +118,7 @@ export function cardPresentation(card: VisibleCard): CardPresentation { guidance: ecaz.summary, gameplay: ecaz.gameplay, availability: ecaz.card.effect === 'recruits' - ? '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.' + ? 'Recruits has development controls during Revival, including clean classic Basic/Advanced Homeworlds. Double the current native free rate, including low-population bonuses, within the seven-force total; separate requests recheck thresholds. Native typed deposits and special-force caps stay. Recorded prior paid returns keep payment under the provisional no-refund policy; pending transactions, unknown ledgers, later Fremen grants and same-turn replay remain guarded.' : ecaz.card.effect === 'harassWithdraw' ? 'Harass & Withdraw has controls in either battle-card slot, with physical allocation after reveal. Classic Homeworlds returns actual typed counters into native reserve custody: Advanced Emperor normal to Kaitain/Sardaukar to Salusa; Basic preserves its original primary World. The card is forbidden on any own Homeworld. Original sealed commitment/support remains. An own concealed No-Field separately follows provisional materialization first without resetting history. Stone compares post-withdrawal counters provisionally. Other profile/module limits remain guarded.' : 'Reinforcements has controls in either battle-card slot, including the explicit Advanced Occupy card variant. It spends three own reserves and adds +2 normal score, not support or physical tokens. Own No-Field reveal must leave three original reserves before this cost; later Harass returns cannot fund it. Original/copy Stone ignores the score bonus but still charges reserves. All-outcome payment and native cost-before-return order are provisional. Other profile/module limits remain guarded.', diff --git a/game/engine.ts b/game/engine.ts index b34d8ffd..d4daa69b 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -13735,7 +13735,7 @@ function ecazTreacheryIntegrity(g: Game) { } function recruitsModeSupported(g: Game) { return ( - !g.homeworlds && + (!g.homeworlds || classicEcazHomeworldProfile(g)) && !g.nexusCards && !g.leaderSkills && !g.techTokens && @@ -13758,7 +13758,7 @@ function combinedCardBattleBlock(g: Game): string | null { } function reinforcementsModeSupported(g: Game) { const pairedEcaz = g.expansions.length === 1 && g.expansions[0] === 'ecaz'; - return !!g.ecazTreachery && recruitsModeSupported(g) && !g.sandtrout && + return !!g.ecazTreachery && !g.homeworlds && recruitsModeSupported(g) && !g.sandtrout && ((g.ecazOccupyPreview === true && ecazOccupyCompositionSupported(g)) || ((g.expansions.length === 0 || pairedEcaz) && g.players.every(player => { const expansion = faction(player.faction).expansion; @@ -13825,7 +13825,7 @@ function recruitsPlayBlock(g: Game, owner: Player, card: Card): string | null { if (!g.ecazTreachery || !ecazTreacheryDefinition(card) || card.id !== RECRUITS_CARD_ID) return 'Choose the physical Recruits card from the enabled Ecaz Treachery Cards variant.'; if (!recruitsModeSupported(g)) - return 'This Recruits prototype currently requires a faction-only game without other optional modules.'; + return 'This Recruits prototype requires a faction-only game or clean classic Homeworlds, without other optional modules.'; if (g.status !== 'playing' || g.phase !== 4) return 'Play Recruits during Revival.'; if (g.recruits?.turn === g.turn) diff --git a/game/reference.ts b/game/reference.ts index b44075ae..ac5587d8 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -4058,7 +4058,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'Homeworlds are an optional module from Ecaz & Moritani, available under the official rules with Basic or Advanced play. They are separate locations outside Arrakis; they do not receive ordinary storm movement or count as ordinary strongholds.', 'Fresh private Homeworld prototypes retain all fourteen Skills, Discovery7/8 or both in classic or supported E1/E2 and standalone Ecaz OR Moritani tables. Original Nexus entries additionally support single or paired one-family E1/E2, natives from BOTH selected Ix+CHOAM families with union47, and standalone OR paired Ecaz/Moritani/ecaz33 with classics, with or without Skills. Basic/Advanced2–6, Tech3+ and Advanced Strongholds2+ retain original setup, native placement, phase-end industry and source-local held cards. Advanced Moritani/paired E3 excludes Harkonnen; otherwise legal standalone no-Skills Ecaz/Harkonnen does not lift its separate Duke Cunning exclusion. Winning typed native Suk saves stay at that home; external Emperor returns allocate physical normal/elite counters to original homes as the existing interpretation. Normal revival separately deposits Sardaukar at Salusa. Nested battles receive no Homeworld bonus. Mixed E3/three-family Nexus, pending effects, public starts and played-save conversion remain separate.', 'Each faction uses its own card and matching world token. In Basic play the Emperor uses Kaitain alone. Advanced play adds Salusa Secundus for its Sardaukar; movement can transfer both force types between the two Emperor worlds.', - 'A fresh classic Basic/Advanced independent36-card/Homeworld prototype additionally connects Harass on Arrakis or a foreign World, never an own World. Exact typed counters return through native reserves: Basic primary; Advanced Emperor normal to Kaitain/Sardaukar to Salusa. Original sealed commitment/support and fully-committed casualty complement stay fixed across population changes. Other variant cards, expansion rosters and overlays keep their separate guards.', + 'A fresh classic Basic/Advanced independent36-card/Homeworld prototype connects Harass on Arrakis or a foreign World, never an own World, with exact typed native returns and fixed original commitment/support. Recruits doubles current native rates, including low-population bonuses, within the seven-force total; separate requests provisionally recheck thresholds and recorded free use. Native deposits and special-force caps remain. Reinforcements, expansion rosters and other overlays keep their separate guards.', 'Native reserves determine high or low population. Salusa counts Sardaukar, while the other cards count the faction’s reserves there. Reaching the high face’s minimum restores high population. Occupation also applies the low penalty.', 'Low population normally grants one additional free force revival and one additional bank spice when collecting CHOAM Charity. Salusa grants neither bonus; CHOAM’s Tupile reverses the usual pattern by granting an advantage while low and a penalty while high.', 'A qualifying charity claim separates its ordinary payer from the extra bank spice. Inflation doubles both portions or prevents the whole collection. Canceling Bene Gesserit charity prevents both portions of that claim.', @@ -4973,15 +4973,15 @@ export const RULE_TOPICS: RuleTopic[] = [ 'The holder may play Recruits from its Revival panel. Discard the card once, then all players use the increased rates while this Revival phase remains open, including players who had already marked themselves ready. Completed returns still count toward their turn totals.', 'Atreides rises from two free forces to four; Fremen from three to six; Guild from one to two. The ordinary force limit becomes seven. Native unlimited CHOAM and Tleilaxu allowances remain unlimited, and their free rates become zero and four. Paid prices and separate elite limits still apply.', 'Free prevention still prevents free returns. Prior paid ordinary returns now have a visibly provisional policy: preserve payment, deduct only recorded free returns from the doubled free allowance and all returns from the total cap. Two free plus one paid Atreides may take two more free without refund; this is not a numerical user ruling. Missing/inconsistent ledgers, later Fremen grants, pending transactions and same-turn replay remain guarded.', - 'The preview supports Basic or Advanced classic-faction games without additional optional modules. Public activation and complete combined-game acceptance remain unfinished.', + 'Clean classic2–6 Basic/Advanced Homeworlds compose through the fresh independent36-card setup. Double current native free rates including low-population bonuses, within the seven-force total. Provisionally recheck population and recorded free use on each separate request: low Fremen may take7 together, but1 crossing high leaves5 further free and cannot buy a seventh without Tleilaxu. Original typed destinations and special-force limits stay. Other overlays/rosters, public activation and whole-mode acceptance remain unfinished.', ], - related: ['revival', 'ecaz-modules', 'expansion-faction-games', 'implementation-checklist'], + related: ['revival', 'homeworlds', 'ecaz-modules', 'expansion-faction-games', 'implementation-checklist'], checklist: [ - { area: 'Implementation', status: 'Partial', detail: 'Genuine independent three-card setup, physical Recruits play/discard, public turn effect and shared revival quotes are connected. Recorded late-paid activation uses a provisional no-refund/free-use policy; unknown ledgers and other timing guards remain. Harass/Reinforcements have separate bounded battle prototypes; complete variant interactions remain gated.', evidence: ['game/ecaz-cards.ts', 'game/recruits.ts', 'game/revival.ts', 'game/engine.ts'] }, + { area: 'Implementation', status: 'Partial', detail: 'Independent physical play/discard and public turn rates use shared revival quotes, now including clean classic Homeworlds and native typed deposits. Separate requests provisionally recompute current thresholds; prior paid returns retain payment and recorded free use. Unknown ledgers, pending repricing and later Fremen grants remain guarded; Reinforcements retains its own World gate.', evidence: ['game/ecaz-cards.ts', 'game/recruits.ts', 'game/revival.ts', 'game/engine.ts'] }, { area: 'Player controls', status: 'Implemented', detail: 'The owned physical card has full inspection, a Revival action and explicit unavailable reasons. All seats see the effective public rates after play.', evidence: ['components/recruits.tsx', 'components/game-table.tsx', 'game/card-presentation.ts'] }, { area: 'AI', status: 'Partial', detail: 'All four profiles use the same legal owned offer for useful own or allied revival, then existing authoritative revival choices. Complete card strategy and calibration remain unfinished.', evidence: ['game/bots.ts'] }, { area: 'Documentation', status: 'Implemented', detail: 'The guide records examples, limits, independent-cap inference and the visibly provisional late-paid policy; source alternatives and late-rate questions remain explicit.', evidence: ['docs/RECRUITS_RULES.md', 'docs/RECRUITS_RUNTIME.md'] }, - { area: 'Verification', status: 'Partial', detail: 'Focused rules, controls, AI and authenticated recovery checks cover the bounded preview. Sample and browser evidence is recorded separately; full variant acceptance remains open.', evidence: ['tests/recruits.test.ts', 'tests/recruits-engine.test.ts', 'tests/recruits-controls.test.tsx', 'tests/recruits-recovery.test.ts', 'tests/prototype-room.test.ts'] }, + { area: 'Verification', status: 'Partial', detail: 'Ten new native regressions cover bulk7 versus split6 at a population crossing, Basic/Advanced typed caps, already-ready paid returns and Emperor destinations. Four actual mobile journeys and16 fresh four-policy programmes/104 accepted card/Revival actions reach usable native continuation. Existing authenticated recovery evidence is separate; full variant/mode/calibration/final assurance stays open.', evidence: ['tests/recruits-homeworld.test.ts', 'tests/fixture-recruits-homeworld.ts', 'tests/recruits.test.ts', 'tests/recruits-engine.test.ts', 'tests/recruits-controls.test.tsx', 'tests/recruits-recovery.test.ts', 'tests/prototype-room.test.ts'] }, ], }, { @@ -5023,7 +5023,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'In explicitly selected Advanced Occupy, only the actual card user’s undialed forces return. An Ecaz user preserves its original mandatory fixed count at free full strength; an allied user uses its native variable commitment, while canceled Occupy uses its selected lead’s ordinary own pool. Returned reserves remain outside losses and Face Dance. Basic/unprofiled co-side, public starts and full acceptance remain gated.', 'In supported same-plan Reinforcements, the provisional order pays three original own reserves before Harass returns undialed counters. Future returns cannot fund or change that reserve quote; neither the +2 score bonus nor that cost adds to the physical commitment. Both cards discard after use. An opposing successful Traitor cancels Harass but not the provisional Reinforcements cost.', 'With an own concealed No-Field, this prototype provisionally uses the original materialization-first order. Before sealing, privately forecast its actual ordinary counters at the original marker sector; do not move them early. Both plans seal before native revelation, then any return allocation uses actual counters. Marker0 remains legal with a leader/cards, reserve shortage caps materialization, and withdrawal does not return/reset the token. Opposing successful Traitor cancels Harass, not that native reveal.', - 'Clean classic2–6 Basic/Advanced Homeworlds may be selected before the independent36-card setup. Harass works on Arrakis or a foreign Homeworld, never any own World. Return only actual undialed board/visitor counters into native reserve custody, not Tanks: Basic primary; Advanced Emperor normal to Kaitain and Sardaukar to Salusa. Original dial/support and all remaining committed counters stay fixed even if returned population crosses a threshold. Native-only Traitors, late defense/explosion and mandatory disposal remain; other cards and overlays retain their guards.', + 'Clean classic2–6 Basic/Advanced Homeworlds may be selected before independent36-card setup. Harass works on Arrakis or a foreign Homeworld, never any own World. Return actual undialed board/visitor counters through native custody, not Tanks: Basic primary; Advanced Emperor normal to Kaitain/Sardaukar to Salusa. Original dial/support and committed counters stay fixed across threshold changes. Native-only Traitors, late defense/explosion and mandatory disposal remain. Recruits has a separate native current-rate composition; Reinforcements and other overlays retain guards.', ], related: ['battle-cards', 'battle', 'card-recruits', 'homeworlds', 'ecaz-modules', 'implementation-checklist'], checklist: [ diff --git a/tests/fixture-recruits-homeworld.ts b/tests/fixture-recruits-homeworld.ts new file mode 100644 index 00000000..b57cd2cf --- /dev/null +++ b/tests/fixture-recruits-homeworld.ts @@ -0,0 +1,146 @@ +import assert from 'node:assert/strict'; +import { + applyAction, createGame, initializeEcazTreacheryGameForAudit, joinGame, + newPlayer, viewGame, type Action, type Game, +} from '../game/engine'; +import { treacheryDeck } from '../game/cards'; +import { ecazTreacheryCards } from '../game/ecaz-cards'; +import { homeworldGameIntegrity } from '../game/homeworld-game'; +import { nativeShipmentSources } from '../game/homeworld-options'; +import { RECRUITS_CARD_ID } from '../game/recruits'; +import { nextHarassHomeworldStep } from './fixture-harass-homeworld'; +import { finishHarassPreparation, takeHarassCard } from './fixture-harass-withdraw'; + +export type RecruitsHomeworldStep = { actor: string; action: Action }; +export type RecruitsHomeworldFixture = { + initial: Game; + afterSetup: Game; + game: Game; + nativeSteps: RecruitsHomeworldStep[]; + staging: string[]; +}; +export const recruitsHomeworldPlayer = (game: Game, id: string) => { + const player = game.players.find(p => p.id === id); assert.ok(player); return player; +}; +export const recruitsHomeworldClean = (game: Game) => + !game.phaseOpening && !game.response && !game.decision && !game.pendingTreacheryDiscard; + +export function advanceRecruitsHomeworld(game: Game, until: (game: Game) => boolean, + steps?: RecruitsHomeworldStep[]): Game { + for (let n = 0; n < 2000; n++) { + if (until(game)) return game; + assert.notEqual(game.status, 'finished', 'Native programme must reach its requested Revival'); + const step = nextHarassHomeworldStep(game); + assert.ok(step, 'Native continuation requires a real battle plan'); + steps?.push(structuredClone(step)); game = applyAction(game, step.actor, step.action); + } + throw Error('Native World Recruits programme did not reach its boundary'); +} + +export function assertRecruitsHomeworldInventory(game: Game): void { + homeworldGameIntegrity(game); + for (const player of game.players) { + assert.equal(player.reserves + player.tanks + Object.values(player.forces).reduce((a, b) => a + b, 0), 20); + assert.ok(Object.keys(player.forces).every(key => !key.startsWith('homeworld:'))); + if (player.elites) assert.equal(player.elites.reserves + player.elites.tanks + + Object.values(player.elites.forces).reduce((a, b) => a + b, 0), player.faction === 'fremen' ? 3 : 5); + } + const cards = [...game.deck, ...game.discard, ...game.players.flatMap(p => p.hand), + ...(game.auction?.cards.slice(game.auction.index) ?? [])]; + assert.deepEqual(cards.map(c => c.id).sort(), [...treacheryDeck([]), ...ecazTreacheryCards()].map(c => c.id).sort()); +} + +/** Fresh classic four-seat setup; no phase, population, wallet, dead-counter or + * revival receipt assignment. Only original hands/unopened Spice Cards are + * positioned, then actual shipments and two zero-dial battles create the dead. */ +export function createRecruitsHomeworldFixture(options: { + advanced?: boolean; + seatIds?: readonly [string, string, string, string]; + initial?: Game; +} = {}): RecruitsHomeworldFixture { + const ids = options.seatIds ?? ['emperor', 'fremen', 'atreides', 'guild']; + let game = options.initial ? structuredClone(options.initial) : + createGame('RECRUITSWORLD', newPlayer(ids[0], 'Emperor', 'emperor'), options.advanced ?? false); + if (!options.initial) for (const [index, faction] of ['fremen', 'atreides', 'guild'].entries()) + joinGame(game, newPlayer(ids[index + 1], faction, faction as 'fremen' | 'atreides' | 'guild')); + assert.equal(game.status, 'lobby'); assert.deepEqual(game.expansions, []); + assert.deepEqual(game.players.map(p => p.faction), ['emperor', 'fremen', 'atreides', 'guild']); + if (!game.homeworlds) game = applyAction(game, game.host, { type: 'homeworlds', enabled: true }); + const nativeSteps: RecruitsHomeworldStep[] = []; + const act = (actor: string, action: Action) => { + nativeSteps.push({ actor, action: structuredClone(action) }); game = applyAction(game, actor, action); + }; + for (const p of game.players) if (!p.ready) act(p.id, { type: 'ready' }); + const initial = structuredClone(game); + const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues'); + crypto.getRandomValues = (array: T): T => { + assert.ok(array); new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(255); return array; + }; + try { game = initializeEcazTreacheryGameForAudit(game); } + finally { + if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor); + else Reflect.deleteProperty(crypto, 'getRandomValues'); + } + game = advanceRecruitsHomeworld(game, g => g.status === 'playing', nativeSteps); + const afterSetup = structuredClone(game); + const staging = ['Return initial real hands to the unopened deck; no battle weapons, fake counters or wallet assignments.']; + for (const p of game.players) { game.deck.push(...p.hand); p.hand = []; } + // Original non-worm cards keep the first Blow away from an unrelated riding + // decision. This is not a fabricated Spice Card or phase assignment. + for (let i = 0; i < (game.advanced ? 4 : 2); i++) { + const at = game.spiceDeck.findIndex((card, index) => index >= i && 'territory' in card && !card.discovery); + assert.ok(at >= 0); game.spiceDeck.splice(i, 0, game.spiceDeck.splice(at, 1)[0]); + } + staging.push('Position unopened original first-Blow non-worm cards; native later decks and history stay.'); + const shipments = new Map([ + [ids[0], { amount: 3, elite: 0, territory: 'sietch_tabr', sector: 14 }], + [ids[1], { amount: 8, elite: 3, territory: 'sietch_tabr', sector: 14 }], + [ids[3], { amount: 3, elite: 0, territory: 'arrakeen', sector: 10 }], + ]); + game = advanceRecruitsHomeworld(game, g => g.phase === 5 && recruitsHomeworldClean(g), nativeSteps); + while (game.phase === 5) { + const actor = game.active!; + const request = recruitsHomeworldClean(game) ? shipments.get(actor) : undefined; + if (request) { + const sources = nativeShipmentSources(viewGame(game, actor), request.amount, request.elite); + assert.ok(sources, 'Original typed shipment must have native counters'); + act(actor, { type: 'ship', ...request, homeworldSources: sources }); + shipments.delete(actor); + } else { + const step = nextHarassHomeworldStep(game); assert.ok(step); act(step.actor, step.action); + } + } + game = advanceRecruitsHomeworld(game, g => g.phase === 6 && recruitsHomeworldClean(g), nativeSteps); + while (game.phase === 6) { + if (recruitsHomeworldClean(game) && !game.battle && game.active) { + const choice = viewGame(game, game.active).battleChoices[0]; + assert.ok(choice, 'Actual invaded stronghold must expose its battle choice'); + act(choice.chooser, { type: 'chooseBattle', territory: choice.territory, + target: choice.attacker === choice.chooser ? choice.defender : choice.attacker }); + } + if (game.battle && !game.battle.revealed) { + game = finishHarassPreparation(game); + const battle = game.battle!; + for (const actor of [battle.attacker, battle.defender]) { + if (game.battle!.plans[actor]) continue; + const p = recruitsHomeworldPlayer(game, actor); + const leaders = p.leaders.filter(l => !l.dead && !l.usedAt).sort((a, b) => a.strength - b.strength); + const leader = actor === ids[0] || actor === ids[3] ? leaders.at(-1) : leaders[0]; assert.ok(leader); + act(actor, { type: 'battlePlan', leader: leader.id, dial: 0, support: 0 }); + game = finishHarassPreparation(game); + } + } else { + const step = nextHarassHomeworldStep(game); assert.ok(step); act(step.actor, step.action); + } + } + game = advanceRecruitsHomeworld(game, g => g.turn === 2 && g.phase === 4 && recruitsHomeworldClean(g), nativeSteps); + // Conserve the actual independent card after real casualties, not a fabricated + // draw/auction claim. Its holder is explicit in all smoke/test descriptions. + takeHarassCard(game, ids[0], RECRUITS_CARD_ID); + staging.push('Conserve the existing physical Recruits from its original zone to Emperor at quiet second-turn Revival.'); + assert.equal(recruitsHomeworldPlayer(game, ids[1]).reserves, 2); + assert.equal(recruitsHomeworldPlayer(game, ids[1]).tanks, 18); + assert.equal(recruitsHomeworldPlayer(game, ids[2]).tanks, 10); + assertRecruitsHomeworldInventory(game); + return { initial, afterSetup, game, nativeSteps, staging }; +} diff --git a/tests/recruits-homeworld.test.ts b/tests/recruits-homeworld.test.ts new file mode 100644 index 00000000..f45fb133 --- /dev/null +++ b/tests/recruits-homeworld.test.ts @@ -0,0 +1,126 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { applyAction, viewGame, type Action, type Game } from '../game/engine'; +import { RECRUITS_CARD_ID } from '../game/recruits'; +import { + advanceRecruitsHomeworld, assertRecruitsHomeworldInventory, + createRecruitsHomeworldFixture, recruitsHomeworldClean, recruitsHomeworldPlayer, +} from './fixture-recruits-homeworld'; +import { + createHarassHomeworldArrakisFixture, revealHarassHomeworldArrakisBattle, + finishHarassHomeworldArrakisBattle, +} from './fixture-harass-homeworld-arrakis'; +import { takeHarassCard } from './fixture-harass-withdraw'; + +const play = { type: 'card', card: RECRUITS_CARD_ID } as const; +function rejectsUnchanged(game: Game, actor: string, action: Action): void { + const before = JSON.stringify(game); + assert.throws(() => applyAction(game, actor, action)); + assert.equal(JSON.stringify(game), before); +} +const nativeGroup = (game: Game, actor: string, secondary = false) => { + const worlds = viewGame(game, actor).homeworlds?.worlds; + assert.ok(worlds, 'Native revival must retain its actual Homeworld custody view'); + const world = worlds.find(w => w.native === actor && w.secondary === secondary); + assert.ok(world); return world.forces[actor]; +}; + +for (const advanced of [false, true]) { + const mode = advanced ? 'Advanced' : 'Basic'; + void test(`${mode} native low Fremen may accept seven free together but never eight or seven additional returns`, () => { + let game = createRecruitsHomeworldFixture({ advanced }).game; + const before = recruitsHomeworldPlayer(game, 'fremen'); + const spice = before.spice; + game = applyAction(game, 'emperor', play); + rejectsUnchanged(game, 'fremen', { type: 'revive', amount: 8, elite: 0 }); + game = applyAction(game, 'fremen', { type: 'revive', amount: 7, elite: 0 }); + const after = recruitsHomeworldPlayer(game, 'fremen'); + assert.deepEqual({ reserves: after.reserves, tanks: after.tanks, revived: after.revived, + free: after.freeForcesRevived, spice: after.spice }, + { reserves: 9, tanks: 11, revived: 7, free: 7, spice }); + assert.deepEqual(nativeGroup(game, 'fremen'), { normal: 9, elite: 0 }); + assert.equal(after.elites!.tanks, 3); + assert.equal(viewGame(game, 'fremen').revival.forcesRemaining, 0); + rejectsUnchanged(game, 'fremen', { type: 'revive', amount: 1, elite: 0 }); + assert.equal(game.discard.filter(c => c.id === RECRUITS_CARD_ID).length, 1); + assertRecruitsHomeworldInventory(game); + }); + + void test(`${mode} separate Recruits requests recheck crossed Fremen population and do not grant a paid seventh without Tleilaxu`, () => { + let game = applyAction(createRecruitsHomeworldFixture({ advanced }).game, 'emperor', play); + const spice = recruitsHomeworldPlayer(game, 'fremen').spice; + game = applyAction(game, 'fremen', { type: 'revive', amount: 1, elite: 0 }); + const quote = viewGame(game, 'fremen'); + assert.equal(quote.revival.homeworldBonus, 0); + assert.equal(quote.revival.freeRemaining, 5); + assert.equal(quote.recruitsPreview!.rates.find(r => r.player === 'fremen')!.freeRate, 6); + rejectsUnchanged(game, 'fremen', { type: 'revive', amount: 6, elite: 0 }); + game = applyAction(game, 'fremen', { type: 'revive', amount: 5, elite: 0 }); + const after = recruitsHomeworldPlayer(game, 'fremen'); + assert.equal(after.revived, 6); + assert.equal(after.freeForcesRevived, 6); + assert.equal(after.reserves, 8); + assert.equal(after.spice, spice); + assert.equal(viewGame(game, 'fremen').revival.limit, 7); + assert.equal(viewGame(game, 'fremen').revival.forcesRemaining, 0); + rejectsUnchanged(game, 'fremen', { type: 'revive', amount: 1, elite: 0 }); + assertRecruitsHomeworldInventory(game); + }); + + void test(`${mode} Recruits preserves typed Fedaykin deposits and native special-force revival limits`, () => { + let game = applyAction(createRecruitsHomeworldFixture({ advanced }).game, 'emperor', play); + game = applyAction(game, 'fremen', { type: 'revive', amount: 1, elite: 1 }); + game = advanceRecruitsHomeworld(game, recruitsHomeworldClean); + assert.deepEqual(nativeGroup(game, 'fremen'), { normal: 2, elite: 1 }); + assert.equal(recruitsHomeworldPlayer(game, 'fremen').elites!.tanks, 2); + assert.equal(viewGame(game, 'fremen').revival.eliteRemaining, advanced ? 0 : 2); + if (advanced) rejectsUnchanged(game, 'fremen', { type: 'revive', amount: 1, elite: 1 }); + else { + game = applyAction(game, 'fremen', { type: 'revive', amount: 2, elite: 2 }); + assert.deepEqual(nativeGroup(game, 'fremen'), { normal: 2, elite: 3 }); + assert.equal(recruitsHomeworldPlayer(game, 'fremen').elites!.tanks, 0); + } + assertRecruitsHomeworldInventory(game); + }); + + void test(`${mode} recorded paid native returns keep their cost and an already-ready Atreides can use the added free allowance`, () => { + let game = createRecruitsHomeworldFixture({ advanced }).game; + const spice = recruitsHomeworldPlayer(game, 'atreides').spice; + game = applyAction(game, 'atreides', { type: 'revive', amount: 3, elite: 0 }); + game = applyAction(game, 'atreides', { type: 'ready' }); + game = applyAction(game, 'emperor', play); + const quote = viewGame(game, 'atreides').revival; + assert.equal(quote.freeRemaining, 2); + assert.equal(quote.forcesRemaining, 4); + game = applyAction(game, 'atreides', { type: 'revive', amount: 2, elite: 0 }); + const after = recruitsHomeworldPlayer(game, 'atreides'); + assert.equal(after.spice, spice - 2); + assert.equal(after.revived, 5); + assert.equal(after.freeForcesRevived, 4); + assert.deepEqual(nativeGroup(game, 'atreides'), { normal: 15, elite: 0 }); + assert.equal(after.tanks, 5); + assert.equal(viewGame(game, 'atreides').revival.freeRemaining, 0); + assertRecruitsHomeworldInventory(game); + }); + + void test(`${mode} an actual mixed Sardaukar loss revives free through original native destinations`, () => { + const fixture = createHarassHomeworldArrakisFixture({ advanced, outcome: 'loser' }); + let game = finishHarassHomeworldArrakisBattle(revealHarassHomeworldArrakisBattle(fixture), + { returns: { [fixture.location]: { normal: 2, elite: 3 } } }); + game = advanceRecruitsHomeworld(game, g => g.turn === 2 && g.phase === 4 && recruitsHomeworldClean(g)); + takeHarassCard(game, fixture.owner, RECRUITS_CARD_ID); + const spice = recruitsHomeworldPlayer(game, fixture.owner).spice; + game = applyAction(game, fixture.owner, play); + game = applyAction(game, fixture.owner, { type: 'revive', amount: 2, elite: 1 }); + const after = recruitsHomeworldPlayer(game, fixture.owner); + assert.equal(after.reserves, 20); + assert.equal(after.tanks, 0); + assert.equal(after.elites!.reserves, 5); + assert.equal(after.elites!.tanks, 0); + assert.equal(after.spice, spice); + assert.equal(after.freeForcesRevived, 2); + assert.deepEqual(nativeGroup(game, fixture.owner), { normal: 15, elite: advanced ? 0 : 5 }); + if (advanced) assert.deepEqual(nativeGroup(game, fixture.owner, true), { normal: 0, elite: 5 }); + assertRecruitsHomeworldInventory(game); + }); +} diff --git a/tools/start-prototype.ts b/tools/start-prototype.ts index b810091e..a220f281 100644 --- a/tools/start-prototype.ts +++ b/tools/start-prototype.ts @@ -36,7 +36,7 @@ async function main() { '\nPrinted native Homeworld capture/Auditor/force-income Karama cancellation costs also offer their card. Accepted combat, paid support, leader death and original casualty/cleanup remain committed; the canceled reward never samples, transfers, inspects, refunds or credits. Earlier allowed income stays intact before a separately canceled audit.', '\n--profile homeworld-occupation --leader-skills composes original all14 Leader Skills with the classic2–6-seat occupied Homeworld income preview. Native setup, reserve custody, occupation history and owner allocation remain original. Bureaucrat diverts two from gross before the occupied percentage split while preserving the full original payer shares. Nexus Cards, expansions, Tech, Strongholds, Discovery, other overlays and conversion of played games remain excluded.', '\n--profile guild-betrayal --leader-skills composes the bounded classic native-Guild Nexus preview with original all14 skills. Original payment/physical shipment and once-per-phase Bureaucrat receipt remain; ordinary Skills use their shared quotes. Homeworlds, expansions, Tech, Strongholds, Discovery, Semuta, other overlays and conversion of played games remain excluded.', - '\n--profile ecaz-treachery --homeworlds selects the independent three physical cards before original classic2–6 Basic/Advanced Homeworld setup (base33+3). Harass works on Arrakis or another faction’s Homeworld, never an own World. Exact typed undialed counters return through native reserves: Advanced Emperor normal to Kaitain/Sardaukar to Salusa; Basic retains its original primary World. Original dial/support, native invasion/Traitor/explosion and disposal stay; other two variant cards retain Homeworld guards. No expansions, other overlays, public start or played-save conversion.', + '\n--profile ecaz-treachery --homeworlds selects the independent three physical cards before original classic2–6 Basic/Advanced Homeworld setup (base33+3). Recruits doubles current native free rates, including low-population bonuses, within the seven-force total; separate requests provisionally recheck thresholds and prior free use. Native typed deposits/special-force caps and no-refund late-play policy stay. Harass works on Arrakis or another faction’s World, never an own World. Typed undialed returns use native reserves: Advanced Emperor normal to Kaitain/Sardaukar to Salusa; Basic primary. Original dial/support, native invasion/Traitor/explosion and disposal stay. Reinforcements remains World-guarded. No expansions, other overlays, public start or played-save conversion.', '\nEcaz-occupy (--profile ecaz-occupy) opts a fresh ready2–6 native Ecaz/classic or selected Advanced native-family lobby. Basic uses exact ecaz33 and provisionally follows the main paragraph for odd/even counts: ceil contribution/loss and floor survivors; the conflicting FAQ is recorded, not adjudicated. Advanced retains original selected ecaz/ix/choam decks and native offers/HMS/Auditor/Face Dancers/cache. Explicit --ecaz-treachery independently adds three physical cards in Advanced. Native Richese ordinary, marker-only and mixed pools compose with selected leads under the provisional additive interpretation. Ordinary dial Prescience remains barred for the physical No-Field army; distinct Advanced special Karama uses the explicit complete private-plan interpretation without early token revelation. Other profile/module and public-start gates remain.' + '\nLeader-skills retains existing classic and Basic native entries and adds native Basic Richese/CHOAM. Advanced Ixians, Tleilaxu, CHOAM and/or Richese plus classic rosters use distinct required family decks and all fourteen skills, with original cache, Face Dancers, Auditor exclusion and typed aftermath. Foreign revival grants no new skill. Separate non-Harkonnen Advanced Moritani uses the ecaz deck and original skill-first assassination preview. Native Ecaz supports its five ordinary discs, original Ambassador/Duke custody and ecaz deck with classic opponents in Basic/Advanced; Advanced Harkonnen retains its existing Duke capture exclusion. Normal Mentat questions use a uniform private card-show step in supported Leader Skills games. Banker-income additionally supports native Richese and Ecaz, never ordinary seller/allowed Emperor income or a bank grant as a payer cost. Basic auction recipients remain unchanged. Public activation, exhausted-cache counts, native Ixian Technology on Richese lots, own mixed No-Field, captured replacement, shared-Duke assignment, combined Occupy skills and other pending module/rulings remain guarded.', '\nStronghold-factions additionally admits standalone Advanced Ecaz OR Moritani with classic opponents and the exact ecaz deck, six Stronghold Cards with optional unused Tech Tokens at 3–6 seats. Original setup and actual end-Mentat claims remain. Ecaz holds shared-control cards and only the actual selected holder-plan benefits. Native Moritani retains original assassination with printed bounty and one private Mentat replacement; Harkonnen is excluded there. An E3 pair, E1/E2/E3 mixtures and other modules remain separate.' +
Current revival rates
FactionFree rateForce limit