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.
78 lines
3.6 KiB
TypeScript
78 lines
3.6 KiB
TypeScript
export type SaphoCyclicAuctionContext = {
|
|
event: string;
|
|
order: readonly string[];
|
|
eligible: readonly string[];
|
|
active: string;
|
|
bidder: string | null;
|
|
passed: readonly string[];
|
|
};
|
|
export type SaphoCyclicAuctionRequest = {
|
|
event: string;
|
|
player: string;
|
|
mode: 'first' | 'last';
|
|
};
|
|
export class SaphoCyclicAuctionError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'SaphoCyclicAuctionError';
|
|
}
|
|
}
|
|
function requireAuction(value: unknown, message: string): asserts value {
|
|
if (!value) throw new SaphoCyclicAuctionError(message);
|
|
}
|
|
const identifier = (value: unknown): value is string =>
|
|
typeof value === 'string' && value.trim().length > 0;
|
|
const ids = (value: unknown): value is readonly string[] =>
|
|
Array.isArray(value) && value.every(identifier) && new Set(value).size === value.length;
|
|
function rotate(order: readonly string[], first: string): string[] {
|
|
const at = order.indexOf(first);
|
|
return [...order.slice(at), ...order.slice(0, at)];
|
|
}
|
|
|
|
/** Current-lot/next-opportunity composition, not a publisher timing ruling.
|
|
* Keep the complete local cycle: a passed, full-hand or high-bid placeholder
|
|
* may matter after a later raise. Eligibility and card custody belong to the
|
|
* engine; this quote changes neither bids nor the physical storm-order opener. */
|
|
export function reorderSaphoCyclicAuction(
|
|
context: SaphoCyclicAuctionContext,
|
|
request: SaphoCyclicAuctionRequest,
|
|
): { order: string[]; active: string } {
|
|
requireAuction(context && typeof context === 'object', 'Choose a valid current auction lot.');
|
|
requireAuction(identifier(context.event), 'The auction requires its current lot event.');
|
|
requireAuction(
|
|
ids(context.order) && context.order.length > 0 && ids(context.eligible) && ids(context.passed) &&
|
|
context.eligible.every(player => context.order.includes(player)) &&
|
|
context.passed.every(player => context.order.includes(player)) &&
|
|
(context.bidder === null || identifier(context.bidder) && context.order.includes(context.bidder)) &&
|
|
(context.bidder === null || !context.passed.includes(context.bidder)),
|
|
'Auction order, eligible seats, passes and high bidder must form a valid unique lot roster.',
|
|
);
|
|
const pending = (player: string) => context.eligible.includes(player) &&
|
|
!context.passed.includes(player) && player !== context.bidder;
|
|
requireAuction(
|
|
identifier(context.active) && pending(context.active),
|
|
'The current auction actor must have a pending eligible bid opportunity.',
|
|
);
|
|
requireAuction(
|
|
request && typeof request === 'object' && request.event === context.event,
|
|
'This auction lot event has expired.',
|
|
);
|
|
requireAuction(request.mode === 'first' || request.mode === 'last', 'Choose first or last.');
|
|
requireAuction(
|
|
identifier(request.player) && pending(request.player),
|
|
'Only an eligible unpassed player other than the current high bidder can change position.',
|
|
);
|
|
const original = rotate(context.order, context.active);
|
|
const others = original.filter(player => player !== request.player);
|
|
const order = request.mode === 'first' ? [request.player, ...others] : [...others, request.player];
|
|
const active = order.find(pending)!;
|
|
// Compare complete effective cycles, including their next active actor.
|
|
// Merely rotating a single pending actor around skipped placeholders is a
|
|
// no-op, while moving behind a placeholder that can reenter is meaningful.
|
|
requireAuction(
|
|
JSON.stringify(original) !== JSON.stringify(rotate(order, active)),
|
|
'This player is already in that effective auction position.',
|
|
);
|
|
return { order, active };
|
|
}
|