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
@@ -79,7 +79,7 @@ export function nextRicheseIxianReplacementNativeStep(
|
||||
keep: owner.hand.length < handLimit(owner) } };
|
||||
}
|
||||
const lot = game.richeseAuction;
|
||||
if (!decision && lot && !lot.outcome) {
|
||||
if (!decision && !game.response && lot && !lot.outcome) {
|
||||
const actor = lot.method === 'silent'
|
||||
? lot.order.find(id => lot.eligible.includes(id) && !Object.hasOwn(lot.sealed, id))
|
||||
: lot.active;
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, handLimit, type Action, type Game } from '../game/engine';
|
||||
import type { Card } from '../game/cards';
|
||||
import type { RicheseAuction } from '../game/richese-auction';
|
||||
import { settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement';
|
||||
import {
|
||||
createRicheseIxianReplacementFixture, nextRicheseIxianReplacementNativeStep,
|
||||
type RicheseIxianReplacementStep,
|
||||
} from './fixture-richese-ixian-replacement';
|
||||
|
||||
/** The two native Richese sources a genuine purchased-card replacement may bind:
|
||||
* the compulsory cache lot and the owner's own Black Market lot. */
|
||||
export type RicheseIxianSpecialReplacementOrigin = 'cache' | 'blackMarket';
|
||||
/** A real positive Once Around sale or the genuine zero-bid outcome. */
|
||||
export type RicheseIxianSpecialReplacementMode = 'sold' | 'unbid';
|
||||
/** The engine's own native lot facts, as bound by the live game. */
|
||||
export type RicheseIxianSpecialReplacementLot =
|
||||
Pick<RicheseAuction, 'event' | 'cardId' | 'source' | 'owner' | 'outcome'>;
|
||||
|
||||
export type RicheseIxianSpecialReplacementOptions = {
|
||||
/** Continue only a fresh authenticated undealt Richese/CHOAM lobby or its admitted setup. */
|
||||
initial?: Game;
|
||||
/** Native profile mode. The Black Market offer exists only in Advanced. */
|
||||
advanced?: boolean;
|
||||
/** Native source of the genuine purchase. Defaults to the compulsory cache lot. */
|
||||
origin?: RicheseIxianSpecialReplacementOrigin;
|
||||
/** Real positive Once Around sale (default) or the genuine zero-bid outcome. */
|
||||
mode?: RicheseIxianSpecialReplacementMode;
|
||||
/** Shared CHOAM addition bought as the ordinary normal lot before the source lot. */
|
||||
purchasedId?: string;
|
||||
/** Original shared CHOAM addition left as the actual top card after that normal purchase. */
|
||||
replacementId?: string;
|
||||
/** Physical cache card the genuine turn-one cache lot keeps; supplies a Black Market offer. */
|
||||
cacheCardId?: string;
|
||||
/** Cache origin: physical cache card the genuine compulsory cache lot offers. */
|
||||
offerCardId?: string;
|
||||
/** Positive winning bid for the genuine Once Around sale. Default 1. */
|
||||
bid?: number;
|
||||
};
|
||||
|
||||
type RicheseIxianSpecialReplacementCommon = {
|
||||
/** Admitted fresh setup before its first turn; only undealt decks were ordered. */
|
||||
setup: Game;
|
||||
/** Genuine unallied Ixian Nexus holder about to close the Nexus; only undealt decks ordered. */
|
||||
beforeNexusDraw: Game;
|
||||
nexusDrawAction: Action;
|
||||
/** The exact physical card this fixture offered in the real native lot. */
|
||||
offered: Card;
|
||||
/** The real native lot's own event; never a fabricated auction row. */
|
||||
lotEvent: string;
|
||||
lotMethod: 'onceAround';
|
||||
/** Live native lot at the returned state, or null once the round retired it. */
|
||||
richeseAuction: RicheseIxianSpecialReplacementLot | null;
|
||||
buyer: string;
|
||||
/** The Richese round owner whose own card or cache card was offered. */
|
||||
seller: string;
|
||||
emperor: string | null;
|
||||
origin: RicheseIxianSpecialReplacementOrigin;
|
||||
advanced: boolean;
|
||||
/** The original physical cache at this point; a purchase may only shrink it by its own lot. */
|
||||
cacheBefore: Card[];
|
||||
/** Cards a genuine turn-one cache lot kept instead of buying. */
|
||||
keptCacheCards: string[];
|
||||
/** Black Market origin: the genuine pre-decline offer decision this fixture replaced. */
|
||||
blackMarketOffer: { actor: string; event: string; card: Card } | null;
|
||||
/** Where the original native suffix resumes after the buyer's choice. */
|
||||
suffix: 'phase' | 'declaration';
|
||||
};
|
||||
|
||||
export type RicheseIxianSpecialReplacementSaleFixture = RicheseIxianSpecialReplacementCommon & {
|
||||
mode: 'sold';
|
||||
/** State with the pending purchased-card offer, after payment and delivery only. */
|
||||
game: Game;
|
||||
/** Public purchased-card event opened for this genuinely paid native sale. */
|
||||
event: string;
|
||||
/** The exact delivered card the offer binds; never a top deck or unsold lot card. */
|
||||
purchased: Card;
|
||||
/** Actual top of the Treachery Deck at the offer: the only legal replacement draw. */
|
||||
nextCard: Card;
|
||||
bid: number;
|
||||
/** Real native state immediately before the action that paid and delivered the lot. */
|
||||
beforePurchase: Game;
|
||||
/** The real native action that closed the lot, and by whom it was taken. */
|
||||
purchaseActor: string;
|
||||
purchaseAction: Action;
|
||||
/** Buyer hand before paying for the source lot; the delivered card is excluded. */
|
||||
buyerHandBefore: Card[];
|
||||
beforeSpice: number;
|
||||
/** Buyer spice at the offer: the native payment is already debited exactly once. */
|
||||
paidSpice: number;
|
||||
/** Seller spice at the offer: the native credit has not happened yet. */
|
||||
sellerSpiceAtOffer: number;
|
||||
/** Emperor spice at the offer; an unallied native sale owes no Emperor gift. */
|
||||
emperorSpiceAtOffer: number | null;
|
||||
};
|
||||
|
||||
export type RicheseIxianSpecialReplacementUnbidFixture = RicheseIxianSpecialReplacementCommon & {
|
||||
mode: 'unbid';
|
||||
/** State where the genuine zero-bid outcome retired the lot; no sale was ever paid. */
|
||||
game: Game;
|
||||
/** Real native state immediately before the action that retired the lot. */
|
||||
beforeFinalAction: Game;
|
||||
finalActor: string;
|
||||
finalAction: Action;
|
||||
/** Cache origin: the keeper's original unbid choice, applied only when room existed. */
|
||||
kept: boolean | null;
|
||||
/** The physical card no purchase ever bound. */
|
||||
card: Card;
|
||||
};
|
||||
|
||||
export type RicheseIxianSpecialReplacementFixture =
|
||||
| RicheseIxianSpecialReplacementSaleFixture
|
||||
| RicheseIxianSpecialReplacementUnbidFixture;
|
||||
|
||||
function player(game: Game, id: string) {
|
||||
const seat = game.players.find(candidate => candidate.id === id);
|
||||
assert.ok(seat, `The original seat ${id} must remain at the table.`);
|
||||
return seat;
|
||||
}
|
||||
|
||||
function liveLot(game: Game): RicheseIxianSpecialReplacementLot | null {
|
||||
const lot = game.richeseAuction;
|
||||
return lot ? {
|
||||
event: lot.event, cardId: lot.cardId, source: lot.source,
|
||||
owner: lot.owner, outcome: structuredClone(lot.outcome),
|
||||
} : null;
|
||||
}
|
||||
|
||||
/** Exported native seam: drive the original undealt Richese/CHOAM turn with the
|
||||
* genuine native policy, so a throwaway API/browser smoke can reach the source. */
|
||||
export function nextRicheseIxianSpecialReplacementNativeStep(
|
||||
game: Game,
|
||||
cacheCardId?: string,
|
||||
): RicheseIxianReplacementStep {
|
||||
return nextRicheseIxianReplacementNativeStep(game, cacheCardId);
|
||||
}
|
||||
|
||||
function nativeStep(game: Game, cacheCardId?: string): Game {
|
||||
const next = nextRicheseIxianSpecialReplacementNativeStep(game, cacheCardId);
|
||||
return applyAction(game, next.actor, next.action);
|
||||
}
|
||||
|
||||
/** Drive only the original native suffix after the buyer's own choice: the cache
|
||||
* lot ends its round into the phase boundary, the Black Market sale resumes the
|
||||
* original declaration. Never bids, pays, draws or invents a new lot. */
|
||||
export function settleRicheseIxianSpecialReplacementSuffix(
|
||||
state: Game,
|
||||
origin: RicheseIxianSpecialReplacementOrigin,
|
||||
cacheCardId?: string,
|
||||
): Game {
|
||||
let game = state;
|
||||
for (let count = 0; count < 50; count++) {
|
||||
if (origin === 'cache'
|
||||
? game.richeseBidding?.stage === 'complete'
|
||||
: game.decision?.kind === 'richeseDeclaration') return game;
|
||||
game = nativeStep(game, cacheCardId);
|
||||
}
|
||||
throw new Error('The original native Richese suffix did not settle.');
|
||||
}
|
||||
|
||||
/** A genuine special-origin purchase for the Ixian Nexus purchased-card choice.
|
||||
*
|
||||
* Provenance, all through the original private initializer and original actions:
|
||||
* the profile is the fresh native Richese + Atreides + Emperor/CHOAM prototype
|
||||
* with the real closing Nexus. The unallied Atreides holder buys a real normal
|
||||
* CHOAM lot at the last scheduled lot and keeps it with its Ixian Nexus card
|
||||
* (cache origin), or the fixture replays the genuine Nexus turn from its own
|
||||
* undealt snapshot (Black Market origin).
|
||||
*
|
||||
* Cache origin then lets the compulsory cache lot run as a real Once Around for
|
||||
* a card the ten-card cache still holds. Black Market origin stops at Richese's
|
||||
* own pre-decline window and offers the one card a genuine turn-one cache lot
|
||||
* actually kept in the owner's hand. The buyer's positive bid is a real funded
|
||||
* native bid. All bid, wallet, clock and card changes come from native actions;
|
||||
* no played state is assigned.
|
||||
*
|
||||
* The returned `game` already contains the paid sale (buyer debited, card
|
||||
* delivered) and the pending purchased-card offer; the original seller credit,
|
||||
* lot retirement and native suffix still belong to the original continuation,
|
||||
* so Use/Keep can never re-debit or re-credit the purchase. */
|
||||
export function createRicheseIxianSpecialReplacementFixture(
|
||||
options: RicheseIxianSpecialReplacementOptions = {},
|
||||
): RicheseIxianSpecialReplacementFixture {
|
||||
const origin = options.origin ?? 'cache';
|
||||
const mode = options.mode ?? 'sold';
|
||||
const advanced = options.advanced ?? origin === 'blackMarket';
|
||||
if (origin === 'blackMarket')
|
||||
assert.ok(advanced,
|
||||
'The native Black Market offer exists only in the Advanced profile; pass advanced: true or leave it unset.');
|
||||
const bid = options.bid ?? 1;
|
||||
assert.ok(Number.isSafeInteger(bid) && bid > 0,
|
||||
'A genuine Once Around purchase needs a positive integer bid.');
|
||||
const cacheCardId = options.cacheCardId;
|
||||
const base = createRicheseIxianReplacementFixture({
|
||||
initial: options.initial, advanced, endingAuction: true,
|
||||
purchasedId: options.purchasedId, replacementId: options.replacementId, cacheCardId,
|
||||
});
|
||||
assert.equal(base.advanced, advanced);
|
||||
const seller = base.setup.players.find(candidate => candidate.faction === 'richese')!.id;
|
||||
const buyer = base.buyer;
|
||||
assert.notEqual(buyer, seller,
|
||||
'The genuine source purchase needs a buyer other than its own Richese seller.');
|
||||
const emperor = base.setup.players.find(candidate => candidate.faction === 'emperor')?.id ?? null;
|
||||
assert.equal(player(base.game, buyer).ally, null,
|
||||
'The source purchase needs the unallied Ixian Nexus holder as its buyer.');
|
||||
assert.equal(base.game.nexusCards!.cards!.hands[buyer], 'ixians',
|
||||
'The source purchase needs its buyer still holding the genuine Ixian Nexus card.');
|
||||
assert.ok(!base.setup.players.some(candidate => candidate.faction === 'ixians'),
|
||||
'The native Richese/CHOAM profile must have native Ixians absent.');
|
||||
const keptCacheCards = [...base.keptCacheCards];
|
||||
const cacheBefore = structuredClone(base.cacheAtPurchase);
|
||||
let game: Game;
|
||||
let offered: Card;
|
||||
let blackMarketOffer: RicheseIxianSpecialReplacementCommon['blackMarketOffer'] = null;
|
||||
|
||||
if (origin === 'cache') {
|
||||
// Keep the real normal purchase and the unallied Ixian Nexus card, then let
|
||||
// Richese's own compulsory cache offer stand on the original physical cache.
|
||||
game = settleIxianReplacementFixture(applyAction(structuredClone(base.game), buyer,
|
||||
{ type: 'nexusIxianReplacementPass', event: base.event }));
|
||||
assert.equal(game.pendingNexusIxianReplacement, null,
|
||||
'The genuine normal purchase must close before the compulsory cache lot.');
|
||||
assert.deepEqual(game.decision, { kind: 'richeseCache', player: seller });
|
||||
assert.equal(game.richeseBidding!.stage, 'cacheOffer');
|
||||
const cache = game.richeseCache;
|
||||
assert.ok(cache, 'The cache lot needs the original physical cache.');
|
||||
assert.deepEqual(cache, cacheBefore,
|
||||
'The genuine normal purchase must not touch the compulsory cache.');
|
||||
const chosen = options.offerCardId ?? cache.find(card => !keptCacheCards.includes(card.id))!.id;
|
||||
const cached = cache.find(card => card.id === chosen);
|
||||
assert.ok(cached, 'The offered cache card must still be in the physical cache.');
|
||||
assert.ok(!keptCacheCards.includes(cached.id),
|
||||
'The cache lot must not re-offer a card an earlier genuine lot already kept.');
|
||||
offered = structuredClone(cached);
|
||||
game = applyAction(game, seller, { type: 'decision', event: game.richeseBidding!.event,
|
||||
card: offered.id, method: 'onceAround', direction: 'clockwise' });
|
||||
} else {
|
||||
game = structuredClone(base.beforeNexusDraw);
|
||||
game = applyAction(game, buyer, base.nexusDrawAction);
|
||||
assert.equal(game.nexusCards!.cards!.hands[buyer], 'ixians');
|
||||
for (const id of game.nexusCards!.phase!.eligible)
|
||||
if (!game.nexusCards!.phase!.done.includes(id))
|
||||
game = applyAction(game, id, { type: 'nexusCardChoice', turn: game.turn,
|
||||
card: null, choice: 'keep', ownRedraws: 0 });
|
||||
for (let count = 0; game.decision?.kind !== 'richeseBlackMarket' && count < 600; count++)
|
||||
game = nativeStep(game, cacheCardId);
|
||||
const decision = game.decision;
|
||||
assert.equal(decision?.kind, 'richeseBlackMarket',
|
||||
'The genuine Advanced round must still offer its own Black Market window.');
|
||||
assert.equal(game.richeseBidding!.stage, 'blackMarketOffer');
|
||||
assert.equal(game.richeseAuction, null);
|
||||
const held = player(game, seller).hand.filter(card => keptCacheCards.includes(card.id));
|
||||
assert.equal(held.length, 1,
|
||||
'The genuine turn-one cache Keep must supply exactly one held Richese card.');
|
||||
const candidate = options.offerCardId
|
||||
? held.find(card => card.id === options.offerCardId)
|
||||
: held[0];
|
||||
assert.ok(candidate, 'The Black Market offer needs a genuinely held Richese card.');
|
||||
offered = structuredClone(candidate);
|
||||
blackMarketOffer = { actor: decision.player, event: game.richeseBidding!.event,
|
||||
card: structuredClone(offered) };
|
||||
assert.equal(game.richeseOfferedCard, null);
|
||||
game = applyAction(game, seller, { type: 'decision', event: game.richeseBidding!.event,
|
||||
card: offered.id, method: 'onceAround', direction: 'clockwise' });
|
||||
assert.equal(game.richeseAuction?.source, 'blackMarket');
|
||||
assert.equal(game.richeseAuction!.cardId, offered.id);
|
||||
assert.equal(game.richeseOfferedCard?.id, offered.id);
|
||||
}
|
||||
|
||||
const open = game.richeseAuction;
|
||||
assert.ok(open, 'The genuine native Once Around lot must be open.');
|
||||
assert.equal(open.source, origin);
|
||||
assert.equal(open.method, 'onceAround');
|
||||
assert.equal(open.cardId, offered.id);
|
||||
assert.equal(open.owner, seller);
|
||||
assert.equal(open.outcome, null);
|
||||
const lotEvent = open.event;
|
||||
let beforeFinalAction = structuredClone(game);
|
||||
let finalActor = '';
|
||||
let finalAction: Action = { type: 'richeseBid', event: lotEvent, amount: null };
|
||||
for (let count = 0; count < 200; count++) {
|
||||
if (mode === 'sold' && game.pendingNexusIxianReplacement) break;
|
||||
if (game.response || game.phaseOpening || game.pendingTreacheryDiscard) {
|
||||
const next = nextRicheseIxianSpecialReplacementNativeStep(game, cacheCardId);
|
||||
beforeFinalAction = structuredClone(game);
|
||||
finalActor = next.actor;
|
||||
finalAction = next.action;
|
||||
game = applyAction(game, next.actor, next.action);
|
||||
continue;
|
||||
}
|
||||
const lot = game.richeseAuction;
|
||||
if (!lot || lot.outcome) break;
|
||||
assert.equal(lot.event, lotEvent, 'The genuine lot must keep its own event.');
|
||||
const actor = lot.active;
|
||||
assert.ok(actor, 'A genuine Once Around lot always names its next bidder.');
|
||||
const action: Action = mode === 'sold' && actor === buyer && lot.bid === 0
|
||||
? { type: 'richeseBid', event: lotEvent, amount: bid }
|
||||
: { type: 'richeseBid', event: lotEvent, amount: null };
|
||||
beforeFinalAction = structuredClone(game);
|
||||
finalActor = actor;
|
||||
finalAction = action;
|
||||
game = applyAction(game, actor, action);
|
||||
}
|
||||
|
||||
const common = {
|
||||
setup: base.setup, beforeNexusDraw: base.beforeNexusDraw,
|
||||
nexusDrawAction: base.nexusDrawAction, offered, lotEvent, lotMethod: 'onceAround' as const,
|
||||
richeseAuction: liveLot(game), buyer, seller, emperor, origin, advanced,
|
||||
cacheBefore, keptCacheCards, blackMarketOffer,
|
||||
suffix: (origin === 'cache' ? 'phase' : 'declaration') as 'phase' | 'declaration',
|
||||
};
|
||||
|
||||
if (mode === 'sold') {
|
||||
const closed = game.richeseAuction;
|
||||
assert.ok(closed, 'The paid native lot must remain the offer’s own source.');
|
||||
assert.deepEqual(closed.outcome, { kind: 'sold', winner: buyer, amount: bid });
|
||||
const source = game.pendingNexusIxianReplacement;
|
||||
assert.ok(source, 'The genuinely paid native sale must open the purchased-card offer.');
|
||||
assert.equal(source.version, 1);
|
||||
assert.equal(source.buyer, buyer);
|
||||
assert.equal(source.turn, game.turn);
|
||||
assert.equal(source.auctionIndex, 0,
|
||||
'A native Richese lot binds lot index 0, never a fabricated Game or normal-auction row.');
|
||||
assert.equal(source.amount, bid);
|
||||
assert.equal(source.free, false);
|
||||
assert.deepEqual(source.card, offered);
|
||||
assert.deepEqual(game.decision, { kind: 'nexusIxianReplacement', player: buyer,
|
||||
event: source.event });
|
||||
assert.equal(game.auction, null);
|
||||
assert.equal(game.currentAuctionSale?.origin, origin);
|
||||
assert.equal(game.players.flatMap(seat => seat.hand)
|
||||
.filter(card => card.id === offered.id).length, 1,
|
||||
'The paid native card must be delivered exactly once.');
|
||||
if (origin === 'cache') {
|
||||
assert.equal(game.richeseCache!.some(card => card.id === offered.id), false);
|
||||
assert.deepEqual(game.richeseCache,
|
||||
cacheBefore.filter(card => card.id !== offered.id));
|
||||
} else {
|
||||
assert.deepEqual(game.richeseCache, cacheBefore,
|
||||
'A Black Market sale must never touch the compulsory cache.');
|
||||
assert.equal(player(game, seller).hand.some(card => card.id === offered.id), false,
|
||||
'The concealed Black Market card must leave the seller’s own hand.');
|
||||
assert.equal(player(beforeFinalAction, seller).hand.some(card => card.id === offered.id),
|
||||
true, 'The Black Market offer must come from a genuinely held card.');
|
||||
}
|
||||
assert.ok(game.deck.length > 0);
|
||||
const nextCard = structuredClone(game.deck[0]);
|
||||
assert.notEqual(nextCard.id, offered.id);
|
||||
return {
|
||||
...common, mode: 'sold', game, event: source.event,
|
||||
purchased: structuredClone(offered), nextCard, bid,
|
||||
beforePurchase: beforeFinalAction, purchaseActor: finalActor, purchaseAction: finalAction,
|
||||
buyerHandBefore: structuredClone(player(beforeFinalAction, buyer).hand),
|
||||
beforeSpice: player(beforeFinalAction, buyer).spice,
|
||||
paidSpice: player(game, buyer).spice,
|
||||
sellerSpiceAtOffer: player(game, seller).spice,
|
||||
emperorSpiceAtOffer: emperor ? player(game, emperor).spice : null,
|
||||
};
|
||||
}
|
||||
|
||||
assert.equal(finalAction.type, 'richeseBid');
|
||||
assert.equal(finalAction.amount, null,
|
||||
'A genuine unbid outcome can only come from real zero offers.');
|
||||
assert.equal(game.pendingNexusIxianReplacement, null,
|
||||
'A lot that was never sold is not a purchase and opens no purchased-card offer.');
|
||||
assert.equal(game.currentAuctionSale, null);
|
||||
if (origin === 'cache') {
|
||||
const lot = game.richeseAuction;
|
||||
assert.ok(lot, 'The unbid cache lot waits for its owner’s own choice.');
|
||||
assert.deepEqual(lot.outcome, { kind: 'unbid' });
|
||||
assert.deepEqual(game.decision, { kind: 'richeseUnbid', player: seller });
|
||||
const sellerSeat = player(game, seller);
|
||||
const kept = sellerSeat.hand.length < handLimit(sellerSeat);
|
||||
game = applyAction(game, seller, { type: 'decision', event: game.richeseBidding!.event,
|
||||
keep: kept });
|
||||
assert.equal(game.pendingNexusIxianReplacement, null);
|
||||
assert.equal(player(game, seller).hand.some(card => card.id === offered.id), kept);
|
||||
assert.equal((game.richeseRemoved ?? []).some(card => card.id === offered.id), !kept);
|
||||
return {
|
||||
...common, richeseAuction: liveLot(game), mode: 'unbid', game,
|
||||
beforeFinalAction, finalActor, finalAction, kept, card: structuredClone(offered),
|
||||
};
|
||||
}
|
||||
assert.equal(game.richeseAuction, null, 'The retained Black Market lot must retire itself.');
|
||||
assert.deepEqual(game.decision, { kind: 'richeseDeclaration', player: seller });
|
||||
assert.equal(game.richeseBidding!.blackMarketSold, false);
|
||||
assert.equal(player(game, seller).hand.some(card => card.id === offered.id), true,
|
||||
'The unbid Black Market card must stay with its own seller.');
|
||||
assert.equal(game.richeseOfferedCard, null);
|
||||
return {
|
||||
...common, richeseAuction: null, mode: 'unbid', game,
|
||||
beforeFinalAction, finalActor, finalAction, kept: null, card: structuredClone(offered),
|
||||
};
|
||||
}
|
||||
@@ -94,6 +94,7 @@ void test('native descriptor admission rejects forged Ix effects and unrelated R
|
||||
const ctx: IxianReplacementContext = {
|
||||
status: f.game.status, phase: f.game.phase, turn: f.game.turn, sequence: source.sequence,
|
||||
expansions: f.game.expansions, richese: false,
|
||||
advanced: f.game.advanced, richeseAuction: null,
|
||||
auction: { ...f.game.auction!, cards: f.game.auction!.cards.map((card, index) =>
|
||||
index === f.auctionIndex ? corrupt : card) },
|
||||
sale: f.game.currentAuctionSale!,
|
||||
|
||||
@@ -173,13 +173,12 @@ void test('public neutral offer is independent of secret held face and rivals se
|
||||
const irrelevant = createIxianNexusReplacementFixture({face:'richese'});
|
||||
const rival = ix.game.players.find(p => p.id !== ix.buyer)!.id;
|
||||
assert.deepEqual(viewGame(ix.game,rival).nexusIxianReplacement,viewGame(irrelevant.game,rival).nexusIxianReplacement);
|
||||
assert.deepEqual(viewGame(ix.game,rival).nexusIxianReplacement,
|
||||
{event:ix.event,buyer:ix.buyer,canPass:false,canUse:false,blocked:null,purchased:null});
|
||||
assert.equal(viewGame(ix.game,rival).nexusIxianReplacement!.purchased,null);
|
||||
assert.equal(viewGame(ix.game,rival).nexusIxianReplacement!.canUse,false);
|
||||
assert.equal(viewGame(ix.game,ix.buyer).nexusIxianReplacement!.canUse,true);
|
||||
const otherOffer = viewGame(irrelevant.game,irrelevant.buyer).nexusIxianReplacement!;
|
||||
assert.equal(otherOffer.canPass,true);
|
||||
assert.equal(otherOffer.canUse,false);
|
||||
assert.equal(otherOffer.blocked,null);
|
||||
reject(irrelevant.game,irrelevant.buyer,{type:'nexusIxianReplacementUse',event:irrelevant.event});
|
||||
const pass = applyAction(irrelevant.game,irrelevant.buyer,{type:'nexusIxianReplacementPass',event:irrelevant.event});
|
||||
assert.equal(pass.nexusCards!.cards!.hands[irrelevant.buyer],'richese');
|
||||
|
||||
@@ -20,8 +20,9 @@ const parent = 'original-native-paid-sale-parent';
|
||||
function context(): IxianReplacementContext {
|
||||
return {
|
||||
status: 'playing', phase: 3, turn: 1, sequence: 0,
|
||||
expansions: [], richese: false,
|
||||
expansions: [], richese: false, advanced: false,
|
||||
auction: { cards: [{ ...purchased }, { ...unsold }], index: 0, bidder: 'buyer', bid: 4 },
|
||||
richeseAuction: null,
|
||||
sale: { winner: 'buyer', amount: 4, free: false, origin: 'normal', seller: null },
|
||||
players: [
|
||||
{ id: 'buyer', hand: [{ ...older }, { ...purchased }], handLimit: 4 },
|
||||
@@ -47,6 +48,117 @@ function source(ctx: IxianReplacementContext = context()) {
|
||||
);
|
||||
}
|
||||
|
||||
function nativeContext(origin: 'cache' | 'blackMarket'): IxianReplacementContext {
|
||||
const card = origin === 'cache' ? richeseCards()[0] : purchased;
|
||||
const ctx = context();
|
||||
ctx.expansions = ['choam'];
|
||||
ctx.richese = true;
|
||||
ctx.advanced = origin === 'blackMarket';
|
||||
ctx.auction = null;
|
||||
ctx.richeseAuction = {
|
||||
event: `native-${origin}-auction`, cardId: card.id, source: origin, owner: 'rival',
|
||||
outcome: { kind: 'sold', winner: 'buyer', amount: 7 },
|
||||
};
|
||||
ctx.sale = { winner: 'buyer', amount: 7, free: false, origin, seller: 'rival' };
|
||||
ctx.players = [{ ...ctx.players[0], hand: [{ ...older }, { ...card }] }, ctx.players[1]];
|
||||
ctx.physicalCards = [{ ...older }, { ...card }, { ...top }];
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void test('native cache and Black Market purchases close exact delivered cards once for Use or Pass', () => {
|
||||
for (const origin of ['cache', 'blackMarket'] as const) {
|
||||
const ctx = nativeContext(origin);
|
||||
const original = source(ctx);
|
||||
const delivered = ctx.players[0].hand.find(card => card.id === ctx.richeseAuction!.cardId)!;
|
||||
assert.deepEqual(original.card, delivered);
|
||||
assert.equal(original.amount, 7);
|
||||
assert.equal(original.free, false);
|
||||
assert.equal(original.auctionIndex, 0);
|
||||
validateIxianReplacementSource(ctx, JSON.parse(JSON.stringify(original)), parent);
|
||||
for (const outcome of ['use', 'pass'] as const) {
|
||||
const closed = closeIxianReplacementSource(
|
||||
original, outcome, outcome === 'use' ? top : null, initialIxianReplacementCursor(),
|
||||
);
|
||||
assert.equal(closed.receipt.outcome, outcome);
|
||||
assert.deepEqual(closed.receipt.replacement, outcome === 'use' ? top : null);
|
||||
assert.throws(() => closeIxianReplacementSource(original, 'pass', null, closed.cursor), IxianReplacementError);
|
||||
assert.throws(() => closeIxianReplacementSource(original, 'use', top, closed.cursor), IxianReplacementError);
|
||||
// A later census need not retain the purchased or drawn card at its old location.
|
||||
validateIxianReplacementHistory(
|
||||
JSON.parse(JSON.stringify([closed.receipt])), closed.cursor, [older],
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void test('successive single native lots use the independent sequence without inventing normal rows', () => {
|
||||
const cache = nativeContext('cache');
|
||||
const first = closeIxianReplacementSource(source(cache), 'pass', null, initialIxianReplacementCursor());
|
||||
const blackMarket = nativeContext('blackMarket');
|
||||
blackMarket.sequence = first.cursor.sequence;
|
||||
const second = closeIxianReplacementSource(source(blackMarket), 'use', top, first.cursor);
|
||||
assert.equal(first.receipt.source.auctionIndex, 0);
|
||||
assert.equal(second.receipt.source.auctionIndex, 0);
|
||||
assert.notEqual(first.receipt.source.event, second.receipt.source.event);
|
||||
validateIxianReplacementHistory([first.receipt, second.receipt], second.cursor);
|
||||
});
|
||||
|
||||
void test('native purchase authority rejects missing, open, unbid, mismatched and competing lots', () => {
|
||||
for (const origin of ['cache', 'blackMarket'] as const) {
|
||||
const ctx = nativeContext(origin);
|
||||
const lot = ctx.richeseAuction!;
|
||||
const original = source(ctx);
|
||||
const variants: IxianReplacementContext[] = [
|
||||
{ ...ctx, richeseAuction: null },
|
||||
{ ...ctx, richeseAuction: { ...lot, outcome: null } },
|
||||
{ ...ctx, richeseAuction: { ...lot, outcome: { kind: 'unbid' } } },
|
||||
{ ...ctx, richeseAuction: { ...lot, outcome: { kind: 'sold', winner: 'buyer', amount: 0 } } },
|
||||
{ ...ctx, richeseAuction: { ...lot, outcome: { kind: 'sold', winner: 'rival', amount: 7 } } },
|
||||
{ ...ctx, richeseAuction: { ...lot, outcome: { kind: 'sold', winner: 'buyer', amount: 8 } } },
|
||||
{ ...ctx, richeseAuction: { ...lot, source: origin === 'cache' ? 'blackMarket' : 'cache' } },
|
||||
{ ...ctx, richeseAuction: { ...lot, cardId: unsold.id } },
|
||||
{ ...ctx, richeseAuction: { ...lot, owner: 'unseated' }, sale: { ...ctx.sale!, seller: 'unseated' } },
|
||||
{ ...ctx, sale: { ...ctx.sale!, seller: null } },
|
||||
{ ...ctx, sale: { ...ctx.sale!, seller: 'buyer' } },
|
||||
{ ...ctx, sale: { ...ctx.sale!, amount: 8 } },
|
||||
{ ...ctx, sale: { ...ctx.sale!, free: true } },
|
||||
{ ...ctx, sale: { ...ctx.sale!, origin: 'normal', seller: null } },
|
||||
{ ...ctx, sale: null },
|
||||
{ ...ctx, auction: context().auction },
|
||||
{ ...ctx, expansions: [] },
|
||||
{ ...ctx, richese: false },
|
||||
...(origin === 'blackMarket' ? [{ ...ctx, advanced: false }] : []),
|
||||
];
|
||||
for (const invalid of variants) {
|
||||
assert.throws(() => source(invalid), IxianReplacementError);
|
||||
assert.throws(() => validateIxianReplacementSource(invalid, original, parent), IxianReplacementError);
|
||||
}
|
||||
// Another canonical card already in the hand cannot replace the actual lot.
|
||||
const swapped = { ...ctx, richeseAuction: { ...lot, cardId: older.id } };
|
||||
assert.throws(() => validateIxianReplacementSource(swapped, original, parent), IxianReplacementError);
|
||||
const absent = {
|
||||
...ctx, players: [{ ...ctx.players[0], hand: [older] }, ctx.players[1]],
|
||||
};
|
||||
assert.throws(() => source(absent), IxianReplacementError);
|
||||
const duplicate = {
|
||||
...ctx, players: [{ ...ctx.players[0], hand: [...ctx.players[0].hand, original.card] }, ctx.players[1]],
|
||||
};
|
||||
assert.throws(() => source(duplicate), IxianReplacementError);
|
||||
}
|
||||
});
|
||||
|
||||
void test('normal purchases preserve their actual nonzero lot index and saved continuation', () => {
|
||||
const ctx = context();
|
||||
ctx.auction = { ...ctx.auction!, cards: [unsold, purchased], index: 1 };
|
||||
const original = createIxianReplacementSource(
|
||||
ctx, ixianReplacementEvent(ctx.turn, 'buyer', ctx.sequence, 1), parent,
|
||||
);
|
||||
assert.equal(original.auctionIndex, 1);
|
||||
validateIxianReplacementSource(ctx, JSON.parse(JSON.stringify(original)), parent);
|
||||
const closed = closeIxianReplacementSource(original, 'pass', null, initialIxianReplacementCursor());
|
||||
validateIxianReplacementHistory(JSON.parse(JSON.stringify([closed.receipt])), closed.cursor);
|
||||
});
|
||||
|
||||
void test('retained purchases keep saved receipt history linear through the game horizon', () => {
|
||||
const physical = baseDeck();
|
||||
const history: IxianReplacementReceipt[] = [];
|
||||
|
||||
@@ -4,7 +4,7 @@ import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { richeseCards } from '../game/richese-cards';
|
||||
import { settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement';
|
||||
import {
|
||||
createRicheseIxianReplacementFixture, nextRicheseIxianReplacementNativeStep,
|
||||
createRicheseIxianReplacementFixture,
|
||||
type RicheseIxianReplacementFixture,
|
||||
} from './fixture-richese-ixian-replacement';
|
||||
|
||||
@@ -121,52 +121,3 @@ void test(`${advanced ? 'Advanced' : 'Basic'} native final-lot CHOAM purchase cl
|
||||
assert.equal(use.nexusCards!.cards!.hands[f.buyer], null);
|
||||
});
|
||||
|
||||
for (const advanced of [false, true])
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} Richese cache lot keeps the Nexus replacement gated to normal purchases`, () => {
|
||||
const f = fixtureFor(advanced, 'atreides', true);
|
||||
const offer = viewGame(f.game, f.buyer).nexusIxianReplacement!;
|
||||
const richese = f.game.players.find(player => player.faction === 'richese')!.id;
|
||||
// Keep the purchased card and the unallied Nexus card, so the buyer is still
|
||||
// an eligible Nexus holder when the round reaches its own compulsory cache lot.
|
||||
let game = settleIxianReplacementFixture(applyAction(reload(f.game), f.buyer,
|
||||
{ type: 'nexusIxianReplacementPass', event: offer.event }));
|
||||
assert.equal(game.nexusCards!.cards!.hands[f.buyer], 'ixians');
|
||||
assert.equal(game.auction, null);
|
||||
assert.equal(game.richeseBidding!.stage, 'cacheOffer');
|
||||
assert.deepEqual(game.decision, { kind: 'richeseCache', player: richese });
|
||||
assert.equal(game.pendingNexusIxianReplacement, null);
|
||||
const cacheCard = game.richeseCache![0];
|
||||
const before = { buyer: owner(game, f.buyer).spice, richese: owner(game, richese).spice,
|
||||
emperor: owner(game, f.emperor!).spice };
|
||||
game = applyAction(game, richese, { type: 'decision', event: game.richeseBidding!.event,
|
||||
card: cacheCard.id, method: 'onceAround', direction: 'clockwise' });
|
||||
assert.equal(game.richeseAuction?.source, 'cache');
|
||||
assert.equal(game.richeseAuction?.cardId, cacheCard.id);
|
||||
assert.equal(game.richeseAuction?.owner, richese);
|
||||
// A real Once Around: the native policy declines for every other seat, the
|
||||
// unallied Nexus holder buys the cache card with its own spice.
|
||||
for (let count = 0; game.richeseAuction && !game.richeseAuction.outcome && count < 20; count++) {
|
||||
const lot: NonNullable<Game['richeseAuction']> = game.richeseAuction;
|
||||
const next = nextRicheseIxianReplacementNativeStep(game);
|
||||
assert.equal(next.actor, lot.active);
|
||||
assert.equal(game.pendingNexusIxianReplacement, null);
|
||||
game = next.actor === f.buyer
|
||||
? applyAction(game, f.buyer, { type: 'richeseBid', event: lot.event,
|
||||
amount: lot.bid === 0 ? 1 : null })
|
||||
: applyAction(game, next.actor, next.action);
|
||||
}
|
||||
// The cache purchase retires its own lot with no replacement frame, while the
|
||||
// same buyer's normal purchase above did open one.
|
||||
assert.equal(game.richeseAuction, null);
|
||||
assert.equal(game.pendingNexusIxianReplacement, null);
|
||||
assert.equal(game.decision, null);
|
||||
assert.equal(game.currentAuctionSale, null);
|
||||
assert.ok(owner(game, f.buyer).hand.some(card => card.id === cacheCard.id));
|
||||
assert.ok(!game.richeseCache!.some(card => card.id === cacheCard.id));
|
||||
assert.equal(owner(game, f.buyer).spice, before.buyer - 1);
|
||||
assert.equal(owner(game, richese).spice, before.richese + 1);
|
||||
assert.equal(owner(game, f.emperor!).spice, before.emperor);
|
||||
assert.equal(game.nexusCards!.cards!.hands[f.buyer], 'ixians');
|
||||
assert.equal(game.richeseBidding!.stage, 'complete');
|
||||
assert.equal(game.phase, 4);
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import {
|
||||
createRicheseIxianSpecialReplacementFixture, settleRicheseIxianSpecialReplacementSuffix,
|
||||
type RicheseIxianSpecialReplacementOrigin, type RicheseIxianSpecialReplacementSaleFixture,
|
||||
type RicheseIxianSpecialReplacementUnbidFixture,
|
||||
} from './fixture-richese-ixian-special-replacement';
|
||||
|
||||
const owner = (game: Game, id: string) => game.players.find(player => player.id === id)!;
|
||||
|
||||
const BUILT: Record<string, RicheseIxianSpecialReplacementSaleFixture> = {};
|
||||
/** One genuine native run per source/mode; each test reloads its own copy. */
|
||||
function saleFixture(
|
||||
origin: RicheseIxianSpecialReplacementOrigin,
|
||||
advanced: boolean,
|
||||
): RicheseIxianSpecialReplacementSaleFixture {
|
||||
const key = `${origin}:${advanced}`;
|
||||
const held = BUILT[key];
|
||||
if (held) return held;
|
||||
const created = createRicheseIxianSpecialReplacementFixture({ origin, advanced });
|
||||
if (created.mode !== 'sold') throw new Error('The paid native source sale is missing.');
|
||||
BUILT[key] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
const UNBID: Partial<Record<RicheseIxianSpecialReplacementOrigin, RicheseIxianSpecialReplacementUnbidFixture>> = {};
|
||||
function unbidFixture(origin: RicheseIxianSpecialReplacementOrigin): RicheseIxianSpecialReplacementUnbidFixture {
|
||||
const held = UNBID[origin];
|
||||
if (held) return held;
|
||||
const created = createRicheseIxianSpecialReplacementFixture({ origin, mode: 'unbid' });
|
||||
if (created.mode !== 'unbid') throw new Error('The genuine unbid outcome is missing.');
|
||||
UNBID[origin] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
const SOURCES: [RicheseIxianSpecialReplacementOrigin, boolean][] = [
|
||||
['cache', false], ['cache', true], ['blackMarket', true],
|
||||
];
|
||||
|
||||
for (const [origin, advanced] of SOURCES)
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} native ${origin} Once Around purchase is already paid when its purchased-card offer opens`, () => {
|
||||
const f = saleFixture(origin, advanced);
|
||||
// The public offer binds the exact concealed/delivered native card, never a
|
||||
// top deck card and never an unsold lot card.
|
||||
const offer = viewGame(f.game, f.buyer).nexusIxianReplacement!;
|
||||
assert.equal(offer.origin, origin);
|
||||
assert.equal(offer.canPass, true);
|
||||
assert.equal(offer.canUse, true);
|
||||
assert.deepEqual(offer.purchased, f.purchased);
|
||||
// The genuine payment is already debited exactly once; the seller is not yet
|
||||
// credited and no Emperor gift exists for this unallied native sale.
|
||||
assert.equal(owner(f.game, f.buyer).spice, f.beforeSpice - f.bid);
|
||||
assert.equal(owner(f.game, f.seller).spice, owner(f.beforePurchase, f.seller).spice);
|
||||
assert.equal(owner(f.game, f.emperor!).spice, owner(f.beforePurchase, f.emperor!).spice);
|
||||
assert.equal(f.game.nexusCards!.cards!.hands[f.buyer], 'ixians');
|
||||
assert.deepEqual(owner(f.game, f.buyer).hand,
|
||||
[...owner(f.beforePurchase, f.buyer).hand, f.purchased]);
|
||||
if (origin === 'cache') {
|
||||
assert.deepEqual(f.game.richeseCache,
|
||||
f.cacheBefore.filter(card => card.id !== f.purchased.id));
|
||||
assert.equal(owner(f.beforePurchase, f.seller).hand.some(card => card.id === f.purchased.id), false);
|
||||
} else {
|
||||
assert.deepEqual(f.game.richeseCache, f.cacheBefore,
|
||||
'A Black Market sale never touches the compulsory cache.');
|
||||
assert.equal(owner(f.beforePurchase, f.seller).hand.some(card => card.id === f.purchased.id), true);
|
||||
assert.equal(owner(f.game, f.seller).hand.some(card => card.id === f.purchased.id), false);
|
||||
}
|
||||
});
|
||||
|
||||
for (const [origin, advanced] of SOURCES)
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} native ${origin} Use spends the Ixian Nexus on that card and leaves the original native suffix intact`, () => {
|
||||
const f = saleFixture(origin, advanced);
|
||||
const offer = viewGame(f.game, f.buyer).nexusIxianReplacement!;
|
||||
const used = settleRicheseIxianSpecialReplacementSuffix(applyAction(structuredClone(f.game), f.buyer,
|
||||
{ type: 'nexusIxianReplacementUse', event: offer.event }), origin);
|
||||
// Only the purchased native card is discarded and only the actual top card
|
||||
// is drawn; the paid purchase is never re-debited or credited.
|
||||
assert.deepEqual(owner(used, f.buyer).hand,
|
||||
[...owner(f.game, f.buyer).hand.filter(card => card.id !== f.purchased.id), f.nextCard]);
|
||||
assert.deepEqual(used.discard, [...f.game.discard, f.purchased]);
|
||||
assert.deepEqual(used.deck, f.game.deck.slice(1));
|
||||
assert.equal(used.nexusCards!.cards!.hands[f.buyer], null);
|
||||
assert.deepEqual(used.nexusCards!.cards!.discard,
|
||||
[...f.game.nexusCards!.cards!.discard, 'ixians']);
|
||||
assert.equal(owner(used, f.buyer).spice, f.paidSpice);
|
||||
assert.equal(owner(used, f.seller).spice, f.sellerSpiceAtOffer + f.bid);
|
||||
assert.equal(owner(used, f.emperor!).spice, f.emperorSpiceAtOffer);
|
||||
assert.equal(used.pendingNexusIxianReplacement, null);
|
||||
assert.equal(used.currentAuctionSale, null);
|
||||
assert.equal(used.richeseAuction, null);
|
||||
assert.equal(used.richeseBidding!.blackMarketSold, origin === 'blackMarket');
|
||||
if (origin === 'cache') {
|
||||
assert.deepEqual(used.richeseCache, f.game.richeseCache);
|
||||
assert.equal(used.richeseBidding!.stage, 'complete');
|
||||
assert.equal(used.decision, null);
|
||||
assert.equal(used.phase, 4);
|
||||
} else {
|
||||
assert.deepEqual(used.richeseCache, f.cacheBefore);
|
||||
assert.equal(used.richeseBidding!.stage, 'declaration');
|
||||
assert.deepEqual(used.decision, { kind: 'richeseDeclaration', player: f.seller });
|
||||
}
|
||||
});
|
||||
|
||||
for (const [origin, advanced] of SOURCES)
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} native ${origin} Keep retains the exact purchased card and the Ixian Nexus while the original seller is credited once`, () => {
|
||||
const f = saleFixture(origin, advanced);
|
||||
const offer = viewGame(f.game, f.buyer).nexusIxianReplacement!;
|
||||
const kept = settleRicheseIxianSpecialReplacementSuffix(applyAction(structuredClone(f.game), f.buyer,
|
||||
{ type: 'nexusIxianReplacementPass', event: offer.event }), origin);
|
||||
assert.deepEqual(owner(kept, f.buyer).hand, owner(f.game, f.buyer).hand);
|
||||
assert.ok(owner(kept, f.buyer).hand.some(card => card.id === f.purchased.id));
|
||||
assert.deepEqual(kept.deck, f.game.deck);
|
||||
assert.deepEqual(kept.discard, f.game.discard);
|
||||
assert.deepEqual(kept.nexusCards!.cards!.hands[f.buyer], f.game.nexusCards!.cards!.hands[f.buyer]);
|
||||
assert.equal(kept.nexusCards!.cards!.hands[f.buyer], 'ixians');
|
||||
assert.equal(owner(kept, f.buyer).spice, f.paidSpice);
|
||||
assert.equal(owner(kept, f.seller).spice, f.sellerSpiceAtOffer + f.bid);
|
||||
assert.equal(owner(kept, f.emperor!).spice, f.emperorSpiceAtOffer);
|
||||
assert.equal(kept.pendingNexusIxianReplacement, null);
|
||||
assert.equal(kept.currentAuctionSale, null);
|
||||
assert.equal(kept.richeseAuction, null);
|
||||
assert.deepEqual(kept.richeseCache,
|
||||
origin === 'cache' ? f.game.richeseCache : f.cacheBefore);
|
||||
if (origin === 'cache') {
|
||||
assert.equal(kept.richeseBidding!.stage, 'complete');
|
||||
assert.equal(kept.decision, null);
|
||||
assert.equal(kept.phase, 4);
|
||||
assert.equal(kept.richeseBidding!.blackMarketSold, false);
|
||||
} else {
|
||||
assert.equal(kept.richeseBidding!.stage, 'declaration');
|
||||
assert.deepEqual(kept.decision, { kind: 'richeseDeclaration', player: f.seller });
|
||||
assert.equal(kept.richeseBidding!.blackMarketSold, true);
|
||||
}
|
||||
});
|
||||
|
||||
void test('Basic native unbid cache lot is not a purchase: the owner keeps the card with no spice, Nexus spend or offer', () => {
|
||||
const f = unbidFixture('cache');
|
||||
// The compulsory cache lot really ran its own Once Around and every genuine
|
||||
// offer was zero, so the lot retired without a sale.
|
||||
assert.equal(f.game.richeseAuction, null);
|
||||
assert.equal(f.game.pendingNexusIxianReplacement, null);
|
||||
assert.notEqual(f.game.decision?.kind, 'nexusIxianReplacement');
|
||||
assert.equal(f.game.currentAuctionSale, null);
|
||||
assert.equal(f.game.richeseBidding!.stage, 'complete');
|
||||
assert.equal(f.game.richeseBidding!.blackMarketSold, false);
|
||||
// A Keep is not a purchase: no spice moved anywhere and no Emperor gift exists.
|
||||
for (const seat of f.game.players)
|
||||
assert.equal(owner(f.game, seat.id).spice, owner(f.beforeFinalAction, seat.id).spice);
|
||||
assert.equal(f.game.nexusCards!.cards!.hands[f.buyer], 'ixians');
|
||||
// The kept card left the original cache into the owner's own hand.
|
||||
assert.equal(f.game.richeseCache!.some(card => card.id === f.card.id), false);
|
||||
assert.equal(owner(f.game, f.seller).hand.some(card => card.id === f.card.id), true);
|
||||
assert.equal((f.game.richeseRemoved ?? []).some(card => card.id === f.card.id), false);
|
||||
assert.deepEqual(f.game.richeseCache, f.cacheBefore.filter(card => card.id !== f.card.id));
|
||||
});
|
||||
|
||||
void test('Advanced native unbid Black Market lot retains its concealed card with no sale, payment or offer', () => {
|
||||
const f = unbidFixture('blackMarket');
|
||||
assert.equal(f.game.richeseAuction, null);
|
||||
assert.equal(f.game.pendingNexusIxianReplacement, null);
|
||||
assert.equal(f.game.currentAuctionSale, null);
|
||||
assert.equal(f.game.richeseBidding!.blackMarketSold, false);
|
||||
assert.equal(f.game.richeseBidding!.stage, 'declaration');
|
||||
assert.deepEqual(f.game.decision, { kind: 'richeseDeclaration', player: f.seller });
|
||||
for (const seat of f.game.players)
|
||||
assert.equal(owner(f.game, seat.id).spice, owner(f.beforeFinalAction, seat.id).spice);
|
||||
assert.equal(f.game.nexusCards!.cards!.hands[f.buyer], 'ixians');
|
||||
assert.ok(owner(f.game, f.seller).hand.some(card => card.id === f.card.id));
|
||||
assert.ok(f.keptCacheCards.includes(f.card.id));
|
||||
assert.deepEqual(f.game.richeseCache, f.cacheBefore,
|
||||
'An unbid Black Market lot must not consume a compulsory cache card.');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user