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
+80
View File
@@ -0,0 +1,80 @@
// Computed achievements derived from bot stats + fight history
// No schema changes — achievements are calculated on the fly
export interface Achievement {
id: string
name: string
description: string
icon: string
}
interface BotStats {
wins: number
losses: number
winStreak: number
bestStreak: number
tier: number
eloRating: number
}
interface FightRecord {
winnerId: string | null
totalRounds: number
botAHp: number
botBHp: number
botAId: string
botBId: string
}
export function computeAchievements(botId: string, stats: BotStats, fights: FightRecord[]): Achievement[] {
const earned: Achievement[] = []
const total = stats.wins + stats.losses
// Win milestones
if (stats.wins >= 1) earned.push({ id: 'first_blood', name: 'First Blood', description: 'Win your first fight', icon: '🩸' })
if (stats.wins >= 10) earned.push({ id: 'veteran', name: 'Veteran', description: 'Win 10 fights', icon: '⚔️' })
if (stats.wins >= 50) earned.push({ id: 'warlord', name: 'Warlord', description: 'Win 50 fights', icon: '👑' })
if (stats.wins >= 100) earned.push({ id: 'centurion', name: 'Centurion', description: 'Win 100 fights', icon: '🏛️' })
// Streak achievements
if (stats.bestStreak >= 3) earned.push({ id: 'hot_streak', name: 'Hot Streak', description: 'Win 3 fights in a row', icon: '🔥' })
if (stats.bestStreak >= 5) earned.push({ id: 'unstoppable', name: 'Unstoppable', description: 'Win 5 fights in a row', icon: '💪' })
if (stats.bestStreak >= 10) earned.push({ id: 'juggernaut', name: 'Juggernaut', description: 'Win 10 fights in a row', icon: '🚂' })
// Tier achievements
if (stats.tier >= 2) earned.push({ id: 'silver', name: 'Silver League', description: 'Reach Silver tier', icon: '🥈' })
if (stats.tier >= 4) earned.push({ id: 'platinum', name: 'Platinum League', description: 'Reach Platinum tier', icon: '💎' })
if (stats.tier >= 6) earned.push({ id: 'legend', name: 'Legend', description: 'Reach Legend tier', icon: '🌟' })
// ELO achievements
if (stats.eloRating >= 1500) earned.push({ id: 'elite', name: 'Elite', description: 'Reach 1500 ELO', icon: '📈' })
if (stats.eloRating >= 2000) earned.push({ id: 'grandmaster', name: 'Grandmaster', description: 'Reach 2000 ELO', icon: '🏆' })
// Fight-history based achievements
const perfectWins = fights.filter(f => {
if (f.winnerId !== botId) return false
const isA = f.botAId === botId
const loserHp = isA ? f.botBHp : f.botAHp
return loserHp <= 0
}).length
if (perfectWins >= 1) earned.push({ id: 'perfect', name: 'Flawless', description: 'Win with a perfect finish', icon: '✨' })
if (perfectWins >= 5) earned.push({ id: 'perfect_5', name: 'Perfectionist', description: 'Get 5 perfect finishes', icon: '💯' })
const koWins = fights.filter(f => {
if (f.winnerId !== botId) return false
return f.totalRounds < 10
}).length
if (koWins >= 1) earned.push({ id: 'ko', name: 'Knockout Artist', description: 'Win by KO', icon: '💥' })
if (koWins >= 10) earned.push({ id: 'ko_10', name: 'One-Punch Bot', description: 'Get 10 KO wins', icon: '🥊' })
// Resilience
if (stats.losses >= 10 && stats.wins > stats.losses) earned.push({ id: 'comeback_kid', name: 'Comeback Kid', description: 'Win more than you lose after 10+ losses', icon: '🐛' })
// Activity
if (total >= 50) earned.push({ id: 'grinder', name: 'Grinder', description: 'Fight 50 total battles', icon: '⚙️' })
if (total >= 100) earned.push({ id: 'gladiator', name: 'Gladiator', description: 'Fight 100 total battles', icon: '🗡️' })
return earned
}