feat: support provisional paid Black Market retention
This commit is contained in:
1 parent
7891517a9b
commit
e70cd48172
21 files changed
+862
-151
No files matched your search
@@ -274,8 +274,9 @@ function calculate(
|
||||
'The paid Richese lot has a stale declaration round.',
|
||||
);
|
||||
requireAuction(
|
||||
lot.source !== 'blackMarket' || (input.advanced && s.seller !== s.winner),
|
||||
'Black Market requires an Advanced sale to another buyer; self-purchase awaits a payment and sale-counting ruling.',
|
||||
lot.source !== 'blackMarket' || (input.advanced &&
|
||||
(s.seller !== s.winner || !s.free && s.amount > 0)),
|
||||
'Black Market requires Advanced play; paid seller retention needs a positive actual price.',
|
||||
);
|
||||
requireAuction(
|
||||
!s.free || (
|
||||
|
||||
+27
-24
@@ -440,6 +440,7 @@ import {
|
||||
} from './richese-funding';
|
||||
import {
|
||||
quoteRicheseSettlement,
|
||||
richeseBidFitsHand,
|
||||
requireRicheseDeclarationCache,
|
||||
RicheseSettlementError,
|
||||
type RicheseSettlementQuote,
|
||||
@@ -12337,6 +12338,8 @@ function richeseBetrayalIntegrity(g: Game): void {
|
||||
}
|
||||
}
|
||||
function beginRicheseBetrayal(g: Game, quote: Extract<RicheseSettlementQuote,{kind:'sold'}>): boolean {
|
||||
// Already-held retention supplies neither a cache acquisition nor seller income.
|
||||
if (g.richeseAuction?.source === 'blackMarket' && quote.winner === g.richeseAuction.owner) return false;
|
||||
if (!g.richeseBetrayalPreview) return false;
|
||||
richeseBetrayalSourcePreflight(g,quote);
|
||||
const required = richeseBetrayalResponders(g);
|
||||
@@ -12464,6 +12467,8 @@ function nexusEmperorSellerOffer(g: Game, id: string) {
|
||||
return { ...quote, blocked, canUse: !blocked, policy: EMPEROR_SELLER_POLICY };
|
||||
}
|
||||
function beginNexusEmperorSellerPurchase(g: Game, quote: Extract<RicheseSettlementQuote, {kind:'sold'}>, bank: boolean): boolean {
|
||||
// The seller/free-acquisition source cannot waive an already-held card's price.
|
||||
if (g.richeseAuction?.source === 'blackMarket' && quote.winner === g.richeseAuction.owner) return false;
|
||||
if (quote.amount <= 0 || !emperorNexusSellerPurchaseModeSupported(g)) return false;
|
||||
requireRule(!g.pendingNexusEmperorSellerPurchase && !g.pendingRicheseBetrayal &&
|
||||
!g.response && !g.decision && !g.truthtrance && !g.phaseOpening &&
|
||||
@@ -12517,10 +12522,13 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
|
||||
: [{payer:owner.id,amount:richeseContribution,recipient:emperor ? 'player' : 'bank'}]) : undefined;
|
||||
if (!free) payWithAlly(g, winner, quote.amount, quote.allyPayment);
|
||||
g.richeseFunding = {};
|
||||
if (lot.source === 'cache')
|
||||
g.richeseCache = g.richeseCache!.filter((c) => c.id !== card.id);
|
||||
else owner.hand = owner.hand.filter((c) => c.id !== card.id);
|
||||
winner.hand.push(card);
|
||||
const retained = lot.source === 'blackMarket' && winner.id === owner.id;
|
||||
if (!retained) {
|
||||
if (lot.source === 'cache')
|
||||
g.richeseCache = g.richeseCache!.filter((c) => c.id !== card.id);
|
||||
else owner.hand = owner.hand.filter((c) => c.id !== card.id);
|
||||
winner.hand.push(card);
|
||||
}
|
||||
g.currentAuctionSale = {
|
||||
...(g.homeworldOccupationPreview ? { occupiedEvent: lot.event,
|
||||
occupiedContributions: free ? [] : [{ payer: winner.id, amount: quote.ownPayment },
|
||||
@@ -12538,7 +12546,7 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
|
||||
if (bank || !emperor) awardBankerIncome(g, incomePayment);
|
||||
log(
|
||||
g,
|
||||
`${winner.name} bought ${lot.source === 'cache' ? card.name : 'the concealed Black Market card'} for ${quote.amount} spice.${free ? ' Emperor Nexus retained the proven price; no seller or bank payment occurred.' : ' The funded bid was paid automatically.'}`,
|
||||
`${winner.name} ${retained ? 'retained their own Black Market card under provisional paid self-purchase' : `bought ${lot.source === 'cache' ? card.name : 'the concealed Black Market card'}`} for ${quote.amount} spice.${free ? ' Emperor Nexus retained the proven price; no seller or bank payment occurred.' : ' The funded bid was paid automatically.'}`,
|
||||
{ faction: winner.faction, name: 'Auction payment' },
|
||||
);
|
||||
const ixians = byFaction(g, 'ixians');
|
||||
@@ -12591,7 +12599,10 @@ function beginRicheseLot(
|
||||
source,
|
||||
owner: owner.id,
|
||||
method: action.method as RicheseAuction['method'],
|
||||
eligible: auctionEligible(g),
|
||||
eligible: g.order.filter(id => {
|
||||
const bidder = getPlayer(g, id);
|
||||
return richeseBidFitsHand(source, owner.id, id, bidder.hand.length, handLimit(bidder));
|
||||
}),
|
||||
order,
|
||||
tieOrder: g.order,
|
||||
});
|
||||
@@ -15141,17 +15152,16 @@ function finishAutomaticDecision(g: Game): boolean {
|
||||
return true;
|
||||
}
|
||||
if (g.phase === 3 && !decision && lot && !lot.outcome) {
|
||||
const active = lot.active ? getPlayer(g, lot.active) : null;
|
||||
const full =
|
||||
lot.method === 'silent'
|
||||
? lot.eligible.find(
|
||||
(id) =>
|
||||
!Object.hasOwn(lot.sealed, id) &&
|
||||
getPlayer(g, id).hand.length >= handLimit(getPlayer(g, id)),
|
||||
)
|
||||
: lot.active &&
|
||||
getPlayer(g, lot.active).hand.length >=
|
||||
handLimit(getPlayer(g, lot.active))
|
||||
? lot.active
|
||||
? lot.eligible.find(id => {
|
||||
const bidder = getPlayer(g, id);
|
||||
return !Object.hasOwn(lot.sealed, id) &&
|
||||
!richeseBidFitsHand(lot.source, lot.owner, id, bidder.hand.length, handLimit(bidder));
|
||||
})
|
||||
: active && !richeseBidFitsHand(lot.source, lot.owner, active.id, active.hand.length, handLimit(active))
|
||||
? active.id
|
||||
: null;
|
||||
if (full) {
|
||||
g.richeseAuction = submitRicheseBid(
|
||||
@@ -29995,7 +30005,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
: lot.bidder === bidder.id;
|
||||
if (hasWinningCommitment)
|
||||
requireRule(
|
||||
bidder.hand.length < handLimit(bidder),
|
||||
richeseBidFitsHand(lot.source, lot.owner, bidder.id, bidder.hand.length, handLimit(bidder)),
|
||||
'Leave room for the card in your committed auction.',
|
||||
);
|
||||
}
|
||||
@@ -32833,14 +32843,7 @@ function applyActionInner(
|
||||
g.phase === 3 && lot && !lot.outcome,
|
||||
'There is no open Richese auction.',
|
||||
);
|
||||
requireRule(p.hand.length < handLimit(p), 'Your hand is full.');
|
||||
requireRule(
|
||||
lot.source !== 'blackMarket' ||
|
||||
p.id !== lot.owner ||
|
||||
action.amount === null ||
|
||||
action.amount === 0,
|
||||
'A positive Black Market self-bid is awaiting a ruling on payment and sale counting.',
|
||||
);
|
||||
requireRule(richeseBidFitsHand(lot.source, lot.owner, p.id, p.hand.length, handLimit(p)), 'Your hand is full.');
|
||||
const amount =
|
||||
action.amount === null
|
||||
? null
|
||||
|
||||
+13
-9
@@ -416,11 +416,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'The collection belongs to Richese in both Basic and Advanced play. Advanced Black Market and special Karama acquisition add ways to obtain cards; they do not change these ten physical identities.',
|
||||
'Development tables support the cache auction first or last, with Once Around or Silent bidding. Advanced Black Market offers a hand card using normal, Once Around or Silent bidding; other players may buy it, and an unbid or canceled offer returns to the seller.',
|
||||
'The controls show the card only to entitled viewers, accept a public Black Market claim, and let bidders choose their own and pledged ally spice. Silent bids remain private until all eligible players submit. A donor cannot reduce pledged funding during unfinished Silent bidding, and a Black Market card reserved for sale cannot be played elsewhere. Atreides inspection, applicable Harkonnen bonus draws and Ixian allied replacement follow the sale’s origin.',
|
||||
'Other buyers pay Richese for a cache or Black Market sale. Richese buying its own cache card pays the Emperor or bank. Only the winner’s committed funding is collected; losing Silent bids do not spend their spice. An unbid cache card may be kept for free if the hand has room, or removed from the game.',
|
||||
'Other buyers pay Richese for a cache or Black Market sale. Richese buying its own cache card pays the Emperor or Bank. Under a visibly provisional paid-retention policy, a positive Black Market seller win likewise pays Emperor or Bank, retains the same held card without another hand slot, and counts as one sale. No self-refund or duplicate acquisition occurs. Only winning committed funding is collected; losing Silent bids spend nothing. Zero/unbid retention remains free and unsold. An unbid cache card may be kept for free only if the hand has room, or removed from the game.',
|
||||
'The opt-in Richese Nexus Betrayal auction preview adds a neutral before-payment acknowledgement. A legal rival holder may veto a public self-cache purchase or send another buyer’s sale payment to the bank. See its separate guide and bounded verification evidence; normal hidden self-purchases and special-Karama acquisition are not connected triggers.',
|
||||
'A player whose hand becomes full is automatically passed, or submits zero in an unfinished Silent lot. A full-hand owner’s unbid cache card is removed automatically. If every hand is full, preparing the normal pool does not draw and lose unseen cards.',
|
||||
'A bidder whose hand becomes full is automatically passed, or submits zero in an unfinished Silent lot, unless it is the Black Market seller retaining its already-held card. The same current hand rule governs eligibility, commitments and settlement. A full-hand owner’s unbid cache card is still removed automatically. If every ordinary hand is full, normal-pool preparation does not draw and lose unseen cards.',
|
||||
'AI supports cache choices and all three offered bidding methods using its own projected information. It currently skips the optional Black Market prelude. A seller in an already offered Black Market lot passes or submits zero.',
|
||||
'Karama may prevent the declared cache auction before the ordinary pool is drawn. Cancellation leaves the cache intact and restores its deducted ordinary lot; first/last position does not create another cache offer that round. The independent reduction for a completed Black Market sale remains. An exhausted cache similarly causes no cache deduction. Positive Black Market self-bids and advanced Ixian Technology substitution on special lots retain their separate boundaries.',
|
||||
'Karama may prevent the declared cache auction before the ordinary pool is drawn. Cancellation leaves the cache intact and restores its deducted ordinary lot; first/last position does not create another cache offer that round. The independent reduction for a completed Black Market sale remains, including provisional paid retention. An exhausted cache causes no cache deduction. The supplied Advanced source excludes Richese lots from Ixian Technology.',
|
||||
'Juice of Sapho now supports bounded first/last Once Around, movement and remaining battle-choice ordering; see its timing guide for current limits. Richese’s Karama uses the existing generic handler, Distrans supports a separate private hand transfer, Nullentropy Box supports a paid private discard search, Ornithopter supports its two movement modes, and Residual Poison supports a random opposing leader death before leader commitment in development fixtures. Portable Snooper supports ordinary and late poison defense. Stone Burner supports a guarded weapon commitment and revealed leader-effect choice. Mirror Weapon copies opposing revealed attacks, with independent copy-first choices for Tooth and Stone in bounded CHOAM/Richese-deck classic-faction battles. A winning physical Mirror may be kept even after copying activated Tooth or Artillery under the user-selected interpretation, not a publisher clarification. Semuta Drug has a bounded clean public ordinary-discard preview; other discard producers and Sapho timing modes remain unfinished. This does not certify every Karama interaction. Richese expansion starts remain disabled. Each component guide identifies its currently available action. Independent continuation and persisted-room checks pass. Desktop checks confirmed all ten enlarged guides, direct topic links, Once Around payment and restored private Silent bidding. Mobile and full Richese-game verification remain unfinished.',
|
||||
],
|
||||
related: [
|
||||
@@ -440,13 +440,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Verified inventory and the first/last cache and advanced Black Market auction pipelines are integrated in development fixtures, including funding, secrecy and sale-origin effects. Card actions and guarded unresolved combinations remain unfinished; full starts stay disabled.',
|
||||
'Original inventory, first/last cache and Advanced Black Market pipelines retain funding, secrecy and source effects. Provisional paid seller retention now keeps the same card/order, pays native Emperor/Bank income, permits full hands without another slot and counts one sale. One shared current-hand rule governs eligibility, automatic skips, commitments and settlement. Card combinations and complete-mode acceptance remain unfinished.',
|
||||
},
|
||||
{
|
||||
area: 'Player controls',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Readable inspectors accompany cache/hand offer selection, method and direction, first/last declaration, public claims, sealed bids, funding splits and free-card/removal choices. Desktop catalog inspection, bidding and sealed-bid restoration passed. Browser rechecking confirmed the printed hand category, disabled unsupported action, visible reason and working inspector. Mobile and ordinary card-effect verification remain unfinished.',
|
||||
'Readable inspectors and original offer/method/direction, declaration, claim, sealed bid, funding and disposition controls remain. A visible provisional paid-retention notice precedes positive seller bidding; an already-held full hand can enter a legal price. Three390px Normal, Once Around and Silent flows exercise real bid/counter/income and next native controls without errors or overflow. Wider card-effect and full-game acceptance remain unfinished.',
|
||||
},
|
||||
{
|
||||
area: 'AI',
|
||||
@@ -464,8 +464,12 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Verification',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Focused checks pass for thirteen engine scenarios, eleven independent reviews, seven persisted-room scenarios, eight AI scenarios across four profiles, eighteen auction mechanisms, nine funding cases, six inventory cases, six presentation cases and five roster cases. Bounded desktop inspection, payment and reconnect checks pass. Full Richese games and mobile coverage remain unverified.',
|
||||
'Earlier inventory, mechanisms, AI, persisted-room and catalog evidence remains dated in the linked contracts. New real producers earn a full hand through cache/ordinary purchases, preserve private Silent offers, distinguish paid versus zero retention, and cover native cancellation, actual Banker legs, bare rules and Nexus source eligibility. Three mobile flows and four legal full-hand seller policies pass. Full modes, comprehensive assurance and deployed acceptance remain open.',
|
||||
evidence: [
|
||||
'tests/richese-self-purchase-rounds.test.ts',
|
||||
'tests/richese-self-purchase-silent.test.ts',
|
||||
'tests/fixture-richese-self-purchase-full-hand.ts',
|
||||
'docs/RICHESE_AUCTION_RULES.md',
|
||||
'tests/richese-cards.test.ts',
|
||||
'tests/richese-card-presentation.test.ts',
|
||||
'tests/richese-engine.test.ts',
|
||||
@@ -4481,7 +4485,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'The Advanced one-per-turn Fedaykin limit still applies across ordinary revivals and this card. The extra total allowance does not permit another elite revival. Fewer than three eligible counters is an unresolved case and remains unavailable.',
|
||||
'The independent revival grant enters original pending revival. Under the visible provisional policy, native Tleilaxu prevention/income, typed reserves, Tech and eligible Homeworld handling remain; ordinary free-rate blocking does not reprice it. Keeping the card creates no extra public confirmation.',
|
||||
'At an eligible original classic/native Nexus normal bank-auction winner payment, prove the positive final bid from your own spice and spend Emperor Nexus to receive the lot without losing spice. Every winner sees the same payment confirmation; the provisional privacy recommendation remains. Canonical native module envelopes preserve original card/capacity, bonus and next-lot inspection; ordinary spice or legal Karama remains available. No bank subsidy or Richese cache/seller route is invented. Strict old physical revival admission is separate and never broadens.',
|
||||
'Original Richese cache and Advanced Black Market offer Pay or eligible Emperor Nexus Use after original Richese Betrayal. The provisional seller-unpaid policy retains your full personally affordable price without bank reimbursement. One original lot, native Harkonnen bonus and source successor continue; legal self-cache is included but self-Black remains prohibited. Original self-purchase forced discard cannot consume this Nexus or acquire a card.',
|
||||
'Original Richese cache and other-buyer Advanced Black Market offer Pay or eligible Emperor Nexus Use after original Richese Betrayal. The provisional seller-unpaid policy retains your full personally affordable price without bank reimbursement. One original lot, native Harkonnen bonus and source successor continue; legal self-cache is included. Already-held Black Market paid retention supplies no acquisition/free-seller source, so it does not invent this confirmation or spend a Nexus. Original self-purchase forced discard cannot consume this Nexus or acquire a card.',
|
||||
'A committed Richese Ambassador trigger now offers its original beneficiary Pay or eligible Emperor Nexus Use, with no new decline. Under the visible provisional purchase composition, prove and keep three own spice while drawing one original deck card; no bank payment, reimbursement or fake auction is made. Original private inability, native bonus and entrant movement/worm continuation remain. Allies cannot Use; actual forming-alliance Nexus disposal stays original.',
|
||||
'Original Advanced Richese special Karama has an eligible private joint option: spend the held Karama plus Emperor Nexus at native purchase commit after proving three own uncommitted spice, keep that spice and acquire one exact unreserved cache card. Native once-use, full-hand/final-cache and enclosing controls remain; no bank money, ally bonus, fake auction or automatic public pause. Ordinary Pay3 remains available. This source composition is visibly provisional, not immunity or broader Kull admission.',
|
||||
],
|
||||
@@ -4514,7 +4518,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'The panel shows the public Richese target, original buyer, cache/Black Market source and winning price. It does not show payer resources, ally balances, rival hands, secret Nexus identities or the saved private receipt. Read your own unavailable reason rather than guessing Use from a held-card face alone.',
|
||||
'A public cache face remains inspectable through existing entitled Card controls. A concealed Black Market face stays unknown unless you already have a separate inspection entitlement; a seller’s public claim is not its face. Nexus inspection shows printed reference text, not anybody’s hidden custody. This response grants no new card inspection.',
|
||||
'Normal-deck Richese self-purchases are outside the connected veto boundary because the Richese-family identity may be hidden. The private advanced special-Karama acquisition has its own activating card and once-use ordering and is also outside this boundary. Native Karama exceptions do not certify Nexus coverage of either producer.',
|
||||
'Existing exhausted-cache, cache-ordering, positive Black Market seller-bid and special-acquisition guards remain. Unsupported counters or any-time overlays must not enter this saved purchase boundary. Finish the response before underlying bidding or other actions can change its reserved card or funding.',
|
||||
'Existing cache-count, ordering and special-acquisition boundaries remain. Already-held Black Market paid retention supplies neither a cache acquisition nor seller income, so no veto/diversion invoice is invented. Unsupported counters or any-time overlays must not enter this saved purchase boundary. Finish a real response before underlying bidding or other actions can change its reserved card or funding.',
|
||||
'Refreshing resumes the same event, passes and original auction continuation. Saved pending and completed history must retain exact physical source and once-only payment without rerolls, raw-action replay or requiring old cards to remain forever in discard. Stale or foreign event submissions reject rather than selecting another lot.',
|
||||
'All four AI difficulties use the same private legal Use/public Pass projection. This is minimal legal participation, not calibrated Richese strategy. Bounded engine, authenticated SQL, real table controls and refreshed phone-browser scenarios are verified locally; full Richese/Nexus games, wider compositions and deployed acceptance remain open.',
|
||||
],
|
||||
@@ -4852,7 +4856,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A selected surviving Planetologist may use a canonical printed green Richese Special or Special - Movement in its weapon slot for two strength. Exact physical identity is required. Stone Burner, Mirror Weapon and Portable Snooper do not qualify. The green card is discarded once without its native effect, fee, search or early leader kill.',
|
||||
'The normal Smuggler own companion is one separate force alongside the marker, not added to its hidden value. Ordinary allied No-Field cancellation and Guild stages retain their exact owner/recipient/token frame; an allied free companion is not granted.',
|
||||
'The authorized Advanced recipient cutover keeps Richese ally contributions distinct: buyer four goes to Richese, funded Richese four goes to Emperor or bank. This also applies when Emperor is the buyer. Cancellation redirects only the original Emperor share, not unrelated buyer credit, with no second donor debit or card transfer. Basic keeps its publisher receipt.',
|
||||
'Separate normal Banker income uses only actual bank-routed payer legs and remains unavailable until actual Mentat. Allowed Emperor income and ordinary seller credit are not bank payments. Bureaucrat evaluates Advanced buyer-to-Richese and Richese-to-Emperor receipts separately, retaining full original costs and one use per phase; Basic keeps its original seller receipt. Supplied Advanced p34 expressly excludes Richese auctions from Ixian Technology, so no per-lot exchange or decline exists; exhausted-cache arithmetic, positive Black Market self-bidding and other module boundaries remain explicit.',
|
||||
'Separate normal Banker income uses only actual bank-routed payer legs and remains unavailable until actual Mentat. Allowed Emperor income and ordinary seller credit are not bank payments. Bureaucrat evaluates Advanced buyer-to-Richese and Richese-to-Emperor receipts separately, retaining full original costs and one use per phase; Basic keeps its original seller receipt. Positive Black Market self-purchase is provisionally paid retention of the same card, with full native Emperor/Bank income and no extra hand slot. Supplied Advanced p34 excludes Richese auctions from Ixian Technology; other module and complete-mode boundaries remain explicit.',
|
||||
],
|
||||
related: ['leader-skills', 'richese-cards', 'richese-no-field', 'spice-banker-income', 'implementation-checklist'],
|
||||
checklist: [
|
||||
|
||||
@@ -42,6 +42,16 @@ export type RicheseSettlementQuote =
|
||||
allyPayment: number;
|
||||
};
|
||||
|
||||
/** A Black Market seller already holds the offered card. Provisional paid
|
||||
* retention therefore needs no additional hand slot; other wins still do. */
|
||||
export function richeseBidFitsHand(
|
||||
source: RicheseAuction['source'], owner: string, bidder: string,
|
||||
handCount: number, handLimit: number,
|
||||
): boolean {
|
||||
return handCount < handLimit ||
|
||||
source === 'blackMarket' && owner === bidder && handCount === handLimit;
|
||||
}
|
||||
|
||||
function requireSettlement(
|
||||
condition: unknown,
|
||||
message: string,
|
||||
@@ -106,13 +116,9 @@ export function quoteRicheseSettlement(
|
||||
const card = reservedCard(input);
|
||||
validHand(winner);
|
||||
requireSettlement(
|
||||
winner.handCount < winner.handLimit,
|
||||
richeseBidFitsHand(lot.source, lot.owner, winner.id, winner.handCount, winner.handLimit),
|
||||
'The winning hand has no room for this card.',
|
||||
);
|
||||
requireSettlement(
|
||||
lot.source !== 'blackMarket' || winner.id !== owner.id,
|
||||
'Black Market self-purchase is awaiting a ruling on payment and sale counting.',
|
||||
);
|
||||
const { funding } = input;
|
||||
requireSettlement(
|
||||
funding && funding.amount === lot.outcome.amount,
|
||||
|
||||
Reference in new issue
Block a user