From 2b8eb77e4e9d762aaee511246a58cfddff561002 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:55:02 +1100 Subject: [PATCH] Retain Richese one-counter pricing through native Guild rate response --- docs/CURRENT_STATUS.md | 19 +++ docs/GUILD_RATE_KARAMA_RULES.md | 15 +++ docs/IMPLEMENTATION_STATUS.md | 46 +++++++ docs/NEXUS_RICHESE_RULES.md | 30 +++++ docs/NEXUS_RICHESE_RUNTIME.md | 44 +++++++ docs/RULE_DECISIONS.md | 1 + game/engine.ts | 33 ++++- game/nexus-richese-options.ts | 2 +- game/reference.ts | 13 +- tests/fixture-richese-native-guild-rate.ts | 88 +++++++++++++ tests/richese-native-guild-rate.test.ts | 143 +++++++++++++++++++++ 11 files changed, 421 insertions(+), 13 deletions(-) create mode 100644 tests/fixture-richese-native-guild-rate.ts create mode 100644 tests/richese-native-guild-rate.test.ts diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 10a30405..2544b776 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -29,6 +29,25 @@ optional skill bands, combinations and public expansion starts remain open. Rules implementation continues; comprehensive assurance and administration remain deferred until all rules are implemented. +**Native Guild rate with Richese Secret Ally — 15 October.** +The original unpaid counter now preserves one billed counter and all five +physical forces: sand1→2, not ten. Unfunded full2 keeps forces/spice and the +ordinary full-price shipment, but its declared Nexus stays spent without a +refund/reusable coupon under the explicit selected affordability policy. +A still-held card quotes an earlier canceled rate correctly; an independent +purchased price remains separate. + +Affected thirteen-file92/92 and eleven original-action runtime/policy paths +pass, including all four existing profiles. Four actual390px journeys prove +typed Junction5/fee2, unfunded release/ordinary replacement, corrected held +source2 then legal stronghold1, and independent purchased1. No overflow, +browser error or application exception. Two transient browser-tool stale-node +errors were followed by fresh evidence, not replay. No worker/D1/real room, +human server, deployment or mode gate changed. +[Canonical source choice and scoped proof](NEXUS_RICHESE_RUNTIME.md#native-guild-rate-followup--15-october-2026). +Final types/lint and production build pass with existing warnings. +Current links and both still-partial five-facet evidence sets resolve. + **Purchased price and immune Junction source choice — 15 October.** The original offered half/full tariff remains immune; normal payees are unchanged. An already played ordinary price no longer blocks the panel; acceptance diff --git a/docs/GUILD_RATE_KARAMA_RULES.md b/docs/GUILD_RATE_KARAMA_RULES.md index a29525ba..4763b4c2 100644 --- a/docs/GUILD_RATE_KARAMA_RULES.md +++ b/docs/GUILD_RATE_KARAMA_RULES.md @@ -2,6 +2,21 @@ Initial primary-source and engine audit, 2026-09-06. The bounded ordinary-reserve cancellation policy below was selected on 2026-09-28 after a targeted publisher-source search found no affordability procedure; the later original audit remains historical evidence for unfinished transport and module combinations. +## Native price with Richese Secret Ally — 15 October 2026 + +The original Guild holder's Richese source now keeps its one-counter bill +through native rate cancellation: sand1→2 while five actual counters remain +five. Its spent Nexus and unchanged ordinary movement are independent. +An unfunded full2 abandons that declaration with no refund or reusable coupon, +but preserves the ordinary full-price shipment. This is the selected existing +no-debt/spent-card composition, not a new publisher worked example. +A still-held source quotes the canceled rate correctly; an independent +purchased ordinary price remains separate. +[Canonical source policy](NEXUS_RICHESE_RULES.md#native-guild-rate-composition--15-october-2026) +and [92-case/eleven-original-path/four-mobile-journey proof](NEXUS_RICHESE_RUNTIME.md#native-guild-rate-followup--15-october-2026) +retain the distinction from native Richese Cunning, which has no Guild rate. + + ## Purchased price with immune sponsorship — 15 October 2026 The subsequent Junction cutover removes the separate purchased-price/ diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 2faa8029..f46b2444 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,51 @@ # Dune implementation status +## 15 October 2026 — Native Guild price counter retains Richese one-counter source + +Removed the explicit Richese Secret Ally exclusion from native rate +cancellation, without canceling the absent faction's Nexus advantage. +The original signed receipt now retains the unpaid rate frame and actual +settled tariff. Five physical native counters cost one billed counter: +sand1 if allowed, full2 if canceled. Original Advanced interception stays +first; no rounded stronghold discount or independent purchased price is +invented as a native cancelable saving. + +The selected existing no-debt/spent-card policy releases an unfunded full2 +without delivery or payment, leaves ordinary full-price shipment/movement +available and keeps the declared Nexus spent without refund/reusable coupon. +A still-held card survives an earlier rate cancellation. Its shared human/ +AI quote now honors that cancellation instead of showing false affordability; +five can still use the legal stronghold1 alternative. No strategic tuning. + +All source scenarios start with printed original setup, conserved undealt +Spice/Nexus entropy, a real paid Ecaz Karama and an actual Ecaz/Moritani +alliance leaving Guild unallied for its real Richese draw. No played hands, +wallets, armies, phases or decisions are staged. Actual optional bribes +reduce only their original sender's available wallet. + +Affected thirteen-file **92/92** passes, including seven new behavior cases. +Eleven actual original-action runtime/policy paths pass, including four +existing profiles spending the held card on a legal full-rate alternative. +Four real styled390×844 GameTable journeys prove Junction5/fee2, unfunded +release then ordinary one-counter replacement, still-held source sand2 +disabled then five-force stronghold1, and independent purchased1. Exact +physical counters/card costs and original turn completion are observed. +Viewport/scroll width390, no browser error or application exception. +Two transient stale-node tool errors after successful transitions were +resolved with fresh state/accessibility without replay. + +Only an owned memory-only host started/stopped. No worker, D1, authenticated +saved room, human server, deployed revision or mode gate was exercised or +changed. Outside-Git source/runtime/table proof and four initial scenes +remain under `/tmp/dune-richese-native-rate-*20261015*`. +The full goal and final assurance remain unfinished. +[Source and selected policy](NEXUS_RICHESE_RULES.md#native-guild-rate-composition--15-october-2026), +[runtime and limitations](NEXUS_RICHESE_RUNTIME.md#native-guild-rate-followup--15-october-2026). +Final `npm run check:quick` and production build pass, with existing +module-register, large-chunk and Unknown-route warnings. Thirteen current +local links and430 checklist evidence paths resolve; both five-facet +Homeworld/Nexus checklists remain Partial. + ## 15 October 2026 — Explicit purchased-price choice for immune Junction Removed the earlier purchased-price/Junction composition fence without diff --git a/docs/NEXUS_RICHESE_RULES.md b/docs/NEXUS_RICHESE_RULES.md index d5f8a56c..dcd44208 100644 --- a/docs/NEXUS_RICHESE_RULES.md +++ b/docs/NEXUS_RICHESE_RULES.md @@ -2,6 +2,36 @@ Audited 10 September 2026, with a bounded Betrayal implementation contract added 30 September. This separates the ordinary Secret Ally shipment, native Cunning's two-marker operation and Betrayal's two auction outcomes. It does not certify full-family implementation or open the Nexus release gate. See [common Nexus rules](NEXUS_CARD_RULES.md), [auction custody and source boundaries](RICHESE_AUCTION_RULES.md), [No-Field custody](RICHESE_NO_FIELD_RULES.md), and [allied No-Field distinctions](RICHESE_ALLIED_SHIPMENT_AUDIT.md). +## Native Guild rate composition — 15 October 2026 + +Secret Ally retains its printed **one charged counter / up to five physical +forces** distinction. A native Guild holder with Richese absent is expressly +eligible under the source contract below. Ordinary Karama can prevent Guild's +independent native discount, not the absent Richese advantage: desert price +changes from1 to2, not to ten for five forces. The original group, Nexus cost, +native reserve source and ordinary movement remain unchanged. + +The original Advanced Guild interception precedes this separate unpaid rate +response. A stronghold's rounded one-counter half/full prices are both1, +so it has no actual discount to cancel. An independently purchased ordinary +Karama price remains1, bank-paid, and is not the native rate. A still-held +Richese card also honors an earlier native cancellation: sand costs2, while +the legal stronghold alternative costs1. + +**Selected affordability composition:** reuse the existing no-debt withdrawal +policy when full2 cannot be paid. The already declared Nexus remains spent, +without a card refund or a reusable one-counter grant; no forces or shipping +spice depart, and the ordinary shipment remains available at its full rate. +This extends the existing spent-card declaration contract, not a newly found +publisher worked timing example. A card that has not yet been declared stays +held. This is distinct from special Guild prevention, which retains its own +existing shipment-usage policy. + +The unallied native Richese two-marker Cunning holder has no native/allied +Guild rate to cancel; this cutover does not manufacture one. +[Connected runtime and scoped evidence](NEXUS_RICHESE_RUNTIME.md#native-guild-rate-followup--15-october-2026). + + ## Printed effects and source status The Richese panel was freshly read from the [original photographed GF9 cards](https://boardgamegeek.com/image/7767032/dune-ecaz-and-moritani), cached at `/tmp/dune-nexus-cards.jpg`. Secret Ally replaces a normal shipment: choose up to five actual reserve forces and ship them at the price of one. It creates no hidden marker and grants none of Richese's auctions or other advantages. Cunning uses two No-Field tokens for a single shipment, still priced as one force; one token is immediately revealed and placed faceup in front of the shield. diff --git a/docs/NEXUS_RICHESE_RUNTIME.md b/docs/NEXUS_RICHESE_RUNTIME.md index 092940dc..66644ee2 100644 --- a/docs/NEXUS_RICHESE_RUNTIME.md +++ b/docs/NEXUS_RICHESE_RUNTIME.md @@ -2,6 +2,50 @@ This checkpoint adds the [source-audited Secret Ally](NEXUS_RICHESE_RULES.md) to an unallied holder's unused ordinary reserve shipment when Richese is absent. Choose one to five actual forces and price the group as one force. The physical force count, starred identity, destination and arrival restrictions remain ordinary shipment rules. It creates no concealed No-Field and no second shipment. +## Native Guild rate followup — 15 October 2026 + +Original Guild/Richese Secret Ally declarations now admit the distinct unpaid +native rate response after original interception. The signed pending receipt +retains all five physical counters while billing one: allowing costs1 on +sand; cancellation costs2. Actual completion preserves movement and consumes +each original physical card once. No marker or force is invented. + +With only1 available, cancellation ends the already spent Nexus declaration +without delivery, debt or refund and leaves an ordinary full-price shipment +available. The controls explain that consequence before cancellation. This is +the selected [affordability source composition](NEXUS_RICHESE_RULES.md#native-guild-rate-composition--15-october-2026), +not new publisher worked-example authority. A still-held card survives an +earlier cancellation and now quotes sand2 rather than falsely affordable1; +it can instead ship five to a stronghold for1. An independent purchased price +continues to charge1 to bank without a native-rate response. + +Affected thirteen-file **92/92** passes, including seven new behavior cases. +Eleven actual runtime/policy paths use original-action sources, including +all four existing profiles executing a legal full-rate one-counter alternative. +No strategic first-candidate pin, tuning or calibration was introduced. +Setup selects conserved undealt entropy, then real paid Karama acquisition, +Ecaz/Moritani alliance and Guild's real Richese Nexus draw. No played hand, +wallet, army, phase or response is assigned. + +Four real styled390×844 GameTable journeys prove native Junction5/fee2; +poor1/full2 release followed by an ordinary1/one-force replacement; +still-held card's disabled sand2 then five-force stronghold1; and independent +purchased1. Shipment/turn and card/force outcomes are observed; viewport and +scroll width remain390, with no browser error or application exception. +Two transient browser-tool stale-node errors after successful transitions +were handled by fresh state/accessibility, never replay. + +Only an owned memory-only loopback host ran: no worker, D1, authenticated +saved room, human server or deployed revision was exercised or changed. +Proof and four initial scenes remain outside Git under +`/tmp/dune-richese-native-rate-*20261015*`. Full rules, final assurance and +public expansion/module starts remain unfinished. +Final `npm run check:quick` and production build pass with the existing +module-register, large-chunk and Unknown-route warnings. Thirteen current +local links and430 checklist evidence paths resolve; all ten checked +Homeworld/Nexus facets remain Partial. + + ## Price and physical custody The shared [source quote](../game/nexus-richese.ts) separates `physicalAmount` from `pricedAmount: 1`. The whole group costs one spice to a stronghold or two elsewhere; native Guild and an active ordinary Karama rate keep their existing half-price rounding. Karama bank routing remains separate from the Nexus source. Fremen retain free reinforcement and their Great Flat range; this is retained-native-rules composition, not a named faction-specific FAQ ruling. diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 2415f4a0..50960f26 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -670,6 +670,7 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP | Setup and Fremen movement | Preserve source-ordered setup and the distinct declared-movement cancellation; do not recreate fixed historical gaps. | [Setup contract](ADVANCED_SETUP_TEST_SEAM.md), [Fremen Karama](FREMEN_MOVEMENT_KARAMA_RULES.md) | | Guild payment | Rate, transport permission and income are distinct; power owner, shipper, contributors and independent rate-card beneficiary remain distinct. | [Rate/cancellation sources](GUILD_RATE_KARAMA_RULES.md), [implemented payment routing](GUILD_SHIPMENT_PAYMENTS.md) | | Guild native rate Karama | Native prices follow original source rather than blanket module flags. Independent Smuggler counters stay free; own/allied No-Field bills one marker, never materialized value. A supplier’s whole-fee1 approval cannot silently become2: release and obtain a new full-price offer/consent. Typed native World/cross/return retains original sources and approved split; deferred Betrayal retains full-price authority until delivery. Earlier bank-only allied-loan, no-debt withdrawal and return policies remain provisional. | [Current composition and proof](GUILD_RATE_KARAMA_RULES.md#permission-purchase-and-allied-marker-composition--15-october-2026) | +| Guild rate with Richese Secret Ally | A native Guild holder retains the Nexus source's one-counter bill for up to five real forces. Native cancellation changes sand1→2, not the actual physical count or absent Richese advantage. Independently purchased prices remain separate. Under selected existing no-debt/spent-card composition, unaffordable full2 ends the declared Nexus effect without refund/reusable coupon but preserves the ordinary full-price shipment. A still-held card instead remains available with the correct full quote. This is not a publisher worked timing example. | [Source and explicit policy](NEXUS_RICHESE_RULES.md#native-guild-rate-composition--15-october-2026), [original-action runtime and controls](NEXUS_RICHESE_RUNTIME.md#native-guild-rate-followup--15-october-2026) | | Guild native transport permission | A distinct unpaid cross/return response precedes rate/payment. Under the standing choose-recommendation instruction, the provisional denied scope is that original shipment opportunity, preventing immediate redeclaration while preserving a legal different shipment/movement. Signed Cunning second opportunity is distinct; common World-to-foreign-World and immune Junction permissions are not canceled by analogy. Selected special-stop→permission→rate order is product composition, not a new publisher timing FAQ. | [Policy, source and genuine/controlled evidence](GUILD_RATE_KARAMA_RULES.md#permission-purchase-and-allied-marker-composition--15-october-2026) | | Purchased Karama typed price | Supplied authorized Advanced PDF physical16 says any player’s Force Shipment may be half normal, paid to bank. Original typed routes use that independent tariff without new route rights or Nexus stacking; the entire approved invoice is bank-only. Completion consumes only the original grant. Special-stop uses existing provisional card-return parity, refunding the exact original physical card to its owner. Immune Junction uses the separate source choice below, not this card tariff. | [Connected typed routes and source distinction](GUILD_HOMEWORLD_TRANSPORT_RULES.md#separate-permission-and-purchased-price--15-october-2026) | | Purchased price with immune Junction | Keep the original offered half/full tariff and normal payees; no quarter price or bank-only overlay. Under the standing choose-recommendation instruction, accepting this source with a purchased grant requires explicit acknowledgment that it ends unused, with no card refund. Consume only upon actual delivery, not failed, stale, unfunded or deferred acceptance; the ordinary purchased route remains available. This is a conservative product composition, not a publisher worked example. | [Source immunity and selected choice](JUNCTION_TRANSPORT_RULES.md#purchased-price-and-immune-offer--15-october-2026), [original versus controlled proof](JUNCTION_TRANSPORT_RUNTIME.md#purchased-price-source-choice--15-october-2026) | diff --git a/game/engine.ts b/game/engine.ts index 557e7c53..c7ecd3b2 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -26693,13 +26693,12 @@ const CLASSIC_FACTIONS: Partial> = { function ordinaryGuildRateFullCost(g: Game, shipment: PendingShipment): number | null { const guild = byFaction(g, 'guild'); const p = getPlayer(g, shipment.player); - const pricedAmount = shipment.noField || shipment.alliedNoField ? 1 + const pricedAmount = shipment.noField || shipment.alliedNoField || shipment.nexusEvent ? 1 : shipment.amount - (shipment.smuggler ? 1 : 0); if (!guild || (p.id !== guild.id && !(p.ally === guild.id && guild.ally === p.id)) || g.karamaShipping?.player === p.id || (g.guildRateBlocked?.turn === g.turn && g.guildRateBlocked.player === p.id) || - shipment.source || shipment.guildSecretEvent || - shipment.nexusEvent || shipment.richesePair) + shipment.source || shipment.guildSecretEvent || shipment.richesePair) return null; const fullCost = reserveShipmentCost({ faction: p.faction, halfRate: false }, territory(shipment.territory).type, pricedAmount); @@ -26713,6 +26712,7 @@ function openGuildRate(g: Game, shipment: PendingShipment): boolean { const cunning = shipment.guildNexusEvent ? validateGuildCunningShipment(g, shipment.player, shipment.guildNexusEvent, 'reserve', shipment) : null; + const richese = shipment.nexusEvent ? currentNexusRicheseShipment(g, shipment) : null; // A validated older Guild-stop declaration may lack this newer turn stamp. shipment.turn ??= g.turn; const event = crypto.randomUUID(); @@ -26723,13 +26723,14 @@ function openGuildRate(g: Game, shipment: PendingShipment): boolean { cunning.shipment!.frame = JSON.stringify(shipment); saveGuildCunning(g, cunning); } + if (richese) saveNexusRicheseShipment(richese, shipment); g.pendingShipment = shipment; g.response = { kind: 'guildRate', owner: byFaction(g, 'guild')!.id, passed: [], guildRateEvent: event, - intent: `${getPlayer(g, shipment.player).name} declared ${shipment.alliedNoField ? 'one allied No-Field shipment' : shipment.noField ? 'one concealed No-Field' : `${shipment.amount} reserve forces`}${shipment.smugglerCompanion ? ' with one free Smuggler companion' : shipment.smuggler ? ' including one free Smuggler companion' : ''} to ${territory(shipment.territory).name} at the Guild rate of ${shipment.cost} spice. Karama can remove this discount before any payment or arrival.${shipment.smuggler || shipment.smugglerCompanion ? ' The independent free companion remains free.' : ''} The same paid units would cost ${fullCost} at full price; if the original authorized split cannot pay that amount, the declaration returns unused and a replacement this turn uses full price.`, + intent: `${getPlayer(g, shipment.player).name} declared ${shipment.alliedNoField ? 'one allied No-Field shipment' : shipment.noField ? 'one concealed No-Field' : shipment.nexusEvent ? `${shipment.amount} physical reserve forces priced as one by Richese Secret Ally` : `${shipment.amount} reserve forces`}${shipment.smugglerCompanion ? ' with one free Smuggler companion' : shipment.smuggler ? ' including one free Smuggler companion' : ''} to ${territory(shipment.territory).name} at the Guild rate of ${shipment.cost} spice. Karama can remove this discount before any payment or arrival.${shipment.smuggler || shipment.smugglerCompanion ? ' The independent free companion remains free.' : ''} The same paid units would cost ${fullCost} at full price; if the original authorized split cannot pay that amount, the declaration returns unused and a replacement this turn uses the full rate.${shipment.nexusEvent ? ' Richese still prices only one counter, not the actual force count. Its Nexus is already spent: an unaffordable declaration ends without delivery or a card refund, while the ordinary shipment remains available.' : ''}`, }; log(g, `${getPlayer(g, shipment.player).name} declared a Guild-rate reserve shipment; payment and arrival wait for the rate response.`); return true; @@ -26791,6 +26792,7 @@ function finishGuildRateResponse(g: Game, response: ResponseWindow, canceled: bo const cunning = shipment.guildNexusEvent ? validateGuildCunningShipment(g, shipper.id, shipment.guildNexusEvent, 'reserve', shipment) : null; + const richese = shipment.nexusEvent ? currentNexusRicheseShipment(g, shipment) : null; g.pendingShipment = null; delete shipment.guildRateEvent; if (canceled) { @@ -26805,6 +26807,10 @@ function finishGuildRateResponse(g: Game, response: ResponseWindow, canceled: bo delete cunning.shipment; saveGuildCunning(g, cunning); } + if (richese) { + saveNexusRicheseShipment(richese, shipment); + finishNexusRicheseShipment(g, shipment, 'stopped'); + } log(g, `Karama removed Guild's half-price rate from ${shipper.name}'s declaration. Its original approved payment split cannot fund the full price, so no spice or forces were spent and shipment remains available at full price this turn.`); return; } @@ -26817,6 +26823,7 @@ function finishGuildRateResponse(g: Game, response: ResponseWindow, canceled: bo cunning.shipment!.frame = JSON.stringify(shipment); saveGuildCunning(g, cunning); } + if (richese) saveNexusRicheseShipment(richese, shipment, shipmentHalfRate(g, shipper)); commitShipment(g, shipment); if (canceled && shipper.shipped && !g.pendingGuildBetrayal) { delete g.guildRateBlocked; @@ -27770,6 +27777,15 @@ function shipmentHalfRate(g: Game, p: Player) { function nexusRicheseSignature(record: NonNullable[number]) { return JSON.stringify([record.receipt.signature,record.stage,record.frame,record.halfRate]); } +function saveNexusRicheseShipment( + record: NonNullable[number], + shipment: PendingShipment, + halfRate = record.halfRate, +) { + record.frame = JSON.stringify(shipment); + record.halfRate = halfRate; + record.signature = nexusRicheseSignature(record); +} function currentNexusRicheseShipment(g: Game, shipment: PendingShipment) { const record = g.nexusRicheseHistory?.find(r => r.receipt.event === shipment.nexusEvent); requireRule(record?.stage === 'pending' && record.frame === JSON.stringify(shipment) && @@ -27816,7 +27832,9 @@ function nexusRicheseIntegrity(g: Game) { const frame = nexusRule(() => JSON.parse(record.frame)) as PendingShipment; requireRule(frame && frame.nexusEvent === record.receipt.event && frame.player === record.receipt.owner && frame.turn === record.receipt.turn && !frame.noField && !frame.alliedNoField && !frame.source && - Object.keys(frame).every(k => ['nexusEvent','turn','player','territory','sector','amount','elite','cost','allyPayment','advisors','homeworldSources'].includes(k)), + Object.keys(frame).every(k => ['nexusEvent','turn','player','territory','sector','amount','elite','cost','allyPayment','advisors','homeworldSources','guildRateEvent'].includes(k)) && + (frame.guildRateEvent === undefined || (record.stage === 'pending' && + typeof frame.guildRateEvent === 'string' && frame.guildRateEvent.length > 0)), 'Richese Secret Ally has changed its original shipment source.'); const quote = nexusRule(() => quoteNexusRicheseShipment({faction:getPlayer(g,frame.player).faction,halfRate:record.halfRate},territory(frame.territory).type,frame.amount)); requireRule(frame.cost === quote.cost && frame.allyPayment === 0 && Number.isSafeInteger(frame.elite) && @@ -27833,8 +27851,11 @@ function nexusRicheseIntegrity(g: Game) { 'Richese Secret Ally has lost its pending shipment or spent Nexus card.'); currentNexusRicheseShipment(g,pending); const decisions = homeworldSavedDecisions(g).filter(d => d.kind === 'guildShipment'); - requireRule(decisions.length > 0, 'Richese Secret Ally has lost its Guild interception decision.'); + const rates = savedNoFieldResponses(g).filter(response => response.kind === 'guildRate'); + requireRule(decisions.length + rates.length === 1, + 'Richese Secret Ally has lost its unique Guild interception or native-rate response.'); for (const decision of decisions) validateGuildShipmentDecision(g,decision); + for (const response of rates) validateGuildRateResponse(g,response); } } function nexusMoritaniOffer(g: Game, owner: string, decision: Game['decision'] = g.decision) { diff --git a/game/nexus-richese-options.ts b/game/nexus-richese-options.ts index fd7ade50..15f010de 100644 --- a/game/nexus-richese-options.ts +++ b/game/nexus-richese-options.ts @@ -50,7 +50,7 @@ export function nexusRicheseQuote( return quoteNexusRicheseShipment( { faction: me.faction, - halfRate: me.faction === 'guild' || g.karamaShipping?.player === g.me, + halfRate: (me.faction === 'guild' && !g.guildRateCanceled) || g.karamaShipping?.player === g.me, }, destination.type, amount, diff --git a/game/reference.ts b/game/reference.ts index 001fb627..01e04bd7 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -2562,6 +2562,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'Holding Karama also permits bids beyond your available spice. A winner can choose to spend it; an outbid player keeps it.', 'Use Cancel with Karama during a power response. Supported responses include Voice, prescience, Atreides auction and spice peeks, spiritual advisors, Emperor gifts and revival, faction income, Harkonnen bonus cards and allied traitors, plus Fremen storm and worm powers, elite combat strength and Fremen free spice support.', 'Native Guild cross/return permission has its own unpaid counter before price. Preventing it keeps a legal different shipment/movement without removing native half price. The conservative provisional scope is that original opportunity, not a separately signed Cunning second opportunity; immune Junction and common world-to-foreign-world permission remain independent. Native rate cancellation bills the same paid units and original approved split. Free counters stay free and own/allied No-Field prices one marker. Supplier whole-fee1 requires a new offer/consent before becoming2.', + 'A native Guild holder of Richese Secret Ally still ships up to five real forces at one-counter cost. Canceling its native rate changes sand1 to2, not five physical forces to ten paid units. If full2 is unaffordable, the declared Nexus stays spent without refund or reusable coupon while ordinary shipment remains available; this is the selected affordability composition. A still-held card uses the corrected full price. An independent purchased Karama price stays separate.', 'Use a held Karama to purchase the active player’s Force Shipment at half normal price, rounded up, paid entirely to bank. Original typed World routes retain this independent source without new route permission or Nexus stacking. Completion consumes only its grant; original Guild special-stop provisionally returns the exact purchase card to its owner and preserves resources/movement while using shipment. Immune Junction instead keeps its offered tariff and normal payees. Accepting it explicitly acknowledges that the separate purchased price ends unused, without a card refund, only upon actual delivery; the ordinary purchased route remains an alternative.', 'Advanced Bene Gesserit may spend Worthless cards as Karama. Each conversion opens a separate cancellation response; the Worthless card is discarded even if the power is canceled. Normal Karama cards resolve without that conversion response.', 'A pending Worthless purchase keeps space for its original auction card. Gifts and hand exchanges must leave enough room for every committed incoming card. Refreshing restores the same auction, unused shipment opportunity or original canceled power.', @@ -4655,7 +4656,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'Ecaz Cunning lets an unallied native holder spend its physical Nexus at an original quiet boundary in any phase to take the existing Duke for this turn, including living control, original captured custody or the set-aside disc in Tanks. Finish committed auctions, Battle, responses, Ambassador and other physical continuations first; no chooser or phase is stolen. New version3 records bind actual phase and custody; legacy ordinary/version2 Battle history remains strict. Tanks preserve deaths and independently clear old use without spice or ordinary allowance; living already-used Duke gains no fresh use. Standalone no-Skills Ecaz/Advanced Harkonnen connects original capture and provisional permanent liberation; one battle or unused current-turn expiry sets Duke aside, never back to the captor. Actual Ecaz Ghola returns set aside before separate Cunning; foreign-Ghola revival remains forbidden. Roster/module and shared-training guards remain.', 'Fremen Betrayal has two bounded advance-declaration alternatives for an unallied holder in a classic Nexus table. Before the first Spice Blow or worm appearance, spend the physical card to prevent Fremen ordinary and Cunning worm rides for the turn; worm survival, destruction and Nexus still resolve. This clean timing is a product boundary, not an official post-appearance priority ruling. Instead, before Fremen move during Shipment and Movement, spend it to suppress their native two-territory advantage for that turn while ordinary movement and independent ornithopters remain. Only one alternative can be used even if the card is recycled later that turn. Owner controls show the currently available choice; combined modules and reactive post-worm timing remain gated.', 'When Ecaz is absent, an unallied holder may spend its Nexus card at a quiet play boundary to ask whether any one player, including themself, holds one of the holder’s native faction leaders as an ordinary Traitor Card. The owner receives a private yes/no answer saved at the moment of use; no name or count is disclosed. Dead or captured native leaders count, but foreign Gholas, Duke Vidal, Cheap Hero and Face Dancers do not. Native-leader scope and private-only audience are explicit product interpretations, not publisher rulings; exceptional controlled-leader and public-disclosure combinations remain open.', - 'When Richese is absent, its Secret Ally replaces your ordinary reserve shipment with up to five physical forces charged as one. Select the explicit shipment checkbox; forces, elite identity, destination restrictions, Homeworld reserve sources and normal movement remain unchanged. Fremen remain free within their normal reinforcement range; Guild and an active Karama rate retain their existing tariff and payment routing. This is not a concealed No-Field or an extra shipment.', + 'When Richese is absent, its Secret Ally replaces your ordinary reserve shipment with up to five physical forces charged as one. Forces, elite identity, destination restrictions, native Homeworld sources and movement remain unchanged. Fremen retain free reinforcement. Native Guild rate cancellation changes the one-counter sand bill from1 to2 without canceling the Nexus source; an already canceled rate is reflected in the quote. Unfunded declared Nexus stays spent without refund/reusable coupon while ordinary shipment remains available under the selected affordability policy. An independently purchased ordinary price remains separate. This is no concealed marker or extra shipment.', 'Native Richese Cunning uses the original signed two-No-Field shipment in supported single/paired E2 or selected mixed Ix+CHOAM native Nexus tables with classics, optional Skills/Homeworlds/Discovery/Tech/Advanced Strongholds. Select two distinct unused physical tokens: one reveals immediately into reserve-capped forces, one remains concealed; pay one marker invoice and spend the Nexus. The previous shipment token cannot be either choice, and the concealed one becomes last-used. Native/Guild response and actual payer/counter sources remain; a nested destination must be revealed. Prevention keeps physical tokens and unpaid spice, after any printed card cost. Sandtrout, arbitrary companions, shared allocation and other pending combinations retain their guards.', 'Richese Betrayal has a separate opt-in auction preview. Before payment, an unallied rival may spend its physical Richese Nexus card to discard a public self-cache win without any purchase debit, or redirect another buyer’s cache/Black Market payment to the bank while preserving delivery and earned bonus. Every publicly possible holder acknowledges the same opportunity; private Use eligibility cannot determine public timing. Normal hidden self-purchases and private special-Karama acquisitions are not connected. See the dedicated guide for its existing local runtime/browser evidence and remaining release gates; that evidence does not verify Nexus Kull.', 'Guild Cunning is declared when finishing your ordinary Shipment and Movement turn. It spends the Nexus and opens one Karama response. Allowance grants a second native shipment at normal Guild prices, including supported cross-shipment, return and Homeworld routes. Cancellation leaves the completed first turn intact. Only an unused Hajr move can follow the second shipment; no ordinary movement or third move is granted. The physical Ornithopter card combination remains unavailable. The separate guild-betrayal profile admits only original native shipment producers and explicitly blocks this second-shipment offer before Nexus spending; Cunning outside that profile is unchanged.', @@ -4668,11 +4669,11 @@ export const RULE_TOPICS: RuleTopic[] = [ ], related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal', 'nexus-guild-betrayal', 'nexus-ixian-replacement', 'nexus-ixian-betrayal'], checklist: [ - {area:"Implementation",status:'Partial',detail:"Earlier physical roles remain integrated. Native cross/return permission and price have distinct unpaid stages; Cunning preserves its own opportunity. Independent purchased typed prices are bank-only without new route rights. Immune Junction instead retains its offered tariff/payees, with explicit unused-price acknowledgment and consumption only upon delivery. Marker approval cannot silently increase; recipient-specific prices connect fresh offers. Legacy rate-only signatures remain; full acceptance stays open.",evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts', "game/nexus-emperor-purchase.ts", "game/auction-continuation-quote.ts", "game/nexus-emperor-purchase.ts", "game/ambassador-phase.ts", "game/ambassador-resume.ts", "game/battle-aftermath-quote.ts", "game/ecaz-duke-acquisition.ts", "game/ecaz-duke-revival.ts", "game/nexus-ecaz-duke-options.ts", "game/nexus-play-boundary.ts", "game/junction-transport-options.ts"]}, - {area:"Player controls",status:'Partial',detail:"Current four original-action390px Junction journeys show explicit unused-price/no-refund choice, full3, poor2 then sponsor half2, ordinary purchased2 alternative, and full6 preserving Guild6→12 plus movement. No overflow/application exception; one initial Vite dependency request aborted. Earlier five source/loan/refund journeys remain scoped historical evidence.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx", "components/junction-transport.tsx"]}, - {area:"AI",status:'Partial',detail:"All four original policies execute the immune full Junction quote with its explicit acknowledgment, preserving normal offered payment and physical shipment. Native permission filters retain legal alternatives; authorized recipient quotes still supply fresh allied offers. Earlier controlled probes permit optional non-reoffer. No strategic first-candidate pin, tuning or calibration.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']}, - {area:"Documentation",status:'Partial',detail:"Supplied Advanced PDF physical16 supports independent half-normal bank-paid shipment. Junction immunity instead preserves original tariff/payees: explicit unused-price/no-refund acknowledgment and delivery-only grant expiry are selected conservative product composition, not a publisher worked example. Earlier denial/refund/loan policies remain provisional. Original/controlled evidence and public gates stay explicit.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/GUILD_RATE_KARAMA_RULES.md', 'docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md', 'docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md", "docs/JUNCTION_TRANSPORT_RULES.md", "docs/JUNCTION_TRANSPORT_RUNTIME.md"]}, - {area:"Verification",status:'Partial',detail:"Current eleven-file131/131 passes, including12 new cases. Fourteen runtime/policy paths pass:11 original-action and3 explicitly controlled (second-card native denial, existing Betrayal Use/Pass). Four actual original-action390px journeys verify immune tariff/payees, explicit unused grant, ordinary alternative and movement. Earlier263-case/source evidence stays historical; no saved/deployed or complete-module assurance.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.ts", "tests/nexus-emperor-bank-runtime.test.ts", "tests/duke-captured-custody.test.ts", "tests/nexus-ecaz-duke-captured.test.ts", "tests/fixture-nexus-duke-capture.ts", "tests/nexus-duke-capture-runtime.test.ts", "tests/nexus-duke-timing-runtime.test.ts", "tests/junction-purchased-price.test.ts"]}, + {area:"Implementation",status:'Partial',detail:"Native cross/return permission and rate remain separate; Cunning preserves its opportunity. Independent typed purchases and explicit immune Junction source choice retain their original tariffs/payees. Richese Secret Ally now keeps one billed counter and all five physical forces through native Guild rate allowance/cancellation; signed pending/settled sources stay bound. Unfunded release retains spent Nexus without debt/refund/reusable coupon and preserves ordinary shipment. Marker approvals still cannot silently increase; full acceptance remains open.",evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts', "game/nexus-emperor-purchase.ts", "game/auction-continuation-quote.ts", "game/nexus-emperor-purchase.ts", "game/ambassador-phase.ts", "game/ambassador-resume.ts", "game/battle-aftermath-quote.ts", "game/ecaz-duke-acquisition.ts", "game/ecaz-duke-revival.ts", "game/nexus-ecaz-duke-options.ts", "game/nexus-play-boundary.ts", "game/junction-transport-options.ts"]}, + {area:"Player controls",status:'Partial',detail:"Four current original-action390px journeys prove native Junction5/full2, poor1/full2 release plus ordinary one-force replacement, still-held source sand2 disabled then five-force stronghold1, and independent purchased1. The response explains the spent-card consequence. No overflow, browser error or application exception; two tool-only stale-node errors were resolved by fresh evidence without replay. Earlier Junction/loan/refund surfaces remain scoped evidence.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx", "components/junction-transport.tsx"]}, + {area:"AI",status:'Partial',detail:"All four original policies execute a legal still-held Richese full-rate one-counter alternative after native cancellation, with actual physical delivery and card cost. Shared human/AI quotes no longer promise a canceled discount. Native permission alternatives, recipient-specific lender prices and immune Junction source choice remain. Optional non-reoffer is not forced. No strategic first-candidate pin, tuning or calibration.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']}, + {area:"Documentation",status:'Partial',detail:"Richese printed one-counter pricing and native Guild eligibility stay distinct from absent-faction cancellation, physical force count and independent purchased price. Unfunded spent-card/no-refund release is selected existing no-debt composition, not a new publisher worked example. Immune Junction and earlier permission/refund/loan interpretations remain explicit. Full rules and public gates stay open.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/GUILD_RATE_KARAMA_RULES.md', 'docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md', 'docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md", "docs/JUNCTION_TRANSPORT_RULES.md", "docs/JUNCTION_TRANSPORT_RUNTIME.md"]}, + {area:"Verification",status:'Partial',detail:"Current thirteen-file92/92 passes, including7 new meaningful cases. Eleven original-action runtime/policy paths and four actual original-action390px journeys pass. Original setup, paid Karama, actual Ecaz/Moritani alliance and real Guild Richese draw retain conserved undealt entropy, without played-state staging. Earlier131-case Junction and263-case source checks remain historical. No saved/deployed or complete-module assurance.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.ts", "tests/nexus-emperor-bank-runtime.test.ts", "tests/duke-captured-custody.test.ts", "tests/nexus-ecaz-duke-captured.test.ts", "tests/fixture-nexus-duke-capture.ts", "tests/nexus-duke-capture-runtime.test.ts", "tests/nexus-duke-timing-runtime.test.ts", "tests/junction-purchased-price.test.ts", "tests/richese-native-guild-rate.test.ts"]}, ], }, { diff --git a/tests/fixture-richese-native-guild-rate.ts b/tests/fixture-richese-native-guild-rate.ts new file mode 100644 index 00000000..7ae1838f --- /dev/null +++ b/tests/fixture-richese-native-guild-rate.ts @@ -0,0 +1,88 @@ +import assert from 'node:assert/strict'; +import { viewGame, type Action } from '../game/engine'; +import { TERRITORIES } from '../game/board'; +import { presenceAt } from '../game/force-presence'; +import { nexusCardAction } from '../game/nexus-card-options'; +import { nexusRicheseAction, nexusRicheseQuote } from '../game/nexus-richese-options'; +import { createGuildAmbassadorIncomeArrival } from './fixture-guild-ambassador-income-arrival'; +import { + advancePairedE3Nexus, pairedE3NexusClean, pairedE3NexusPlayer, + pairedE3NexusReload, quietPairedE3NexusChoice, stepPairedE3Nexus, +} from './fixture-paired-e3-nexus-modules'; +import { cancelNativeGuildRateModules } from './fixture-native-guild-rate-modules'; + +/** Original setup, real paid Ecaz Karama, real Ecaz/Moritani alliance and + * Guild Nexus draw. Only conserved UNDEALT Spice/Nexus order is selected; + * no played hand, wallet, army, phase or response is assigned. */ +export function createRicheseNativeGuildRate(options: { + poor?: boolean; + priorCancellation?: boolean; + purchased?: boolean; + homeworlds?: boolean; + destinationType?: 'sand' | 'stronghold'; +} = {}) { + const original = createGuildAmbassadorIncomeArrival({ homeworlds: !!options.homeworlds }); + const prefix = original.actions.findIndex(s => s.actor === original.entrant && + s.action.type === 'alliance' && s.action.target === original.guild); + assert.ok(prefix >= 0); + const actions = original.actions.slice(0, prefix); + let game = pairedE3NexusReload(original.beforeAlliance); + const cards = game.nexusCards!.cards!; + assert.ok(Object.values(cards.hands).every(card => card === null)); + const at = cards.deck.indexOf('richese'); assert.ok(at >= 0); + cards.deck.unshift(cards.deck.splice(at, 1)[0]); + for (const [actor, target] of [[original.ecaz, original.entrant], [original.entrant, original.ecaz]]) + game = stepPairedE3Nexus(game, { actor, action: { type: 'alliance', target } }, actions); + game = advancePairedE3Nexus(game, state => state.nexusCards?.phase?.stage === 'drawing' && + pairedE3NexusClean(state), actions, quietPairedE3NexusChoice); + assert.equal(pairedE3NexusPlayer(game, original.guild).ally, null); + const draw = nexusCardAction(viewGame(game, original.guild), 'draw'); assert.ok(draw); + game = stepPairedE3Nexus(game, { actor: original.guild, action: draw }, actions); + assert.equal(game.nexusCards!.cards!.hands[original.guild], 'richese'); + game = advancePairedE3Nexus(game, state => state.phase === 5 && state.active === original.guild && + pairedE3NexusClean(state), actions, quietPairedE3NexusChoice); + const source = pairedE3NexusReload(game); + const destination = TERRITORIES.find(t => t.type === (options.destinationType ?? 'sand') && + t.sectors.some(sector => sector !== game.storm) && + game.players.every(player => presenceAt(player, t.id) === 0)); + assert.ok(destination); + const sector = destination.sectors.find(s => s !== game.storm)!; + if (options.poor || options.priorCancellation) { + const excess = pairedE3NexusPlayer(game, original.guild).spice - 1; + assert.ok(excess >= 0); + if (excess) game = stepPairedE3Nexus(game, { actor: original.guild, + action: { type: 'bribe', target: original.ecaz, amount: excess } }, actions); + } + if (options.priorCancellation) { + game = stepPairedE3Nexus(game, { actor: original.guild, + action: { type: 'ship', territory: destination.id, sector, amount: 1, elite: 0 } }, actions); + if (game.decision?.kind === 'guildShipment') game = stepPairedE3Nexus(game, + { actor: original.guild, action: { type: 'decision', allow: true } }, actions); + const canceled = cancelNativeGuildRateModules(game, original.ecaz, original.karama); + game = canceled.game; actions.push(...canceled.actions); + assert.equal(pairedE3NexusPlayer(game, original.guild).shipped, false); + assert.equal(game.nexusCards!.cards!.hands[original.guild], 'richese'); + } + if (options.purchased) game = stepPairedE3Nexus(game, { actor: original.ecaz, + action: { type: 'card', card: original.karama, mode: 'shipment', target: original.guild } }, actions); + const view = viewGame(game, original.guild); + const quote = nexusRicheseQuote(view, destination.id, 5); assert.ok(quote); + const shipment: Action = { type: 'ship', territory: destination.id, sector, + amount: 5, elite: 0, allyPayment: 0 }; + if (options.homeworlds) shipment.homeworldSources = { 'homeworld:guild': { normal: 5, elite: 0 } }; + const action = nexusRicheseAction(view, view.nexusRichese!.event, shipment); assert.ok(action); + return { original, source, actions, beforeAction: pairedE3NexusReload(game), action, quote, + guild: original.guild, canceler: original.ecaz, karama: original.karama, + destination: destination.id, sector, target: `${destination.id}:${sector}`, + provenance: [...original.provenance, + 'Conserved original undealt Richese Nexus moved to the next draw; no card identity invented.', + 'Actual Ecaz/Moritani alliance leaves Guild unallied; original Guild chooses its real Nexus draw.', + 'Actual quiet progression to Guild turn; optional bribe, prior rate cancellation or purchased price are real actions.'] }; +} + +export function declareRicheseNativeGuildRate(fixture: ReturnType) { + let game = stepPairedE3Nexus(fixture.beforeAction, { actor: fixture.guild, action: fixture.action }); + if (game.decision?.kind === 'guildShipment') game = stepPairedE3Nexus(game, + { actor: fixture.guild, action: { type: 'decision', allow: true } }); + return game; +} diff --git a/tests/richese-native-guild-rate.test.ts b/tests/richese-native-guild-rate.test.ts new file mode 100644 index 00000000..798bb8d0 --- /dev/null +++ b/tests/richese-native-guild-rate.test.ts @@ -0,0 +1,143 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { applyAction, viewGame, type Action, type Game } from '../game/engine'; +import { TERRITORIES } from '../game/board'; +import { presenceAt } from '../game/force-presence'; +import { nexusRicheseAction } from '../game/nexus-richese-options'; +import { createRicheseNativeGuildRate, declareRicheseNativeGuildRate } from './fixture-richese-native-guild-rate'; +import { + allowNativeGuildRateModules, cancelNativeGuildRateModules, + settleNativeGuildRateModules, +} from './fixture-native-guild-rate-modules'; +import { pairedE3NexusPlayer, pairedE3NexusReload, stepPairedE3Nexus } from './fixture-paired-e3-nexus-modules'; + +function reject(game: Game, actor: string, action: Action) { + const before = pairedE3NexusReload(game); + assert.throws(() => applyAction(game, actor, action)); + assert.deepEqual(game, before); +} +function spentOnce(game: Game, guild: string) { + assert.equal(game.nexusCards!.cards!.hands[guild], null); + assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1); +} + +for (const canceled of [false, true]) void test(`native Guild ${canceled ? 'cancellation' : 'allowance'} retains five actual Richese forces and one charged counter`, () => { + const f = createRicheseNativeGuildRate(); + let game = declareRicheseNativeGuildRate(f); + assert.equal(game.response?.kind, 'guildRate'); + assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 15); + assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 6); + assert.equal(pairedE3NexusPlayer(game, f.guild).shipped, false); + spentOnce(game, f.guild); + assert.ok(viewGame(game, f.canceler).responseControls!.cancelCards.includes(f.karama)); + reject(game, f.guild, f.action); + game = canceled ? cancelNativeGuildRateModules(game, f.canceler, f.karama).game + : allowNativeGuildRateModules(game).game; + game = settleNativeGuildRateModules(game).game; + const owner = pairedE3NexusPlayer(game, f.guild); + assert.equal(owner.spice, canceled ? 4 : 5); + assert.equal(owner.reserves, 10); + assert.equal(owner.forces[f.target], 5); + assert.equal(owner.shipped, true); + assert.equal(owner.moved, 0); + assert.equal(game.discard.filter(card => card.id === f.karama).length, canceled ? 1 : 0); + assert.equal(pairedE3NexusPlayer(game, f.canceler).hand.some(card => card.id === f.karama), !canceled); + spentOnce(game, f.guild); + reject(game, f.guild, f.action); + game = stepPairedE3Nexus(game, { actor: f.guild, action: { type: 'endMovement' } }); + assert.notEqual(game.active, f.guild); + assert.equal(pairedE3NexusPlayer(game, f.guild).forces[f.target], 5); +}); + +void test('an unaffordable full one-counter fee ends the spent Nexus declaration without delivery, debt or a second card use', () => { + const f = createRicheseNativeGuildRate({ poor: true }); + let game = declareRicheseNativeGuildRate(f); + assert.equal(game.response?.kind, 'guildRate'); + game = cancelNativeGuildRateModules(game, f.canceler, f.karama).game; + const owner = pairedE3NexusPlayer(game, f.guild); + assert.equal(owner.spice, 1); + assert.equal(owner.reserves, 15); + assert.equal(owner.forces[f.target] ?? 0, 0); + assert.equal(owner.shipped, false); + assert.equal(owner.moved, 0); + assert.equal(game.response, null); + assert.equal(game.pendingShipment, null); + spentOnce(game, f.guild); + reject(game, f.guild, f.action); + const destination = TERRITORIES.find(t => t.type === 'stronghold' && + t.sectors.some(s => s !== game.storm) && game.players.every(p => presenceAt(p, t.id) === 0)); + assert.ok(destination); + const sector = destination.sectors.find(s => s !== game.storm)!; + game = stepPairedE3Nexus(game, { actor: f.guild, + action: { type: 'ship', territory: destination.id, sector, amount: 1, elite: 0 } }); + if (game.decision?.kind === 'guildShipment') game = stepPairedE3Nexus(game, + { actor: f.guild, action: { type: 'decision', allow: true } }); + game = settleNativeGuildRateModules(game).game; + assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 0); + assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 14); + assert.equal(pairedE3NexusPlayer(game, f.guild).forces[`${destination.id}:${sector}`], 1); + spentOnce(game, f.guild); +}); + +void test('earlier native cancellation quotes the held source at full price and keeps a legal one-spice alternative', () => { + const f = createRicheseNativeGuildRate({ priorCancellation: true }); + assert.equal(f.quote.cost, 2); + assert.equal(viewGame(f.beforeAction, f.guild).guildRateCanceled, true); + assert.equal(pairedE3NexusPlayer(f.beforeAction, f.guild).spice, 1); + reject(f.beforeAction, f.guild, f.action); + const destination = TERRITORIES.find(t => t.type === 'stronghold' && + t.sectors.some(s => s !== f.beforeAction.storm) && + f.beforeAction.players.every(p => presenceAt(p, t.id) === 0)); + assert.ok(destination); + const sector = destination.sectors.find(s => s !== f.beforeAction.storm)!; + const view = viewGame(f.beforeAction, f.guild); + const action = nexusRicheseAction(view, view.nexusRichese!.event, + { type: 'ship', territory: destination.id, sector, amount: 5, elite: 0 }); + assert.ok(action); + let game = declareRicheseNativeGuildRate({ ...f, action }); + game = settleNativeGuildRateModules(game).game; + assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 0); + assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 10); + assert.equal(pairedE3NexusPlayer(game, f.guild).forces[`${destination.id}:${sector}`], 5); + spentOnce(game, f.guild); +}); + +void test('the independently purchased price remains one bank spice for the original five-force source', () => { + const f = createRicheseNativeGuildRate({ purchased: true }); + let game = declareRicheseNativeGuildRate(f); + assert.notEqual(game.response?.kind, 'guildRate'); + game = settleNativeGuildRateModules(game).game; + assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 5); + assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 10); + assert.equal(pairedE3NexusPlayer(game, f.guild).forces[f.target], 5); + assert.equal(game.karamaShipping, null); + assert.equal(game.discard.filter(card => card.id === f.karama).length, 1); + spentOnce(game, f.guild); +}); + +void test('a rounded stronghold price with no actual discount has no native rate counter', () => { + const f = createRicheseNativeGuildRate({ destinationType: 'stronghold' }); + assert.equal(f.quote.cost, 1); + let game = declareRicheseNativeGuildRate(f); + assert.notEqual(game.response?.kind, 'guildRate'); + game = settleNativeGuildRateModules(game).game; + assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 5); + assert.equal(pairedE3NexusPlayer(game, f.guild).forces[f.target], 5); + assert.ok(pairedE3NexusPlayer(game, f.canceler).hand.some(card => card.id === f.karama)); + spentOnce(game, f.guild); +}); + +void test('the native Junction withdrawal stays five physical counters while cancellation charges only one full counter', () => { + const f = createRicheseNativeGuildRate({ homeworlds: true }); + reject(f.beforeAction, f.guild, { ...f.action, + homeworldSources: { 'homeworld:ecaz': { normal: 5, elite: 0 } } }); + let game = declareRicheseNativeGuildRate(f); + assert.equal(game.response?.kind, 'guildRate'); + game = cancelNativeGuildRateModules(game, f.canceler, f.karama).game; + game = settleNativeGuildRateModules(game).game; + assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 4); + assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 10); + assert.equal(pairedE3NexusPlayer(game, f.guild).forces[f.target], 5); + assert.equal(pairedE3NexusPlayer(game, f.guild).moved, 0); + spentOnce(game, f.guild); +});