Connect Richese Nexus to ordinary auction purchases
This commit is contained in:
1 parent
724d9dce1e
commit
cf268243f3
19 files changed
+1241
-46
No files matched your search
@@ -9,7 +9,8 @@ export function botNexusRicheseBetrayalActions(game: GameView): Action[] {
|
||||
const own = game.players.find((player) => player.id === game.me);
|
||||
if (!(own?.bot ?? own?.autopilot)) return [];
|
||||
return [{
|
||||
type: reaction.canUse && !reaction.blocked ? 'richeseBetrayalUse' : 'richeseBetrayalPass',
|
||||
// Minimal policies keep the card instead of guessing a concealed ordinary lot.
|
||||
type: reaction.source !== 'normal' && reaction.canUse && !reaction.blocked ? 'richeseBetrayalUse' : 'richeseBetrayalPass',
|
||||
event: reaction.event,
|
||||
}];
|
||||
}
|
||||
+106
-16
@@ -531,7 +531,7 @@ import { STORM_START_SECTOR, PLAYER_CIRCLE_SECTORS } from './player-positions';
|
||||
import { cashInCards } from './choam-karama';
|
||||
import { kullBlocksKarama, activeKullRestrictions, validateKullPhaseRestrictions, validateKullAttemptStamp, kullNativeCostCards, kullNexusCostCards, kullCounterCards, type KullPhaseRestriction } from './choam-kull';
|
||||
import { kullCancellationPurpose, kullSpecialPurpose, kullPurposeLabel, type KullPurpose } from './kull-purpose';
|
||||
import { quoteRicheseBetrayalInvoice, requiredRicheseBetrayalResponders, richeseBetrayalEligibility, richeseBetrayalEvent, createRicheseBetrayalReceipt, validateRicheseBetrayalCursor, type RicheseBetrayalCursor, type RicheseBetrayalReceipt } from './nexus-richese-betrayal';
|
||||
import { quoteRicheseBetrayalInvoice, quoteRicheseNormalBetrayalInvoice, richeseBetrayalVetoesPurchase, requiredRicheseBetrayalResponders, richeseBetrayalEligibility, richeseBetrayalEvent, createRicheseBetrayalReceipt, validateRicheseBetrayalCursor, type RicheseBetrayalCursor, type RicheseBetrayalReceipt } from './nexus-richese-betrayal';
|
||||
import { saleOptions, quoteSale, type ChoamMarket } from './choam-market';
|
||||
import { highKaitainDiscardsAvailable, quoteKaitainDiscards, homeworldWorthlessSaleBlock } from './homeworld-card-economy';
|
||||
import { biddingEndError, biddingEndQuiet, biddingEndPubliclyEmpty, type BiddingEnd } from './bidding-end';
|
||||
@@ -1370,10 +1370,14 @@ export type Game = {
|
||||
richeseBetrayal?: RicheseBetrayalCursor;
|
||||
pendingRicheseBetrayal?: {
|
||||
receipt: RicheseBetrayalReceipt;
|
||||
signature: string;
|
||||
} & ({
|
||||
lot: RicheseAuction;
|
||||
quote: Extract<RicheseSettlementQuote, {kind:'sold'}>;
|
||||
signature: string;
|
||||
} | null;
|
||||
} | {
|
||||
/** The actual unpaid ordinary pool, never a fabricated Richese cache lot. */
|
||||
normal: { round: string; lot: string; index: number };
|
||||
}) | null;
|
||||
/** Original normal-auction obligation; neutral acknowledgements precede funding. */
|
||||
nexusEmperorPayment?: { sequence: number; current: string | null; completed: EmperorPaymentReceipt[] };
|
||||
pendingNexusEmperorPayment?: { receipt: EmperorPaymentReceipt } | null;
|
||||
@@ -12718,6 +12722,12 @@ function richeseBetrayalResponders(g: Game): string[] {
|
||||
})));
|
||||
}
|
||||
function richeseBetrayalSourceSignature(g: Game, frame: NonNullable<Game['pendingRicheseBetrayal']>): string {
|
||||
if ('normal' in frame) return JSON.stringify({
|
||||
event:frame.receipt.event,normal:frame.normal,turn:g.turn,phase:g.phase,status:g.status,
|
||||
round:g.richeseBidding,auction:g.auction,order:g.order,aid:g.aid,
|
||||
players:g.players.map(p => ({id:p.id,faction:p.faction,ally:p.ally,hand:p.hand,spice:p.spice})),
|
||||
active:g.active,decision:g.decision,response:g.response,phaseOpening:g.phaseOpening,
|
||||
});
|
||||
return JSON.stringify({
|
||||
event:frame.receipt.event,lot:frame.lot,quote:frame.quote,turn:g.turn,phase:g.phase,status:g.status,
|
||||
round:g.richeseBidding,auction:g.richeseAuction,order:g.order,positions:normalizedPlayerPositions(g),
|
||||
@@ -12736,6 +12746,38 @@ function richeseBetrayalInvoice(g: Game, quote: Extract<RicheseSettlementQuote,{
|
||||
physicalCards:physicalTreacheryCards(g),
|
||||
}));
|
||||
}
|
||||
function normalRicheseBetrayalInvoice(g: Game) {
|
||||
const auction = g.auction!;
|
||||
return nexusRule(() => quoteRicheseNormalBetrayalInvoice({
|
||||
players:g.players,auction,donor:auction.allyPayment ? getPlayer(g,auction.bidder!).ally : null,
|
||||
originalRecipient:byFaction(g,'emperor')?.id ?? null,physicalCards:physicalTreacheryCards(g),
|
||||
}));
|
||||
}
|
||||
/** This is an unpaid ordinary winner, not a fabricated cache sale. Family is
|
||||
* deliberately absent from admission, acknowledgement and payment choices. */
|
||||
function normalRicheseBetrayalPreflight(g: Game) {
|
||||
requireRule(richeseBetrayalModeSupported(g) && g.status === 'playing' && g.phase === 3 &&
|
||||
g.richeseBidding?.stage === 'normal' && g.richeseBidding.turn === g.turn &&
|
||||
g.auction && !g.richeseAuction && !g.currentAuctionSale &&
|
||||
!g.response && !g.decision && !g.phaseOpening && !g.truthtrance &&
|
||||
!g.pendingKarama && !g.pendingNullentropy && !g.pendingTreacheryDiscard &&
|
||||
!g.pendingNexusEmperorPayment && !g.pendingNexusEmperorSellerPurchase &&
|
||||
!g.pendingKull && !g.pendingChoamWorthless && !g.choamMarket && !g.pendingIxAlly &&
|
||||
!g.pendingRicheseGift && !g.pendingRichesePurchaseIncome && !g.biddingEnd &&
|
||||
!g.bureaucratPayments?.pending && !pendingNexusTraitors(g),
|
||||
'Richese ordinary-purchase Betrayal needs its original unpaid normal winner.');
|
||||
const auction=g.auction!;
|
||||
const next=quoteNormalAuctionNext({order:auction.saphoOrder ?? g.order,
|
||||
eligible:g.order.filter(id=>getPlayer(g,id).hand.length<handLimit(getPlayer(g,id))),
|
||||
active:auction.active,bid:auction.bid,bidder:auction.bidder,passed:auction.passed});
|
||||
requireRule(next.kind !== 'bid' && next.kind !== 'unbid' && next.player === auction.bidder,
|
||||
'Finish the original bidding cycle before its neutral Betrayal acknowledgement.');
|
||||
// Native payment still chooses spice/Karama and validates the actual wallet
|
||||
// or escrow. This boundary neither debits a bid nor spends a payment card.
|
||||
const invoice=normalRicheseBetrayalInvoice(g);
|
||||
currentAuctionContinuationQuote(g,{kind:'next'});
|
||||
return invoice;
|
||||
}
|
||||
function richeseBetrayalSourcePreflight(g: Game, quote: Extract<RicheseSettlementQuote,{kind:'sold'}>): void {
|
||||
requireRule(richeseBetrayalModeSupported(g) && g.status === 'playing' && g.phase === 3 &&
|
||||
g.richeseBidding?.stage === 'lot' && g.richeseBidding.turn === g.turn &&
|
||||
@@ -12795,22 +12837,35 @@ function richeseBetrayalIntegrity(g: Game): void {
|
||||
nexusCardsIntegrity(g);
|
||||
nexusRule(() => validateRicheseBetrayalCursor(g,cursor,frame?.receipt ?? null));
|
||||
if (frame) {
|
||||
requireRule(Object.keys(frame).sort().join(',') === 'lot,quote,receipt,signature' &&
|
||||
frame.quote?.kind === 'sold' && frame.lot && typeof frame.signature === 'string',
|
||||
'The saved Richese Betrayal source frame is malformed.');
|
||||
richeseBetrayalSourcePreflight(g,frame.quote);
|
||||
requireRule(frame.signature === richeseBetrayalSourceSignature(g,frame) &&
|
||||
JSON.stringify(frame.lot) === JSON.stringify(g.richeseAuction) &&
|
||||
JSON.stringify(frame.quote) === JSON.stringify(richeseSettlementQuote(g)) &&
|
||||
JSON.stringify(frame.receipt.invoice) === JSON.stringify(richeseBetrayalInvoice(g,frame.quote)) &&
|
||||
frame.receipt.round === g.richeseBidding!.event && frame.receipt.lot === g.richeseAuction!.event &&
|
||||
frame.receipt.turn === g.turn &&
|
||||
JSON.stringify(frame.receipt.required) === JSON.stringify(richeseBetrayalResponders(g)),
|
||||
'The saved Richese Betrayal opportunity lost its original quote, custody, responders or source continuation.');
|
||||
if ('normal' in frame) {
|
||||
const invoice=normalRicheseBetrayalPreflight(g);
|
||||
requireRule(Object.keys(frame).sort().join(',') === 'normal,receipt,signature' &&
|
||||
Object.keys(frame.normal).sort().join(',') === 'index,lot,round' &&
|
||||
frame.normal.index === g.auction!.index && frame.normal.round === g.richeseBidding!.event &&
|
||||
frame.normal.lot === frame.receipt.lot && frame.receipt.round === frame.normal.round &&
|
||||
frame.receipt.turn === g.turn && frame.receipt.invoice.source === 'normal' &&
|
||||
JSON.stringify(frame.receipt.invoice) === JSON.stringify(invoice) &&
|
||||
frame.signature === richeseBetrayalSourceSignature(g,frame) &&
|
||||
JSON.stringify(frame.receipt.required) === JSON.stringify(richeseBetrayalResponders(g)),
|
||||
'The ordinary Richese reaction lost its original unpaid pool, invoice or acknowledgements.');
|
||||
} else {
|
||||
requireRule(Object.keys(frame).sort().join(',') === 'lot,quote,receipt,signature' &&
|
||||
frame.quote?.kind === 'sold' && frame.lot && typeof frame.signature === 'string',
|
||||
'The saved Richese Betrayal source frame is malformed.');
|
||||
richeseBetrayalSourcePreflight(g,frame.quote);
|
||||
requireRule(frame.signature === richeseBetrayalSourceSignature(g,frame) &&
|
||||
JSON.stringify(frame.lot) === JSON.stringify(g.richeseAuction) &&
|
||||
JSON.stringify(frame.quote) === JSON.stringify(richeseSettlementQuote(g)) &&
|
||||
JSON.stringify(frame.receipt.invoice) === JSON.stringify(richeseBetrayalInvoice(g,frame.quote)) &&
|
||||
frame.receipt.round === g.richeseBidding!.event && frame.receipt.lot === g.richeseAuction!.event &&
|
||||
frame.receipt.turn === g.turn &&
|
||||
JSON.stringify(frame.receipt.required) === JSON.stringify(richeseBetrayalResponders(g)),
|
||||
'The saved Richese Betrayal opportunity lost its original quote, custody, responders or source continuation.');
|
||||
}
|
||||
}
|
||||
const sale = g.currentAuctionSale;
|
||||
const completed = cursor.completed.at(-1);
|
||||
if (completed && completed.lot === g.richeseAuction?.event) {
|
||||
if (completed && completed.invoice.source !== 'normal' && completed.lot === g.richeseAuction?.event) {
|
||||
const waiting = g.pendingNexusEmperorSellerPurchase;
|
||||
requireRule(g.status === 'playing' && g.phase === 3 && completed.turn === g.turn &&
|
||||
completed.round === g.richeseBidding?.event &&
|
||||
@@ -12868,6 +12923,27 @@ function beginRicheseBetrayal(g: Game, quote: Extract<RicheseSettlementQuote,{ki
|
||||
g.pendingRicheseBetrayal = frame;
|
||||
return true;
|
||||
}
|
||||
function beginNormalRicheseBetrayal(g: Game): boolean {
|
||||
if (!g.richeseBetrayalPreview || !g.auction?.bidder ||
|
||||
getPlayer(g,g.auction.bidder).faction !== 'richese') return false;
|
||||
const invoice=normalRicheseBetrayalPreflight(g), cursor=g.richeseBetrayal!;
|
||||
const round=g.richeseBidding!.event;
|
||||
// Passing or a spent miss resumes this exact invoice, never its raw action.
|
||||
if (cursor.completed.some(receipt=>receipt.round===round && receipt.invoice.source==='normal' &&
|
||||
JSON.stringify(receipt.invoice)===JSON.stringify(invoice))) return false;
|
||||
const required=richeseBetrayalResponders(g);
|
||||
if (!required.length) return false;
|
||||
const sequence=cursor.sequence+1,lot=crypto.randomUUID();
|
||||
const receipt=nexusRule(()=>createRicheseBetrayalReceipt(g,{
|
||||
event:richeseBetrayalEvent(g.turn,sequence,round,lot),turn:g.turn,sequence,round,lot,
|
||||
invoice,required,passed:[],stage:'pending',actor:null,nexusDiscardIndex:null,
|
||||
}));
|
||||
const frame:NonNullable<Game['pendingRicheseBetrayal']>={receipt,
|
||||
normal:{round,lot,index:g.auction.index},signature:''};
|
||||
frame.signature=richeseBetrayalSourceSignature(g,frame);
|
||||
cursor.sequence=sequence;cursor.current=receipt.event;g.pendingRicheseBetrayal=frame;
|
||||
return true;
|
||||
}
|
||||
function decideRicheseBetrayal(g: Game, p: Player, action: Action): void {
|
||||
const frame = g.pendingRicheseBetrayal!, receipt = frame.receipt;
|
||||
requireRule(['richeseBetrayalPass','richeseBetrayalUse'].includes(action.type) &&
|
||||
@@ -12890,6 +12966,19 @@ function decideRicheseBetrayal(g: Game, p: Player, action: Action): void {
|
||||
g.richeseBetrayal!.completed.push(next);
|
||||
g.richeseBetrayal!.current = null;
|
||||
g.pendingRicheseBetrayal = null;
|
||||
if ('normal' in frame) {
|
||||
// Cost precedes the private family test. A miss cannot be a free oracle.
|
||||
if (use && richeseBetrayalVetoesPurchase(receipt.invoice)) {
|
||||
const continuation=currentAuctionContinuationQuote(g,{kind:'next'});
|
||||
g.discard.push(g.auction!.cards[g.auction!.index]);
|
||||
log(g,`${p.name} spent Richese Nexus Betrayal and prevented the ordinary purchase. The reserved card was discarded without payment or acquisition; its face stays private.`);
|
||||
commitAuctionContinuation(g,continuation);
|
||||
} else {
|
||||
if (use) log(g,`${p.name} spent a committed Richese Nexus Betrayal attempt. The original ordinary purchase continues; the response grants no card inspection.`);
|
||||
auctionNext(g);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (use && receipt.invoice.kind === 'purchase') {
|
||||
const card = frame.quote.card;
|
||||
g.richeseCache = g.richeseCache!.filter(c => c.id !== card.id);
|
||||
@@ -13968,6 +14057,7 @@ function auctionNext(g: Game) {
|
||||
finishNormalBidding(g);
|
||||
return;
|
||||
}
|
||||
if (beginNormalRicheseBetrayal(g)) return;
|
||||
if (beginNexusEmperorPayment(g)) return;
|
||||
g.decision = { kind: 'auctionPayment', player: next.player };
|
||||
g.active = next.player;
|
||||
|
||||
@@ -35,12 +35,14 @@ export function homeworldRevivalDestinations(
|
||||
const effectiveSector = t.id === MOBILE_STRONGHOLD
|
||||
? splitLocation(g.mobileStronghold!.location!).sector : sector;
|
||||
let blocked = grant.blocked;
|
||||
if (!blocked && t.id === MOBILE_STRONGHOLD)
|
||||
blocked = grant.kind === 'fedaykin'
|
||||
? 'Southern Hemisphere placement does not bypass the mobile stronghold’s entry route. Reach its pointing territory and enter by Force Movement.'
|
||||
: 'Only Ixians may enter the mobile stronghold directly from off-planet reserves. Tleilax revival deployment does not grant that route.';
|
||||
if (!blocked && effectiveSector !== 0 && effectiveSector === g.storm)
|
||||
blocked = grant.kind === 'fedaykin'
|
||||
? 'Fedaykin revival placement into storm awaits its own permission ruling.'
|
||||
? 'Southern Hemisphere placement must use a sector outside the storm. The Fremen storm-shipment exception does not grant revival placement.'
|
||||
: 'Revival deployment cannot enter a sector in storm.';
|
||||
if (!blocked && t.id === MOBILE_STRONGHOLD)
|
||||
blocked = 'Revival placement in the Hidden Mobile Stronghold awaits its shipment-classification ruling.';
|
||||
blocked ??= territoryEntryBlock(g.players, p.id, t.id);
|
||||
return { id: location(t.id, sector), name: `${t.name}${sector ? ` · sector ${sector}` : ''}`,
|
||||
territory: t.id, sector, blocked };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Card } from './cards';
|
||||
import type { Game } from './engine';
|
||||
import { nexusCardMode, validateNexusCards, validateNexusPlayers, type NexusPlayer, type NexusState } from './nexus-cards';
|
||||
import type { RicheseAuction } from './richese-auction';
|
||||
import { richeseCardDefinition } from './richese-cards';
|
||||
@@ -8,7 +9,7 @@ export type RicheseBetrayalInvoice = {
|
||||
kind: 'purchase' | 'sale';
|
||||
target: string;
|
||||
buyer: string;
|
||||
source: 'cache' | 'blackMarket';
|
||||
source: 'cache' | 'blackMarket' | 'normal';
|
||||
price: number;
|
||||
ownPayment: number;
|
||||
allyPayment: number;
|
||||
@@ -87,18 +88,20 @@ function validateInvoice(players: readonly NexusPlayer[], invoice: RicheseBetray
|
||||
const target = players.find(p => p.id === invoice.target);
|
||||
const buyer = players.find(p => p.id === invoice.buyer);
|
||||
requireBetrayal(target?.faction === 'richese' && buyer &&
|
||||
['cache', 'blackMarket'].includes(invoice.source) &&
|
||||
['cache', 'blackMarket', 'normal'].includes(invoice.source) &&
|
||||
whole(invoice.price) && invoice.price > 0 && whole(invoice.ownPayment) && whole(invoice.allyPayment) &&
|
||||
invoice.allyPayment <= invoice.price && invoice.ownPayment === invoice.price - invoice.allyPayment &&
|
||||
(invoice.allyPayment === 0 ? invoice.donor === null :
|
||||
text(invoice.donor) && invoice.donor !== invoice.buyer && players.some(p => p.id === invoice.donor)) &&
|
||||
validCard(invoice.card), 'Richese Betrayal needs a positive original price, exact funding split and physical card.');
|
||||
if (invoice.kind === 'purchase') {
|
||||
requireBetrayal(invoice.source === 'cache' && buyer.id === target.id && richeseCardDefinition(invoice.card) &&
|
||||
requireBetrayal((invoice.source === 'normal' || invoice.source === 'cache' && richeseCardDefinition(invoice.card)) &&
|
||||
buyer.id === target.id &&
|
||||
invoice.originalRecipient === (players.find(p => p.faction === 'emperor')?.id ?? null),
|
||||
'Richese self-veto requires a real public cache purchase paid to Emperor or bank.');
|
||||
'Richese self-veto requires an original cache or normal purchase paid to Emperor or bank.');
|
||||
} else {
|
||||
requireBetrayal(invoice.kind === 'sale' && buyer.id !== target.id && invoice.originalRecipient === target.id &&
|
||||
requireBetrayal(invoice.kind === 'sale' && invoice.source !== 'normal' &&
|
||||
buyer.id !== target.id && invoice.originalRecipient === target.id &&
|
||||
(invoice.source !== 'cache' || richeseCardDefinition(invoice.card)),
|
||||
'Richese sale diversion requires another buyer and income originally owed to Richese.');
|
||||
}
|
||||
@@ -143,6 +146,51 @@ export function quoteRicheseBetrayalInvoice(input: {
|
||||
return invoice;
|
||||
}
|
||||
|
||||
/** Bind an ordinary Richese purchase without using the concealed card's family
|
||||
* to decide whether the blind Nexus counter is available. Native settlement
|
||||
* separately proves the payer's actual wallet and pledged ally funding. */
|
||||
export function quoteRicheseNormalBetrayalInvoice(input: {
|
||||
players: readonly NexusPlayer[];
|
||||
auction: Pick<NonNullable<Game['auction']>, 'cards' | 'index' | 'bid' | 'bidder' | 'allyPayment'>;
|
||||
donor: string | null;
|
||||
originalRecipient: string | null;
|
||||
physicalCards: readonly Card[];
|
||||
}): RicheseBetrayalInvoice {
|
||||
requireBetrayal(plain(input), 'Richese Betrayal needs an original normal auction.');
|
||||
validatePlayers(input.players);
|
||||
const auction = input.auction;
|
||||
requireBetrayal(plain(auction) && Array.isArray(auction.cards) && auction.cards.every(validCard) &&
|
||||
whole(auction.index) && auction.index < auction.cards.length && whole(auction.bid) && auction.bid > 0 &&
|
||||
text(auction.bidder) && input.players.some(player => player.id === auction.bidder && player.faction === 'richese'),
|
||||
'Richese Betrayal needs the native Richese winner and its current positive normal lot.');
|
||||
const allyPayment = auction.allyPayment === undefined ? 0 : auction.allyPayment;
|
||||
const invoice: RicheseBetrayalInvoice = {
|
||||
kind: 'purchase', target: auction.bidder, buyer: auction.bidder, source: 'normal',
|
||||
price: auction.bid, ownPayment: auction.bid - allyPayment, allyPayment,
|
||||
donor: input.donor, originalRecipient: input.originalRecipient,
|
||||
card: { ...auction.cards[auction.index] },
|
||||
};
|
||||
validateInvoice(input.players, invoice);
|
||||
const buyer = input.players.find(player => player.id === invoice.buyer)!;
|
||||
requireBetrayal(!invoice.donor || buyer.ally === invoice.donor &&
|
||||
input.players.find(player => player.id === invoice.donor)?.ally === buyer.id,
|
||||
'Richese Betrayal cannot substitute an unallied contributor.');
|
||||
for (const cards of [auction.cards, input.physicalCards]) {
|
||||
requireBetrayal(Array.isArray(cards) && cards.every(validCard), 'Richese Betrayal needs physical source custody.');
|
||||
const matches = cards.filter(card => card.id === invoice.card.id);
|
||||
requireBetrayal(matches.length === 1 && sameCard(matches[0], invoice.card),
|
||||
'Richese Betrayal has lost or duplicated its exact source card.');
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
|
||||
/** Evaluate only after the physical Nexus has been committed. A normal-lot
|
||||
* miss still records a used counter; it grants no earlier face inspection. */
|
||||
export function richeseBetrayalVetoesPurchase(invoice: RicheseBetrayalInvoice): boolean {
|
||||
return invoice.kind === 'purchase' &&
|
||||
(invoice.source !== 'normal' || !!richeseCardDefinition(invoice.card));
|
||||
}
|
||||
|
||||
/** Public held-presence only: private Nexus identities never affect timing. */
|
||||
export function requiredRicheseBetrayalResponders(
|
||||
players: readonly (NexusPlayer & { nexusHeld: boolean })[],
|
||||
|
||||
+11
-10
@@ -4623,7 +4623,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. Richese High now connects actual paid reserve companions, one combined provisional invoice, original marker proof/responses, physical withdrawal and later reserve-capped reveal. Nexus Cunning retains that explicit paid group in its pair receipt, reserving paid counters before immediate materialization; original counter and spent-card outcomes remain. Ix High now returns physical Suboids for paid Cyborgs with provisional additional allowance and shortage handling, retaining typed free allocation, original invoice and free-event income. Southern and Tleilax preserve the exact eligible revival group through one optional placement. Provisional physical entry now opens native BG, Terror and Ambassador responses, retaining the original held income, independent counters and ordered overlap; a committed Sneak group has its own arrival source. Southern storm and Hidden Mobile Stronghold classification remain unresolved. 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 Tupile provisionally follows current sole control: observed departure, contest or native high population ends its slot, while a new activation waits for earlier normal-limit cleanup. Continuous turns do not stack slots or reset intelligence use. 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.",
|
||||
"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. Richese High now connects actual paid reserve companions, one combined provisional invoice, original marker proof/responses, physical withdrawal and later reserve-capped reveal. Nexus Cunning retains that explicit paid group in its pair receipt, reserving paid counters before immediate materialization; original counter and spent-card outcomes remain. Ix High now returns physical Suboids for paid Cyborgs with provisional additional allowance and shortage handling, retaining typed free allocation, original invoice and free-event income. Southern and Tleilax preserve the exact eligible revival group through one optional placement. Provisional physical entry now opens native BG, Terror and Ambassador responses, retaining the original held income, independent counters and ordered overlap; a committed Sneak group has its own arrival source. Southern placement provisionally retains the ordinary storm and HMS entry route; Tleilax retains the Ixian-only direct off-planet restriction. 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 Tupile provisionally follows current sole control: observed departure, contest or native high population ends its slot, while a new activation waits for earlier normal-limit cleanup. Continuous turns do not stack slots or reset intelligence use. 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/faction-module-profile.ts',
|
||||
'game/reinforcements-homeworld.ts',
|
||||
@@ -5032,33 +5032,34 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
title: 'Richese Nexus Betrayal: veto purchase or divert sale payment',
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Verified',
|
||||
summary: 'Before a supported Richese auction settles, an unallied rival may spend the physical Richese Nexus card to discard a public self-cache lot without payment, or keep another buyer’s purchase intact while sending the original sale payment to the Spice Bank.',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Before a supported Richese purchase settles, an unallied rival may spend the physical Richese Nexus to veto a Richese-family purchase or divert another buyer’s sale payment. A concealed ordinary lot uses a visibly provisional committed blind attempt; a miss still spends the Nexus.',
|
||||
steps: [
|
||||
'This is an explicit local Richese Betrayal preview, not a public expansion start. It supports fresh two-to-six-seat Basic or Advanced tables with CHOAM, Richese and otherwise classic factions, the physical CHOAM deck and Nexus alone. Other optional modules, expansion factions, Semuta and Kull previews remain outside this boundary; older games do not silently acquire the effect.',
|
||||
'To use either alternative, remain unallied and hold the singleton Richese Nexus card while another seat is native Richese. A successful Use discards that physical Nexus card once. It spends no ordinary Treachery card, hand slot, Karama or activation spice. Current own-card promises and committed-resource legality still apply.',
|
||||
'To use either alternative, remain unallied and hold the singleton Richese Nexus card while another seat is native Richese. Use commits that physical Nexus card once. It spends no ordinary Treachery card, hand slot, Karama or activation spice. Current own-card promises and committed-resource legality still apply.',
|
||||
'The opportunity opens after a valid winning quote but before any purchase payment, source removal or hand delivery. You do not choose a different card, buyer, price or recipient: those terms are bound to the original auction.',
|
||||
'Purchase veto applies to Richese winning its own public cache lot through Once Around or Silent bidding. The exact selected cache card enters ordinary Treachery discard instead of Richese’s hand. No buyer or ally spice is spent; no payment goes to Richese, Emperor or bank, and no purchase bonus is created. The original lot retires once.',
|
||||
'A positive Richese win in the ordinary auction also opens the neutral response before payment choice, even when the face is concealed. Under the provisional blind-attempt convention, commit your Richese Nexus first. If the reserved card is a Richese-family card, it is discarded without payment, delivery or purchase bonus. Otherwise the Nexus remains spent and the original payment choice continues. There is no pre-spend family check, inspection or refund. The exact discarded face stays private.',
|
||||
'Sale diversion applies when another buyer owes a positive payment to Richese for a cache auction or an Advanced Black Market auction using normal, Once Around or Silent bidding. The same buyer receives the same physical card for the same price and own/authorized-ally split. That original payment goes to the bank, with zero sale income for Richese; it is not an extra charge, refund or second purchase.',
|
||||
'A diverted sale still earns the original buyer consequences. In particular, an applicable Harkonnen purchase bonus continues once. Losing offers do not spend spice. A vetoed purchase, zero/free cache keep or unsold Black Market retention does not fabricate a bonus or a positive sale.',
|
||||
'Your voluntary Nexus spend must preserve your own binding shipment promises. If the involuntary purchase veto makes an opponent’s promise impossible, the existing native reconciliation may release that promise; it does not pretend Richese acquired the vetoed card or paid the price.',
|
||||
'All publicly possible responders receive the same neutral acknowledgement: unallied non-Richese seats with public held-Nexus presence. A seat may pass even if its secret card is irrelevant; only a privately eligible Richese-card holder sees Use. If no seat is publicly possible, settlement is automatic. This privacy-preserving application protocol is not publisher-prescribed timing or a general Nexus/Karama priority ruling.',
|
||||
'Pass spends nothing and keeps your Nexus card. Your recorded pass cannot be undone within this event. Once every required responder passes, the original quoted purchase settles once with its original payment recipient, card delivery and earned consequences. An accepted Use closes the opportunity with its one selected outcome.',
|
||||
'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.',
|
||||
'Pass spends nothing and keeps your Nexus card. Your recorded pass cannot be undone within this event. Once every required responder passes, the original purchase resumes its native payment choice and settles once with its original recipient, card delivery and earned consequences. An accepted Use closes the opportunity with its one selected outcome.',
|
||||
'The panel shows the public Richese target, original buyer, cache/Black Market/ordinary 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.',
|
||||
'The normal-lot blind-attempt rule is online composition, not a printed general right to discard a Nexus card at any time. Its normal winning-bid boundary precedes the existing spice/Karama payment choice. Proactive immediate Karama acquisition and the private Advanced special-Karama acquisition retain their separate activating-card and once-use boundaries; this prototype does not certify those producers.',
|
||||
'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.',
|
||||
'All four AI difficulties use the same private legal Use/public Pass projection and pass blind ordinary attempts while strategic play is deferred. Prior cache/Black Market SQL and phone-browser evidence does not certify the new normal-lot source. Full Richese/Nexus games, wider compositions and deployed acceptance remain open.',
|
||||
],
|
||||
example: 'Richese wins a revealed cache lot for 4: Betrayal veto discards that card, charges nobody and delivers nothing. If Harkonnen instead buys it for 4, sale diversion charges the original four spice once, sends it to the bank, delivers the card and preserves the applicable Harkonnen bonus. If everyone passes, the original purchase settles unchanged: Richese’s self-buy pays Emperor/bank; the other buyer’s sale pays Richese.',
|
||||
related: ['nexus-cards', 'richese-cards', 'richese-acquisition', 'bidding', 'alliance-funding', 'privacy', 'faction-richese', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Bounded public self-cache veto and other-buyer cache/Black Market recipient override bind original source, invoice and continuation with independent pending/completed history. Hidden normal self-buys, special acquisition and combined responses remain excluded.', evidence: ['game/nexus-richese-betrayal.ts', 'game/engine.ts', 'game/auction-continuation-quote.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Public self-cache veto, original ordinary winning-bid blind attempt and other-buyer cache/Black Market recipient override bind actual sources and continuations. Ordinary attempts commit Nexus before family outcome; success skips payment/acquisition and advances once, pass/miss resumes the original chooser. Proactive/special acquisition and wider responses remain unfinished.', evidence: ['game/nexus-richese-betrayal.ts', 'game/engine.ts', 'game/auction-continuation-quote.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Mounted preview controls separate public neutral pass/wait from private legal Use, retain existing entitled inspectors and suspend the underlying auction panel without revealing the unknown Black Market face.', evidence: ['components/nexus-richese-betrayal.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'All four minimal legal policies consume canonical own-view eligibility and acknowledge publicly required opportunities; full strategy and calibration remain unfinished.', evidence: ['game/bot-nexus-richese-betrayal.ts', 'game/bots.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Prepayment veto, same-invoice diversion, singleton cost, pass behavior, neutral protocol, entitled faces and excluded producer/counter boundaries are explained without full-family or publisher-timing claims.', evidence: ['docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_CARD_RULES.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: '80 focused cases and 5,953 offline checks cover both alternatives/all-pass, exact custody/escrow/bonus, identity parity, four legal bot profiles, real table controls and authenticated SQLite restart/races/orphan rejection. Actual CLI/runtime and phone controls prove Basic veto, Advanced all-pass and cache/Black Market diversion with refresh. Local HTTP 51/55 retains three 503s and one timeout; full-family, wider-module and deployed acceptance remain open.', evidence: ['tests/nexus-richese-betrayal.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-controls.test.tsx', 'tests/bot-nexus-richese-betrayal.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'docs/NEXUS_RICHESE_RULES.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'New ordinary source has separate invoice, actual auction-continuation and connected controls/minimal-profile cases with a labelled controlled recycled-card fixture. Historical cache/Black Market full-suite, SQL and phone-browser evidence remains dated legacy scope; it does not prove normal-lot recovery or deployed acceptance. Full-family, broader combinations and final assurance remain open.', evidence: ['tests/nexus-richese-betrayal-normal.test.ts', 'tests/richese-normal-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-normal-controls.test.tsx', 'tests/fixture-richese-normal-betrayal.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'docs/NEXUS_RICHESE_RULES.md'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user