Bind Ixian Nexus replacement to original Richese seller lots
This commit is contained in:
1 parent
634ed293af
commit
32b139281c
18 files changed
+881
-118
No files matched your search
+12
-10
@@ -9946,14 +9946,15 @@ function ixianNexusReplacementParent(g: Game): string {
|
||||
players:g.players.map(p => ({id:p.id,faction:p.faction,ally:p.ally,spice:p.spice,hand:p.hand})),
|
||||
deck:g.deck,discard:g.discard,aid:g.aid,nexus:g.nexusCards,
|
||||
...(byFaction(g, 'richese') ? { richeseBidding:g.richeseBidding,
|
||||
richeseCache:g.richeseCache,richeseRemoved:g.richeseRemoved } : {}),
|
||||
richeseCache:g.richeseCache,richeseRemoved:g.richeseRemoved,
|
||||
...(g.richeseAuction ? { richeseAuction:g.richeseAuction } : {}) } : {}),
|
||||
decision:g.decision,response:g.response,phaseOpening:g.phaseOpening});
|
||||
}
|
||||
function ixianNexusReplacementContext(g: Game) {
|
||||
requireRule(g.auction, 'The Nexus purchased-card choice lost its normal auction.');
|
||||
requireRule(g.auction || g.richeseAuction, 'The Nexus purchased-card choice lost its original auction.');
|
||||
return {status:g.status,phase:g.phase,turn:g.turn,sequence:g.nexusIxianReplacementCursor!.sequence,
|
||||
expansions:g.expansions,richese:!!byFaction(g,'richese'),
|
||||
auction:g.auction,sale:g.currentAuctionSale ?? null,
|
||||
advanced:g.advanced,expansions:g.expansions,richese:!!byFaction(g,'richese'),
|
||||
auction:g.auction,richeseAuction:g.richeseAuction ?? null,sale:g.currentAuctionSale ?? null,
|
||||
players:g.players.map(p => ({id:p.id,hand:p.hand,handLimit:handLimit(p)})),
|
||||
physicalCards:physicalTreacheryCards(g)};
|
||||
}
|
||||
@@ -9980,7 +9981,8 @@ function ixianNexusReplacementIntegrity(g: Game): void {
|
||||
g.decision.event === pending.event &&
|
||||
Object.keys(g.decision).every(key => ['kind','player','event'].includes(key)) &&
|
||||
!g.response && !g.phaseOpening && !g.truthtrance && !g.pendingKarama && !g.pendingNullentropy &&
|
||||
!g.pendingTreacheryDiscard && !g.richeseAuction,
|
||||
!g.pendingTreacheryDiscard && (g.currentAuctionSale?.origin === 'normal'
|
||||
? !g.richeseAuction : !g.auction && !!g.richeseAuction),
|
||||
'The purchased-card choice lost its buyer or native decision priority.');
|
||||
nexusRule(() => validateIxianReplacementSource(ixianNexusReplacementContext(g),pending,
|
||||
ixianNexusReplacementParent(g)));
|
||||
@@ -9989,11 +9991,11 @@ function offerIxianNexusReplacement(g: Game): boolean {
|
||||
if (!g.nexusIxianReplacementPreview) return false;
|
||||
const sale = g.currentAuctionSale;
|
||||
const buyer = sale && getPlayer(g,sale.winner);
|
||||
if (!sale || !buyer || sale.origin !== 'normal' ||
|
||||
buyer.ally || !g.nexusCards?.cards?.hands[buyer.id]) return false;
|
||||
if (!sale || !buyer || buyer.ally || !g.nexusCards?.cards?.hands[buyer.id]) return false;
|
||||
requireRule(!g.response && !g.phaseOpening && !g.truthtrance && !g.pendingKarama,
|
||||
'Finish the original auction priority before offering the purchased-card choice.');
|
||||
const event = ixianReplacementEvent(g.turn,buyer.id,g.nexusIxianReplacementCursor!.sequence,g.auction!.index);
|
||||
if (sale.origin !== 'normal') currentAuctionContinuationQuote(g, { kind: 'sale', free: sale.free });
|
||||
const event = ixianReplacementEvent(g.turn,buyer.id,g.nexusIxianReplacementCursor!.sequence,g.auction?.index ?? 0);
|
||||
g.decision = {kind:'nexusIxianReplacement',player:buyer.id,event};
|
||||
g.active = buyer.id;
|
||||
g.pendingNexusIxianReplacement = nexusRule(() => createIxianReplacementSource(
|
||||
@@ -10004,7 +10006,7 @@ function ixianNexusReplacementView(g: Game,id: string) {
|
||||
const source = g.pendingNexusIxianReplacement;
|
||||
if (!source) return null;
|
||||
const own = source.buyer === id;
|
||||
return {event:source.event,buyer:source.buyer,canPass:own,
|
||||
return {event:source.event,buyer:source.buyer,origin:g.currentAuctionSale!.origin,canPass:own,
|
||||
canUse:own && g.nexusCards?.cards?.hands[id] === 'ixians',
|
||||
blocked:null as string | null,purchased:own ? structuredClone(source.card) : null};
|
||||
}
|
||||
@@ -12348,7 +12350,7 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
|
||||
if (lot.source === 'blackMarket' && ixians && winner.ally === ixians.id) {
|
||||
g.pendingIxAlly = { player: winner.id, card: card.id, free };
|
||||
g.decision = { kind: 'ixAllyCard', player: winner.id };
|
||||
} else continueAuctionSale(g, free);
|
||||
} else if (!offerIxianNexusReplacement(g)) continueAuctionSale(g, free);
|
||||
}
|
||||
function beginRicheseLot(
|
||||
g: Game,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { baseDeck, choamDeck, ixDeck, type Card } from './cards';
|
||||
import { richeseCards } from './richese-cards';
|
||||
import type { RicheseAuction } from './richese-auction';
|
||||
|
||||
export class IxianReplacementError extends Error {}
|
||||
|
||||
@@ -10,12 +11,15 @@ export type IxianReplacementContext = {
|
||||
sequence: number;
|
||||
expansions: readonly string[];
|
||||
richese: boolean;
|
||||
/** Native Black Market bidding only opens in Advanced play. */
|
||||
advanced: boolean;
|
||||
auction: {
|
||||
cards: readonly Card[];
|
||||
index: number;
|
||||
bidder: string | null;
|
||||
bid: number;
|
||||
} | null;
|
||||
richeseAuction: Pick<RicheseAuction, 'event' | 'cardId' | 'source' | 'owner' | 'outcome'> | null;
|
||||
sale: {
|
||||
winner: string;
|
||||
amount: number;
|
||||
@@ -128,7 +132,7 @@ function savedSource(value: unknown): value is IxianReplacementSource {
|
||||
text(value.parent) && text(value.signature);
|
||||
}
|
||||
function validateSavedSource(value: unknown): asserts value is IxianReplacementSource {
|
||||
requireReplacement(savedSource(value), 'The replacement lost its canonical normal purchase receipt.');
|
||||
requireReplacement(savedSource(value), 'The replacement lost its canonical purchase receipt.');
|
||||
requireReplacement(
|
||||
value.event === ixianReplacementEvent(value.turn, value.buyer, value.sequence, value.auctionIndex) &&
|
||||
value.signature === sourceSignature(value),
|
||||
@@ -143,7 +147,48 @@ function validatePhysical(physical: readonly Card[], ctx?: IxianReplacementConte
|
||||
);
|
||||
}
|
||||
|
||||
/** Authorize the original paid/Karama NORMAL sale, not an arbitrary held card.
|
||||
/** Select only a delivered card backed by the current native sale authority. */
|
||||
function purchasedCard(ctx: IxianReplacementContext): Card {
|
||||
const sale = ctx.sale;
|
||||
requireReplacement(sale, 'Replacement needs its original auction sale.');
|
||||
if (sale.origin === 'normal') {
|
||||
const auction = ctx.auction;
|
||||
requireReplacement(
|
||||
auction && ctx.richeseAuction === null && sale.seller === null &&
|
||||
auction.bidder === sale.winner && auction.bid === sale.amount &&
|
||||
whole(auction.index) && Array.isArray(auction.cards) &&
|
||||
selectedCard(auction.cards[auction.index], ctx),
|
||||
'Replacement must retain its exact original normal auction purchase.',
|
||||
);
|
||||
return auction.cards[auction.index];
|
||||
}
|
||||
const lot = ctx.richeseAuction;
|
||||
requireReplacement(
|
||||
ctx.auction === null && ctx.richese && ctx.expansions.includes('choam') &&
|
||||
lot && text(lot.event) && text(lot.cardId) &&
|
||||
(lot.source === 'cache' || (lot.source === 'blackMarket' && ctx.advanced)) &&
|
||||
lot.source === sale.origin &&
|
||||
text(lot.owner) && lot.owner === sale.seller &&
|
||||
Array.isArray(ctx.players) && ctx.players.some(player => plain(player) && player.id === lot.owner),
|
||||
'Replacement needs its exact native Richese lot and seated owner.',
|
||||
);
|
||||
const outcome = lot.outcome;
|
||||
requireReplacement(
|
||||
outcome !== null && outcome.kind === 'sold' &&
|
||||
whole(outcome.amount) && outcome.amount > 0 && sale.free === false &&
|
||||
outcome.winner === sale.winner && outcome.amount === sale.amount,
|
||||
'The native Richese lot must have been sold to the buyer at the recorded price.',
|
||||
);
|
||||
const buyer = ctx.players.find(player => plain(player) && player.id === sale.winner);
|
||||
requireReplacement(buyer && Array.isArray(buyer.hand),
|
||||
'Replacement needs the native lot in its original buyer hand.');
|
||||
const card = buyer.hand.find((card: Card) => card?.id === lot.cardId);
|
||||
requireReplacement(selectedCard(card, ctx),
|
||||
'Replacement needs the exact canonical delivered native Richese card.');
|
||||
return card;
|
||||
}
|
||||
|
||||
/** Authorize the original normal or native Richese purchase, not an arbitrary held card.
|
||||
* The engine supplies its current source-specific parent and live custody census.
|
||||
* Neither payment nor draw allocation is reconstructed here. */
|
||||
export function validateIxianReplacementSource(
|
||||
@@ -156,14 +201,11 @@ export function validateIxianReplacementSource(
|
||||
text(parent) && source.parent === parent,
|
||||
'The purchased card choice has lost its current parent or cursor.',
|
||||
);
|
||||
const auction = ctx.auction, sale = ctx.sale;
|
||||
const card = purchasedCard(ctx), sale = ctx.sale!;
|
||||
requireReplacement(
|
||||
auction && sale && sale.origin === 'normal' && sale.seller === null &&
|
||||
sale.winner === source.buyer && sale.amount === source.amount && sale.free === source.free &&
|
||||
auction.bidder === source.buyer && auction.bid === source.amount &&
|
||||
whole(auction.index) && auction.index === source.auctionIndex && Array.isArray(auction.cards) &&
|
||||
selectedCard(auction.cards[auction.index], ctx) && sameCard(auction.cards[auction.index], source.card),
|
||||
'Replacement must retain its exact original normal auction purchase.',
|
||||
sale.winner === source.buyer && sale.amount === source.amount && sale.free === source.free &&
|
||||
source.auctionIndex === (ctx.auction?.index ?? 0) && sameCard(card, source.card),
|
||||
'Replacement must retain its exact original auction purchase.',
|
||||
);
|
||||
validatePhysical(ctx.physicalCards, ctx);
|
||||
requireReplacement(
|
||||
@@ -191,16 +233,11 @@ export function validateIxianReplacementSource(
|
||||
export function createIxianReplacementSource(
|
||||
ctx: IxianReplacementContext, event: string, parent: string,
|
||||
): IxianReplacementSource {
|
||||
const auction = ctx.auction, sale = ctx.sale;
|
||||
requireReplacement(
|
||||
auction && sale && Array.isArray(auction.cards) && whole(auction.index) &&
|
||||
canonicalCard(auction.cards[auction.index]),
|
||||
'Replacement needs an actual canonical purchased auction card.',
|
||||
);
|
||||
const card = purchasedCard(ctx), sale = ctx.sale!;
|
||||
const source: IxianReplacementSource = {
|
||||
version: 1, event, buyer: sale.winner, turn: ctx.turn, sequence: ctx.sequence,
|
||||
auctionIndex: auction.index, amount: sale.amount, free: sale.free,
|
||||
card: { ...auction.cards[auction.index] }, parent, signature: '',
|
||||
auctionIndex: ctx.auction?.index ?? 0, amount: sale.amount, free: sale.free,
|
||||
card: { ...card }, parent, signature: '',
|
||||
};
|
||||
source.signature = sourceSignature(source);
|
||||
validateIxianReplacementSource(ctx, source, parent);
|
||||
|
||||
+10
-9
@@ -4510,13 +4510,14 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Verified',
|
||||
summary: 'In the fresh local ixian-replacement preview, an unallied holder with Ixians absent may spend the physical Ixian Nexus to discard exactly the card just bought in a normal auction and draw the next Treachery deck card privately.',
|
||||
searchText: 'Ixian Nexus Secret Ally purchased card replacement normal auction Karama full hand empty deck same card',
|
||||
summary: 'An unallied holder with Ixians absent may spend Ixian Nexus to replace the exact card just bought in a normal auction or supported native Richese sale, preserving original settlement and drawing the actual deck top privately.',
|
||||
searchText: 'Ixian Nexus Secret Ally purchased card replacement normal auction cache Black Market Karama full hand empty deck same card',
|
||||
steps: [
|
||||
'This fresh local profile admits two-to-six unique seats in Basic/Advanced: classics with base33, native Tleilaxu plus classics with Ix47, or native Richese plus classics with CHOAM35 and its separate ten-card cache, always with Ixians absent and Nexus alone. Only normal-auction purchases qualify; original cache setup/auctions remain distinct from a cache-origin replacement. Optional Sandtrout is omitted only for fresh native Ix setup. Other natives, Leader Skills, Homeworlds, Semuta, Discoveries, Ecaz variants and overlays remain guarded. No public start or played-save upgrade.',
|
||||
'The fresh local profile admits two-to-six unique seats in Basic/Advanced: classics/base33, Tleilaxu plus classics/Ix47, or Richese plus classics/CHOAM35 and original cache, with Ixians absent and Nexus alone. Normal purchases qualify, as do genuine positive Richese cache sales in Basic/Advanced and Black Market sales in Advanced. Sandtrout is omitted only for fresh native Ix setup. Other natives/modules and public starts or played-save conversion remain excluded.',
|
||||
'Nexus has no starting deal. A draw requires a qualifying closing Nexus with an alliance. A two-seat profile can continue its base game but cannot naturally create an unallied Secret Ally holder; actual replacement needs at least three seats, or four for two simultaneous unallied recipients.',
|
||||
'The photographed publisher face grants an independent Secret Ally operation when Ixians are absent. Remain unallied and hold the actual singleton Ixian Nexus. Native Ixian allied replacement is a different cancelable advantage and is not removed or used as a fallback. The adopted unofficial Advanced rulebook supplies relevant core rules, not this absent Nexus face or a new Nexus ruling.',
|
||||
'Wait for a genuine normal-auction purchase, paid with spice or through Karama, including direct acquisition before a positive bid. A successful native Advanced BG Worthless-as-Karama conversion gets the same independent replacement choice after its native counter resolves. Canceling direct acquisition supplies no purchase; a canceled winning-payment conversion follows native funded spice recovery and, if bought, supplies a paid receipt instead. Cache, Black Market and special-purchase origins retain their separate guarded Nexus scope. Gifts, bonus draws, Technology swaps and old hand cards are not purchases.',
|
||||
'A normal purchase may be paid with spice or Karama, including successful Advanced BG conversion after its native counter. A denied direct conversion supplies no purchase; funded winning-payment recovery instead supplies a paid receipt. Actual Richese native sales are already paid and delivered before the same owned choice; original recipient/income and native lot continuation follow once. Unbid owner keeps/retains, gifts, bonus draws, Technology swaps and old hand cards supply no purchase. Direct Richese special-Karama sources remain separate.',
|
||||
'Provisional cache scope: the independent absent-Ixian Nexus grant applies to a genuine cache purchase under the selected literal reading. It is not the native Ixian alliance advantage: that advantage’s printed cache exclusion is unchanged. The table explains this policy before Use; it is product interpretation, not a newly located publisher FAQ or an explicit user-selected rule.',
|
||||
'Every publicly unallied supported buyer with public held-Nexus presence receives the same neutral Purchased card choice, irrespective of the secret face. An irrelevant-face buyer may Pass but cannot Use. This is an inferred development privacy convention, not publisher-prescribed timing or a general resolution of pending anytime and response-priority questions.',
|
||||
'Only the buyer may inspect its already-owned purchased card in this choice. You may inspect your own actually held Nexus through the existing authorized inspector; the prompt does not identify anybody else’s Nexus. Rivals receive no purchased or replacement face, private price, receipt, deck order or eligibility reason. Earlier Atreides auction inspection does not grant a peek of the replacement.',
|
||||
'Pass keeps your purchased card and Nexus and closes this purchase opportunity. Use spends your physical Ixian Nexus once and discards exactly that just-purchased Treachery Card before drawing the actual next deck card. You cannot select another held card, replacement face, price or recipient; both actions bind only the offered event.',
|
||||
@@ -4529,11 +4530,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
example: 'You buy a card for four spice and inspect it privately. Pass keeps it and the Nexus. Use keeps the original four-spice purchase settled, spends Ixian Nexus, discards that exact card and draws the next deck card. If the draw deck is empty, its new discard joins the actual recycled pile and can be drawn again.',
|
||||
related: ['nexus-cards', 'bidding', 'ix-technology', 'card-karama', 'alliance-funding', 'privacy', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Normal purchase, physical replacement and closed history bind original settlement. Native Richese adds selected CHOAM35/cache custody without enabling special-origin sales; unselected card families still reject. BG conversion/paid recovery and Harkonnen replacement-before-bonus remain source-specific. Other modules remain guarded.', evidence: ['game/nexus-ixian-replacement.ts', 'game/engine.ts', 'tools/prototype-room.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Neutral Purchased card choice separates buyer-only inspection and Use/Keep from waiting. Original390px Richese-family normal purchases exercise Basic Atreides Use and Advanced Richese Keep, preserving original income/cache and next lot. Two tool-only stale-node races were recovered without replay; no application error or overflow. Harkonnen retains its visible provisional ordering. Broader acceptance remains open.', evidence: ['components/nexus-ixian-replacement.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Four minimal legal policies use only their own projected event while native settlement waits. Sixteen original-state actions cover native Richese normal purchases across both modes and buyer factions; earlier BG/Harkonnen/Tleilaxu paths remain source-qualified in the contract. Full strategy/calibration remain unfinished.', evidence: ['game/bots.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Printed replacement, original settlement, selected catalogue/cache membership, full-hand/same-card semantics and provisional Harkonnen order are documented. Native Richese normal purchases are distinct from still-gated cache/Black Market/special origins, competing effects and full-family acceptance; no publisher priority is invented.', evidence: ['docs/NEXUS_IXIAN_REPLACEMENT_RULES.md', 'docs/NEXUS_CARD_RULES.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Earlier source-qualified evidence remains in the contract. Native Richese adds six selected-catalogue and twelve original behavior cases in affected65/65, eight original runtime paths, sixteen legal policy actions and two390px normal-purchase journeys. Original final-lot/cache continuation and the still-gated real cache purchase are exercised. Source repairs follow actual automatic response advancement; typing annotations do not alter behavior. No new build, saved/deployed or broad assurance; all families/modules remain Partial.', evidence: ['tests/nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-engine.test.ts', 'tests/fixtures/nexus-ixian-native-depleted.json', 'tests/fixture-nexus-ixian-replacement.ts', 'tests/fixture-native-ixian-replacement.ts', 'tests/native-ixian-replacement.test.ts', 'tests/bg-ixian-replacement.test.ts', 'tests/fixture-bg-ixian-replacement.ts', 'tests/harkonnen-ixian-replacement.test.ts', 'tests/fixture-harkonnen-ixian-replacement.ts', 'tests/richese-ixian-replacement.test.ts', 'tests/fixture-richese-ixian-replacement.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Original normal and native Richese sold-lot receipts bind delivered card, price and native continuation. Cache works in both modes under a labelled provisional independent grant; Black Market stays Advanced-only. Unbid retention is not a purchase. Single native lots use index0 without a fabricated normal row; v1 receipt shape and normal parents remain unchanged. Direct special sources and other modules stay separate.', evidence: ['game/nexus-ixian-replacement.ts', 'game/engine.ts', 'tools/prototype-room.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Buyer-only purchased-card inspection and Use/Keep reuse the neutral protocol. Actual390px cache Use shows the provisional scope, retires to Revival and preserves one seller credit; Advanced Black Market Keep resumes original Richese first/last controls. Origin is public category only. Harkonnen keeps its separate visible provisional order. No overflow or browser/application error; broader acceptance remains open.', evidence: ['components/nexus-ixian-replacement.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Four minimal legal policies use their own projected offer while original settlement waits. Twelve original-state actions cover cache Basic/Advanced and Advanced Black Market; earlier source-qualified paths remain in the contract. No full strategy or calibration is claimed.', evidence: ['game/bots.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Real sold-lot authority, v1 source preservation, original recipient/continuation, selected catalogue, same-card/full-hand semantics and explicit provisional cache/Harkonnen interpretations are documented. The native ally cache exclusion is unchanged; direct special sources, other modules and publisher priority remain separate.', evidence: ['docs/NEXUS_IXIAN_REPLACEMENT_RULES.md', 'docs/NEXUS_CARD_RULES.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Affected nine-file98/98, types/lint/build, six original runtime paths, twelve legal policy actions and two390px native-origin journeys pass. Eleven actual native scenario cases cover already-paid timing, Use/Keep and unbid retention; source tests reject missing/mismatched/competing/open/unbid lots and Basic Black Market. The fixture follows actual response priority. An obsolete DTO shape pin was deleted without losing semantic rival privacy assertions. No new broad, HTTP, saved/deployed assurance; all five facets remain Partial.', evidence: ['tests/nexus-ixian-replacement.test.ts', 'tests/nexus-ixian-replacement-engine.test.ts', 'tests/fixtures/nexus-ixian-native-depleted.json', 'tests/fixture-nexus-ixian-replacement.ts', 'tests/fixture-native-ixian-replacement.ts', 'tests/native-ixian-replacement.test.ts', 'tests/bg-ixian-replacement.test.ts', 'tests/fixture-bg-ixian-replacement.ts', 'tests/harkonnen-ixian-replacement.test.ts', 'tests/fixture-harkonnen-ixian-replacement.ts', 'tests/richese-ixian-replacement.test.ts', 'tests/fixture-richese-ixian-replacement.ts', 'tests/richese-ixian-special-replacement.test.ts', 'tests/fixture-richese-ixian-special-replacement.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user