From 8f9f980075539a4b3364bfd576ce3327571af19e Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:30:11 +1100 Subject: [PATCH] Compose purchased shipment price with immune Junction source --- components/junction-transport.tsx | 19 +- docs/CURRENT_STATUS.md | 26 ++- docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md | 5 +- docs/GUILD_RATE_KARAMA_RULES.md | 15 ++ docs/IMPLEMENTATION_STATUS.md | 44 +++++ docs/JUNCTION_TRANSPORT_RULES.md | 24 +++ docs/JUNCTION_TRANSPORT_RUNTIME.md | 33 ++++ docs/RULE_DECISIONS.md | 3 +- game/engine.ts | 24 ++- game/junction-transport-options.ts | 7 +- game/reference.ts | 219 +++++++++++----------- tests/fixture-junction-purchased-price.ts | 62 ++++++ tests/junction-purchased-price.test.ts | 181 ++++++++++++++++++ 13 files changed, 540 insertions(+), 122 deletions(-) create mode 100644 tests/fixture-junction-purchased-price.ts create mode 100644 tests/junction-purchased-price.test.ts diff --git a/components/junction-transport.tsx b/components/junction-transport.tsx index fe593167..0ebe6c4f 100644 --- a/components/junction-transport.tsx +++ b/components/junction-transport.tsx @@ -42,6 +42,13 @@ export function JunctionTransport({ cross-shipment at half or full price, including travel to and from Homeworlds. They choose whether to use it for their shipment.
+ {g.karamaShipping?.player === option.recipient && ( ++ Junction’s permission and offered tariff resist Karama. If the + recipient uses this offer, their separate purchased price ends + unused; its played card is not refunded. +
+ )} {option.offer && (Current offer: {option.offer.rate} price.
)} @@ -99,6 +106,14 @@ export function JunctionTransport({ elsewhere. The Guild’s {option.offer.rate}-price offer applies to the total; fractions round up. + {quote.forfeitPurchasedRate && ( ++ This immune Junction offer keeps its {option.offer.rate} tariff + and normal payment recipients. Your separate purchased Karama + price will end unused if you accept; its played card is not + refunded. You may instead choose an ordinary purchased-price route. +
+ )} {option.blocked && ( )} @@ -220,7 +235,9 @@ export function JunctionTransport({ if (quote.action) act(quote.action); }} > - Use Junction transport + {quote.forfeitPurchasedRate + ? 'Use Junction transport · purchased price ends unused' + : 'Use Junction transport'} > diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index d3d64d66..10a30405 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. +**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 +explicitly acknowledges that it ends unused, with no card refund. +Only actual delivery consumes it, including deferred Betrayal. Failed, +unfunded or stale actions preserve it; an ordinary purchased route remains +an alternative. This is a conservative product composition, not a publisher +worked example. + +Affected eleven-file131/131 and fourteen actual runtime/policy paths pass +(eleven original-action/three controlled). Four actual390px original-action +journeys prove full3, poor2/full3 then replacement half2, ordinary purchased2, +and normal full6 Guild income plus movement. No overflow/application +exception; one initial Vite dependency request aborted. No real room, +database, human server, deployment or public-start gate changed. +[Canonical policy and scoped evidence](JUNCTION_TRANSPORT_RUNTIME.md#purchased-price-source-choice--15-october-2026). +Final types/lint and production build pass with existing warnings. +Current local links and both still-partial five-facet evidence sets resolve. + **Separate transport permission, purchased typed price and allied marker — 15 October.** Native cross/return now has its own unpaid counter before price. Denial preserves legal alternatives and half price; the bounded original-opportunity @@ -46,9 +65,10 @@ runtime/policy paths pass (15 original-action/five controlled), plus four controlled optional-offer/recipient continuations. No overflow/application exception; one initial Vite dependency fetch aborted. Only the owned memory-only QA engine reloaded, retaining its exact synthetic states. -Purchased-price/Junction sponsorship remains a separately tracked source -composition; full combinations, assurance, public starts and full goal remain open. -[Current authority and explicit interpretation](GUILD_RATE_KARAMA_RULES.md#permission-purchase-and-allied-marker-composition--15-october-2026). +The separately tracked purchased-price/Junction source composition is +connected by the newer checkpoint above; full combinations, assurance, +public starts and full goal remain open. +[Earlier authority and explicit interpretation](GUILD_RATE_KARAMA_RULES.md#permission-purchase-and-allied-marker-composition--15-october-2026). Affected25-file263/263 passes across frozen237+26 selections; final saved/ deployed assurance is deliberately not claimed by these source checks. Final types/lint/build and repeated20+4 source smoke pass; existing build diff --git a/docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md b/docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md index 4da09ac2..f8aea158 100644 --- a/docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md +++ b/docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md @@ -26,7 +26,10 @@ finish the original turn. A separate original typed return allows native permission, then cancels rate: fee3, approved donor1, Guild7→6 after existing donor income, Junction15→18 and Tuek5→2. Controlled second-card special-stop instead returns the original gifted card without any departure/payment. -[Current authority, policy distinction and remaining sponsored-price gate](GUILD_RATE_KARAMA_RULES.md#permission-purchase-and-allied-marker-composition--15-october-2026). +[Current authority and explicit interpretation](GUILD_RATE_KARAMA_RULES.md#permission-purchase-and-allied-marker-composition--15-october-2026). +The subsequent [immune Junction source choice](JUNCTION_TRANSPORT_RULES.md#purchased-price-and-immune-offer--15-october-2026) +keeps its original tariff/payees and explicitly acknowledges that a separately +purchased price ends unused only upon actual delivery, without a card refund. ## Native rate cancellation runtime — 15 October 2026 diff --git a/docs/GUILD_RATE_KARAMA_RULES.md b/docs/GUILD_RATE_KARAMA_RULES.md index 6a89d201..a29525ba 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. +## Purchased price with immune sponsorship — 15 October 2026 + +The subsequent Junction cutover removes the separate purchased-price/ +sponsorship gate recorded below. The offered Homeworld tariff remains immune +and normal payment routing is unchanged: no quarter price or borrowed bank-only rule. +Choosing it with a purchased ordinary grant requires an explicit unused-price +acknowledgment, no card refund, and actual delivery before grant consumption. +Failed, declined or deferred acceptance retains the grant; the ordinary +purchased route remains an alternative. This is a selected conservative +product composition, not a new publisher worked example. +[Canonical source choice](JUNCTION_TRANSPORT_RULES.md#purchased-price-and-immune-offer--15-october-2026) +and [131-case, fourteen-path, four-mobile-journey evidence](JUNCTION_TRANSPORT_RUNTIME.md#purchased-price-source-choice--15-october-2026) +distinguish eleven original runtime paths from three controlled continuations. + + ## Permission purchase and allied marker composition — 15 October 2026 Three distinct sources now remain distinct end to end: diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index d9976888..2faa8029 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,49 @@ # Dune implementation status +## 15 October 2026 — Explicit purchased-price choice for immune Junction + +Removed the earlier purchased-price/Junction composition fence without +canceling or repricing the Homeworld source. Original half/full fee and +normal payees remain: no quarter price and no card bank-only overlay. +Accepting with a separate grant requires `forfeitPurchasedRate: true`; +actual controls explain that it ends unused and its played card is not +refunded. The independently legal ordinary purchased route stays available. +This is a conservative selected product composition, not a publisher +worked timing example. + +Original card-first/offer-first orders bind the active grant in their +existing declaration identity. Missing/false/stale acknowledgment rejects +without mutation. Unfunded, declined and deferred acceptance preserves the +grant; only actual delivery consumes it. Existing Guild Betrayal keeps the +same offered invoice through Use/Pass. Original movement remains available. +Pure Junction tariff and offer rules are intentionally unchanged. + +Affected eleven-file **131/131** passes, including twelve new meaningful +regressions. Fourteen actual runtime/policy paths pass: eleven original-action, +three explicitly controlled (second physical card native denial plus +existing Betrayal Use/Pass). All four original policies execute the immune +full quote without strategic tuning or calibration. + +Four real styled390×844 GameTable journeys use original-action scenes: +full3; poor2 rejecting full3 then actual sponsor replacement half2; +ordinary purchased2 instead; full6 native arrival with Guild6→12 and +subsequent ordinary movement. Exact counters and the original spent card +remain, no refund; successful shipment consumes the grant once. +Viewport and scroll width390, no application exception; one initial +Vite dependency fetch aborted during optimization. + +Only an owned memory-only host was started/stopped. No worker, D1, +authenticated saved room, human server, deployment or publication gate +was exercised or changed. Outside-Git source/API/table JSON and four initial +scenes are retained under `/tmp/dune-junction-purchased-*20261015*`. +The full goal, remaining rule combinations and final assurance remain open. +[Source choice](JUNCTION_TRANSPORT_RULES.md#purchased-price-and-immune-offer--15-october-2026), +[runtime contract and scope](JUNCTION_TRANSPORT_RUNTIME.md#purchased-price-source-choice--15-october-2026). +Final `npm run check:quick` and production build pass; existing +module-register, large-chunk and Unknown-route warnings remain. +Sixteen current local links and429 evidence paths resolve; both +five-facet checklists remain Partial, with no complete-mode claim. + ## 15 October 2026 — Separate native permission, independent typed price and loan approval Native cross/return now uses a distinct unpaid `guildTransportPermission` diff --git a/docs/JUNCTION_TRANSPORT_RULES.md b/docs/JUNCTION_TRANSPORT_RULES.md index 0d6d00d1..25f10e08 100644 --- a/docs/JUNCTION_TRANSPORT_RULES.md +++ b/docs/JUNCTION_TRANSPORT_RULES.md @@ -2,6 +2,30 @@ 9 September 2026. Implementation contract for Junction's high-face transport advantage. This is a source audit, not certification of a completed transport module. It supplements [the component audit](HOMEWORLD_COMPONENT_AUDIT.md), [invasion rules](HOMEWORLD_INVASION_RULES.md), and [transport source update](HOMEWORLD_TRANSPORT_SOURCE_UPDATE.md). +## Purchased price and immune offer — 15 October 2026 + +The accepted Junction source keeps its printed half/full tariff and normal +payment recipients. An already purchased ordinary Karama price does not +reprice that immune Homeworld advantage, stack into quarter price or impose +its bank-only payment rule. The recipient may instead choose an independently +legal ordinary purchased-price route. + +**Selected product composition, not a publisher worked example:** accepting +Junction with that separate grant active requires an explicit +`forfeitPurchasedRate: true` acknowledgment. The controls explain that the +price ends unused and its already played card is not refunded. Only actual +delivery consumes the grant; an unfunded, rejected, stale or declined offer +does not. Deferred Guild Betrayal retains it until the original shipment +settles. Its fee remains the original immune offered fee. + +The existing declaration identity binds the active grant as well as the +original offer and opportunity. An older quote cannot acknowledge a later +purchase. No extra shipment, permission counter or special-stop choice is +created. This conservative source choice replaces the old purchased-price/ +Junction composition gate, not the remaining complete-module gates. +[Connected engine, controls and scoped proof](JUNCTION_TRANSPORT_RUNTIME.md#purchased-price-source-choice--15-october-2026). + + ## Evidence and precedence Fresh publisher-indexed passages were retrieved from the [GF9 E3 rulebook, pp.10 and 15](https://www.gf9games.com/dune/wp-content/uploads/EcazMoritani-Rulebook-LOWRES.pdf#page=10), [base rulebook, pp.9 and 19](https://www.gf9games.com/dunegame/wp-content/uploads/Dune-Rulebook.pdf#page=19), [November 2020 FAQ, pp.2 and 5](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf#page=2), and [E1 cancellation table, p.12](https://www.gf9games.com/dunegame/wp-content/uploads/2020/09/IxianAndTleilaxuRulebook.pdf#page=12). Direct publisher PDF opens returned HTTP 403. This review does not claim a new binary download or new visual inspection of the physical card: the previously inspected research images under `/tmp/dune-rules` are absent in the current environment. diff --git a/docs/JUNCTION_TRANSPORT_RUNTIME.md b/docs/JUNCTION_TRANSPORT_RUNTIME.md index 2cac1ae5..c937b30b 100644 --- a/docs/JUNCTION_TRANSPORT_RUNTIME.md +++ b/docs/JUNCTION_TRANSPORT_RUNTIME.md @@ -2,6 +2,39 @@ 9 September 2026. [Source contract and adjudicated compositions](JUNCTION_TRANSPORT_RULES.md). This extends [Guild transport](GUILD_HOMEWORLD_TRANSPORT_RUNTIME.md) with Junction's distinct high-population grant. Homeworld game starts remain disabled pending completion of the entire module. +## Purchased price source choice — 15 October 2026 + +An active purchased ordinary price no longer blocks the original Junction +panel. Its offered half/full tariff remains immune and normal payment routing +is unchanged: no quarter price or bank-only overlay. The recipient can use the ordinary +purchased route instead. Accepting the sponsored source explicitly says +**“purchased price ends unused”**, with no refund of the already played card. +The server requires that acknowledgment and consumes the grant only after +actual delivery, including deferred Betrayal. Failed/unfunded/stale actions +retain it. This is an explicit [product source composition](JUNCTION_TRANSPORT_RULES.md#purchased-price-and-immune-offer--15-october-2026), +not a newly located publisher timing example. + +The affected eleven-file selection passes **131/131**, including twelve new +consumer-visible cases. Fourteen actual runtime/policy paths pass: eleven +original-action and three explicitly controlled (second-card native denial +and existing Betrayal Use/Pass continuations). All four original AI policies +execute the same full offered quote; no strategy or calibration changed. + +Four actual styled390×844 GameTable journeys use original-action scenes: +full3 acceptance; poor2/full3 rejection then fresh half2 replacement; ordinary +purchased2 instead of full3; and full6 native-to-sand arrival preserving +Guild6→12. Physical counters/card custody, unused grant lifetime, original +movement and turn completion are observed. Viewport/scroll width remain390. +One initial Vite dependency request aborted; no application exception. +The host was owned and memory-only: no worker, D1, authenticated saved room, +human server, deployed revision or public-start gate was exercised or changed. +Proof is retained outside Git under `/tmp/dune-junction-purchased-*20261015*`. +Final `npm run check:quick` (types/lint) and production build pass. +Existing module-register, large-chunk and Unknown-route warnings remain. +Sixteen current local links and both five-facet evidence sets resolve; +all ten facets remain Partial. Complete rules and deployed acceptance remain open. + + ## Offers and routes Guild can offer half or full price to the faction currently taking its unused shipment. Offers are optional: the recipient can use one or continue ordinary actions. A replacement has a new identity; the old proposal cannot authorize a shipment. Current actor, turn, unused shipment, native high population, typed source custody, funding and destination legality are revalidated before acceptance. Expired offers cannot be reused in a later opportunity. The grant gives no extra shipment or movement. diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index bc237032..2415f4a0 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -671,7 +671,8 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP | 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 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. An already-purchased card combined with Junction sponsorship retains its separate unfinished composition gate. | [Connected typed routes and source distinction](GUILD_HOMEWORLD_TRANSPORT_RULES.md#separate-permission-and-purchased-price--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) | | Deferred shipment arrival | Quote unsupported ordinary arrivals before saving Guild interception. A validated legacy ordinary declaration may return unspent on the typed arrival error; corrupted declarations and special parent/card receipts are not silently cleared. This repairs continuation without deciding simultaneous reaction priority. | [Reproduction and bounded recovery](DEFERRED_SHIPMENT_ARRIVAL.md) | | Native Ambassador own-arrival continuation | Under the standing choose-recommended-option instruction, reuse committed entry after its own Guild income, BG Intrusion and accompaniment controls. Existing callbacks put a legitimate accompanying BG fighter child before the deferred original entrant; each sees the then-live token, so declining the child preserves a later original offer and consuming it does not recreate a token. Advisors remain exempt. This child-first/live-token composition is provisional, not a located publisher priority ruling. Actual busy controls, unsupported paired/module guards and separate native rate-window admission remain distinct. | [Native original-action evidence and boundary](DEFERRED_SHIPMENT_ARRIVAL.md#native-own-arrival-continuations--15-october-2026) | | Truthtrance | Current facts are boolean disclosures, not promises to preserve resources. Structured Basic and base Advanced active-opportunity shipment promises bind within their documented scope (Advanced excludes Guild and optional modules); prose is not automatically enforced. | [Spice facts](TRUTHTRANCE_SPICE_FACTS.md), [card facts](TRUTHTRANCE_CARD_COUNT.md), [shipment promises](TRUTHTRANCE_SHIPMENT_PROMISES.md), [remaining audit](TRUTHTRANCE_NONBATTLE_READINESS_20260907.md) | diff --git a/game/engine.ts b/game/engine.ts index da72e4b2..557e7c53 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -1179,6 +1179,8 @@ type JunctionTransportDeclaration = { destination: string; sources: HomeworldShipmentIntent['sources']; allyPayment: number; + /** Explicit acceptance that the independent purchased price ends unused. */ + forfeitPurchasedRate?: true; }; type GuildBetrayalContinuation = | {source: 'reserve'; shipment: PendingShipment} @@ -9891,7 +9893,9 @@ function guildBetrayalAuthority(g: Game, continuation: GuildBetrayalContinuation case 'junction': { const declaration = continuation.transport; requireRule(declaration && Object.keys(declaration).sort().join(',') === - 'allyPayment,destination,event,offer,player,sources,turn', + (declaration.forfeitPurchasedRate === undefined + ? 'allyPayment,destination,event,offer,player,sources,turn' + : 'allyPayment,destination,event,forfeitPurchasedRate,offer,player,sources,turn'), 'The saved Junction declaration is malformed.'); const quote = quoteJunctionDeclaration(g, getPlayer(g, declaration.player), declaration); shipper = declaration.player; price = quote.quote.cost; allyPayment = declaration.allyPayment; @@ -26054,9 +26058,7 @@ function junctionTransportWindow(g: Game) { if (!owner || !recipient || owner === recipient.id || g.phase !== 5 || g.status !== 'playing' || recipient.shipped) return null; const offer = currentJunctionOffer(g); - const blocked = g.karamaShipping?.player === recipient.id - ? 'Junction transport with a purchased Karama price awaits its separate source composition.' - : homeworldShipmentBlock(g, recipient, false); + const blocked = homeworldShipmentBlock(g, recipient, false); return {owner, recipient: recipient.id, offer, blocked, canOffer: !blocked, offerEvent: junctionOfferEvent(g), event: JSON.stringify([homeworldShipmentEvent(g, recipient, 'arrakis'), offer])}; @@ -26070,6 +26072,10 @@ function quoteJunctionDeclaration(g: Game, p: Player, declaration: JunctionTrans option?.blocked ?? 'Wait for Guild to offer Junction transport during your unused shipment.'); requireRule(action.event === option.event && action.offer === option.offer.event, 'This Junction offer or physical source selection has changed.'); + const purchasedRate = g.karamaShipping?.player === p.id; + requireRule(purchasedRate ? declaration.forfeitPurchasedRate === true + : declaration.forfeitPurchasedRate === undefined, + 'Explicitly accept the immune Junction tariff and that the purchased price ends unused.'); const rate = option.offer.rate; const destination = stringField(action.destination); const context = {...homeworldContext(g), storm: g.storm, discoveries:g.discoveries, @@ -26130,6 +26136,7 @@ function commitJunctionTransport(g: Game, p: Player, declaration: JunctionTransp } else if (p.advisors) delete p.advisors[arrival.territory]; } p.shipped = true; + if (declaration.forfeitPurchasedRate) g.karamaShipping = null; g.junctionOffer = null; finishShipmentPromises(g, p, promise); // Native Fremen reinforcement is on Arrakis; all other world departures @@ -26147,7 +26154,7 @@ function commitJunctionTransport(g: Game, p: Player, declaration: JunctionTransp { own: ordinaryIncome && p.id !== option.owner, ally: p.ally !== option.owner }); const sourceName = quote.originKind === 'arrakis' ? territory(quote.origin).name : quote.sources.map((s) => combatLocationName(g, s.key)).join(' and '); - log(g, `${p.name} accepted ${getPlayer(g, option.owner).name}’s Junction ${rate}-price offer and transported ${quote.amount} physical forces (${quote.elite} special) from ${sourceName} to ${arrival ? `${territory(arrival.territory).name}, sector ${arrival.sector}` : combatLocationName(g, destination)} for ${quote.cost} spice (${quote.cost - allyPayment} own, ${allyPayment} pledged). The shipment is used; movement remains available.`, + log(g, `${p.name} accepted ${getPlayer(g, option.owner).name}’s Junction ${rate}-price offer and transported ${quote.amount} physical forces (${quote.elite} special) from ${sourceName} to ${arrival ? `${territory(arrival.territory).name}, sector ${arrival.sector}` : combatLocationName(g, destination)} for ${quote.cost} spice (${quote.cost - allyPayment} own, ${allyPayment} pledged). The shipment is used; movement remains available.${declaration.forfeitPurchasedRate ? ' The separate purchased Karama price ended unused; no card is refunded.' : ''}`, {faction: 'guild', name: 'Junction transport'}); if (arrival) { const bg = byFaction(g, 'beneGesserit'); @@ -26160,7 +26167,7 @@ function commitJunctionTransport(g: Game, p: Player, declaration: JunctionTransp } function performJunctionTransport(g: Game, p: Player, action: Action) { requireRule(Object.keys(action).every(key => - ['type', 'event', 'offer', 'destination', 'sources', 'allyPayment'].includes(key)), + ['type', 'event', 'offer', 'destination', 'sources', 'allyPayment', 'forfeitPurchasedRate'].includes(key)), 'Use the offered tariff and explicit physical forces without concealed tokens.'); // Decode once into a source-specific declaration. Saved continuations are // never actions and never dispatch through the action engine. @@ -26169,6 +26176,11 @@ function performJunctionTransport(g: Game, p: Player, action: Action) { destination: stringField(action.destination), sources: action.sources as HomeworldShipmentIntent['sources'], allyPayment: typeof action.allyPayment === 'number' ? action.allyPayment : 0, }; + if (action.forfeitPurchasedRate !== undefined) { + requireRule(action.forfeitPurchasedRate === true, + 'Accept only the explicit unused-purchased-price choice.'); + declaration.forfeitPurchasedRate = true; + } const quote = quoteJunctionDeclaration(g, p, declaration, action.allyPayment === undefined); declaration.allyPayment = action.allyPayment === undefined ? quote.allyPayment : contribution(g, p, quote.quote.cost, action.allyPayment); diff --git a/game/junction-transport-options.ts b/game/junction-transport-options.ts index 8eec2514..54d93240 100644 --- a/game/junction-transport-options.ts +++ b/game/junction-transport-options.ts @@ -125,6 +125,7 @@ export function junctionTransportChoice( allyPayment: number; advisors: boolean; action: Action | null; + forfeitPurchasedRate: boolean; } = { blocked: null, cost: 0, @@ -134,6 +135,7 @@ export function junctionTransportChoice( allyPayment, advisors: false, action: null, + forfeitPurchasedRate: false, }; const me = view.players.find((player) => player.id === view.me); const option = view.junctionTransport; @@ -155,6 +157,7 @@ export function junctionTransportChoice( 'Guild must offer Junction transport during your unused shipment turn.'; return result; } + result.forfeitPurchasedRate = view.karamaShipping?.player === me.id; if (option.blocked) { result.blocked = option.blocked; return result; @@ -270,7 +273,7 @@ export function junctionTransportChoice( if (!result.blocked && result.ownPayment > (me.spice ?? 0)) result.blocked = 'Your spice and selected allied contribution cannot pay this transport.'; - if (!result.blocked) + if (!result.blocked) { result.action = { type: 'junctionShip', event: option.event, @@ -279,6 +282,8 @@ export function junctionTransportChoice( sources, allyPayment, }; + if (result.forfeitPurchasedRate) result.action.forfeitPurchasedRate = true; + } } catch (error) { if (!(error instanceof HomeworldCustodyError)) throw error; result.blocked = error.message; diff --git a/game/reference.ts b/game/reference.ts index 10557320..001fb627 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -1436,7 +1436,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Documentation', status: 'Partial', detail: - 'Source-bound effects, copies and Duke guides remain. Native own-arrival child-first/live-token order stays provisional. Separate native transport permission, independent purchased typed prices and own/allied marker billing now preserve original sources and approvals. Cunning opportunity scope and special-stop card-return policy are explicit interpretations; sponsored/purchased composition and full combinations remain open.', + 'Source-bound effects, copies and Duke guides remain. Native own-arrival child-first/live-token order stays provisional. Separate native transport permission, independent purchased typed prices and own/allied marker billing preserve original sources and approvals. Immune Junction now keeps its offered tariff/payees with an explicit unused-purchase choice. Cunning opportunity scope, special-stop card return and unused-price expiry are selected interpretations; full combinations remain open.', evidence: ['docs/ECAZ_AMBASSADORS_RULES.md', 'docs/DEFERRED_SHIPMENT_ARRIVAL.md', 'docs/BOT_ARRIVAL_PREFLIGHT.md'], }, { @@ -2562,7 +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.', - '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 now 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. An already purchased card with Junction sponsorship remains a separate unfinished composition.', + '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.', 'Atreides full-plan inspection, Emperor revival, Fremen’s summoned worm, Harkonnen hand exchange the Guild shipment stop and Tleilaxu revival prevention are implemented as once-per-game uses. Other cancellations and special Karamas are still being implemented.', @@ -4023,7 +4023,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'Occupation removes Tupile’s low advantage. New Homeworld games record qualifying foreign presence from setup, including brief sole occupation during ordered battle losses and presence at turn boundaries. Earlier saves without that history, or histories with a Tupile qualifier whose continuing entitlement is unresolved, cannot make a new intelligence request. Already learned observations remain readable. Recorded qualification does not itself award occupied income or settle its expiry.', 'During Spice Collection, Grumman with at least eight native reserves may add one available Terror token to an ordinary stronghold already holding Terror, or return one unrevealed placed token to hidden supply, then collect four bank spice. Hidden supply identities rotate without revealing the returned face. This opportunity is separate from Mentat placement and follows pending shared spice allocation. Declining collects nothing. Under the user-selected Cunning interpretation, an ordinary eligible addition must first exist; an unallied Moritani holder may then spend its Nexus to broaden that addition’s destination to static Arrakis.', 'When several Terror tokens share a stronghold, Moritani privately selects one for an eligible entry, then chooses whether to reveal it or offer an alliance. The other tokens remain hidden. The board shows the public stack count; refresh preserves the selected token and original arrival.', - 'While Junction has high population, Guild may offer another faction transport at half or full price during that faction’s shipment. The recipient can choose exact forces from one territory or Homeworld, including a return to their own world. An allied Homeworld remains forbidden. Half-price totals round up; five forces to a Homeworld cost three spice. The Junction route and offered rate resist Karama.', + 'While Junction has high population, Guild may offer another faction transport at half or full price during that faction’s shipment. The recipient can choose exact forces from one territory or Homeworld, including a return to their own world. An allied Homeworld remains forbidden. Half-price totals round up; five forces to a Homeworld cost three spice. The Junction route and tariff resist Karama; payment recipients retain their separate rules. With an ordinary purchased price active, acceptance explicitly acknowledges that it ends unused without a card refund; only actual delivery consumes that grant. This selected product composition does not halve again or impose bank-only payment.', 'Homeworld shipment, battle and occupation have special rules. Occupiers receive the printed Collection income and card effects. An alliance can gain a special Homeworld victory through high-population Ecaz; this is separate from ordinary stronghold victory.', 'The fresh local homeworld-occupation profile uses original selected decks and no other modules. Advanced occupation begins with sole foreign presence and its benefits remain until the occupier’s last force leaves, including across contests, native population recovery and later turns. A later sole arrival after departure creates a new source epoch. Basic uses a visibly provisional current-sole policy: the present sole foreign faction controls occupied income, Bidding effects, defenses, percentage shares and the Ecaz-victory holding, and that entitlement ends immediately on departure, contested presence or native repopulation. Basic Tupile slot leasing keeps its own conservative original-epoch gate. Recorded history, never a live garrison snapshot, always proves entitlement.', 'Printed occupied bank income is collected once and can be immediately allocated to a reciprocal ally. Separate Kaitain and Junction paid-receipt shares and Richese’s actual net sale income use frozen original invoices without charging the payer again. Southern Hemisphere rounds one completed actual native Collection total, including settled shared and occupied-bank credits; it redistributes rather than collecting twice. Ordinary native income cancellation remains distinct from printed occupied effects.', @@ -4048,7 +4048,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Implementation', status: 'Partial', detail: - 'All 13 cards, saved typed custody, supported-deck setup, Emperor movement, native Arrakis shipment sources, revival destinations, low charity/free-revival benefits, Tleilax’s phase-start income penalty and high-Salusa free Sardaukar support are connected. World-to-world invasion, Guild transport from Arrakis and Guild native returns use exact source pools, funding and interception. Homeworld battles connect native bonuses, limited explosion casualties, native-only Traitors and Face Dancers, and Ixian substitution. Junction offers connect sponsored board and Homeworld routes, including own returns, foreign departures and their distinct payments. Caladan foresight, Wallach advisor counts, Ix stronghold movement and Richese token movement now respect native population. High Tupile blocks Worthless sales and Ambassador payouts, including after an interrupted Ghola revival. Kaitain paid discards share the closing Bidding opportunity with CHOAM; Ecaz poison income uses actual physical disposal and current native population. Mandatory winning-card disposal follows winner casualties. Supported low Kaitain and Junction payment receipts preserve the payer’s cost and use current native population; divergent allied rounding remains guarded. Giedi grants its once-per-phase bonus on actual positive desert receipts, including settled Ecaz shares. Low Grumman uses original public entry counts across Terror and competing-reaction checks. Southern and Tleilax preserve the exact eligible revival group through one optional placement, with held income and explicit unresolved destination rules. Fresh occupation additionally connects bank and percentage receipts, completed Southern Collection, immediate sharing, Caladan inspection, displaced Ix pool, Richese card-only choice with native terms, real Giedi receiver choice and native protections. Fresh native CHOAM entries additionally connect original Tupile slots, actual normal-limit cleanup and restored low-query authority without resetting usage. Advanced retains sole-qualified ownership until last departure; Basic lifecycle, exceptional Duke, concealed transport and full/public acceptance remain unfinished.', + "All 13 cards, saved typed custody, supported-deck setup, Emperor movement, native Arrakis shipment sources, revival destinations, low charity/free-revival benefits, Tleilax’s phase-start income penalty and high-Salusa free Sardaukar support are connected. World-to-world invasion, Guild transport from Arrakis and Guild native returns use exact source pools, funding and interception. Homeworld battles connect native bonuses, limited explosion casualties, native-only Traitors and Face Dancers, and Ixian substitution. Junction offers connect sponsored board and Homeworld routes, including own returns, foreign departures and their distinct payments. Caladan foresight, Wallach advisor counts, Ix stronghold movement and Richese token movement now respect native population. High Tupile blocks Worthless sales and Ambassador payouts, including after an interrupted Ghola revival. Kaitain paid discards share the closing Bidding opportunity with CHOAM; Ecaz poison income uses actual physical disposal and current native population. Mandatory winning-card disposal follows winner casualties. Supported low Kaitain and Junction payment receipts preserve the payer’s cost and use current native population; divergent allied rounding remains guarded. Giedi grants its once-per-phase bonus on actual positive desert receipts, including settled Ecaz shares. Low Grumman uses original public entry counts across Terror and competing-reaction checks. Southern and Tleilax preserve the exact eligible revival group through one optional placement, with held income and explicit unresolved destination rules. Fresh occupation additionally connects bank and percentage receipts, completed Southern Collection, immediate sharing, Caladan inspection, displaced Ix pool, Richese card-only choice with native terms, real Giedi receiver choice and native protections. Fresh native CHOAM entries additionally connect original Tupile slots, actual normal-limit cleanup and restored low-query authority without resetting usage. Advanced retains sole-qualified ownership until last departure; Basic lifecycle, exceptional Duke, concealed transport and full/public acceptance remain unfinished. Already purchased ordinary Karama now composes with immune Junction through explicit unused-price acknowledgment; original tariff/payees stay unchanged and only delivered transport consumes its grant.", evidence: [ 'game/homeworld-cards.ts', 'game/homeworld-game.ts', @@ -4100,7 +4100,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Player controls', status: 'Partial', detail: - 'Card inspection is available in the reference. Development tables display native reserves, explicit Imperial shipment sources, world-to-world shipment and Guild transport from Arrakis with costs and pledged funding, Guild interception, optional Junction half/full offers and typed sponsored transport, transfers between Kaitain and Salusa, charity payment sources, current revival allowances, population-based movement restrictions and one/two-advisor choices, native battle bonuses, Homeworld casualties, replacement controls, Tupile sale explanations, shared end-of-Bidding actions, Ecaz’s private poison-income record, current low-population payment splits, automatic Giedi collection history, count-dependent Ambassador destinations and the optional revived-group placement. Fresh occupation adds bounded actual percentage allocation, real bonus receiver choice, source-authorized Ix pool/cache controls and separate native Richese terms. Normal starts remain unavailable.', + "Card inspection is available in the reference. Development tables display native reserves, explicit Imperial shipment sources, world-to-world shipment and Guild transport from Arrakis with costs and pledged funding, Guild interception, optional Junction half/full offers and typed sponsored transport, transfers between Kaitain and Salusa, charity payment sources, current revival allowances, population-based movement restrictions and one/two-advisor choices, native battle bonuses, Homeworld casualties, replacement controls, Tupile sale explanations, shared end-of-Bidding actions, Ecaz’s private poison-income record, current low-population payment splits, automatic Giedi collection history, count-dependent Ambassador destinations and the optional revived-group placement. Fresh occupation adds bounded actual percentage allocation, real bonus receiver choice, source-authorized Ix pool/cache controls and separate native Richese terms. Normal starts remain unavailable. Four current original-action390px journeys show the unused-price warning, full3, poor2 then replacement half2, ordinary purchased2 alternative and full6 normal Guild income; movement remains available.", evidence: [ 'components/homeworld-cards.tsx', 'components/homeworld-table.tsx', @@ -4123,14 +4123,14 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'AI', status: 'Partial', detail: - 'Shared public choices supply native shipment counters, Imperial transfers and current low revival allowances. All four profiles use Salusa’s per-type support legality and respect unresolved special-Karama and mid-battle Ghola timing boundaries. All four profiles handle world-to-world shipment, Guild transport from Arrakis, native returns and interception, Homeworld battle choices, typed losses, late defense and supported replacement actions. All four profiles can offer and use Junction transport through public quotes, respect population-based movement restrictions and choose legal Spiritual Advisor quantities. Market choices exclude high-Tupile Worthless sales. All four profiles use eligible unused low-Tupile requests without reading opponents’ private hands or balances. All four profiles use the shared closing opportunity for CHOAM actions, affordable Kaitain discards and readiness. Ecaz income needs no separate claim. Fresh occupied decisions supply legal bank/percentage allocation, one eligible real bonus receiver, displaced Ix/cache selection and native Richese terms. Concealed transport, remaining card-effect strategy and difficulty calibration are unfinished.', + "Shared public choices supply native shipment counters, Imperial transfers and current low revival allowances. All four profiles use Salusa’s per-type support legality and respect unresolved special-Karama and mid-battle Ghola timing boundaries. All four profiles handle world-to-world shipment, Guild transport from Arrakis, native returns and interception, Homeworld battle choices, typed losses, late defense and supported replacement actions. All four profiles can offer and use Junction transport through public quotes, respect population-based movement restrictions and choose legal Spiritual Advisor quantities. Market choices exclude high-Tupile Worthless sales. All four profiles use eligible unused low-Tupile requests without reading opponents’ private hands or balances. All four profiles use the shared closing opportunity for CHOAM actions, affordable Kaitain discards and readiness. Ecaz income needs no separate claim. Fresh occupied decisions supply legal bank/percentage allocation, one eligible real bonus receiver, displaced Ix/cache selection and native Richese terms. Concealed transport, remaining card-effect strategy and difficulty calibration are unfinished. All four original profiles execute the immune full offered quote with its explicit acknowledgment. This is minimal legal participation, not strategy tuning.", evidence: ['game/homeworld-options.ts', 'game/homeworld-shipment-options.ts', 'game/guild-homeworld-shipment-options.ts', 'game/junction-transport-options.ts'], }, { area: 'Documentation', status: 'Partial', detail: - 'All 26 faces, global rules and source discrepancies are documented. Imperial shipment and movement controls explain sources, costs and unavailable actions. Card-economy notes explain shared closing actions, private poison income, winner-discard ordering and low-population payment splits. The authorized Advanced sole/last-departure cutover, original occupied economic/Bidding ownership and user-ruled whole-invoice rounding are explicit. Remaining occupation variants and interactions stay separate; full guidance remains in progress.', + "All 26 faces, global rules and source discrepancies are documented. Imperial shipment and movement controls explain sources, costs and unavailable actions. Card-economy notes explain shared closing actions, private poison income, winner-discard ordering and low-population payment splits. The authorized Advanced sole/last-departure cutover, original occupied economic/Bidding ownership and user-ruled whole-invoice rounding are explicit. Remaining occupation variants and interactions stay separate; full guidance remains in progress. Current Junction source choice distinguishes printed immunity from the selected unused-price/no-refund product composition; failed or deferred transport retains the grant.", evidence: [ 'docs/HOMEWORLD_COMPONENT_AUDIT.md', 'docs/HOMEWORLD_RULES.md', @@ -4161,104 +4161,105 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Verification', status: 'Partial', detail: - 'Mixed E1/E2 and paired E3 Nexus adds31 original physical programmes plus2 Loyalty-stock regressions. Final489/489 affected checks across42 files pass with types/lint. All64 original families finish258/288 six-seat games with338931 accepted/339426 attempted actions and9013 JSON across three frozen trees and preserved-prefix continuations. All30 remaining source-guard traces are read. Actual390px original Ixian offer/training, typed native and foreign-world payment/free Suboid plan and paired Ecaz foreign entry/living shared Duke preserve20 physical counters. Human Ixian loses, so no winning Suk rescue is claimed. Earlier qualified evidence remains canonical; full rules, assurance, calibration and deployed acceptance remain open.', + "Mixed E1/E2 and paired E3 Nexus adds31 original physical programmes plus2 Loyalty-stock regressions. Final489/489 affected checks across42 files pass with types/lint. All64 original families finish258/288 six-seat games with338931 accepted/339426 attempted actions and9013 JSON across three frozen trees and preserved-prefix continuations. All30 remaining source-guard traces are read. Actual390px original Ixian offer/training, typed native and foreign-world payment/free Suboid plan and paired Ecaz foreign entry/living shared Duke preserve20 physical counters. Human Ixian loses, so no winning Suk rescue is claimed. Earlier qualified evidence remains canonical; full rules, assurance, calibration and deployed acceptance remain open. Current Junction follow-up passes131/131 affected cases and14 runtime/policy paths (11 original/3 controlled), with four actual original-action390px journeys. No saved/deployed acceptance is claimed.", 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/homeworld-classic-skills-runtime.test.ts', - 'tests/homeworld-classic-discovery-runtime.test.ts', - 'tests/homeworld-skills-discovery-runtime.test.ts', - 'tests/homeworld-optional-modules-runtime.test.ts', - 'tests/homeworld-native-e1e2-modules-runtime.test.ts', - 'tests/homeworld-native-e3-modules-runtime.test.ts', - 'tests/homeworld-classic-nexus-skills-runtime.test.ts', - 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', - 'tests/homeworld-paired-nexus-runtime.test.ts', - 'tests/homeworld-standalone-nexus-runtime.test.ts', - 'tests/e3-nexus-ecaz-runtime.test.ts', - 'tests/e3-nexus-moritani-runtime.test.ts', - 'tests/homeworld-custody.test.ts', - 'tests/homeworld-cards.test.ts', - 'tests/homeworld-payment-income.test.ts', - 'tests/homeworld-payment-income-engine.test.ts', - 'tests/homeworld-payment-income-recovery.test.ts', - 'tests/homeworld-collection.test.ts', - 'tests/homeworld-collection-engine.test.ts', - 'tests/homeworld-collection-recovery.test.ts', - 'tests/homeworld-grumman-engine.test.ts', - 'tests/homeworld-grumman-recovery.test.ts', - 'tests/homeworld-setup-engine.test.ts', - 'tests/homeworld-actions-engine.test.ts', - 'tests/homeworld-basic-counters.test.ts', - 'tests/homeworld-salusa-engine.test.ts', - 'tests/homeworld-combat.test.ts', - 'tests/homeworld-battle-resolution.test.ts', - 'tests/homeworld-battle-engine.test.ts', - 'tests/homeworld-battle-recovery.test.ts', - 'tests/homeworld-shipment-engine.test.ts', - 'tests/homeworld-shipment-recovery.test.ts', - 'tests/homeworld-shipment-controls.test.ts', - 'tests/homeworld-shipment-review.test.ts', - 'tests/guild-homeworld-shipment-engine.test.ts', - 'tests/guild-homeworld-shipment-recovery.test.ts', - 'tests/guild-homeworld-shipment-controls.test.ts', - 'tests/junction-transport.test.ts', - 'tests/junction-transport-engine.test.ts', - 'tests/junction-transport-recovery.test.ts', - 'tests/junction-transport-controls.test.ts', - 'tests/junction-transport-review.test.ts', - 'tests/homeworld-card-economy.test.ts', - 'tests/homeworld-tupile-sales-engine.test.ts', - 'tests/homeworld-tupile-sales-recovery.test.ts', - 'tests/bidding-end-engine.test.ts', - 'tests/bidding-end-controls.test.ts', - 'tests/bidding-end-recovery.test.ts', - 'tests/ecaz-poison-income.test.ts', - 'tests/ecaz-poison-income-engine.test.ts', - 'tests/homeworld-mobility.test.ts', - 'tests/homeworld-mobility-engine.test.ts', - 'tests/homeworld-mobility-recovery.test.ts', - 'tests/homeworld-spiritual-advisors.test.ts', - 'tests/homeworld-spiritual-advisors-recovery.test.ts', - 'tests/homeworld-advisor-ambassador.test.ts', - 'tests/homeworld-replacement-engine.test.ts', - 'tests/homeworld-recovery.test.ts', - 'tests/homeworld-charity-engine.test.ts', - 'tests/homeworld-revival-benefits-engine.test.ts', - 'tests/homeworld-revival-deployment.test.ts', - 'tests/homeworld-revival-return.test.ts', - 'tests/homeworld-revival-destinations.test.ts', - 'tests/homeworld-revival-deployment-engine.test.ts', - 'tests/homeworld-revival-deployment-controls.test.ts', - 'tests/homeworld-revival-deployment-recovery.test.ts', - 'tests/homeworld-victory-reinforcement.test.ts', - 'tests/caladan-reinforcement-controls.test.ts', - 'tests/homeworld-victory-reinforcement-recovery.test.ts', - 'tests/tupile-intelligence.test.ts', - 'tests/tupile-intelligence-answer.test.ts', - 'tests/tupile-intelligence-state.test.ts', - 'tests/tupile-intelligence-controls.test.ts', - 'tests/tupile-intelligence-engine.test.ts', - 'tests/tupile-intelligence-recovery.test.ts', - 'tests/homeworld-occupation-history.test.ts', - 'tests/homeworld-occupied-income.test.ts', - 'tests/homeworld-occupied-percentage.test.ts', - 'tests/homeworld-occupied-bidding.test.ts', - 'tests/homeworld-occupied-defenses.test.ts', - 'tests/homeworld-occupation-runtime.test.ts', - 'tests/homeworld-occupied-producers-engine.test.ts', - 'tests/homeworld-occupied-private-lots.test.ts', - 'tests/homeworld-occupied-tupile.test.ts', - 'tests/homeworld-occupied-tupile-runtime.test.ts', - 'tests/ecaz-homeworld-victory.test.ts', - 'tests/ecaz-homeworld-victory-runtime.test.ts', - 'tests/grumman-collection.test.ts', - 'tests/grumman-collection-engine.test.ts', - 'tests/grumman-collection-controls.test.ts', - 'tests/grumman-collection-recovery.test.ts', - 'tests/grumman-supply-return.test.ts', - 'tests/grumman-cunning-runtime.test.ts', - 'tests/nexus-moritani-grumman.test.ts', - 'tests/fixture-ruled-grumman.ts', - 'tests/grumman-selection-integrity.test.ts', - 'tests/homeworld-benefits-recovery.test.ts',], + 'tests/homeworld-classic-discovery-runtime.test.ts', + 'tests/homeworld-skills-discovery-runtime.test.ts', + 'tests/homeworld-optional-modules-runtime.test.ts', + 'tests/homeworld-native-e1e2-modules-runtime.test.ts', + 'tests/homeworld-native-e3-modules-runtime.test.ts', + 'tests/homeworld-classic-nexus-skills-runtime.test.ts', + 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', + 'tests/homeworld-paired-nexus-runtime.test.ts', + 'tests/homeworld-standalone-nexus-runtime.test.ts', + 'tests/e3-nexus-ecaz-runtime.test.ts', + 'tests/e3-nexus-moritani-runtime.test.ts', + 'tests/homeworld-custody.test.ts', + 'tests/homeworld-cards.test.ts', + 'tests/homeworld-payment-income.test.ts', + 'tests/homeworld-payment-income-engine.test.ts', + 'tests/homeworld-payment-income-recovery.test.ts', + 'tests/homeworld-collection.test.ts', + 'tests/homeworld-collection-engine.test.ts', + 'tests/homeworld-collection-recovery.test.ts', + 'tests/homeworld-grumman-engine.test.ts', + 'tests/homeworld-grumman-recovery.test.ts', + 'tests/homeworld-setup-engine.test.ts', + 'tests/homeworld-actions-engine.test.ts', + 'tests/homeworld-basic-counters.test.ts', + 'tests/homeworld-salusa-engine.test.ts', + 'tests/homeworld-combat.test.ts', + 'tests/homeworld-battle-resolution.test.ts', + 'tests/homeworld-battle-engine.test.ts', + 'tests/homeworld-battle-recovery.test.ts', + 'tests/homeworld-shipment-engine.test.ts', + 'tests/homeworld-shipment-recovery.test.ts', + 'tests/homeworld-shipment-controls.test.ts', + 'tests/homeworld-shipment-review.test.ts', + 'tests/guild-homeworld-shipment-engine.test.ts', + 'tests/guild-homeworld-shipment-recovery.test.ts', + 'tests/guild-homeworld-shipment-controls.test.ts', + 'tests/junction-transport.test.ts', + 'tests/junction-transport-engine.test.ts', + 'tests/junction-transport-recovery.test.ts', + 'tests/junction-transport-controls.test.ts', + 'tests/junction-transport-review.test.ts', + 'tests/homeworld-card-economy.test.ts', + 'tests/homeworld-tupile-sales-engine.test.ts', + 'tests/homeworld-tupile-sales-recovery.test.ts', + 'tests/bidding-end-engine.test.ts', + 'tests/bidding-end-controls.test.ts', + 'tests/bidding-end-recovery.test.ts', + 'tests/ecaz-poison-income.test.ts', + 'tests/ecaz-poison-income-engine.test.ts', + 'tests/homeworld-mobility.test.ts', + 'tests/homeworld-mobility-engine.test.ts', + 'tests/homeworld-mobility-recovery.test.ts', + 'tests/homeworld-spiritual-advisors.test.ts', + 'tests/homeworld-spiritual-advisors-recovery.test.ts', + 'tests/homeworld-advisor-ambassador.test.ts', + 'tests/homeworld-replacement-engine.test.ts', + 'tests/homeworld-recovery.test.ts', + 'tests/homeworld-charity-engine.test.ts', + 'tests/homeworld-revival-benefits-engine.test.ts', + 'tests/homeworld-revival-deployment.test.ts', + 'tests/homeworld-revival-return.test.ts', + 'tests/homeworld-revival-destinations.test.ts', + 'tests/homeworld-revival-deployment-engine.test.ts', + 'tests/homeworld-revival-deployment-controls.test.ts', + 'tests/homeworld-revival-deployment-recovery.test.ts', + 'tests/homeworld-victory-reinforcement.test.ts', + 'tests/caladan-reinforcement-controls.test.ts', + 'tests/homeworld-victory-reinforcement-recovery.test.ts', + 'tests/tupile-intelligence.test.ts', + 'tests/tupile-intelligence-answer.test.ts', + 'tests/tupile-intelligence-state.test.ts', + 'tests/tupile-intelligence-controls.test.ts', + 'tests/tupile-intelligence-engine.test.ts', + 'tests/tupile-intelligence-recovery.test.ts', + 'tests/homeworld-occupation-history.test.ts', + 'tests/homeworld-occupied-income.test.ts', + 'tests/homeworld-occupied-percentage.test.ts', + 'tests/homeworld-occupied-bidding.test.ts', + 'tests/homeworld-occupied-defenses.test.ts', + 'tests/homeworld-occupation-runtime.test.ts', + 'tests/homeworld-occupied-producers-engine.test.ts', + 'tests/homeworld-occupied-private-lots.test.ts', + 'tests/homeworld-occupied-tupile.test.ts', + 'tests/homeworld-occupied-tupile-runtime.test.ts', + 'tests/ecaz-homeworld-victory.test.ts', + 'tests/ecaz-homeworld-victory-runtime.test.ts', + 'tests/grumman-collection.test.ts', + 'tests/grumman-collection-engine.test.ts', + 'tests/grumman-collection-controls.test.ts', + 'tests/grumman-collection-recovery.test.ts', + 'tests/grumman-supply-return.test.ts', + 'tests/grumman-cunning-runtime.test.ts', + 'tests/nexus-moritani-grumman.test.ts', + 'tests/fixture-ruled-grumman.ts', + 'tests/grumman-selection-integrity.test.ts', + 'tests/homeworld-benefits-recovery.test.ts', + "tests/junction-purchased-price.test.ts"], }, ], }, @@ -4667,11 +4668,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 native price now have distinct original unpaid stages; Cunning opportunity scope preserves its source. Independent purchased typed prices are bank-only, with exact completion/stop-card handling and no new route. Own/allied marker bills one unit; supplier approval cannot silently increase. Authorized recipient pricing now connects fresh human/AI offers. Legacy rate-only signatures remain; purchased Junction composition and full acceptance stay 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"]}, - {area:"Player controls",status:'Partial',detail:"Five current390px journeys exercise original prevention then immune Junction/movement, actual gifted World3 for bank2, controlled supplier1 release and fresh full2 consent, controlled special-stop/refund then movement, and original typed permission allowance then rate cancellation. Fresh lender quote drift was observed and fixed with authoritative recipient pricing. Three original scenes/two controlled; no overflow/application exception, with one aborted initial Vite dependency request. Earlier surfaces remain historical.",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"]}, - {area:"AI",status:'Partial',detail:"Native permission denial filters native routes but preserves legal ordinary alternatives; all four original policies execute them. Typed budgets retain independent purchased price without granting a permission. Fresh allied offer calculations use the authorized recipient quote, not a personal-view guess. Four controlled low-budget probes choose no optional reoffer and complete legal recipient shipments. 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 authorized Advanced PDF physical16 supports any-player half-normal bank-paid shipment. Distinct native permission, independent typed price and one-marker loan approvals are documented. Original-opportunity denial scope and prepayment ordering are explicit conservative product choices, not new publisher timing rulings; older refund/bank-loan/return policies remain provisional. Original versus controlled evidence and the remaining purchased-Junction composition/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"]}, - {area:"Verification",status:'Partial',detail:"Current affected25-file263/263 passes across frozen237+26 selections. Twenty main runtime/policy paths pass:15 original-action/five controlled, plus four explicitly controlled optional-offer/recipient probes. Five actual390px journeys prove distinct permission/price, bank-only gift, fresh supplier consent, exact refund and original movement. Controlled Fremen typed/free and BG conversion/counter cases retain original sources. Earlier lifecycle 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"]}, + {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"]}, ], }, { diff --git a/tests/fixture-junction-purchased-price.ts b/tests/fixture-junction-purchased-price.ts new file mode 100644 index 00000000..3ffa0b4c --- /dev/null +++ b/tests/fixture-junction-purchased-price.ts @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import { viewGame, type Action } from '../game/engine'; +import { TERRITORIES } from '../game/board'; +import { junctionTransportChoice } from '../game/junction-transport-options'; +import { createPurchasedHomeworldRate } from './fixture-purchased-homeworld-rate'; +import { pairedE3NexusReload, stepPairedE3Nexus, type PairedE3NexusStep } from './fixture-paired-e3-nexus-modules'; +import { holdGuildCunningCard } from './fixture-nexus-guild-cunning'; + +export type PurchasedJunctionRoute = 'world' | 'arrakisReturn' | 'nativeArrival'; +/** Actual auctioned card and original Guild high offer in either order. No + * played wallet/army/phase/response is assigned. Poverty is an actual bribe; + * the optional native-denial SECOND physical card is explicitly controlled. */ +export function createJunctionPurchasedPrice(options: { + rate?: 'half' | 'full'; order?: 'cardFirst' | 'offerFirst'; + route?: PurchasedJunctionRoute; poor?: boolean; nativeDenied?: boolean; +} = {}) { + const original = createPurchasedHomeworldRate('world'); + const shipper = original.shipper, guild = original.guild, cardOwner = original.ecaz; + const order = options.order ?? 'cardFirst', rate = options.rate ?? 'full', route = options.route ?? 'world'; + const actions: PairedE3NexusStep[] = []; + let game = pairedE3NexusReload(order === 'cardFirst' ? original.beforeAction : original.beforePurchase); + const source = pairedE3NexusReload(game); + if (options.poor) game = stepPairedE3Nexus(game, { actor: shipper, + action: { type: 'bribe', target: cardOwner, amount: game.players.find(p => p.id === shipper)!.spice - 2 } }, actions); + const offer = () => { + const projected = viewGame(game, guild).junctionTransport!; + assert.ok(projected.canOffer); + game = stepPairedE3Nexus(game, { actor: guild, + action: { type: 'offerJunctionTransport', event: projected.offerEvent, rate } }, actions); + }; + if (order === 'offerFirst') { + offer(); + game = stepPairedE3Nexus(game, { actor: cardOwner, + action: { type: 'card', mode: 'shipment', card: original.karama, target: shipper } }, actions); + } + if (options.nativeDenied) { + const second = holdGuildCunningCard(game, cardOwner, 'karama'); + game = stepPairedE3Nexus(game, { actor: shipper, action: { type: 'guildShip', + from: 'polar_sink:0', territory: 'carthag', sector: 11, amount: 3, elite: 0, allyPayment: 0 } }, actions); + assert.equal(game.response?.kind, 'guildTransportPermission'); + game = stepPairedE3Nexus(game, { actor: cardOwner, + action: { type: 'card', mode: 'cancel', card: second.id } }, actions); + assert.equal(viewGame(game, shipper).guildTransportCanceled, true); + } + if (order === 'cardFirst') offer(); + const sand = TERRITORIES.find(t => t.type === 'sand' && t.sectors.some(s => s !== game.storm) && + game.players.every(p => Object.keys(p.forces).every(key => key.split(':')[0] !== t.id)))!; + assert.ok(sand); + const origin = route === 'arrakisReturn' ? 'polar_sink:0' : 'homeworld:moritani'; + const destination = route === 'arrakisReturn' ? 'homeworld:moritani' : route === 'nativeArrival' + ? `${sand.id}:${sand.sectors.find(s => s !== game.storm)!}` : 'homeworld:ecaz'; + const sources = { [origin]: { normal: 3, elite: 0 } }; + const quote = junctionTransportChoice(viewGame(game, shipper), destination, sources, 0); + assert.equal(quote.forfeitPurchasedRate, true); + return { original, source, beforeAction: pairedE3NexusReload(game), actions, shipper, guild, + cardOwner, card: original.karama, rate, order, route, origin, destination, sources, quote, + action: quote.action as Action | null, + provenance: [...original.provenance, + `Actual ${order} order; immutable ${rate} Junction offer, original typed ${route} group.`, + ...(options.poor ? ['Actual bribe leaves beneficiary two available spice.'] : []), + ...(options.nativeDenied ? ['CONTROLLED conserved second physical Karama; actual native permission denial, original purchased grant retained.'] : [])] }; +} diff --git a/tests/junction-purchased-price.test.ts b/tests/junction-purchased-price.test.ts new file mode 100644 index 00000000..057448ba --- /dev/null +++ b/tests/junction-purchased-price.test.ts @@ -0,0 +1,181 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { applyAction, viewGame, type Action, type Game } from '../game/engine'; +import { junctionTransportChoice, junctionTransportActions } from '../game/junction-transport-options'; +import { homeworldGameIntegrity } from '../game/homeworld-game'; +import { createJunctionPurchasedPrice } from './fixture-junction-purchased-price'; +import { createPurchasedHomeworldRate, completePurchasedHomeworldRate } from './fixture-purchased-homeworld-rate'; +import { holdGuildCunningCard } from './fixture-nexus-guild-cunning'; +import { createGuildBetrayalFixture, settleGuildBetrayalFixture } from './fixture-nexus-guild-betrayal'; +import { settleNativeGuildRateModules } from './fixture-native-guild-rate-modules'; +const player = (g: Game, id: string) => g.players.find(p => p.id === id)!; +const act = (g: Game, actor: string, action: Action) => applyAction(structuredClone(g), actor, action); +function reject(g: Game, actor: string, action: Action) { + const copy = structuredClone(g), before = structuredClone(copy); + assert.throws(() => applyAction(copy, actor, action)); + assert.deepEqual(copy, before); +} + +for (const rate of ['half', 'full'] as const) for (const order of ['cardFirst', 'offerFirst'] as const) { + void test(`original ${order} ${rate} Junction source retains immune fee and requires explicit unused-card acceptance`, () => { + const f = createJunctionPurchasedPrice({ rate, order }), before = f.beforeAction; + assert.ok(f.action); + const cost = rate === 'half' ? 2 : 3; + assert.equal(f.quote.cost, cost); + assert.equal(f.quote.forfeitPurchasedRate, true); + assert.equal(f.action.forfeitPurchasedRate, true); + assert.equal(before.junctionOffer?.rate, rate); + const missing = { ...f.action }; delete missing.forfeitPurchasedRate; + reject(before, f.shipper, missing); + reject(before, f.shipper, { ...f.action, forfeitPurchasedRate: false }); + const done = act(before, f.shipper, f.action); + assert.equal(player(done, f.shipper).spice, player(before, f.shipper).spice - cost); + assert.equal(player(done, f.guild).spice, player(before, f.guild).spice); + assert.equal(player(done, f.shipper).reserves, player(before, f.shipper).reserves - 3); + assert.deepEqual(done.homeworlds!.custody!.visitors[f.destination][f.shipper], { normal: 3, elite: 0 }); + assert.equal(player(done, f.shipper).shipped, true); + assert.equal(player(done, f.shipper).moved, player(before, f.shipper).moved); + assert.ok(!done.karamaShipping); + assert.equal(done.discard.filter(c => c.id === f.card).length, 1); + assert.ok(!player(done, f.cardOwner).hand.some(c => c.id === f.card)); + assert.ok(!done.response); + assert.ok(!done.decision); + reject(done, f.shipper, f.action); + homeworldGameIntegrity(done); + const next = act(done, f.shipper, { type: 'endMovement' }); + assert.notEqual(next.active, f.shipper); + assert.equal(player(next, f.shipper).spice, player(done, f.shipper).spice); + }); +} + +void test('original unaffordable immune full offer preserves the purchased grant; half replacement needs its own current identity', () => { + const f = createJunctionPurchasedPrice({ poor: true }), before = f.beforeAction; + assert.equal(player(before, f.shipper).spice, 2); + assert.equal(f.quote.cost, 3); + assert.ok(!f.action); + assert.ok(f.quote.blocked); + const option = viewGame(before, f.shipper).junctionTransport!; + const original: Action = { type: 'junctionShip', event: option.event, offer: option.offer!.event, + destination: f.destination, sources: f.sources, allyPayment: 0, forfeitPurchasedRate: true }; + reject(before, f.shipper, original); + assert.ok(player(before, f.cardOwner).hand.every(c => c.id !== f.card)); + assert.equal(before.karamaShipping?.card, f.card); + const sponsor = viewGame(before, f.guild).junctionTransport!; + const changed = act(before, f.guild, { type: 'offerJunctionTransport', event: sponsor.offerEvent, rate: 'half' }); + reject(changed, f.shipper, original); + assert.equal(changed.karamaShipping?.card, f.card); + const replacement = junctionTransportChoice(viewGame(changed, f.shipper), f.destination, f.sources); + assert.equal(replacement.cost, 2); + assert.ok(replacement.action); + const done = act(changed, f.shipper, replacement.action); + assert.equal(player(done, f.shipper).spice, 0); + assert.deepEqual(done.homeworlds!.custody!.visitors[f.destination][f.shipper], { normal: 3, elite: 0 }); + assert.ok(!done.karamaShipping); +}); + +void test('original optional Junction offer does not force forfeiture when the recipient chooses its ordinary purchased route', () => { + const f = createJunctionPurchasedPrice({ rate: 'full' }); + const done = completePurchasedHomeworldRate(act(f.beforeAction, f.shipper, f.original.action)).game; + assert.equal(player(done, f.shipper).spice, player(f.beforeAction, f.shipper).spice - 2); + assert.deepEqual(done.homeworlds!.custody!.visitors[f.destination][f.shipper], { normal: 3, elite: 0 }); + assert.equal(done.discard.filter(c => c.id === f.card).length, 1); + assert.ok(!done.karamaShipping); + assert.equal(player(done, f.guild).spice, player(f.beforeAction, f.guild).spice); +}); + +void test('a Junction quote made before actual purchased-card activation cannot silently accept the new unused-grant effect', () => { + const f = createPurchasedHomeworldRate('world'); + let g = structuredClone(f.beforePurchase); + const sponsor = viewGame(g, f.guild).junctionTransport!; + g = act(g, f.guild, { type: 'offerJunctionTransport', event: sponsor.offerEvent, rate: 'full' }); + const old = junctionTransportChoice(viewGame(g, f.shipper), f.destination, f.sources); + assert.ok(old.action); + assert.equal(old.cost, 3); + g = act(g, f.ecaz, { type: 'card', mode: 'shipment', card: f.karama, target: f.shipper }); + reject(g, f.shipper, old.action); + const current = junctionTransportChoice(viewGame(g, f.shipper), f.destination, f.sources); + assert.ok(current.action); + assert.equal(current.cost, 3); + const without = { ...current.action }; delete without.forfeitPurchasedRate; + reject(g, f.shipper, without); + assert.equal(g.karamaShipping?.card, f.karama); +}); + +void test('original unused purchased price does not cancel the separate native Guild receipt on sponsored reserve arrival', () => { + const f = createJunctionPurchasedPrice({ route: 'nativeArrival', rate: 'full' }); + assert.ok(f.action); + assert.equal(f.quote.cost, 6); + const done = settleNativeGuildRateModules(act(f.beforeAction, f.shipper, f.action)).game; + assert.equal(player(done, f.shipper).spice, player(f.beforeAction, f.shipper).spice - 6); + assert.equal(player(done, f.guild).spice, player(f.beforeAction, f.guild).spice + 6); + assert.equal(player(done, f.shipper).forces[f.destination], 3); + assert.ok(!done.karamaShipping); +}); + +void test('controlled second-card native denial does not remove immune Junction permission or change its acknowledged full tariff', () => { + const f = createJunctionPurchasedPrice({ nativeDenied: true, route: 'arrakisReturn', rate: 'full' }); + assert.equal(viewGame(f.beforeAction, f.shipper).guildTransportCanceled, true); + assert.equal(f.beforeAction.karamaShipping?.card, f.card); + assert.ok(f.action); + assert.equal(f.quote.cost, 3); + const done = act(f.beforeAction, f.shipper, f.action); + assert.equal(player(done, f.shipper).spice, player(f.beforeAction, f.shipper).spice - 3); + assert.equal(player(done, f.shipper).reserves, player(f.beforeAction, f.shipper).reserves + 3); + assert.equal(player(done, f.shipper).forces[f.origin], player(f.beforeAction, f.shipper).forces[f.origin] - 3); + assert.ok(!done.karamaShipping); + assert.ok(!done.response); +}); + +for (const use of [false, true]) { + void test(`controlled purchased Junction acknowledgement survives deferred Betrayal ${use ? 'Use' : 'Pass'} until actual delivery`, () => { + const f = createGuildBetrayalFixture({ source: 'junction', payer: 'holder' }); + const before = structuredClone(f.before); + // Explicit controlled physical acquisition on the existing native invoice + // fixture; gift, offer acceptance and both source continuations are real. + const card = holdGuildCunningCard(before, f.guild, 'karama'); + let g = act(before, f.guild, { type: 'card', mode: 'shipment', card: card.id, target: f.shipper }); + const choice = junctionTransportChoice(viewGame(g, f.shipper), String(f.declaration.destination), + f.declaration.sources as Record