feat: v5 — boxing poster fight cards, 12-char names, diverse mock bots
- Fight Card page: dramatic poster background with cross-hatch, spotlights, vignettes, corner brackets, scan lines; 3D VS orb with punch animation; selectable undercard with main event always pinned at top - PosterSprite: high-quality 480px poster frame with 6-pass renderer (aura, glow, bevel, specular, particles); PixelGlove component - 12-char bot name limit across all forms and server validation - Mock bots: all 100 now have diverse archetypes (25 types), 25% human fighters; seedMockBots updates existing bots on restart - Leaderboard: inline SpritePreview next to each bot name - Nostr auth: persistent login, nsec copy button - Wallet: NWC + Lightning Address, ranked fight flow - Server: payments, ranked queue, customization endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ccf4196647
commit
f6eb7d2845
@@ -5,7 +5,7 @@ import { randomArena, type Arena } from './arenas.js'
|
||||
import { pickChallenge, type Challenge } from './challenges.js'
|
||||
import { scoreRound, calculateElo, calculateTier } from './scoring.js'
|
||||
import { fightEvents } from './events.js'
|
||||
import { generateMockBotResponse } from './mock.js'
|
||||
import { generateMockBotResponse, isClassicBot, generateClassicBotResponse } from './mock.js'
|
||||
import { setCooldown } from './queue.js'
|
||||
import { isHumanPlayer, waitForHumanResponse } from './human-responses.js'
|
||||
import { payWinner, refundEntry, ENTRY_FEE_SATS } from './payments.js'
|
||||
@@ -222,6 +222,18 @@ async function getBotResponse(
|
||||
return { answer: result.answer, trashTalk: result.trashTalk, timeMs: elapsed, timedOut: result.timedOut, error: false }
|
||||
}
|
||||
|
||||
if (isClassicBot(bot.webhookUrl)) {
|
||||
console.log(`[fight] ${bot.name} is classic bot, generating response`)
|
||||
const classic = generateClassicBotResponse(challenge, bot.name)
|
||||
return {
|
||||
answer: classic.answer || null,
|
||||
trashTalk: '',
|
||||
timeMs: classic.timeMs,
|
||||
timedOut: classic.timedOut,
|
||||
error: classic.error,
|
||||
}
|
||||
}
|
||||
|
||||
if (isMockBot(bot.webhookUrl)) {
|
||||
console.log(`[fight] ${bot.name} is mock bot, generating response`)
|
||||
const mock = generateMockBotResponse(challenge, bot.name)
|
||||
@@ -273,7 +285,7 @@ async function createFightRecord(botA: BotRecord, botB: BotRecord, arena: Arena,
|
||||
|
||||
// Track webhook errors per bot
|
||||
async function trackWebhookResult(botId: string, webhookUrl: string, succeeded: boolean) {
|
||||
if (isMockBot(webhookUrl) || isHumanPlayer(webhookUrl)) return
|
||||
if (isMockBot(webhookUrl) || isClassicBot(webhookUrl) || isHumanPlayer(webhookUrl)) return
|
||||
if (succeeded) {
|
||||
await db.update(schema.bots).set({ consecutiveErrors: 0 }).where(eq(schema.bots.id, botId))
|
||||
} else {
|
||||
@@ -404,8 +416,8 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
)
|
||||
|
||||
// Finalize fight + update bot stats atomically
|
||||
const isMockFight = isMockBot(botA.webhookUrl) || isMockBot(botB.webhookUrl)
|
||||
const kFactor = isMockFight ? 12 : 32 // Dampened Elo for mock fights
|
||||
const isMockFight = isMockBot(botA.webhookUrl) || isMockBot(botB.webhookUrl) || isClassicBot(botA.webhookUrl) || isClassicBot(botB.webhookUrl)
|
||||
const kFactor = isMockFight ? 12 : 32 // Dampened Elo for mock/classic fights
|
||||
|
||||
const finalize = sqlite.transaction(() => {
|
||||
// Mark fight finished
|
||||
@@ -471,11 +483,17 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
|
||||
// Ranked fight payout
|
||||
if (mode === 'ranked') {
|
||||
if (winnerId) {
|
||||
payWinner(fightId, winnerId).catch(err => {
|
||||
// Dev mode: always pay the human bot (not mock), regardless of win/loss
|
||||
const devMode = process.env.NODE_ENV !== 'production'
|
||||
const isMockA = botA.webhookUrl.startsWith('http://mock.local')
|
||||
const isMockB = botB.webhookUrl.startsWith('http://mock.local')
|
||||
const humanBotId = devMode ? (isMockA ? botB.id : isMockB ? botA.id : winnerId) : winnerId
|
||||
|
||||
if (humanBotId) {
|
||||
payWinner(fightId, humanBotId).catch(err => {
|
||||
console.error(`[payments] payout failed for fight ${fightId}:`, err)
|
||||
})
|
||||
} else {
|
||||
} else if (!winnerId) {
|
||||
// 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'`)
|
||||
|
||||
Reference in New Issue
Block a user