feat: add difficulty field to prompts and wire into fight pacing
Round 1-2 picks easy prompts, 3-4 medium, 5+ hard (when tagged). Graceful fallback to any prompt when no difficulty-tagged prompts exist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8d0eb4b5f9
commit
a4b4963c32
@@ -1,12 +1,14 @@
|
|||||||
// Challenge prompt data — separated from challenge logic
|
// Challenge prompt data — separated from challenge logic
|
||||||
|
|
||||||
export type PromptTheme = 'bitcoin' | 'conspiracy' | 'pc_culture' | 'bot_coding'
|
export type PromptTheme = 'bitcoin' | 'conspiracy' | 'pc_culture' | 'bot_coding'
|
||||||
|
export type PromptDifficulty = 'easy' | 'medium' | 'hard'
|
||||||
|
|
||||||
export interface PromptEntry {
|
export interface PromptEntry {
|
||||||
prompt: string
|
prompt: string
|
||||||
answers?: string[]
|
answers?: string[]
|
||||||
choices?: string[]
|
choices?: string[]
|
||||||
theme?: PromptTheme
|
theme?: PromptTheme
|
||||||
|
difficulty?: PromptDifficulty
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChallengeTemplate {
|
export interface ChallengeTemplate {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
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', () => {
|
describe('pickChallenge', () => {
|
||||||
it('returns a valid challenge', () => {
|
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', () => {
|
describe('prompt data integrity', () => {
|
||||||
it('all 800 prompts are accessible via pickChallenge', () => {
|
it('all 800 prompts are accessible via pickChallenge', () => {
|
||||||
// Run enough picks to verify the system works
|
// Run enough picks to verify the system works
|
||||||
|
|||||||
@@ -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 { EXTRA_PROMPTS } from './challenges-extra.js'
|
||||||
import { pick } from '../lib/utils.js'
|
import { pick } from '../lib/utils.js'
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export interface Challenge {
|
|||||||
displayPrompt?: string
|
displayPrompt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { ChallengeTemplate, PromptEntry, PromptTheme }
|
export type { ChallengeTemplate, PromptEntry, PromptTheme, PromptDifficulty }
|
||||||
|
|
||||||
|
|
||||||
// Merge extra prompts into templates
|
// Merge extra prompts into templates
|
||||||
@@ -54,7 +54,14 @@ function pickTheme(): PromptTheme | undefined {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickChallenge(usedTypes: Set<string>, _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<string>, _arenaModifier: string | null, themeBias?: PromptTheme, roundNumber?: number): Challenge {
|
||||||
let available = TEMPLATES.filter(t => !usedTypes.has(t.type))
|
let available = TEMPLATES.filter(t => !usedTypes.has(t.type))
|
||||||
if (available.length === 0) available = TEMPLATES
|
if (available.length === 0) available = TEMPLATES
|
||||||
|
|
||||||
@@ -71,7 +78,8 @@ export function pickChallenge(usedTypes: Set<string>, _arenaModifier: string | n
|
|||||||
|
|
||||||
const template = pick(pool)
|
const template = pick(pool)
|
||||||
const targetTheme = themeBias || pickTheme()
|
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 */
|
/** Ranked challenge: no multiple choice, only harder creative/open-ended prompts */
|
||||||
@@ -102,13 +110,18 @@ export function pickRankedChallenge(usedTypes: Set<string>): 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
|
// Prefer prompts matching target theme if any are tagged
|
||||||
let prompts = template.prompts
|
let prompts = template.prompts
|
||||||
if (targetTheme) {
|
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
|
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)
|
const entry = pick(prompts)
|
||||||
|
|
||||||
// Determine choices
|
// Determine choices
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
|||||||
lastRound = round
|
lastRound = round
|
||||||
const challenge = round === retroRound
|
const challenge = round === retroRound
|
||||||
? generateRetroChallenge()
|
? generateRetroChallenge()
|
||||||
: pickChallenge(usedTypes, arena.modifier)
|
: pickChallenge(usedTypes, arena.modifier, undefined, round)
|
||||||
usedTypes.add(challenge.type)
|
usedTypes.add(challenge.type)
|
||||||
|
|
||||||
emit(fightId, 'round_start', {
|
emit(fightId, 'round_start', {
|
||||||
|
|||||||
Reference in New Issue
Block a user