Files
Dune/game/normal-auction.ts
T
Quintine b7ec94015a Add interactive bidding practice with shared ordinary auction quotes
Expand the internal introduction to eight lessons and six Basic exercises. A Fremen practice auction teaches budget limits, raising, passing and re-entry, full-hand exclusion, all-pass termination, winner-only payment and private purchase inspection. Two explicitly fixed opponent scripts make legal teaching moves without a confirmation step or changes to actual AI strategies.

Extract ordinary bid validation and next-seat/payment/unbid selection into shared pure helpers used by both the practice and live engine. The engine retains actual card custody, ally contribution, Karama, Bureaucrat, payment and response handling. Inconsistent order/eligibility fails closed instead of an unbounded loop. No mode/expansion/publication gate changes.

Version 3 lesson saves retain the existing key and migrate all five v1 and seven v2 positions by identity. Prior committed movement, battle and Traitor choices persist. Only learner bid/pass choices are saved and semantically replayed; over-budget, underbid, full-hand and post-terminal choices are rejected, and unrelated fields are stripped.

Independent review found no material defect. Types, lint, 4925 offline tests and production build pass on stable final source. Nine new tests cover shared bidding order/eligibility/funding and practice outcomes, actual engine payment/card-custody comparisons, all-pass/full-hand paths, all v2 lesson migrations and saved replay. Existing v1, movement, combat and Traitor checks remain green. HTTP/auth/schema code did not change; offline checks include existing in-memory persistence tests, and no live HTTP suite was rerun for this checkpoint.

Browser checks confirm preserved older movement practice, heading focus, keyboard budget control, invalid bid blocking, pass/re-entry, refresh during and after the auction, private Shield inspection, full-hand and all-pass results. The completed bid display stays at the actual committed amount. At 390px controls fit at 309px wide with at least 44px height and no document overflow; the viewport was reset. Screenshot capture still times out, so visual screenshot acceptance remains unverified. The page has no external links and the existing Learn tab is retained.

All 1337 original rooms, 2805 seats and every existing recovery/entry/handover/delegation/migration row are unchanged. SQLite integrity and foreign keys pass. No reset, migration, restart, automation, account-usage check or AI strength calibration occurred. Bounded current-state review also confirmed Extortion, Atomics and Kull still cross previously recorded material rulings; no duplicate questions or house rules were introduced. Alliances, faction/Advanced/expansion teaching and the full game objective remain unfinished.

Verified source tree: b5d318722db155645e98ec3319ffea47f4caa049a06ff5d97abc850db59cb717
2026-09-20 17:19:18 +10:00

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)
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 };
}