feat: ranked queue Elo-bracket matchmaking and harder prompts
Elo-bracket matchmaking: prefer ±200, widen by 100 every 15s of waiting. Add pickRankedChallenge() that filters to creative/open-ended only, never multiple choice. Show ranked queue status, wait estimate, and "HARDER PROMPTS" notice on JoinBoutPage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e229bfb8fc
commit
156c727f82
@@ -1011,6 +1011,34 @@ export function pickChallenge(usedTypes: Set<string>, _arenaModifier: string | n
|
||||
return templateToChallenge(template)
|
||||
}
|
||||
|
||||
/** Ranked challenge: no multiple choice, only harder creative/open-ended prompts */
|
||||
export function pickRankedChallenge(usedTypes: Set<string>): Challenge {
|
||||
let available = TEMPLATES.filter(t => !usedTypes.has(t.type))
|
||||
if (available.length === 0) available = TEMPLATES
|
||||
|
||||
// Filter to creative-only (open-ended, harder) — no factual multiple choice
|
||||
const creative = available.filter(t => t.scoring === 'creative')
|
||||
const pool = creative.length > 0 ? creative : available
|
||||
|
||||
const template = pool[Math.floor(Math.random() * pool.length)]
|
||||
|
||||
// Pick a prompt that has no choices array (open-ended)
|
||||
const openPrompts = template.prompts.filter(p => !p.choices)
|
||||
const prompts = openPrompts.length > 0 ? openPrompts : template.prompts
|
||||
const entry = prompts[Math.floor(Math.random() * prompts.length)]
|
||||
|
||||
return {
|
||||
type: template.type,
|
||||
label: template.label,
|
||||
prompt: entry.prompt,
|
||||
answers: entry.answers,
|
||||
choices: undefined, // Never multiple choice in ranked
|
||||
timeout_ms: template.timeout_ms,
|
||||
scoring: template.scoring,
|
||||
baseDamage: template.baseDamage,
|
||||
}
|
||||
}
|
||||
|
||||
function templateToChallenge(template: ChallengeTemplate): Challenge {
|
||||
const entry = template.prompts[Math.floor(Math.random() * template.prompts.length)]
|
||||
|
||||
|
||||
@@ -26,8 +26,11 @@ export function setRankedCooldown(botId: string) {
|
||||
rankedCooldowns.set(botId, Date.now() + COOLDOWN_MS)
|
||||
}
|
||||
|
||||
export function getRankedQueueStatus(): { waiting: number } {
|
||||
return { waiting: rankedQueue.length }
|
||||
export function getRankedQueueStatus(): { waiting: number; estimatedWaitSec: number } {
|
||||
const waiting = rankedQueue.length
|
||||
// If someone is waiting, new joiner matches instantly; otherwise estimate ~15s
|
||||
const estimatedWaitSec = waiting > 0 ? 5 : 15
|
||||
return { waiting, estimatedWaitSec }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,21 +97,38 @@ export async function joinRankedQueue(botId: string, paymentId: string): Promise
|
||||
old.reject(new Error('Rejoined ranked queue'))
|
||||
}
|
||||
|
||||
// Check if someone is already waiting — instant match by closest ELO
|
||||
// Elo-bracket matchmaking: prefer opponents within bracket, widen over time
|
||||
if (rankedQueue.length > 0) {
|
||||
rankedQueue.sort((a, b) => {
|
||||
const diffA = Math.abs(a.eloRating - bot.eloRating)
|
||||
const diffB = Math.abs(b.eloRating - bot.eloRating)
|
||||
return diffA - diffB
|
||||
})
|
||||
const now = Date.now()
|
||||
// Find best match: base bracket ±200, widens by 100 every 15s of waiting
|
||||
let bestMatch: RankedQueueEntry | null = null
|
||||
let bestDiff = Infinity
|
||||
|
||||
const opponent = rankedQueue.shift()!
|
||||
clearTimeout(opponent.timeoutHandle)
|
||||
for (const entry of rankedQueue) {
|
||||
const waitSec = (now - entry.joinedAt) / 1000
|
||||
const bracket = 200 + Math.floor(waitSec / 15) * 100
|
||||
const eloDiff = Math.abs(entry.eloRating - bot.eloRating)
|
||||
if (eloDiff <= bracket && eloDiff < bestDiff) {
|
||||
bestMatch = entry
|
||||
bestDiff = eloDiff
|
||||
}
|
||||
}
|
||||
|
||||
// Start ranked fight and link both payments
|
||||
const fightId = await runFightAsync(opponent.botId, botId, 'ranked')
|
||||
await linkPaymentsToFight(fightId, [opponent.paymentId, paymentId])
|
||||
opponent.resolve(fightId)
|
||||
// If no one in bracket, fall back to closest overall
|
||||
if (!bestMatch) {
|
||||
rankedQueue.sort((a, b) =>
|
||||
Math.abs(a.eloRating - bot.eloRating) - Math.abs(b.eloRating - bot.eloRating)
|
||||
)
|
||||
bestMatch = rankedQueue[0]
|
||||
}
|
||||
|
||||
const idx = rankedQueue.indexOf(bestMatch)
|
||||
rankedQueue.splice(idx, 1)
|
||||
clearTimeout(bestMatch.timeoutHandle)
|
||||
|
||||
const fightId = await runFightAsync(bestMatch.botId, botId, 'ranked')
|
||||
await linkPaymentsToFight(fightId, [bestMatch.paymentId, paymentId])
|
||||
bestMatch.resolve(fightId)
|
||||
return fightId
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user