feat: ranked queue + orchestrator payout wiring
Add ranked-queue.ts with 60s timeout, ELO matching, no-mock guard, and automatic refund on timeout. Wire orchestrator to accept mode param, track satsWagered in finalize transaction, trigger payWinner on ranked win and refundEntry on ranked draw. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
abe5040742
commit
e046cb13eb
@@ -8,6 +8,7 @@ import { fightEvents } from './events.js'
|
||||
import { generateMockBotResponse } from './mock.js'
|
||||
import { setCooldown } from './queue.js'
|
||||
import { isHumanPlayer, waitForHumanResponse } from './human-responses.js'
|
||||
import { payWinner, refundEntry, ENTRY_FEE_SATS } from './payments.js'
|
||||
|
||||
interface BotRecord {
|
||||
id: string
|
||||
@@ -247,7 +248,7 @@ async function loadBots(botAId: string, botBId: string): Promise<[BotRecord, Bot
|
||||
return [botARows[0] as BotRecord, botBRows[0] as BotRecord]
|
||||
}
|
||||
|
||||
async function createFightRecord(botA: BotRecord, botB: BotRecord, arena: Arena): Promise<string> {
|
||||
async function createFightRecord(botA: BotRecord, botB: BotRecord, arena: Arena, mode: 'free' | 'ranked' = 'free'): Promise<string> {
|
||||
const fightId = nanoid(12)
|
||||
const now = new Date().toISOString()
|
||||
await db.insert(schema.fights).values({
|
||||
@@ -256,6 +257,9 @@ async function createFightRecord(botA: BotRecord, botB: BotRecord, arena: Arena)
|
||||
botBId: botB.id,
|
||||
arena: arena.id,
|
||||
status: 'live',
|
||||
mode,
|
||||
potSats: mode === 'ranked' ? 42 : 0,
|
||||
payoutStatus: mode === 'ranked' ? 'pending' : undefined,
|
||||
startedAt: now,
|
||||
createdAt: now,
|
||||
})
|
||||
@@ -287,7 +291,7 @@ async function trackWebhookResult(botId: string, webhookUrl: string, succeeded:
|
||||
}
|
||||
}
|
||||
|
||||
async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRecord, arena: Arena): Promise<void> {
|
||||
async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRecord, arena: Arena, mode: 'free' | 'ranked' = 'free'): Promise<void> {
|
||||
let hpA = 200
|
||||
let hpB = 200
|
||||
let comboA = 0
|
||||
@@ -442,6 +446,16 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
db.update(schema.bots).set({ lastFightAt: new Date().toISOString() }).where(eq(schema.bots.id, botA.id)).run()
|
||||
db.update(schema.bots).set({ lastFightAt: new Date().toISOString() }).where(eq(schema.bots.id, botB.id)).run()
|
||||
}
|
||||
|
||||
// Update sats wagered for ranked fights
|
||||
if (mode === 'ranked') {
|
||||
db.update(schema.bots).set({
|
||||
satsWagered: sql`${schema.bots.satsWagered} + ${ENTRY_FEE_SATS}`,
|
||||
}).where(eq(schema.bots.id, botA.id)).run()
|
||||
db.update(schema.bots).set({
|
||||
satsWagered: sql`${schema.bots.satsWagered} + ${ENTRY_FEE_SATS}`,
|
||||
}).where(eq(schema.bots.id, botB.id)).run()
|
||||
}
|
||||
})
|
||||
|
||||
finalize()
|
||||
@@ -451,12 +465,32 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
winnerName,
|
||||
isPerfect,
|
||||
finalHp: { a: hpA, b: hpB },
|
||||
mode,
|
||||
potSats: mode === 'ranked' ? 42 : 0,
|
||||
})
|
||||
|
||||
// Ranked fight payout
|
||||
if (mode === 'ranked') {
|
||||
if (winnerId) {
|
||||
payWinner(fightId, winnerId).catch(err => {
|
||||
console.error(`[payments] payout failed for fight ${fightId}:`, err)
|
||||
})
|
||||
} else {
|
||||
// Draw — refund both entry fees
|
||||
const entryPayments = await db.select().from(schema.payments)
|
||||
.where(sql`${schema.payments.fightId} = ${fightId} AND ${schema.payments.direction} = 'in' AND ${schema.payments.status} = 'confirmed'`)
|
||||
for (const payment of entryPayments) {
|
||||
refundEntry(payment.id).catch(err => {
|
||||
console.error(`[payments] draw refund failed for ${payment.id}:`, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fightEvents.cleanup(fightId)
|
||||
}
|
||||
|
||||
export async function runFight(botAId: string, botBId: string): Promise<string> {
|
||||
export async function runFight(botAId: string, botBId: string, mode: 'free' | 'ranked' = 'free'): Promise<string> {
|
||||
if (botAId === botBId) throw new Error('A bot cannot fight itself')
|
||||
if (activeFighters.has(botAId)) throw new Error(`Bot ${botAId} is already in a fight`)
|
||||
if (activeFighters.has(botBId)) throw new Error(`Bot ${botBId} is already in a fight`)
|
||||
@@ -467,8 +501,8 @@ export async function runFight(botAId: string, botBId: string): Promise<string>
|
||||
try {
|
||||
const [botA, botB] = await loadBots(botAId, botBId)
|
||||
const arena = randomArena()
|
||||
const fightId = await createFightRecord(botA, botB, arena)
|
||||
await executeFightRounds(fightId, botA, botB, arena)
|
||||
const fightId = await createFightRecord(botA, botB, arena, mode)
|
||||
await executeFightRounds(fightId, botA, botB, arena, mode)
|
||||
return fightId
|
||||
} finally {
|
||||
activeFighters.delete(botAId)
|
||||
@@ -479,7 +513,7 @@ export async function runFight(botAId: string, botBId: string): Promise<string>
|
||||
}
|
||||
|
||||
/** Creates the fight record and returns the ID immediately. Rounds run in background. */
|
||||
export async function runFightAsync(botAId: string, botBId: string): Promise<string> {
|
||||
export async function runFightAsync(botAId: string, botBId: string, mode: 'free' | 'ranked' = 'free'): Promise<string> {
|
||||
if (botAId === botBId) throw new Error('A bot cannot fight itself')
|
||||
if (activeFighters.has(botAId)) throw new Error(`Bot ${botAId} is already in a fight`)
|
||||
if (activeFighters.has(botBId)) throw new Error(`Bot ${botBId} is already in a fight`)
|
||||
@@ -489,9 +523,9 @@ export async function runFightAsync(botAId: string, botBId: string): Promise<str
|
||||
|
||||
const [botA, botB] = await loadBots(botAId, botBId)
|
||||
const arena = randomArena()
|
||||
const fightId = await createFightRecord(botA, botB, arena)
|
||||
const fightId = await createFightRecord(botA, botB, arena, mode)
|
||||
|
||||
executeFightRounds(fightId, botA, botB, arena)
|
||||
executeFightRounds(fightId, botA, botB, arena, mode)
|
||||
.catch(err => {
|
||||
console.error(`[botfights] fight ${fightId} error:`, err)
|
||||
// Mark fight as cancelled so it doesn't stay 'live' forever
|
||||
|
||||
Reference in New Issue
Block a user