Time sequential Richese auctions
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
e7520e8679
commit
7fc6f1cd43
15 files changed
+692
-38
No files matched your search
@@ -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
@@ -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
@@ -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'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in new issue
Block a user