Files
Dune/game/homeworld-occupied-bidding.ts
T

125 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { FactionId } from './catalog';
import { handLimit, type Game } from './engine';
import type { HomeworldId } from './homeworld-cards';
import { HomeworldCustodyError } from './homeworld-custody';
import { quoteStableHomeworldOccupation } from './homeworld-stable-occupation';
export type OccupiedBiddingKind =
| 'atreidesInspection'
| 'ixAuction'
| 'richeseCache'
| 'harkonnenBonus';
export type OccupiedBiddingAuthority = {
kind: OccupiedBiddingKind;
source: 'auctionLot' | 'ixAuctionPool' | 'richeseCache' | 'treacheryStock';
/** Original native power provider, not a substitute buyer, seller or payer. */
provider: string | null;
controller: string | null;
inspectionAudience: string[];
/** Alternatives for ONE existing purchase bonus, never one card per player. */
bonusRecipients: string[];
bonusStockAvailable: boolean | null;
occupied: boolean;
blocked: string | null;
};
export class OccupiedBiddingError extends HomeworldCustodyError {}
const sources: Record<OccupiedBiddingKind, {
card: HomeworldId;
faction: FactionId;
source: OccupiedBiddingAuthority['source'];
}> = {
atreidesInspection: { card: 'caladan', faction: 'atreides', source: 'auctionLot' },
ixAuction: { card: 'ix', faction: 'ixians', source: 'ixAuctionPool' },
richeseCache: { card: 'richese', faction: 'richese', source: 'richeseCache' },
harkonnenBonus: { card: 'giedi_prime', faction: 'harkonnen', source: 'treacheryStock' },
};
/** Authority for an ORIGINAL native Bidding effect only. Its producer still
* validates the invoice/lot/window and physical card custody. This pure quote
* neither initiates another inspection/draw nor confers captured Nexus powers.
* Sources: original Caladan, Ix, Richese and Giedi Prime occupied card faces;
* HOMEWORLD_OCCUPATION_RULES7–33,35–56 through the shared stable-source gate. */
export function quoteOccupiedBiddingAuthority(
game: Game,
kind: OccupiedBiddingKind,
): OccupiedBiddingAuthority {
const rule = sources[kind];
if (!rule) throw new OccupiedBiddingError('Unknown original Homeworld Bidding effect.');
const native = game.players.find(player => player.faction === rule.faction);
const bonusStockAvailable = kind === 'harkonnenBonus'
? game.deck.length > 0 || game.discard.length > 0 : null;
const quote: OccupiedBiddingAuthority = {
kind, source: rule.source, provider: native?.id ?? null, controller: null,
inspectionAudience: [], bonusRecipients: [], bonusStockAvailable,
occupied: false, blocked: null,
};
if (!native) return quote;
const occupation = quoteStableHomeworldOccupation(game, rule.card);
if (occupation.blocked) return { ...quote, blocked: occupation.blocked };
const entitlement = occupation.entitlement;
quote.occupied = entitlement !== null;
quote.controller = entitlement?.occupier ?? native.id;
switch (kind) {
case 'atreidesInspection':
// SHARES: native inspection survives. No borrowing by the occupier's ally.
quote.controller = native.id;
quote.inspectionAudience = entitlement ? [native.id, entitlement.occupier] : [native.id];
break;
case 'ixAuction':
// CONTROLS: the actual drawn pool/selection has one decision owner and
// audience, not an extra native peek. Setup/Technology are separate powers.
quote.inspectionAudience = [quote.controller];
break;
case 'richeseCache':
// CHOOSES: only this cache-card choice changes owner. The original cache
// remains native custody/knowledge; declaration, Black Market, unbid-card
// disposal, seller/payment and special-lot substitution do not transfer.
quote.inspectionAudience = entitlement ? [native.id, entitlement.occupier] : [native.id];
break;
case 'harkonnenBonus': {
// The printed condition is the RECEIVER's real hand space, not the
// native buyer's post-purchase space. The same draw uses deck/discard;
// cards in cache, a pending pool or other hands are not drawable stock.
const candidates = entitlement
? [entitlement.occupier, ...(entitlement.ally ? [entitlement.ally] : [])]
: [native.id];
if (bonusStockAvailable) quote.bonusRecipients = candidates.filter(id => {
const recipient = game.players.find(player => player.id === id);
return recipient !== undefined && recipient.hand.length < handLimit(recipient);
});
break;
}
}
return quote;
}
/** Validate the decision actor without replacing the original source provider. */
export function requireOccupiedBiddingController(
game: Game,
kind: OccupiedBiddingKind,
player: string,
): OccupiedBiddingAuthority {
const quote = quoteOccupiedBiddingAuthority(game, kind);
if (quote.blocked) throw new OccupiedBiddingError(quote.blocked);
if (kind === 'atreidesInspection'
? !quote.inspectionAudience.includes(player)
: quote.controller !== player)
throw new OccupiedBiddingError('This player does not control the original Bidding effect.');
return quote;
}
/** Requote at acceptance: alliances, genuine hand space and draw stock can
* change while the original purchase continuation is awaiting its recipient. */
export function requireOccupiedBiddingBonusRecipient(
game: Game,
recipient: string,
): OccupiedBiddingAuthority {
const quote = quoteOccupiedBiddingAuthority(game, 'harkonnenBonus');
if (quote.blocked) throw new OccupiedBiddingError(quote.blocked);
if (!quote.bonusRecipients.includes(recipient))
throw new OccupiedBiddingError('This player cannot receive the original purchase bonus.');
return quote;
}