diff --git a/server/src/engine/challenge-data.ts b/server/src/engine/challenge-data.ts index 447dbd4..9c7b3d1 100644 --- a/server/src/engine/challenge-data.ts +++ b/server/src/engine/challenge-data.ts @@ -1,12 +1,14 @@ // Challenge prompt data — separated from challenge logic export type PromptTheme = 'bitcoin' | 'conspiracy' | 'pc_culture' | 'bot_coding' +export type PromptDifficulty = 'easy' | 'medium' | 'hard' export interface PromptEntry { prompt: string answers?: string[] choices?: string[] theme?: PromptTheme + difficulty?: PromptDifficulty } export interface ChallengeTemplate { diff --git a/server/src/engine/challenges.test.ts b/server/src/engine/challenges.test.ts index 11c8463..915a388 100644 --- a/server/src/engine/challenges.test.ts +++ b/server/src/engine/challenges.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { pickChallenge, pickRankedChallenge, getAllChallengeTypes, getAnswerPool } from './challenges.js' +import { pickChallenge, pickRankedChallenge, getAllChallengeTypes, getAnswerPool, roundToDifficulty } from './challenges.js' describe('pickChallenge', () => { it('returns a valid challenge', () => { @@ -172,6 +172,45 @@ describe('getAnswerPool', () => { }) }) +describe('roundToDifficulty', () => { + it('rounds 1-2 are easy', () => { + expect(roundToDifficulty(1)).toBe('easy') + expect(roundToDifficulty(2)).toBe('easy') + }) + + it('rounds 3-4 are medium', () => { + expect(roundToDifficulty(3)).toBe('medium') + expect(roundToDifficulty(4)).toBe('medium') + }) + + it('rounds 5+ are hard', () => { + expect(roundToDifficulty(5)).toBe('hard') + expect(roundToDifficulty(6)).toBe('hard') + expect(roundToDifficulty(10)).toBe('hard') + }) +}) + +describe('pickChallenge with roundNumber', () => { + it('accepts roundNumber parameter without error', () => { + const c = pickChallenge(new Set(), null, undefined, 1) + expect(c).toBeTruthy() + expect(c.prompt).toBeTruthy() + }) + + it('works for all round numbers 1-10', () => { + for (let r = 1; r <= 10; r++) { + const c = pickChallenge(new Set(), null, undefined, r) + expect(c.type).toBeTruthy() + expect(c.prompt).toBeTruthy() + } + }) + + it('still works without roundNumber (backward compatible)', () => { + const c = pickChallenge(new Set(), null) + expect(c).toBeTruthy() + }) +}) + describe('prompt data integrity', () => { it('all 800 prompts are accessible via pickChallenge', () => { // Run enough picks to verify the system works diff --git a/server/src/engine/challenges.ts b/server/src/engine/challenges.ts index d00490e..38c1d4f 100644 --- a/server/src/engine/challenges.ts +++ b/server/src/engine/challenges.ts @@ -1,4 +1,4 @@ -import { TEMPLATES, type ChallengeTemplate, type PromptEntry, type PromptTheme } from './challenge-data.js' +import { TEMPLATES, type ChallengeTemplate, type PromptEntry, type PromptTheme, type PromptDifficulty } from './challenge-data.js' import { EXTRA_PROMPTS } from './challenges-extra.js' import { pick } from '../lib/utils.js' @@ -14,7 +14,7 @@ export interface Challenge { displayPrompt?: string } -export type { ChallengeTemplate, PromptEntry, PromptTheme } +export type { ChallengeTemplate, PromptEntry, PromptTheme, PromptDifficulty } // Merge extra prompts into templates @@ -54,7 +54,14 @@ function pickTheme(): PromptTheme | undefined { return undefined } -export function pickChallenge(usedTypes: Set, _arenaModifier: string | null, themeBias?: PromptTheme): Challenge { +/** 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 @@ -71,7 +78,8 @@ export function pickChallenge(usedTypes: Set, _arenaModifier: string | n const template = pick(pool) const targetTheme = themeBias || pickTheme() - return templateToChallenge(template, targetTheme) + const targetDifficulty = roundNumber ? roundToDifficulty(roundNumber) : undefined + return templateToChallenge(template, targetTheme, targetDifficulty) } /** Ranked challenge: no multiple choice, only harder creative/open-ended prompts */ @@ -102,13 +110,18 @@ export function pickRankedChallenge(usedTypes: Set): Challenge { } } -function templateToChallenge(template: ChallengeTemplate, targetTheme?: PromptTheme): Challenge { +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 = template.prompts.filter(p => p.theme === 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 diff --git a/server/src/engine/orchestrator.ts b/server/src/engine/orchestrator.ts index a374a91..edd54fd 100644 --- a/server/src/engine/orchestrator.ts +++ b/server/src/engine/orchestrator.ts @@ -360,7 +360,7 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec lastRound = round const challenge = round === retroRound ? generateRetroChallenge() - : pickChallenge(usedTypes, arena.modifier) + : pickChallenge(usedTypes, arena.modifier, undefined, round) usedTypes.add(challenge.type) emit(fightId, 'round_start', {