diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index f0b825ac..58c13dc5 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -29,6 +29,22 @@ optional skill bands, combinations and public expansion starts remain open. Rules implementation continues; comprehensive assurance and administration remain deferred until all rules are implemented. +**Ixian Nexus replacement follows native BG purchases — 16 October.** +Successful original BG Worthless-as-Karama acquisition/payment now earns the +same normal purchased-card replacement, after its native counter. Denied +direct acquisition supplies no purchase; native funded winning-payment +recovery supplies a paid, not free, receipt. No printed-cost-only flag remains. + +Four new behavior cases; affected nine-file74/74, four original-action runtime +paths and eight legal policy actions pass. Two original-source390px journeys +replace Hajr with the actual Ghola or keep the winning Shield, preserving +BG5/Emperor12 and one auction continuation. Width/scroll390; no browser/ +application errors. Counter custody is explicitly controlled in two cases. +No human server, D1, saved room, deployment or public gate changed. +[Source and bounded evidence](NEXUS_IXIAN_REPLACEMENT_RULES.md#native-bg-converted-purchases--16-october-2026). +Final types/lint pass; current source anchor and17 topic evidence paths +resolve. No production build or broad assurance run for this slice. + **Semuta connects both physical Shrine roles — 16 October.** Fresh private `semuta --discoveries` setup retains original Richese/CHOAM, optional Ix, unused Tech3+/Advanced Strongholds and original Discovery7/8. diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index a9f7acf4..28cb2e73 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,25 @@ # Dune implementation status +## 16 October 2026 — Ixian Nexus replacement after native BG conversion + +Removed the printed-Karama-only producer flag from normal auction settlement. +Successful original native BG converted acquisition or winning payment now +offers the same source-bound purchased-card choice after its native counter. +Denied direct acquisition supplies none; native funded winning-payment +recovery still purchases for spice and supplies the original paid receipt. +No new price, refund, source or Nexus cancellation policy is added. + +Four new meaningful cases replace the old guard pin. Affected nine-file74/74, +four original-action runtime paths, eight legal policy actions and two actual +styled390px original-source journeys pass. Direct acquisition replaces actual +Hajr with the next Ghola; winning payment keeps actual Shield and Nexus. +Both retain BG5/Emperor12 and advance the native auction once. Two counter +cases explicitly control only conserved counter custody. No human server, +D1, saved room, deployment or public gate changed. +[Source and scoped proof](NEXUS_IXIAN_REPLACEMENT_RULES.md#native-bg-converted-purchases--16-october-2026). +Final types/lint pass; current source anchor and17 topic evidence paths +resolve without reference parse diagnostics. No build or broad assurance run. + ## 16 October 2026 — Semuta connects physical Shrine-role costs Fresh private `semuta --discoveries` composes original Richese/CHOAM and diff --git a/docs/NEXUS_IXIAN_REPLACEMENT_RULES.md b/docs/NEXUS_IXIAN_REPLACEMENT_RULES.md index 150d09b5..c9fb49ef 100644 --- a/docs/NEXUS_IXIAN_REPLACEMENT_RULES.md +++ b/docs/NEXUS_IXIAN_REPLACEMENT_RULES.md @@ -2,6 +2,45 @@ Source audit: 2026-09-10. Bounded local prototype integration and verification: 2026-10-01. **Partial development verified**, not complete Ixian/Nexus family or expansion acceptance. This document records the source and runtime boundary of the fresh `ixian-replacement` profile. Existing [Nexus release gates](NEXUS_CARD_RUNTIME.md) remain in force. +## Native BG converted purchases — 16 October 2026 + +Original normal-auction purchases now offer the same independent Ixian +replacement after native Advanced BG Worthless-as-Karama succeeds, for both +direct acquisition and winning-bid payment. The original native counter +resolves first; a converted card is not a printed Karama, but its successful +purchase is still a purchase under the existing source analysis below. +The engine no longer carries a printed-cost-only settlement flag. + +Canceling direct acquisition leaves the lot unpurchased and offers no Nexus +replacement. Canceling winning payment preserves native payment recovery: +when the original bid is funded, ordinary automatic spice payment buys the +lot and earns a **paid**, not free, replacement opportunity. Original +payment/income and Nexus discard/draw each finish once. No new cancellation, +refund, bonus priority, profile or module combination is introduced. + +Four new behavior cases and the affected nine-file union pass **74/74**. +The obsolete guard-pinning substitution test was removed, not re-pinned. +Four original-action Use/Pass runtime paths and eight legal policy actions +pass. Genuine setup, actual worm alliance/Nexus deal, paid Worthless purchase +and first-lot Keep precede the second-lot conversion; no played state is +staged. Two additional cancellation cases explicitly conserve the unused +starting Karama into Emperor custody to isolate the native counter. + +Two actual styled390×844 original-source table journeys pass. Direct free +acquisition replaces Hajr with the actual next Ghola, spends Ixian Nexus +and retains BG spice5/Emperor12. Winning free payment keeps the actual Shield +and Ixian Nexus, also retaining5/12. Both advance the original auction once, +with width/scroll390 and no browser/application error. Only an owned +memory-only loopback host ran; no human server, D1, saved room, deployment +or public gate changed. Proof remains outside Git: +`/tmp/dune-bg-ixian-runtime.json`, `/tmp/dune-bg-ixian-ui-proof.json`. +Broader profiles, Harkonnen bonus priority, special sale origins and final +assurance remain separate; all five checklist facets remain Partial. +Final `npm run check:quick` passes; the current source anchor and17 topic +evidence paths resolve, with no reference parse diagnostics. No production +build, broad suite or HTTP assurance was run for this bounded rule slice. + + ## Primary evidence - **Face:** [photograph of the twelve original GF9 Nexus Cards](https://boardgamegeek.com/image/7767032/dune-ecaz-and-moritani), independently inspected again at `/tmp/dune-nexus-cards.jpg`. The photographed publisher text is the authority, not commentary by its uploader. @@ -65,7 +104,7 @@ and never add seats to an admitted game or fabricate Nexus hands. ### Connected purchase and choice -1. Only an actual normal-auction purchase is connected: paid spice or a genuine printed Karama-paid purchase, including direct printed Karama acquisition before a positive bid is recorded. The free purchase must originate from the actual printed Karama; native Bene Gesserit Worthless/Truthtrance substitutions continue unchanged without this new offer. This is a bounded producer restriction, not a ruling that those acquisitions cease to be purchases. The original source, purchased physical card and native sale continuation are bound after the genuine acquisition. Gifts, bonus draws, Technology swaps, old hand cards and historical purchase IDs cannot create an opportunity. +1. Only an actual normal-auction purchase is connected: paid spice or genuine Karama acquisition/payment, including direct acquisition before a positive bid. Native Advanced BG Worthless conversion must first complete its existing response; successful free purchases then get the same replacement choice. A canceled direct conversion makes no purchase; canceled winning payment follows its original funded spice-payment recovery and, if purchased, supplies a paid receipt. The original source, purchased physical card and native sale continuation remain bound after genuine acquisition. Gifts, bonus draws, Technology swaps, old hand cards and historical purchase IDs cannot create an opportunity. 2. A Harkonnen **buyer** and Richese cache, Black Market or special-acquisition origins are guarded, not adjudicated. A Harkonnen rival is allowed and retains its ordinary native play outside this offer. These bounds preserve the unresolved independent Nexus cache scope and Harkonnen bonus priority above; they are not printed prohibitions or imports of the native `ixAllyCard` origin filter. 3. Every publicly possible supported buyer receives the same event and public buyer identity, regardless of its secret Nexus face. Only that buyer receives Pass and the exact already-owned purchased card for inspection; only a truly eligible unallied holder of the physical Ixian Nexus receives Use. Opponents receive neither the purchased/replacement face nor private price, receipt, deck order or eligibility reason. The buyer may inspect its own actually held Nexus through the existing authorized inspector; the neutral prompt grants no rival entitlement. 4. **Pass** retains the purchased card and Nexus, closes this opportunity and continues the original sale once. **Use** spends that singleton Nexus once, discards exactly the just-purchased physical Treachery Card, then draws the actual next deck card privately. There is no card selector, arbitrary hand exchange, new Karama counter, refund, repayment or re-auction. diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 790581fe..a7366e1d 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -694,7 +694,7 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP | Advanced Richese ally contribution | The authorized supplied Advanced Richese Bidding clause routes Richese's contribution to its ally's cache/Black Market purchase to Emperor or bank, even when Emperor buys. Preserve the original donor leg separately from buyer-to-Richese credit; cancellation redirects only the Emperor leg without paying twice. Basic retains R2's earlier receipt. Native Banker counts only final bank legs; exhausted-cache counts, positive Black Market self-bidding and Technology custody remain unresolved. | [Source distinction and recipient contract](RICHESE_AUCTION_RULES.md#authorized-advanced-contribution-recipients--4-october-2026) | | Richese Nexus Betrayal auctions | Pause at a valid original prepayment quote. Public self-cache veto discards the exact card without debit/acquisition; another buyer's positive cache/Black Market invoice keeps price/contributions/delivery and earned Harkonnen bonus while overriding the recipient to bank. All publicly possible unallied other-faction held-Nexus seats acknowledge uniformly; private identity controls only own Use. This neutral protocol is an application choice, not publisher timing. Normal hidden-family self-purchases, special-Karama veto and unsupported overlays remain excluded; no native Karama exception certifies them. Bounded engine/SQL/bot/phone scenarios are verified locally; broader verification remains Partial and public starts/full Nexus gates stay closed. | [Printed face, profile, API and saved continuation](NEXUS_RICHESE_RULES.md#bounded-betrayal-auction-contract--30-september-2026), [common privacy protocol](NEXUS_CARD_RULES.md#richese-betrayal-prepayment-protocol), [auction source boundaries](RICHESE_AUCTION_RULES.md) | | Guild Nexus Betrayal shipment payment | Replace one original funded positive fee in full, including own pre-funded payment and the printed full occupied-Junction override; preserve original payer/contributions, consume already-debited donor escrow once and deliver the original physical shipment once. Native applicable stop/rate responses precede the bounded gate; zero/stopped shipments supply no award or Nexus cost. Public possible held-Nexus seats acknowledge uniformly; private Use changes no public price/source disclosure. Fresh classic/native-Guild/Nexus with optional genuinely seeded Homeworlds only; unsupported native Cunning/Richese shipment overlays block before card/parent commitment, and restored transport independently requires the native Guild grant. Bounded engine/SQL/bot/CLI/phone custody and refresh are locally verified; no new priority/immunity, ordinary occupation/rounding ruling or extra shipment. Broad/public/live gates remain closed. | [Printed source and bounded contract](NEXUS_GUILD_RULES.md#nexus-guild-betrayal), [common protocol](NEXUS_CARD_RULES.md#guild-betrayal-funded-payment-protocol), [internal checklist](../game/reference.ts) | -| Ixian Nexus Secret Ally replacement | Fresh classic/base33 Basic/Advanced or Basic Tleilaxu/classic Ix47, Ixians absent and Nexus alone. Spend the singleton Nexus to discard exactly the normal paid/printed-Karama purchased card before drawing the actual deck top; preserve original payment, income and auction suffix once. Full-hand and exhausted-deck same-card redraw remain legal. Uniform buyer Use/Pass is the existing development privacy policy, not publisher timing. Harkonnen buyers, special origins, other overlays and unresolved cache/bonus priority stay guarded; no public start or retrofit. Optional Sandtrout is omitted only in the fresh native branch. | [Printed operation and base contract](NEXUS_IXIAN_REPLACEMENT_RULES.md#bounded-local-runtime-contract), [native integration](NEXUS_IXIAN_REPLACEMENT_RULES.md#native-basic-tleilaxu-integration--2-october-2026) | +| Ixian Nexus Secret Ally replacement | Fresh classic/base33 Basic/Advanced or Basic Tleilaxu/classic Ix47, Ixians absent and Nexus alone. Spend the singleton Nexus to discard exactly the normal purchased card before drawing the actual deck top; paid, printed Karama and successful native BG conversion purchases retain their original payment/income/suffix. Native BG cancellation resolves first: denied direct acquisition supplies no purchase; funded winning-payment recovery supplies a paid receipt. Full-hand/same-card redraw remain legal. Neutral buyer timing is product policy, not publisher priority. Harkonnen buyers, special origins and other overlays remain guarded; no public start or retrofit. | [Printed operation and base contract](NEXUS_IXIAN_REPLACEMENT_RULES.md#bounded-local-runtime-contract), [BG composition](NEXUS_IXIAN_REPLACEMENT_RULES.md#native-bg-converted-purchases--16-october-2026), [native integration](NEXUS_IXIAN_REPLACEMENT_RULES.md#native-basic-tleilaxu-integration--2-october-2026) | | Ixian Nexus Betrayal: Bidding or Technology | Fresh native-Ixian/classic-or-Tleilaxu Basic/Advanced local `ixian-betrayal` uses Ix47 Treachery and Nexus alone, with no initial Nexus deal or old-save retrofit. Resolve actual native Karama/BG counters first; a native denial opens no Nexus gate or cost. After all native passes, uniform publicly possible unallied non-Ixian held-Nexus seats acknowledge before the native effect; concealed face governs only private Use. These are bounded privacy/ordering inferences, not publisher universal priority. One physical Ixian Nexus denies one current Bidding attempt (normal count, no Ixian inspection/return) or one Advanced Technology declaration (both card locations preserved, attempt remains used); all-pass allows the original effect and suffix once. No whole-phase ban, completed undo, invented direct-Nexus Karama counter or blanket immunity. Richese special-lot exchange, wider priority/modules, complete games, strategy and public/live release remain open; verification is Planned pending frozen direct evidence. | [Photographed face and native source protocol](NEXUS_CARD_RULES.md#bounded-ixian-betrayal-source-protocol--1-october-2026), [fresh local runtime and acceptance](NEXUS_CARD_RUNTIME.md#bounded-ixian-betrayal-local-runtime--1-october-2026), [five-facet internal topic](../game/reference.ts) | | Harkonnen Nexus Betrayal local preview | Fresh classic/base33/Nexus-only `harkonnen-betrayal` admits genuine personal/allied native matching calls. Native allied Karama/BG allowance comes first; a provisional uniform acknowledgement precedes the original outcome. Use returns the exact physical traitor immediately and preserves one automatic private actual Mentat replacement. The local ordering/privacy convention is not publisher/user priority and does not answer the previously asked universal reaction policy; public starts, old-game retrofit, arbitrary stacks and combined modules remain gated. Bounded native/private/recovery/CLI/phone verification is Partial development verified, not complete modes or deployment. | [Bounded source contract](NEXUS_HARKONNEN_RULES.md#bounded-betrayal-preview-contract--1-october-2026), [runtime limits](NEXUS_CARD_RUNTIME.md) | | CHOAM Secret Ally | Absent-faction Collection trade spends one actual Worthless card and the Nexus for two bank spice. It is separate from native Cunning and Auditor; the battle inspection alternative remains missing. | [Prototype contract](NEXUS_CHOAM_SECRET_ALLY.md) | diff --git a/game/engine.ts b/game/engine.ts index 3aefc479..7f9a7f47 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -9979,11 +9979,11 @@ function ixianNexusReplacementIntegrity(g: Game): void { nexusRule(() => validateIxianReplacementSource(ixianNexusReplacementContext(g),pending, ixianNexusReplacementParent(g))); } -function offerIxianNexusReplacement(g: Game, printedKarama: boolean): boolean { +function offerIxianNexusReplacement(g: Game): boolean { if (!g.nexusIxianReplacementPreview) return false; const sale = g.currentAuctionSale; const buyer = sale && getPlayer(g,sale.winner); - if (!sale || !buyer || sale.origin !== 'normal' || (sale.free && !printedKarama) || + if (!sale || !buyer || sale.origin !== 'normal' || buyer.faction === 'harkonnen' || buyer.ally || !g.nexusCards?.cards?.hands[buyer.id]) return false; requireRule(!g.response && !g.phaseOpening && !g.truthtrance && !g.pendingKarama, 'Finish the original auction priority before offering the purchased-card choice.'); @@ -13218,7 +13218,7 @@ function auctionNext(g: Game) { a.active = next.player; g.active = a.active; } -function settleAuction(g: Game, free = false, automatic = false, emperorNexus = false, printedKarama = false) { +function settleAuction(g: Game, free = false, automatic = false, emperorNexus = false) { const a = g.auction!; const winner = getPlayer(g, a.bidder!); if (!free) payWithAlly(g, winner, a.bid, a.allyPayment ?? 0); @@ -13254,7 +13254,7 @@ function settleAuction(g: Game, free = false, automatic = false, emperorNexus = if (ixians && winner.ally === ixians.id) { g.pendingIxAlly = { player: winner.id, card: a.cards[a.index].id, free }; g.decision = { kind: 'ixAllyCard', player: winner.id }; - } else if (!offerIxianNexusReplacement(g,printedKarama)) continueAuctionSale(g, free); + } else if (!offerIxianNexusReplacement(g)) continueAuctionSale(g, free); } function currentAuctionContinuationQuote( g: Game, @@ -14630,7 +14630,7 @@ function completeKarama(g: Game, p: Player, use: KaramaUse, card?: Card) { ); } else { if (use.kind === 'purchase') g.auction!.bidder = p.id; - settleAuction(g, true, false, false, card?.effect === 'karama'); + settleAuction(g, true); } } function auctionSpicePaymentFunded(g: Game, p: Player) { diff --git a/game/reference.ts b/game/reference.ts index 1613c33b..9e91efd3 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -4516,7 +4516,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'This fresh local profile admits two-to-six unique classic-faction seats in Basic or Advanced with base Treachery, or additionally Basic Tleilaxu plus classic opponents with the original Ix47 deck and Ixians absent. Nexus alone; optional Sandtrout is omitted only in the fresh native variant. Advanced Tleilaxu, other native expansions, Leader Skills, Homeworlds, Semuta, Discoveries, Ecaz variants and other overlays remain guarded. No public start or saved-game upgrade.', 'Nexus has no starting deal. A draw requires a qualifying closing Nexus with an alliance. A two-seat profile can continue its base game but cannot naturally create an unallied Secret Ally holder; actual replacement needs at least three seats, or four for two simultaneous unallied recipients.', 'The photographed publisher face grants an independent Secret Ally operation when Ixians are absent. Remain unallied and hold the actual singleton Ixian Nexus. Native Ixian allied replacement is a different cancelable advantage and is not removed or used as a fallback. The adopted unofficial Advanced rulebook supplies relevant core rules, not this absent Nexus face or a new Nexus ruling.', - 'Wait for a genuine normal-auction purchase, paid with spice or through an actual printed Karama purchase, including direct printed acquisition before a positive bid. Native Bene Gesserit Worthless/Truthtrance substitutions continue unchanged without this bounded offer; that producer limit is not a ruling that they are not purchases. A Harkonnen buyer and cache, Black Market or special-purchase origins are guarded pending bonus priority and independent Nexus cache-scope rulings. A Harkonnen rival remains allowed. Gifts, bonus draws, Technology swaps and old hand cards are not eligible purchases.', + 'Wait for a genuine normal-auction purchase, paid with spice or through Karama, including direct acquisition before a positive bid. A successful native Advanced BG Worthless-as-Karama conversion gets the same independent replacement choice after its native counter resolves. Canceling direct acquisition supplies no purchase; a canceled winning-payment conversion follows native funded spice recovery and, if bought, supplies a paid receipt instead. Harkonnen buyers and cache, Black Market or special-purchase origins remain guarded pending their separate bonus priority and Nexus scope. Gifts, bonus draws, Technology swaps and old hand cards are not purchases.', 'Every publicly unallied supported buyer with public held-Nexus presence receives the same neutral Purchased card choice, irrespective of the secret face. An irrelevant-face buyer may Pass but cannot Use. This is an inferred development privacy convention, not publisher-prescribed timing or a general resolution of pending anytime and response-priority questions.', 'Only the buyer may inspect its already-owned purchased card in this choice. You may inspect your own actually held Nexus through the existing authorized inspector; the prompt does not identify anybody else’s Nexus. Rivals receive no purchased or replacement face, private price, receipt, deck order or eligibility reason. Earlier Atreides auction inspection does not grant a peek of the replacement.', 'Pass keeps your purchased card and Nexus and closes this purchase opportunity. Use spends your physical Ixian Nexus once and discards exactly that just-purchased Treachery Card before drawing the actual next deck card. You cannot select another held card, replacement face, price or recipient; both actions bind only the offered event.', @@ -4528,11 +4528,11 @@ export const RULE_TOPICS: RuleTopic[] = [ example: 'You buy a card for four spice and inspect it privately. Pass keeps it and the Nexus. Use keeps the original four-spice purchase settled, spends Ixian Nexus, discards that exact card and draws the next deck card. If the draw deck is empty, its new discard joins the actual recycled pile and can be drawn again.', related: ['nexus-cards', 'bidding', 'ix-technology', 'card-karama', 'alliance-funding', 'privacy', 'implementation-checklist'], checklist: [ - { area: 'Implementation', status: 'Partial', detail: 'The fresh bounded profile binds the exact canonical normal paid/printed Karama purchase, physical replacement and closed-event history to native payment/discard/draw/auction continuation. Harkonnen buyers, special origins and combined modules remain guarded.', evidence: ['game/nexus-ixian-replacement.ts', 'game/engine.ts', 'tools/prototype-room.ts'] }, + { area: 'Implementation', status: 'Partial', detail: 'The fresh bounded profile binds the exact canonical normal purchase, physical replacement and closed-event history to native settlement. Successful BG converted acquisition/payment now composes after its native counter; denied direct acquisition and funded paid recovery remain distinct. Harkonnen buyers, special origins and combined modules remain guarded.', evidence: ['game/nexus-ixian-replacement.ts', 'game/engine.ts', 'tools/prototype-room.ts'] }, { area: 'Player controls', status: 'Partial', detail: 'Neutral Purchased card choice separates buyer-only purchased-card inspection and legal Use/Pass from public waiting; own-held Nexus inspection grants no rival entitlement. Broader presentation acceptance remains open.', evidence: ['components/nexus-ixian-replacement.tsx', 'components/game-table.tsx'] }, - { area: 'AI', status: 'Partial', detail: 'All four minimal legal policies consume only their own projected event and eligibility while the underlying auction waits. Full strategy and difficulty calibration remain unfinished.', evidence: ['game/bots.ts'] }, + { area: 'AI', status: 'Partial', detail: 'All four minimal legal policies consume only their own projected event and eligibility while the underlying auction waits. Eight original-state legal actions cover BG converted acquisition/payment. Full strategy and difficulty calibration remain unfinished.', evidence: ['game/bots.ts'] }, { area: 'Documentation', status: 'Partial', detail: 'Photographed face, exact physical target, full-hand/empty-deck rules, original settlement once, privacy convention and unresolved cache/Harkonnen/priority boundaries are recorded without PDF Nexus authority or full-family claims.', evidence: ['docs/NEXUS_IXIAN_REPLACEMENT_RULES.md', 'docs/NEXUS_CARD_RULES.md', 'docs/RULE_DECISIONS.md'] }, - { area: 'Verification', status: 'Partial', detail: 'Original base acquisition/full-hand/depleted-pool/native ledger and private/SQLite/phone evidence remains historical. Native Basic Tleilaxu/Ix47: four meaningful regressions, shared affected110 and final native/guide26, twelve actual card/policy programs, original CLI and390px Thumper-to-Amal/spent-Nexus/buyer3/Emperor10-to12 before phase4 opening. No new assurance campaign; full families/modules and deployment remain open.', evidence: ['tests/nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-engine.test.ts', 'tests/fixtures/nexus-ixian-native-depleted.json', 'tests/fixture-nexus-ixian-replacement.ts', 'tests/fixture-native-ixian-replacement.ts', 'tests/native-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-controls.test.tsx', 'tests/bot-nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-recovery.test.ts', 'tests/prototype-room.test.ts', 'docs/NEXUS_IXIAN_REPLACEMENT_RULES.md'] }, + { area: 'Verification', status: 'Partial', detail: 'Earlier base/native Tleilaxu acquisition, custody, private/recovery and phone evidence remains historical. New BG composition: four meaningful behavior cases, affected nine-file74/74, four original Use/Pass runtime paths, eight legal policy actions and two actual390px original-source replacement/Keep journeys pass. Controlled counter custody separately covers denied direct acquisition versus native funded paid recovery. No new saved/deployed assurance; full families/modules remain open.', evidence: ['tests/nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-engine.test.ts', 'tests/fixtures/nexus-ixian-native-depleted.json', 'tests/fixture-nexus-ixian-replacement.ts', 'tests/fixture-native-ixian-replacement.ts', 'tests/native-ixian-replacement.test.ts', 'tests/bg-ixian-replacement.test.ts', 'tests/fixture-bg-ixian-replacement.ts'] }, ], }, { diff --git a/tests/bg-ixian-replacement.test.ts b/tests/bg-ixian-replacement.test.ts new file mode 100644 index 00000000..a79cb82e --- /dev/null +++ b/tests/bg-ixian-replacement.test.ts @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { applyAction, viewGame } from '../game/engine'; +import { createOriginalBgIxianReplacement, allowBgIxianConversion } from './fixture-bg-ixian-replacement'; +import { settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement'; + +for (const use of ['purchase', 'auctionPayment'] as const) { + void test(`native BG ${use} gives the exact purchased card its independent Ixian replacement without another fee`, () => { + const f = createOriginalBgIxianReplacement(use); + const game = allowBgIxianConversion(f.game); + const offer = viewGame(game, f.buyer).nexusIxianReplacement; + assert.ok(offer?.canUse); + assert.equal(offer.purchased?.id, f.lot.id); + assert.equal(game.pendingNexusIxianReplacement?.free, true); + assert.equal(game.pendingNexusIxianReplacement?.amount, use === 'purchase' ? 0 : 2); + const before = f.beforeCost.players.find(p => p.id === f.buyer)!; + const emperorSpice = f.beforeCost.players.find(p => p.faction === 'emperor')!.spice; + for (const replace of [false, true]) { + const done = settleIxianReplacementFixture(applyAction(game, f.buyer, + { type: replace ? 'nexusIxianReplacementUse' : 'nexusIxianReplacementPass', event: offer.event })); + const owner = done.players.find(p => p.id === f.buyer)!; + assert.equal(owner.spice, before.spice); + assert.equal(done.players.find(p => p.faction === 'emperor')!.spice, emperorSpice); + assert.equal(owner.hand.some(c => c.id === f.cost.id), false); + assert.equal(done.discard.filter(c => c.id === f.cost.id).length, 1); + assert.equal(owner.hand.some(c => c.id === f.lot.id), !replace); + assert.equal(owner.hand.some(c => c.id === f.next.id), replace); + assert.equal(done.discard.filter(c => c.id === f.lot.id).length, replace ? 1 : 0); + assert.equal(done.nexusCards!.cards!.hands[f.buyer], replace ? null : 'ixians'); + assert.equal(done.nexusCards!.cards!.discard.filter(c => c === 'ixians').length, replace ? 1 : 0); + assert.equal(done.nexusIxianReplacementHistory!.length, 2); + assert.equal(done.nexusIxianReplacementHistory!.at(-1)!.source.card.id, f.lot.id); + assert.equal(done.pendingKarama, null); + assert.equal(done.pendingNexusIxianReplacement, null); + assert.equal(done.auction!.index, f.beforeCost.auction!.index + 1); + } + }); + + void test(`canceled native BG ${use} preserves the native unpurchased lot or funded payment`, () => { + const f = createOriginalBgIxianReplacement(use); + const parent = structuredClone(f.beforeCost); + const owner = parent.players.find(p => p.id === f.buyer)!; + const emperor = parent.players.find(p => p.faction === 'emperor')!; + // CONTROLLED counter custody only: move the conserved unused starting + // Karama to Emperor, leaving native auctions, wallets and Nexus untouched. + const index = owner.hand.findIndex(c => c.effect === 'karama'); assert.ok(index >= 0); + const counter = owner.hand.splice(index, 1)[0]; emperor.hand.push(counter); + const balance = owner.spice; + let game = applyAction(parent, f.buyer, f.action); + assert.equal(game.response?.kind, 'worthlessKarama'); + assert.equal(game.pendingNexusIxianReplacement, null); + game = applyAction(game, emperor.id, { type: 'card', card: counter.id, mode: 'cancel' }); + game = settleIxianReplacementFixture(game); + if (use === 'purchase') { + assert.equal(game.pendingNexusIxianReplacement, null); + assert.equal(game.nexusIxianReplacementHistory!.length, 1); + assert.equal(game.nexusCards!.cards!.hands[f.buyer], 'ixians'); + assert.equal(game.players.find(p => p.id === f.buyer)!.spice, balance); + assert.equal(game.players.find(p => p.id === f.buyer)!.hand.some(c => c.id === f.lot.id), false); + assert.equal(game.auction!.cards[game.auction!.index].id, f.lot.id); + } else { + // The native winning-bid fallback automatically pays a funded bid; + // that real paid purchase, not the denied free benefit, earns the choice. + const source = game.pendingNexusIxianReplacement!; + assert.equal(source.free, false); + assert.equal(source.amount, 2); + assert.equal(source.card.id, f.lot.id); + assert.equal(game.players.find(p => p.id === f.buyer)!.spice, balance - 2); + game = settleIxianReplacementFixture(applyAction(game, f.buyer, + { type: 'nexusIxianReplacementUse', event: source.event })); + assert.equal(game.players.find(p => p.id === f.buyer)!.spice, balance - 2); + assert.equal(game.players.find(p => p.faction === 'emperor')!.spice, emperor.spice + 2); + assert.equal(game.players.find(p => p.id === f.buyer)!.hand.some(c => c.id === f.next.id), true); + assert.equal(game.nexusCards!.cards!.hands[f.buyer], null); + } + assert.equal(game.discard.filter(c => c.id === f.cost.id).length, 1); + assert.equal(game.discard.filter(c => c.id === counter.id).length, 1); + }); +} diff --git a/tests/fixture-bg-ixian-replacement.ts b/tests/fixture-bg-ixian-replacement.ts new file mode 100644 index 00000000..052e5478 --- /dev/null +++ b/tests/fixture-bg-ixian-replacement.ts @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import { applyAction, createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine'; +import { createIxianNexusReplacementFixture, settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement'; + +/** Original Advanced BG setup, native worm/alliance/Nexus deal and paid + * Worthless purchase. Keep that first lot, then spend it on a second actual + * normal auction lot. Only original undealt deck/Nexus order is selected. */ +export function createOriginalBgIxianReplacement(use: 'purchase' | 'auctionPayment' = 'purchase') { + const lobby = createGame('BGIXIANREPLACE', newPlayer('b', 'Bene Gesserit', 'beneGesserit'), true, []); + joinGame(lobby, newPlayer('e', 'Emperor', 'emperor')); + joinGame(lobby, newPlayer('g', 'Guild', 'guild')); + const first = createIxianNexusReplacementFixture({ initial: lobby, purchasedKind: 'worthless' }); + let game = settleIxianReplacementFixture(applyAction(first.game, first.buyer, + { type: 'nexusIxianReplacementPass', event: first.event })); + assert.ok(game.auction && !game.response && !game.decision && !game.phaseOpening); + const cost = game.players.find(p => p.id === first.buyer)!.hand.find(c => c.id === first.purchased.id)!; + assert.equal(cost.kind, 'worthless'); + const lot = game.auction.cards[game.auction.index]; + const next = game.deck[0]; assert.ok(next); + if (use === 'auctionPayment') { + for (let n = 0; game.auction!.active !== first.buyer && n < 10; n++) + game = applyAction(game, game.auction!.active, { type: 'passBid' }); + game = applyAction(game, first.buyer, { type: 'bid', amount: 2 }); + for (let n = 0; !game.decision && n < 10; n++) + game = applyAction(game, game.auction!.active, { type: 'passBid' }); + assert.deepEqual(game.decision, { kind: 'auctionPayment', player: first.buyer }); + } + const beforeCost = structuredClone(game); + const action: Action = use === 'purchase' + ? { type: 'card', card: cost.id, mode: 'purchase' } + : { type: 'decision', karama: true, card: cost.id }; + game = applyAction(game, first.buyer, action); + return { game, beforeCost, buyer: first.buyer, cost, lot, next, action, use, + provenance: 'Original setup, qualifying Nexus draw, paid Worthless purchase, original first-lot Pass, then native second-lot BG conversion; no played state staged.' }; +} +export function allowBgIxianConversion(game: Game): Game { + return settleIxianReplacementFixture(game); +} diff --git a/tests/nexus-ixian-replacement-engine.test.ts b/tests/nexus-ixian-replacement-engine.test.ts index 35d6ecab..2f636ae1 100644 --- a/tests/nexus-ixian-replacement-engine.test.ts +++ b/tests/nexus-ixian-replacement-engine.test.ts @@ -322,24 +322,3 @@ void test('direct printed Karama acquisition preserves the original zero bid and inventory(use); }); -void test('nonprinted Bene Gesserit Karama substitution continues native sale without enabling the bounded offer',() => { - const lobby = createGame('BGCONVERT',newPlayer('b','Bene Gesserit','beneGesserit'),true,[]); - joinGame(lobby,newPlayer('e','Emperor','emperor')); - joinGame(lobby,newPlayer('g','Guild','guild')); - const f = createIxianNexusReplacementFixture({initial:lobby}); - const parent = reload(f.beforePayment); - const index = parent.deck.findIndex(c => c.kind === 'worthless'); - assert.ok(index >= 0); - const worthless = parent.deck.splice(index,1)[0]; - owner(parent,f.buyer).hand.push(worthless); - const balance = owner(parent,f.buyer).spice; - let game = applyAction(parent,f.buyer,{type:'decision',karama:true,card:worthless.id}); - game = settleIxianReplacementFixture(game); - assert.equal(game.pendingNexusIxianReplacement,null); - assert.equal(viewGame(game,f.buyer).nexusIxianReplacement,null); - assert.equal(game.nexusCards!.cards!.hands[f.buyer],'ixians'); - assert.equal(owner(game,f.buyer).spice,balance); - assert.ok(owner(game,f.buyer).hand.some(c => c.id === f.purchased.id)); - assert.equal(game.nexusIxianReplacementHistory!.length,0); - inventory(game); -});