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 { 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 displayPrompt?: string } 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) } // ═══════════════════════════════════════════════ // Challenge selection + generation // ═══════════════════════════════════════════════ function shuffleArray(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: 30% bitcoin, 20% conspiracy, 20% pc_culture, 30% bot_coding const THEME_WEIGHTS: Record = { bitcoin: 0.3, conspiracy: 0.2, pc_culture: 0.2, bot_coding: 0.3, } 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, _arenaModifier: string | null, themeBias?: PromptTheme, roundNumber?: number): Challenge { let available = TEMPLATES.filter(t => !usedTypes.has(t.type)) if (available.length === 0) available = TEMPLATES // Bias toward factual challenges (70% factual, 30% creative) const factual = available.filter(t => t.scoring === 'factual') const creative = available.filter(t => t.scoring === 'creative') let pool: ChallengeTemplate[] if (factual.length > 0 && creative.length > 0) { pool = Math.random() < 0.7 ? factual : creative } else { pool = available } const template = pick(pool) const targetTheme = themeBias || pickTheme() const targetDifficulty = roundNumber ? roundToDifficulty(roundNumber) : undefined return templateToChallenge(template, targetTheme, targetDifficulty) } /** Ranked challenge: no multiple choice, only harder creative/open-ended prompts */ export function pickRankedChallenge(usedTypes: Set): Challenge { let available = TEMPLATES.filter(t => !usedTypes.has(t.type)) if (available.length === 0) available = TEMPLATES // Filter to creative-only (open-ended, harder) — no factual multiple choice const creative = available.filter(t => t.scoring === 'creative') const pool = creative.length > 0 ? creative : available const template = pick(pool) // Pick a prompt that has no choices array (open-ended) const openPrompts = template.prompts.filter(p => !p.choices) const prompts = openPrompts.length > 0 ? openPrompts : template.prompts const entry = pick(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): 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 let choices: string[] | undefined if (entry.choices) { choices = shuffleArray(entry.choices) } else if (entry.answers?.length === 1 && ['true', 'false'].includes(entry.answers[0].toLowerCase())) { // Auto-generate True/False choices for boolean questions 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) }