feat: computed achievement system — 20 achievements derived from bot stats

Achievements computed on-the-fly from existing stats + fight history:
win milestones, streaks, tiers, ELO, perfect/KO wins, resilience, activity.
Wired into /api/bots/:name/stats endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 00:24:18 +00:00
co-authored by Claude Opus 4.6
parent 0ae3970433
commit 3b8208be08
3 changed files with 130 additions and 27 deletions
+27 -4
View File
@@ -4,6 +4,7 @@ import { createHash, randomBytes } from 'crypto'
import { db, schema } from '../db/index.js'
import { eq, or, desc, sql } from 'drizzle-orm'
import { TIER_NAMES, TIER_COLORS } from '../engine/scoring.js'
import { computeAchievements } from '../engine/achievements.js'
import { isAllowedWebhookUrl } from '../engine/orchestrator.js'
import { testWebhook } from '../engine/webhook-test.js'
import { rateLimit } from '../middleware/rate-limit.js'
@@ -180,15 +181,27 @@ botsRouter.get('/:name/stats', async (c) => {
allBots.sort((a, b) => b.eloRating - a.eloRating)
const rank = allBots.findIndex(b => b.id === bot.id) + 1
// Recent fights (last 10)
const fights = await db.select()
.from(schema.fights)
// All fights for achievements
const allFights = await db.select({
winnerId: schema.fights.winnerId,
totalRounds: schema.fights.totalRounds,
botAHp: schema.fights.botAHp,
botBHp: schema.fights.botBHp,
botAId: schema.fights.botAId,
botBId: schema.fights.botBId,
createdAt: schema.fights.createdAt,
endedAt: schema.fights.endedAt,
id: schema.fights.id,
arena: schema.fights.arena,
}).from(schema.fights)
.where(or(
eq(schema.fights.botAId, bot.id),
eq(schema.fights.botBId, bot.id),
))
.orderBy(desc(schema.fights.createdAt))
.limit(10)
// Recent fights (last 10)
const fights = allFights.slice(0, 10)
// Resolve opponent names
const opponentIds = new Set<string>()
@@ -217,6 +230,15 @@ botsRouter.get('/:name/stats', async (c) => {
}
})
const achievements = computeAchievements(bot.id, {
wins: bot.wins,
losses: bot.losses,
winStreak: bot.winStreak,
bestStreak: bot.bestStreak,
tier: bot.tier,
eloRating: bot.eloRating,
}, allFights)
return c.json({
...bot,
tierName: TIER_NAMES[bot.tier] || 'BABY',
@@ -226,6 +248,7 @@ botsRouter.get('/:name/stats', async (c) => {
rank,
totalBots: allBots.length,
recentFights,
achievements,
})
})