2026-03-09 08:05:05 +00:00
|
|
|
import { TEMPLATES, type ChallengeTemplate, type PromptEntry, type PromptTheme, type PromptDifficulty } from './challenge-data.js'
|
2026-03-08 21:14:26 +00:00
|
|
|
import { EXTRA_PROMPTS } from './challenges-extra.js'
|
2026-03-09 08:15:31 +00:00
|
|
|
import { BITCOIN_PROMPTS } from './challenges-bitcoin.js'
|
2026-03-09 08:17:47 +00:00
|
|
|
import { CONSPIRACY_PROMPTS } from './challenges-conspiracy.js'
|
2026-03-09 08:20:10 +00:00
|
|
|
import { PC_PROMPTS } from './challenges-pc.js'
|
2026-03-08 21:45:20 +00:00
|
|
|
import { pick } from '../lib/utils.js'
|
2026-03-08 21:14:26 +00:00
|
|
|
|
2026-03-06 16:27:54 +00:00
|
|
|
export interface Challenge {
|
|
|
|
|
type: string
|
|
|
|
|
label: string
|
|
|
|
|
prompt: string
|
2026-03-07 21:15:42 +00:00
|
|
|
answers?: string[]
|
|
|
|
|
choices?: string[]
|
2026-03-06 16:27:54 +00:00
|
|
|
timeout_ms: number
|
2026-03-06 23:44:40 +00:00
|
|
|
scoring: 'factual' | 'creative'
|
2026-03-06 16:27:54 +00:00
|
|
|
baseDamage: number
|
2026-03-08 16:23:47 +00:00
|
|
|
displayPrompt?: string
|
2026-03-06 16:27:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:05:05 +00:00
|
|
|
export type { ChallengeTemplate, PromptEntry, PromptTheme, PromptDifficulty }
|
2026-03-06 16:27:54 +00:00
|
|
|
|
2026-03-08 21:14:26 +00:00
|
|
|
|
|
|
|
|
// Merge extra prompts into templates
|
|
|
|
|
for (const t of TEMPLATES) {
|
|
|
|
|
const extras = EXTRA_PROMPTS[t.type]
|
|
|
|
|
if (extras) t.prompts.push(...extras)
|
2026-03-09 08:15:31 +00:00
|
|
|
const btc = BITCOIN_PROMPTS[t.type]
|
|
|
|
|
if (btc) t.prompts.push(...btc)
|
2026-03-09 08:17:47 +00:00
|
|
|
const conspiracy = CONSPIRACY_PROMPTS[t.type]
|
|
|
|
|
if (conspiracy) t.prompts.push(...conspiracy)
|
2026-03-09 08:20:10 +00:00
|
|
|
const pc = PC_PROMPTS[t.type]
|
|
|
|
|
if (pc) t.prompts.push(...pc)
|
2026-03-08 21:14:26 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-07 21:15:42 +00:00
|
|
|
// ═══════════════════════════════════════════════
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 07:56:51 +00:00
|
|
|
// Target theme distribution: 30% bitcoin, 20% conspiracy, 20% pc_culture, 30% bot_coding
|
|
|
|
|
const THEME_WEIGHTS: Record<PromptTheme, number> = {
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:05:05 +00:00
|
|
|
/** 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): Challenge {
|
2026-03-06 16:27:54 +00:00
|
|
|
let available = TEMPLATES.filter(t => !usedTypes.has(t.type))
|
2026-03-06 23:44:40 +00:00
|
|
|
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
|
2026-03-06 16:27:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-08 21:45:20 +00:00
|
|
|
const template = pick(pool)
|
2026-03-09 07:56:51 +00:00
|
|
|
const targetTheme = themeBias || pickTheme()
|
2026-03-09 08:05:05 +00:00
|
|
|
const targetDifficulty = roundNumber ? roundToDifficulty(roundNumber) : undefined
|
|
|
|
|
return templateToChallenge(template, targetTheme, targetDifficulty)
|
2026-03-06 16:27:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:18:23 +00:00
|
|
|
/** Ranked challenge: no multiple choice, only harder creative/open-ended prompts */
|
|
|
|
|
export function pickRankedChallenge(usedTypes: Set<string>): 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
|
|
|
|
|
|
2026-03-08 21:45:20 +00:00
|
|
|
const template = pick(pool)
|
2026-03-08 20:18:23 +00:00
|
|
|
|
|
|
|
|
// 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
|
2026-03-08 21:45:20 +00:00
|
|
|
const entry = pick(prompts)
|
2026-03-08 20:18:23 +00:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:05:05 +00:00
|
|
|
function templateToChallenge(template: ChallengeTemplate, targetTheme?: PromptTheme, targetDifficulty?: PromptDifficulty): Challenge {
|
2026-03-09 07:56:51 +00:00
|
|
|
// Prefer prompts matching target theme if any are tagged
|
|
|
|
|
let prompts = template.prompts
|
|
|
|
|
if (targetTheme) {
|
2026-03-09 08:05:05 +00:00
|
|
|
const themed = prompts.filter(p => p.theme === targetTheme)
|
2026-03-09 07:56:51 +00:00
|
|
|
if (themed.length > 0) prompts = themed
|
|
|
|
|
}
|
2026-03-09 08:05:05 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-03-09 07:56:51 +00:00
|
|
|
const entry = pick(prompts)
|
2026-03-07 21:15:42 +00:00
|
|
|
|
|
|
|
|
// 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'])
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 16:27:54 +00:00
|
|
|
return {
|
|
|
|
|
type: template.type,
|
|
|
|
|
label: template.label,
|
2026-03-06 23:44:40 +00:00
|
|
|
prompt: entry.prompt,
|
|
|
|
|
answers: entry.answers,
|
2026-03-07 21:15:42 +00:00
|
|
|
choices,
|
2026-03-06 16:27:54 +00:00
|
|
|
timeout_ms: template.timeout_ms,
|
|
|
|
|
scoring: template.scoring,
|
|
|
|
|
baseDamage: template.baseDamage,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getAllChallengeTypes(): string[] {
|
|
|
|
|
return TEMPLATES.map(t => t.type)
|
|
|
|
|
}
|
2026-03-07 19:42:49 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|