Files
botfights/server/src/engine/challenges.ts
T
2026-03-11 08:35:03 +00:00

153 lines
5.1 KiB
TypeScript

import { TEMPLATES, type ChallengeTemplate, type PromptEntry, type PromptTheme, type PromptDifficulty } from './challenge-data.js'
import { EXTRA_PROMPTS } from './challenges-extra.js'
import { BITCOIN_PROMPTS } from './challenges-bitcoin.js'
import { CONSPIRACY_PROMPTS } from './challenges-conspiracy.js'
import { PC_PROMPTS } from './challenges-pc.js'
import { VIBE_PROMPTS } from './challenges-vibe.js'
import { pick } from '../lib/utils.js'
export interface Challenge {
type: string
label: string
prompt: string
answers?: string[]
choices?: string[]
timeout_ms: number
scoring: 'factual' | 'creative'
baseDamage: number
}
export type { ChallengeTemplate, PromptEntry, PromptTheme, PromptDifficulty }
// Merge extra prompts into templates
for (const t of TEMPLATES) {
const extras = EXTRA_PROMPTS[t.type]
if (extras) t.prompts.push(...extras)
const btc = BITCOIN_PROMPTS[t.type]
if (btc) t.prompts.push(...btc)
const conspiracy = CONSPIRACY_PROMPTS[t.type]
if (conspiracy) t.prompts.push(...conspiracy)
const pc = PC_PROMPTS[t.type]
if (pc) t.prompts.push(...pc)
const vibe = VIBE_PROMPTS[t.type]
if (vibe) t.prompts.push(...vibe)
}
// ═══════════════════════════════════════════════
// Challenge selection + generation
// ═══════════════════════════════════════════════
function shuffleArray<T>(arr: T[]): T[] {
const s = [...arr]
for (let i = s.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[s[i], s[j]] = [s[j], s[i]]
}
return s
}
// Target theme distribution: 50% bitcoin, 15% conspiracy, 15% vibe_coding, 10% pc_culture, 10% bot_coding
const THEME_WEIGHTS: Record<PromptTheme, number> = {
bitcoin: 0.5,
conspiracy: 0.15,
vibe_coding: 0.15,
pc_culture: 0.1,
bot_coding: 0.1,
}
function pickTheme(): PromptTheme | undefined {
const roll = Math.random()
let cumulative = 0
for (const [theme, weight] of Object.entries(THEME_WEIGHTS)) {
cumulative += weight
if (roll < cumulative) return theme as PromptTheme
}
return undefined
}
/** Map round number to target difficulty: Round 1-2 easy, 3-4 medium, 5+ hard */
export function roundToDifficulty(round: number): PromptDifficulty {
if (round <= 2) return 'easy'
if (round <= 4) return 'medium'
return 'hard'
}
export function pickChallenge(usedTypes: Set<string>, _arenaModifier: string | null, themeBias?: PromptTheme, roundNumber?: number, forHuman = false): Challenge {
let available = TEMPLATES.filter(t => !usedTypes.has(t.type))
if (available.length === 0) available = TEMPLATES
const template = pick(available)
const targetTheme = themeBias || pickTheme()
const targetDifficulty = roundNumber ? roundToDifficulty(roundNumber) : undefined
return templateToChallenge(template, targetTheme, targetDifficulty, forHuman)
}
/** Ranked challenge: no multiple choice, bots answer via webhook */
export function pickRankedChallenge(usedTypes: Set<string>): Challenge {
let available = TEMPLATES.filter(t => !usedTypes.has(t.type))
if (available.length === 0) available = TEMPLATES
const template = pick(available)
const entry = pick(template.prompts)
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, targetTheme?: PromptTheme, targetDifficulty?: PromptDifficulty, forHuman = false): Challenge {
// Prefer prompts matching target theme if any are tagged
let prompts = template.prompts
if (targetTheme) {
const themed = prompts.filter(p => p.theme === targetTheme)
if (themed.length > 0) prompts = themed
}
// Prefer prompts matching target difficulty if any are tagged
if (targetDifficulty) {
const byDifficulty = prompts.filter(p => p.difficulty === targetDifficulty)
if (byDifficulty.length > 0) prompts = byDifficulty
}
const entry = pick(prompts)
// Determine choices — only for human fights (bots answer via webhook)
let choices: string[] | undefined
if (forHuman) {
if (entry.choices) {
choices = shuffleArray(entry.choices)
} else if (entry.answers?.length === 1 && ['true', 'false'].includes(entry.answers[0].toLowerCase())) {
choices = shuffleArray(['True', 'False'])
}
}
return {
type: template.type,
label: template.label,
prompt: entry.prompt,
answers: entry.answers,
choices,
timeout_ms: template.timeout_ms,
scoring: template.scoring,
baseDamage: template.baseDamage,
}
}
export function getAllChallengeTypes(): string[] {
return TEMPLATES.map(t => t.type)
}
export function getAnswerPool(type: string): string[] {
const template = TEMPLATES.find(t => t.type === type)
if (!template) return []
return template.prompts
.flatMap(p => p.answers || [])
.filter((v, i, arr) => arr.indexOf(v) === i)
}