Time sequential Richese auctions
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 02:11:18 +11:00
1 parent e7520e8679
commit 7fc6f1cd43
15 files changed
+692 -38

No files matched your search

+4 -4
View File
@@ -42,7 +42,7 @@ function validateClock(value: unknown): asserts value is AuctionClock {
identifier(value.event) && identifier(value.source) && identifier(value.player) && identifier(value.controlEpoch) &&
finite(value.remainingMs) && value.remainingMs >= 0 && value.remainingMs <= AUCTION_BID_MS &&
(value.startedAt === null || finite(value.startedAt)),
'Retain the complete original ordinary-auction clock, finite budget and control epoch.');
'Retain the complete original auction clock, finite budget and control epoch.');
}
function remaining(clock: AuctionClock, now: number): number {
@@ -65,11 +65,11 @@ export function quoteAuctionClock(input: {
check(object(input) && (keys(input, 'blocked,controlEpoch,event,now,opportunity') ||
keys(input, 'blocked,clock,controlEpoch,event,now,opportunity')) && finite(input.now) &&
typeof input.blocked === 'boolean' && identifier(input.controlEpoch) && identifier(input.event),
'Supply the exact ordinary-auction opportunity, finite time, blocker and control epoch.');
'Supply the exact auction opportunity, finite time, blocker and control epoch.');
if (input.clock !== undefined) validateClock(input.clock);
check(input.opportunity === null || object(input.opportunity) && keys(input.opportunity, 'player,source') &&
identifier(input.opportunity.source) && identifier(input.opportunity.player),
'Choose the original ordinary-auction source and current bidder, or no opportunity.');
'Choose the original auction source and current bidder, or no opportunity.');
if (input.opportunity === null) return { clock: undefined, expired: false, delayMs: null };
const { opportunity, now, blocked, controlEpoch } = input;
@@ -95,7 +95,7 @@ export function quoteAuctionClock(input: {
/** Suspended/expired frames retain their original event for the client. */
export function viewAuctionClock(clock: AuctionClock, now: number): AuctionClockView {
validateClock(clock);
check(finite(now), 'Project the ordinary-auction clock at a finite time.');
check(finite(now), 'Project the auction clock at a finite time.');
const remainingMs = remaining(clock, now);
return { event: clock.event, player: clock.player, remainingMs,
running: clock.startedAt !== null && remainingMs > 0 };
+50 -18
View File
@@ -15501,6 +15501,7 @@ function beginRicheseLot(
order,
tieOrder: g.order,
});
if (lot.method !== 'silent') lot.clockVersion = 1;
g.richeseAuction = lot;
if (source === 'cache' && g.earlyRicheseBidPromises?.length) {
const attached = earlyRicheseBidRule(() => attachEarlyRicheseBids(g.earlyRicheseBidPromises!, g.turn, lot.event));
@@ -17906,11 +17907,28 @@ function ordinaryAuctionClockOpportunity(g: Game) {
bid: auction.bid, bidder: auction.bidder, passed: auction.passed,
opener: auction.opener, order: auction.saphoOrder ?? g.order }) };
}
export function hasAuctionClockOpportunity(g: Game): boolean {
return ordinaryAuctionClockOpportunity(g) !== null;
/** A source is public auction progress, never the concealed Black Market card.
* A saved sequential lot without this fresh version remains untimed. */
function richeseAuctionClockOpportunity(g: Game) {
const lot = g.richeseAuction;
if (g.status !== 'playing' || g.phase !== 3 || lot?.clockVersion !== 1 ||
lot.method === 'silent' || lot.outcome || !lot.active ||
!lot.eligible.includes(lot.active) || !lot.order.includes(lot.active) ||
!g.players.some(player => player.id === lot.active)) return null;
return { player: lot.active, source: JSON.stringify({kind:'richese', turn:g.turn,
event:lot.event, method:lot.method, source:lot.source, active:lot.active,
bid:lot.bid, bidder:lot.bidder, passed:lot.passed, acted:lot.acted}) };
}
function ordinaryAuctionClockBlocked(g: Game): boolean {
return !!(g.active !== g.auction?.active || g.richeseAuction || g.truthtrance || g.response ||
function currentAuctionClockOpportunity(g: Game) {
return richeseAuctionClockOpportunity(g) ?? ordinaryAuctionClockOpportunity(g);
}
export function hasAuctionClockOpportunity(g: Game): boolean {
return currentAuctionClockOpportunity(g) !== null;
}
function auctionClockBlocked(g: Game): boolean {
const richese = richeseAuctionClockOpportunity(g);
return !!(g.active !== (richese ? g.richeseAuction?.active : g.auction?.active) ||
!richese && g.richeseAuction || g.truthtrance || g.response ||
g.decision || g.phaseOpening || g.pendingTreacheryDiscard ||
g.pendingNullentropy || g.pendingKull || g.pendingHomeworldTupileCleanup ||
g.pendingRicheseBetrayal || g.pendingGuildBetrayal ||
@@ -17921,10 +17939,10 @@ function ordinaryAuctionClockBlocked(g: Game): boolean {
pendingNexusTraitors(g));
}
function auctionClockQuote(g: Game, now: number, controlEpoch: string, blocked = false) {
const opportunity = ordinaryAuctionClockOpportunity(g);
const opportunity = currentAuctionClockOpportunity(g);
try {
return quoteAuctionClock({ clock: g.auctionClock, opportunity, now, controlEpoch,
blocked: blocked || ordinaryAuctionClockBlocked(g),
blocked: blocked || auctionClockBlocked(g),
event: opportunity && (!g.auctionClock || g.auctionClock.source !== opportunity.source ||
g.auctionClock.player !== opportunity.player || g.auctionClock.controlEpoch !== controlEpoch)
? crypto.randomUUID() : g.auctionClock?.event ?? 'no-auction' });
@@ -17980,19 +17998,33 @@ export function advanceAuctionClock(source: Game, now: number, controlEpoch: str
if (!quote.expired || !game.auctionClock) return game;
const actor = game.auctionClock.player;
let next: Game, compelled = false;
try { next = applyAction(game, actor, { type: 'passBid' }); }
const lot = richeseAuctionClockOpportunity(game) && game.richeseAuction;
try { next = applyAction(game, actor, lot
? {type:'richeseBid',event:lot.event,amount:null,allyPayment:0}
: { type: 'passBid' }); }
catch (error) {
if (!(error instanceof RuleError)) throw error;
const whole = liveTurnActionPromises(game.turnActionPromises ?? [], actor, game.turn).length
? turnActionCompletion(game, actor)?.bid : undefined;
const opportunity = bidOpportunity(game);
const required = opportunity && bidRequired(game.bidPromises ?? [], actor, opportunity);
const phaseGuide = projectedPhaseBidGuide(game, actor)?.action;
if (!whole && !required && phaseGuide?.type !== 'bid') throw error;
const completion = phaseGuide?.type === 'bid' ? phaseGuide : minimumCompelledAuctionBid(game, actor);
requireRule(completion, 'The expired ordinary bid needs a legal completion of its binding Truthtrance answer.');
next = applyAction(game, actor, completion);
compelled = true;
if (lot) {
const context = {turn:game.turn,event:lot.event,player:actor};
const required = richeseBidRequired(game.richeseBidPromises ?? [], context);
if (!required) throw error;
const completion = projectedRicheseBidGuide(game, actor)?.action;
requireRule(completion?.type === 'richeseBid',
'The expired Richese bid needs a legal completion of its binding Truthtrance answer.');
next = applyAction(game, actor, completion);
compelled = true;
} else {
const whole = liveTurnActionPromises(game.turnActionPromises ?? [], actor, game.turn).length
? turnActionCompletion(game, actor)?.bid : undefined;
const opportunity = bidOpportunity(game);
const required = opportunity && bidRequired(game.bidPromises ?? [], actor, opportunity);
const phaseGuide = projectedPhaseBidGuide(game, actor)?.action;
if (!whole && !required && phaseGuide?.type !== 'bid') throw error;
const completion = phaseGuide?.type === 'bid' ? phaseGuide : minimumCompelledAuctionBid(game, actor);
requireRule(completion, 'The expired ordinary bid needs a legal completion of its binding Truthtrance answer.');
next = applyAction(game, actor, completion);
compelled = true;
}
}
log(next, compelled
? `${getPlayer(game, actor).name}'s ten-second bidding time expired. The legal bid required by their binding Truthtrance answer was submitted.`
@@ -18000,7 +18032,7 @@ export function advanceAuctionClock(source: Game, now: number, controlEpoch: str
return synchronizeAuctionClock(next, now, controlEpoch, blocked);
}
function projectedAuctionClock(g: Game, now?: number): AuctionClockView | null {
const opportunity = ordinaryAuctionClockOpportunity(g);
const opportunity = currentAuctionClockOpportunity(g);
if (!opportunity) return null;
const clock = g.auctionClock;
if (!clock || clock.source !== opportunity.source || clock.player !== opportunity.player)
+8 -8
View File
@@ -45,7 +45,7 @@ export const PHASE_HELP = [
'Complete the table’s storm dials or card reveal. After the distance is revealed, play any storm cards and confirm movement.',
'Reveal a spice blow, resolve worms and card responses, then finish any Nexus and Fremen rides.',
'Claim charity once if below two spice. Advanced Bene Gesserit receives two regardless of wealth. New Advanced phases follow the First Player counterclockwise: claim if eligible, then finish your Charity turn. Basic and older unstamped phases retain concurrent readiness.',
'Bid or pass in turn. New ordinary lots allow ten seconds per bidder while native decisions hold the clock. A winning bidder chooses how to pay before income and bonus-card responses resolve.',
'Bid or pass in turn. New ordinary and sequential Richese lots allow ten seconds per bidder while native decisions hold the clock. A winning bidder chooses how to pay before income and bonus-card responses resolve.',
'Return eligible forces and leaders from the tanks to your reserves. New Advanced Revival phases begin from the First Player counterclockwise; Sapho may change unfinished positions. Finish your own force and leader choices before the next player acts. Basic and already-opened older phases retain their current readiness timing.',
'Ship before moving. Select a destination sector and, for a move, the forces leaving one territory.',
'Choose a battle, resolve faction powers, seal plans, decide traitors, then choose which winning cards to keep.',
@@ -127,7 +127,7 @@ const phaseDetails = [
[
'Before each auction, resolve the Atreides foresight response. The card stays hidden until that response is allowed. Cancellation affects this card only.',
'The highlighted player may raise the bid or pass. A new high bid clears previous passes.',
'New ordinary lots give the current bidder ten seconds once bidding is ready. Silence becomes a native pass; if passing would break a binding Truthtrance answer, the online convention submits only its required legal bid. Native decisions hold remaining time. Administrative controls re-arm the clock after resumption. Old open unstamped lots retain their saved protocol.',
'New ordinary lots and sequential Richese lots give the current bidder ten seconds once bidding is ready. Silence becomes a native pass; if passing would break a binding Truthtrance answer, the online convention submits only its required legal bid. Native decisions hold remaining time. Administrative controls re-arm the clock after resumption. Once Around timing is provisional; Silent auctions remain untimed. Old open unstamped lots retain their saved protocol.',
'Holding Karama permits a bid above your available spice. If outbid, keep the card. A winning bid above your funds requires Karama.',
'After everyone else passes, the winner chooses spice payment or Karama. The table then resolves applicable Emperor-income and Harkonnen-bonus responses.',
'In the fresh local ixian-betrayal preview, native Ixian Bidding inspection and Advanced Technology first finish their original Karama responses, then pause for neutral Nexus acknowledgements before their effects. A legal rival Use prevents only the current attempt; all required passes allow it. See Ixian Nexus Betrayal for both alternatives, private-card limits and the unchanged public release gate.',
@@ -2638,24 +2638,24 @@ export const RULE_TOPICS: RuleTopic[] = [
},
{
id: 'ordinary-auction-clock',
title: 'Ordinary auction: ten seconds to bid',
title: 'Sequential auctions: ten seconds to bid',
category: 'Turn phases', coverage: 'Partial', developmentStage: 'Prototyped',
summary: 'The server times each new ordinary bidding opportunity and uses the native pass when its ten seconds expire.',
summary: 'The server times each new ordinary or sequential Richese bidding opportunity and uses the native pass when its ten seconds expire.',
steps: [
'Watch the persistent ordinary-auction countdown. Bid or pass before it reaches zero; server time decides the deadline. The local display never submits a pass.',
'Watch the persistent countdown for ordinary, Once Around and normal Black Market bidding. Bid or pass before it reaches zero; server time decides the deadline. The local display never submits a pass.',
'Inspection, Truthtrance, cancellation, payment and other current native decisions hold the remaining budget. Unrelated polls do not grant more time. The next actual bidder receives ten seconds.',
'If passing would violate a binding Truthtrance answer, the explicit online convention submits only its required legal bid. Normal payment is owed if that bid wins; no extra strategic bid is chosen.',
'Administrative room-control or closure changes conservatively re-arm ten seconds once play resumes, including pauses no client observed. Paused, closed and removed rooms do not progress.',
'An already-open older unstamped lot keeps its saved untimed protocol; the next new native lot is timed. Pure offline games do not use wall time. A stored online deadline is evaluated on the next authenticated request after a stopped host.',
'Silent and Once-Around procedures retain their own timing. Full special-auction, browser, deployed and comprehensive acceptance remain unfinished.',
'Once Around timing is a provisional application of the normal bid deadline. Silent auctions remain untimed. Full special-auction, browser, deployed and comprehensive acceptance remain unfinished.',
],
related: ['bidding', 'truthtrance-bid', 'truthtrance-turn-actions', 'ai-pacing'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Versioned native source, trusted clock, held budget, canonical pass and required-Truth bid use the original room and native rule paths.', evidence: ['game/auction-clock.ts', 'game/engine.ts', 'db/rooms.ts', 'db/room-continuation.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Persistent countdown distinguishes starting, running, interrupted, paused and server-resolution states; no client action is dispatched.', evidence: ['components/auction-countdown.tsx', 'components/game-table.tsx'] },
{ area: 'Player controls', status: 'Partial', detail: 'Persistent countdowns distinguish starting, running, interrupted, paused and server-resolution states for ordinary and sequential Richese lots; no client action is dispatched.', evidence: ['components/auction-countdown.tsx', 'components/richese-auction-countdown.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Existing online AI pacing runs alongside the deadline; no strategy refinement or calibration is added.', evidence: ['db/room-continuation.ts', 'db/rooms.ts', 'tests/bot-pacing.test.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'Printed Basic/Advanced source, provisional online conventions and legacy timing boundaries are explicit.', evidence: ['docs/AUCTION_TIME_LIMIT.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused helper, native, display and trusted-room clock tests; final hosted and full-mode acceptance remains.', evidence: ['tests/auction-clock.test.ts', 'tests/auction-clock-runtime.test.ts', 'tests/auction-countdown.test.tsx', 'tests/rooms-auction-clock.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused helper, native, display, trusted-room and dedicated HTTP clock tests; final hosted and full-mode acceptance remains.', evidence: ['tests/auction-clock.test.ts', 'tests/auction-clock-runtime.test.ts', 'tests/auction-countdown.test.tsx', 'tests/rooms-auction-clock.test.ts', 'tests/richese-auction-clock-runtime.test.ts', 'tests/richese-auction-countdown.test.tsx', 'tests/rooms-richese-auction-clock.test.ts', 'tests/richese-auction-clock-http.test.ts'] },
],
},
{
+2
View File
@@ -11,6 +11,8 @@ export type RicheseAuction = {
source: RicheseAuctionSource;
owner: string;
method: RicheseAuctionMethod;
/** Fresh sequential lots opt into trusted room deadlines. Older saves omit it. */
clockVersion?: 1;
eligible: string[];
order: string[];
/** Original native normal-lot opener; Sapho changes this lot's cycle only. */