121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
import { TEMPLATES, type ChallengeTemplate, type PromptEntry } from './challenge-data.js'
|
|
import { EXTRA_PROMPTS } from './challenges-extra.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 }
|
|
|
|
|
|
// Merge extra prompts into templates
|
|
for (const t of TEMPLATES) {
|
|
const extras = EXTRA_PROMPTS[t.type]
|
|
if (extras) t.prompts.push(...extras)
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════
|
|
// 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
|
|
}
|
|
|
|
export function pickChallenge(usedTypes: Set<string>, _arenaModifier: string | null): 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)
|
|
return templateToChallenge(template)
|
|
}
|
|
|
|
/** 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
|
|
|
|
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): Challenge {
|
|
const entry = pick(template.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)
|
|
}
|