Provisionally apply first/last to the current ordinary or native Advanced normal Black Market lot. Preserve bids, funding and passes; skip passed seats until a real raise. Use a local full cycle and actual post-discard hand capacity. Keep the original Black Market opener in both settlement consumers and clear ordinary overrides for the next lot. Connect persistent controls, native disposal and existing minimal AI options. Validation:10-file134/134, types/lint/build and eight original-action/rendered-table continuations. Public setup/cache1, real lobby circle choice, native Black Market and paid full hand preserve prior1,2/3/4 raises, one payment4 and original next opener or final-lot Revival. Eight minimal policies are legal. Corrected fixture opener/completion assumptions and external default-history/final-lot assertions; only runtime was rerun against identical source. Bounded consumer review found no concrete defect. Other timing scopes, direct combined Semuta claim/capture, full modes, final assurance and browser/deployed acceptance remain open. No saved-room/reset/restart/deployment operation.
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
export type NormalAuctionNext =
|
|
| { kind: 'unbid' }
|
|
| { kind: 'payment'; player: string }
|
|
| { kind: 'bid'; player: string };
|
|
|
|
export type NormalAuctionBid =
|
|
| { ok: true; amount: number }
|
|
| { ok: false; reason: string };
|
|
|
|
/**
|
|
* Quote the next ordinary auction step. The engine owns card custody, payment,
|
|
* phase progression and the hand-limit eligibility calculation.
|
|
*/
|
|
export function quoteNormalAuctionNext(context: {
|
|
order: readonly string[];
|
|
eligible: readonly string[];
|
|
active: string;
|
|
bid: number;
|
|
bidder: string | null;
|
|
passed: readonly string[];
|
|
}): NormalAuctionNext {
|
|
const eligible = new Set(context.eligible);
|
|
const others = context.eligible.filter((id) => id !== context.bidder);
|
|
if (others.every((id) => context.passed.includes(id)))
|
|
return context.bidder
|
|
? { kind: 'payment', player: context.bidder }
|
|
: { kind: 'unbid' };
|
|
|
|
const activeIndex = context.order.indexOf(context.active);
|
|
for (let offset = 1; offset <= context.order.length; offset++) {
|
|
const index = (Math.max(activeIndex, -1) + offset) % context.order.length;
|
|
const player = context.order[index];
|
|
if (eligible.has(player) && player !== context.bidder && !context.passed.includes(player))
|
|
return { kind: 'bid', player };
|
|
}
|
|
|
|
// A malformed restored state must fail closed instead of spinning forever or
|
|
// fabricating an auction outcome.
|
|
throw new Error(
|
|
'Ordinary auction has no eligible next bidder in table order.',
|
|
);
|
|
}
|
|
|
|
/** The engine supplies the freshly calculated funding maximum. */
|
|
export function quoteNormalAuctionBid(context: {
|
|
currentBid: number;
|
|
amount: unknown;
|
|
maximum: number;
|
|
}): NormalAuctionBid {
|
|
const minimum = context.currentBid + 1;
|
|
if (
|
|
typeof context.amount !== 'number' ||
|
|
!Number.isSafeInteger(context.amount) ||
|
|
context.amount < minimum ||
|
|
context.amount > context.maximum
|
|
)
|
|
return {
|
|
ok: false,
|
|
reason: `Bid must be an integer from ${minimum} to ${context.maximum}.`,
|
|
};
|
|
return { ok: true, amount: context.amount };
|
|
}
|