From 8d55b8f3f798435f287b10fd5a3c149f83fb47a7 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:15:48 +1100 Subject: [PATCH] Connect provisional Basic Guild and Karama allied No-Field prices --- components/richese-allied-no-field.tsx | 7 ++++- docs/CURRENT_STATUS.md | 6 +++++ docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md | 36 +++++++++++++++++++++++++- docs/RICHESE_NO_FIELD_ENGINE_AUDIT.md | 8 ++++++ docs/RICHESE_NO_FIELD_RULES.md | 15 ++++++++++- docs/RULE_DECISIONS.md | 2 ++ game/bots.ts | 8 +++--- game/engine.ts | 13 +++++++--- game/reference.ts | 6 ++--- game/shipment-price.ts | 18 +++++++++++++ tests/richese-allied-no-field.test.ts | 5 ---- 11 files changed, 107 insertions(+), 17 deletions(-) diff --git a/components/richese-allied-no-field.tsx b/components/richese-allied-no-field.tsx index 17d62230..2133c4c4 100644 --- a/components/richese-allied-no-field.tsx +++ b/components/richese-allied-no-field.tsx @@ -3,6 +3,7 @@ import { useId, useState } from 'react'; import type { Action, GameView } from '@/game/engine'; import { territory, MOBILE_STRONGHOLD } from '@/game/board'; +import { alliedNoFieldShipmentPricing } from '@/game/shipment-price'; import { Button } from './ui/button'; import { Input } from './ui/input'; @@ -46,6 +47,7 @@ export function RicheseAlliedNoFieldControls({ ? 'Each ally pays 1 spice' : `${game.players.find((player) => player.id === offer.payer)?.name} pays ${offer.cost} spice`}{' '} if the shipment completes. Declining costs nothing. + {offer.pricingPolicy && {offer.pricingPolicy}}

); if (game.phase !== 5 || game.active !== ally.id || ally.shipped) return null; @@ -55,7 +57,8 @@ export function RicheseAlliedNoFieldControls({ const selected = available.find((candidate) => candidate.id === token) ?? available[0]; const target = territory(destination); - const cost = target.type === 'stronghold' ? 1 : 2; + const pricing = alliedNoFieldShipmentPricing(game, ally); + const cost = target.type === 'stronghold' ? pricing.stronghold : pricing.other; const selectedPayer = payer === 'both' && cost === 2 ? 'both' @@ -91,6 +94,7 @@ export function RicheseAlliedNoFieldControls({ token reveals when the shipment resolves. No payment or existing marker changes occur on decline.

+ {pricing.policy &&

{pricing.policy}

} {info.private.deployed && (

If accepted and allowed, your existing concealed No-Field must @@ -223,6 +227,7 @@ export function RicheseAlliedNoFieldDecision({ if the shipment completes.

)} + {offer.pricingPolicy &&

{offer.pricingPolicy}

}

The chosen token and force count are private to this alliance until the shipment reveals them. Accepting uses your normal shipment opportunity; diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index f8a2c00d..0c3a7790 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -59,6 +59,12 @@ physical forces plus reserve-limited token materialization, visibly marked as a provisional formula. Native sealing/reveal, casualties, controls and legal AI are connected. One before/after engine smoke completes the battle; types pass. Advanced mixed battles and combined Occupy remain guarded. +**Basic Richese allied discounted prices prototype.** Guild recipients and +already-active Karama rates can receive allied No-Field shipments. The explicit +provisional policy is one spice at the rounded one-force Guild rate, entirely +to bank even when Richese pays. Shared quotes connect engine, owned controls +and AI. Two before/after engine scenarios pass, including an actual native +Karama purchase and Easy consent; types pass. Advanced counterparts remain guarded. diff --git a/docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md b/docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md index 88dcd9e2..9f54691c 100644 --- a/docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md +++ b/docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md @@ -2,6 +2,40 @@ Primary-source audit, 2026-09-06. This narrows the broad integration warnings in the earlier No-Field audits. Ordinary turn ownership, consent and typed force selection can be composed without inventing extra faction powers. The genuinely unresolved payment and interruption intersections below remain distinct. The ordinary contract is now implemented in development fixtures as recorded below; this document does not activate a mode or certify all combinations. +## Basic discounted allied shipment prototype — 8 October 2026 + +The Basic-first pass connects Guild recipients and already-active Karama +shipment rates. **Provisional price/payee policy:** use the rounded +one-force Guild fee, one spice for either printed destination class, and +send the entire payment to the bank even when Richese funds it. This +chooses the ordinary discounted-rate reading for a usable first version; +it is not a publisher answer to the precedence conflicts below. + +`alliedNoFieldShipmentPricing` supplies the same immutable price/policy +quote to the engine, supplier controls and legal AI. Private pending-offer +and recipient-consent views carry the explanatory `pricingPolicy` only +to the pair. One payer funds the one-spice cost; the two-spice equal split +is not offered for this price. Native acceptance/commit revalidation, +token history, recipient force composition, cancellation/prevention, +prior-marker reveal and shipment ownership remain in their original paths. +Karama must already have been played through its own grant; this does not +spend a second card implicitly or grant another shipment. + +The Guild income adapter routes these Basic payments entirely to bank; +Guild gains no self-shipment/donor fee. The native Heighliner call still +uses the force recipient, not the token supplier. Advanced Guild/active +Karama combinations remain guarded. Ordinary undiscounted allied prices, +on-planet Fremen exclusion, destination restrictions and public-mode gates +are unchanged. + +**Brief smoke:** both old Basic guards reproduced. Guild-recipient and +actual native Karama-purchase scenarios now reach a private one-spice +offer and legal Easy consent, materialize five actual recipient forces, +spend only the supplier's one spice, leave Guild income unchanged and +consume only the recipient shipment. Supplier/consent SSR controls were +observed in Chromium with matching price and provisional warning. Types +pass; no broad suite, recovery campaign or deployed acceptance was run. + ## Sources checked - **R2**, [GF9 CHOAM & Richese rulebook](https://www.gf9games.com/dune/wp-content/uploads/2021/11/CHOAM-Rulebook-low-res.pdf), pp6,11–12: allied offer, prior-marker reveal, shared last-use history, explicit one/two-spice payment and either payer, Guild-recipient Heighliner exception, No-Field cancellation. The full publisher-authored mirror `/tmp/dune-rules/choam-lelekan-mirror.pdf` and `.txt` were read; fresh official-domain search results reproduced the relevant pages. Mirror provenance is in `RICHESE_NO_FIELD_RULES.md`. @@ -55,7 +89,7 @@ Test supplier-versus-recipient turn ownership, both full payers and any supporte ## Implemented ordinary contract before final checks -**Implementation — Partial.** The engine opens `richeseAllyOpportunity` once for the active eligible recipient’s unused shipment, holding that turn for Richese to offer or pass. A direct voluntary offer also records the opportunity. `richeseAllyShipment` then requires the recipient’s explicit acceptance of exact private terms and legal typed mix, or decline. The proposal/payment implementation supports either full payer, plus `payer: both` only at cost two with one spice each. The recipient’s actual regular/elite reserves constrain composition. Fremen’s ordinary on-planet pool remains ineligible; Guild-recipient and active-Karama pricing remain guarded pending the specific ruling. +**Implementation — Partial.** The engine opens `richeseAllyOpportunity` once for the active eligible recipient's unused shipment, holding that turn for Richese to offer or pass. `richeseAllyShipment` requires recipient consent and legal typed composition, or decline. Ordinary full/equal-split funding remains supported. Basic Guild and already-active Karama recipients now use the explicit discounted prototype above; their Advanced counterparts remain guarded. Fremen's ordinary on-planet reserves remain ineligible. Prevention precedes commitment. At successful commit the prior owner marker reveals first, the new allied token materializes immediately, exact funding settles once and only the recipient’s shipment is consumed. Decline/cancellation preserves the prior concealed marker and custody. This is the documented transaction composition above, not an explicit publisher FAQ about online action ordering. diff --git a/docs/RICHESE_NO_FIELD_ENGINE_AUDIT.md b/docs/RICHESE_NO_FIELD_ENGINE_AUDIT.md index d1fd0db3..0e5f29aa 100644 --- a/docs/RICHESE_NO_FIELD_ENGINE_AUDIT.md +++ b/docs/RICHESE_NO_FIELD_ENGINE_AUDIT.md @@ -14,6 +14,14 @@ states the unresolved formula and refinement boundaries. The earlier mixed-battle absence/avoidance claims in the checkpoint below are historical, not the current Basic implementation. +Basic Guild-recipient and already-active Karama allied offers now also use +a visibly provisional one-spice/bank quote, with connected supplier, +recipient and AI paths. Two direct before/after cases and current types +pass; SSR offer/consent surfaces were observed in Chromium. Advanced +counterparts remain guarded. This supersedes the earlier blanket price +absence below; [the payment contract](RICHESE_ALLIED_SHIPMENT_AUDIT.md) +retains the unresolved publisher precedence. + ## Current resolution of the audited hooks - **Implemented in development fixtures:** separate physical `Player.noField` custody; public `force-presence` counting and location keys; own No-Field shipment with cancellation/payment stages; physical-plus-marker or marker-only movement; event-fenced voluntary reveal; collection and occupancy/control consumers; exact exposed storm sector and unprotected worm materialization/casualties; marker-only prospective battle pools, joint reveal and persistent zero-battle identity; server-enforced ordinary prescience no-dial restriction; allied opportunity/private consent/typed immediate shipment with fixed full or equal-split funding; Gamont reveal at a selected marker location followed by one actual force returned when present, including zero and colocated ordinary-unit cases. diff --git a/docs/RICHESE_NO_FIELD_RULES.md b/docs/RICHESE_NO_FIELD_RULES.md index d48e613e..9cb89232 100644 --- a/docs/RICHESE_NO_FIELD_RULES.md +++ b/docs/RICHESE_NO_FIELD_RULES.md @@ -2,6 +2,19 @@ Audit date: 2026-09-06. This document distinguishes the printed token lifecycle from engine integration and unresolved combinations. It does not certify Richese, Homeworlds, or any other gated game mode. +## Basic discounted allied price prototype — 8 October 2026 + +Guild recipients and already-active Karama rates now have a connected +Basic first version: rounded one-force Guild price, one spice, entirely to +bank even when Richese pays. Supplier and recipient controls label this +as provisional; it does not settle the printed price/payee precedence. +The shared quote supplies engine, controls and AI without charging for +the materialized unit count or granting another shipment. +[Canonical payment contract](RICHESE_ALLIED_SHIPMENT_AUDIT.md). +Two before/after direct engine cases cover a Guild recipient and an actual +native Karama purchase, private offers, Easy consent, five actual arrivals +and one bank payment. Types pass. Advanced counterparts remain guarded. + ## Basic mixed battle prototype — 8 October 2026 The Basic-first pass now admits an ordinary Richese battle containing both @@ -92,7 +105,7 @@ This table records the original adapter contract. The implemented subset and cur 3. **Full-plan special Karama and structured Truthtrance.** The ordinary Atreides dial prohibition is explicit. Whether a broader entire-plan power or a particular promise question overrides it was not resolved by a retrieved specific answer. Do not leak the value through private legal-plan previews, public maximum dials, error strings or bot reasoning. 4. **Homeworld use.** R3 recommends Richese in a Homeworld combination but supplies no retrieved No-Field-specific rule defining off-planet hidden custody, one-on-planet versus off-planet coexistence, or homeworld threshold disclosure. General module availability is not an answer to those lifecycle questions. The isolated model represents a single Arrakis marker and does not authorize Homeworld placement. 5. **Capturing, transferring or returning a concealed marker as a force.** Movement-as-a-force and Gamont's explicit forced reveal do not establish a general conversion for every card or Guild transport-to-reserves. A generic return helper must not erase a hidden token without a source-backed reveal/custody cause. This module exposes no such automatic escape from last-use history. -6. **Allied discounts and reserve source.** Ordinary rules compose recipient consent, typed physical-force allocation and allied funding; these are now integrated as described in `RICHESE_ALLIED_SHIPMENT_AUDIT.md`. Guild-recipient and active-Karama pricing/payee precedence remain guarded pending a ruling. Ordinary Fremen reserves are on-planet and do not qualify for this off-planet shipment method; this is a source-location exclusion, not a missing elite-allocation rule. +6. **Allied discounts and reserve source.** Ordinary consent, typed allocation and funding are integrated. Basic Guild-recipient and already-active Karama price/payee have the explicitly provisional prototype above; their Advanced counterparts remain guarded pending resolution. Ordinary Fremen reserves are on-planet and remain ineligible for this off-planet method. See `RICHESE_ALLIED_SHIPMENT_AUDIT.md`. 7. **Supply orientation.** Concealed denomination is explicit. Hiding unused IDs/values in public JSON is the minimum privacy consequence, not a claim about an otherwise undocumented physical supply layout. The last revealed used face is public until another deployment; historical log knowledge may legitimately remain. ## Required verification diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 48fa8b6d..da0efe91 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -16,6 +16,8 @@ They supersede earlier software-absence claims only within their named scope. | Tleilaxu Ambassador leader alternative | First-death own native leader revives free instead of forces, independently of ordinary revival allowance. | [Ambassador](TLEILAXU_AMBASSADOR_RULES.md) | | Ordinary allied shipment transit | Depart during the shipper's turn; excess new physical visitors still present at ending go to Tanks, even when exit was prevented. Old co-occupation timing is separate. | [Allied transit](ALLIED_TRANSIT_RULES.md) | | Ordinary mixed No-Field battle | Private pool is physical forces plus reserve-limited token materialization; both plans seal before one reveal. Advanced and combined Occupy remain separate. | [No-Field](RICHESE_NO_FIELD_RULES.md) | +| Guild-recipient allied No-Field | Rounded one-force Guild fee: one spice to bank, even when Richese funds it; no Guild self-shipment income. | [Allied No-Field payment](RICHESE_ALLIED_SHIPMENT_AUDIT.md) | +| Already-active Karama / allied No-Field | Honor the already-played rounded one-force Guild rate and bank routing; no implicit additional card or shipment. | [Allied No-Field payment](RICHESE_ALLIED_SHIPMENT_AUDIT.md) | No publication or faction/mode gate is opened. The exact 7 October user rulings below remain distinct from these prototype choices. diff --git a/game/bots.ts b/game/bots.ts index 2b4eb95b..463acb2c 100644 --- a/game/bots.ts +++ b/game/bots.ts @@ -82,7 +82,7 @@ import { canUseAsKaramaRole, shrineTruthtranceBotAction, } from './shrine'; -import { reserveShipmentCost } from './shipment-price'; +import { reserveShipmentCost, alliedNoFieldShipmentPricing } from './shipment-price'; import { quoteSmugglerShipment } from './smuggler-shipment'; import { quoteSmugglerNoField } from './smuggler-no-field'; import { spiceBankerBattleMaximum, spiceBankerModeSupported } from './spice-banker'; @@ -153,15 +153,17 @@ const variation = (g: GameView, salt: string) => { }; function alliedNoFieldOffer(g: GameView): Action | null { const me = g.players.find((p) => p.id === g.me)!; + const ally = g.players.find(p => p.id === me.ally); const level = rank(g); const alliedNoField = g.richeseNoField; if ( alliedNoField?.owner === me.id && alliedNoField.canOfferAlly && !alliedNoField.allyDeclined && - alliedNoField.private + alliedNoField.private && ally ) { const inventory = alliedNoField.private; + const pricing = alliedNoFieldShipmentPricing(g, ally); const oldMarker = inventory.deployed; const oldValue = inventory.tokens.find((token) => token.id === oldMarker?.tokenId) @@ -202,7 +204,7 @@ function alliedNoFieldOffer(g: GameView): Action | null { .sort((a, b) => b.score - a.score); if (token) { const target = targets.find( - ({ t }) => (t.type === 'stronghold' ? 1 : 2) <= (me.spice ?? 0), + ({ t }) => (t.type === 'stronghold' ? pricing.stronghold : pricing.other) <= (me.spice ?? 0), ); if (target) return { diff --git a/game/engine.ts b/game/engine.ts index c49fe617..ddabda13 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -469,6 +469,7 @@ import { guildShipmentCost, guildShipmentIncome, shipmentPaymentBounds, + alliedNoFieldShipmentPricing, } from './shipment-price'; import { TERROR_DEFINITIONS, @@ -3941,7 +3942,7 @@ function noFieldAllyOfferBlock( return 'No-Field inventory is not initialized.'; if (ally.faction === 'fremen') return 'Fremen reserves are on the planet; this ability ships off-planet reserves.'; - if (ally.faction === 'guild' || g.karamaShipping?.player === ally.id) + if (g.advanced && (ally.faction === 'guild' || g.karamaShipping?.player === ally.id)) return 'Allied No-Field pricing with Guild or Karama discounts awaits a ruling.'; if ( g.richeseAllyBlocked?.turn === g.turn && @@ -4000,7 +4001,8 @@ function alliedNoFieldQuote( false, advisors, ); - const cost = territory(offer.territory).type === 'stronghold' ? 1 : 2; + const pricing = alliedNoFieldShipmentPricing(g, recipient); + const cost = territory(offer.territory).type === 'stronghold' ? pricing.stronghold : pricing.other; requireRule( offer.payer !== 'both' || cost === 2, 'An equal split requires the two-spice shipment price.', @@ -25665,7 +25667,8 @@ function commitShipment(g: Game, shipment: PendingShipment, settlement?: {recipi ally: p.ally, cost, allyPayment, - bankOnly: g.karamaShipping?.player === p.id, + bankOnly: g.karamaShipping?.player === p.id || + !!(shipment.alliedNoField && alliedNoFieldShipmentPricing(g, p).bankOnly), }); if (!settlement?.recipient && guild && guildPayment > 0) g.response = guildPaymentResponse(g, guild.id, shipmentIncomeContributions(g, p, cost, allyPayment), @@ -32764,6 +32767,8 @@ export function viewGame(state: Game, id: string) { if (!offer || ![offer.owner, offer.recipient].includes(id)) return null; const { event, owner, recipient, territory, sector, payer } = offer; + const recipientSeat = g.players.find(player => player.id === recipient); + const pricingPolicy = recipientSeat ? alliedNoFieldShipmentPricing(g, recipientSeat).policy : null; try { const { cost, @@ -32789,6 +32794,7 @@ export function viewGame(state: Game, id: string) { eliteMin, eliteMax, blocked: null as string | null, + ...(pricingPolicy ? { pricingPolicy } : {}), }; } catch (error) { if (!(error instanceof RuleError)) throw error; @@ -32807,6 +32813,7 @@ export function viewGame(state: Game, id: string) { eliteMin: 0, eliteMax: 0, blocked: error.message, + ...(pricingPolicy ? { pricingPolicy } : {}), }; } })(), diff --git a/game/reference.ts b/game/reference.ts index de9023ea..53bae9a9 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -1016,7 +1016,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'A normal No-Field shipment pays the one-force price. The concealed token moves like a force. Before Battle, Richese may reveal it voluntarily; a token involved in battle is revealed with the Battle Plan. Revelation brings the indicated forces from reserves, limited by what remains there.', 'The revealed token stays face up until another token is placed. Revealing it does not clear the previous-shipment restriction. An allied No-Field shipment reveals immediately and uses the ally’s reserves; an existing token must be revealed before another shipment.', 'Before an eligible ally uses its shipment, the game holds an offer opportunity for Richese. Richese may pass or privately propose a token, destination and payment. The ally accepts the exact offer and chooses its permitted regular/elite mix, or declines without payment. Success consumes the ally’s shipment, not Richese’s shipment or either player’s movement.', - 'Allied shipment costs one spice into a stronghold or two elsewhere. Either ally may pay the full price; a two-spice shipment may instead cost each ally one. Resolve prevention before payment or revealing an existing Richese marker; a successful shipment reveals that old marker first, then immediately reveals and materializes the allied token. Fremen reserves are on the planet, so they are not eligible for this off-planet method. Guild recipients and active Karama shipment discounts remain unavailable while their pricing is unresolved.', + 'Ordinary allied shipment costs one spice into a stronghold or two elsewhere, funded by either ally or one each at cost two. Basic Guild recipients and already-active Karama rates now use a visibly provisional one-spice price, paid entirely to bank even if Richese funds it; their Advanced counterparts remain guarded. Prevention precedes payment and the old-marker/new-token reveals. Fremen reserves are on the planet and remain ineligible.', 'Storm and worm exposure reveal the token and destroy its forces. A zero token still had one-force presence before revelation. Trip to Gamont forces a reveal, then returns one force if present. Revealing a token already in a territory does not trigger Terror a second time.', 'Development tables support your own concealed shipment, movement of a marker alone or with a physical group, voluntary reveal and one-force board presence. Physical reserves remain unchanged until reveal. Karama can prevent the No-Field shipment, with Guild shipment prevention and payment handled separately. Price and chosen allied funding are shown before shipment.', 'The map shows a concealed marker without its denomination to everyone. Richese privately sees its token values, last-use restriction and reveal control. Movement selection keeps the marker separate from physical forces. No hidden denomination or physical token identity is included in another player’s projected board state.', @@ -1024,7 +1024,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'Basic ordinary mixed battles have a provisional private pool: physical forces already here plus the token value capped by current reserves. The Battle Wheel labels that interpretation explicitly. Both plans seal before materialization, then native casualties and aftermath use actual forces. This first version does not adjudicate the FAQ mixed dial cap.', 'Concealed collection, occupancy and control use one-force presence. Exposed storm sectors and unprotected worm destruction reveal and remove the resulting actual forces. Storm shelter and other sectors remain separate. Trip to Gamont at a location with a marker reveals it, even if ordinary forces also occupy that sector, then returns one actual force if present. It is still used if zero forces remain. Canceling the CHOAM power leaves the marker concealed.', 'All four AI profiles choose from their own private token inventory, pay only the one-force shipment price, move the marker explicitly and reveal positive tokens when useful. They retain zero-token presence and use only public information to assess opponents. They also offer safe positive tokens from their own inventory during the allied opportunity, fund them with their own spice and accept or decline private offers using their own force and payment information. A human Richese player’s offer window holds an AI ally’s shipment. Pending responses, decisions and Truthtrance retain priority.', - 'Advanced mixed-force battles, combined Ecaz Occupy and entire-plan special Karama inspection against a No-Field remain guarded. Guild-recipient and active-Karama allied pricing, Homeworld custody, broader Truthtrance and interrupted compositions remain unfinished. The Basic mixed-pool prototype is not a completed or publisher-verified formula. Richese starts remain disabled.', + 'Advanced mixed-force battles, combined Ecaz Occupy, Advanced Guild/Karama allied prices and entire-plan special Karama inspection against a No-Field remain guarded. Homeworld custody, broader Truthtrance and interrupted compositions remain unfinished. Basic mixed-pool and discounted-price prototypes are not publisher-adjudicated formulas. Richese starts remain disabled.', ], related: [ 'richese-cards', @@ -1038,7 +1038,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Implementation', status: 'Partial', detail: - 'Own shipment, movement, voluntary reveal, effective presence, collection, storm/unprotected-worm destruction, Gamont reveal/return and marker-only battle integration work in development fixtures. Basic mixed physical/marker battle has a connected provisional pool, native reveal and aftermath. Allied offers, typed recipient consent, full/split funding, prevention and immediate reveal are integrated. Advanced/combined mixed battles, whole-plan inspection, Guild/Karama allied pricing and broader combinations remain guarded or unfinished.', + 'Own shipment, movement, reveal, effective presence, collection, hazards, Gamont and marker-only battles work in development fixtures. Basic mixed battles have a provisional private pool and native aftermath. Allied consent, typed forces, full/split funding, prevention and immediate reveal are integrated; Basic Guild/active-Karama offers have a shared provisional one-spice/bank quote. Advanced/combined mixed battles, Advanced discounted allied prices, whole-plan inspection and wider combinations remain separate.', }, { area: 'Player controls', diff --git a/game/shipment-price.ts b/game/shipment-price.ts index 2398251e..9ec649e1 100644 --- a/game/shipment-price.ts +++ b/game/shipment-price.ts @@ -24,6 +24,24 @@ export function guildShipmentCost( return halfRate ? Math.ceil(normal / 2) : normal; } +const ordinaryAlliedNoFieldPricing = Object.freeze({ + stronghold: 1, other: 2, bankOnly: false, policy: null, +}); +const basicDiscountedAlliedNoFieldPricing = Object.freeze({ + stronghold: 1, other: 1, bankOnly: true, + policy: 'Provisional Basic pricing: a Guild recipient or already-active Karama rate uses the rounded one-force Guild fee (1 spice), paid entirely to the bank. This price/payee precedence is not an adjudicated publisher ruling.', +}); + +/** Basic first version; Advanced keeps its separate unresolved-entry guard. */ +export function alliedNoFieldShipmentPricing( + context: { advanced: boolean; karamaShipping?: { player: string } | null }, + recipient: { id: string; faction: FactionId }, +) { + return !context.advanced && + (recipient.faction === 'guild' || context.karamaShipping?.player === recipient.id) + ? basicDiscountedAlliedNoFieldPricing : ordinaryAlliedNoFieldPricing; +} + /** A pledge is already escrowed. Selecting a share never spends a donor's * unpledged balance or enlarges the contribution they authorized. */ diff --git a/tests/richese-allied-no-field.test.ts b/tests/richese-allied-no-field.test.ts index 6ac1d51d..e543e26b 100644 --- a/tests/richese-allied-no-field.test.ts +++ b/tests/richese-allied-no-field.test.ts @@ -279,12 +279,7 @@ void test('invalid payer, occupancy, repeated token, wrong turn and on-planet Fr assert.deepEqual(g, before); g.players[1].faction = 'fremen'; assert.throws(() => offer(g), /on the planet/); - g.players[1].faction = 'guild'; - assert.throws(() => offer(g), /awaits a ruling/); g.players[1].faction = 'atreides'; - g.karamaShipping = { player: 'a', owner: 'r' }; - assert.throws(() => offer(g), /awaits a ruling/); - g.karamaShipping = null; g.active = 'r'; assert.throws(() => offer(g), /ally’s unused/); g.active = 'a';