161 lines
7.3 KiB
TypeScript
161 lines
7.3 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|||
|
|
import { roundToDifficulty, pickChallenge, getAllChallengeTypes } from './challenges.js'
|
||
|
|
import { TEMPLATES, 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 { VIBE_PROMPTS } from './challenges-vibe.js'
|
||
|
|
import { writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
||
|
|
import { resolve, dirname } from 'node:path'
|
||
|
|
import { fileURLToPath } from 'node:url'
|
||
|
|
|
||
|
|
// Build merged templates (non-mutating copy for counting)
|
||
|
|
function buildMergedTemplates() {
|
||
|
|
return TEMPLATES.map(t => {
|
||
|
|
const allPrompts = [...t.prompts]
|
||
|
|
for (const source of [EXTRA_PROMPTS, BITCOIN_PROMPTS, CONSPIRACY_PROMPTS, PC_PROMPTS, VIBE_PROMPTS]) {
|
||
|
|
const extras = source[t.type]
|
||
|
|
if (extras) allPrompts.push(...extras)
|
||
|
|
}
|
||
|
|
return { ...t, prompts: allPrompts }
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('roundToDifficulty', () => {
|
||
|
|
it('rounds 1-2 return easy', () => {
|
||
|
|
expect(roundToDifficulty(1)).toBe('easy')
|
||
|
|
expect(roundToDifficulty(2)).toBe('easy')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('rounds 3-4 return medium', () => {
|
||
|
|
expect(roundToDifficulty(3)).toBe('medium')
|
||
|
|
expect(roundToDifficulty(4)).toBe('medium')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('rounds 5+ return hard', () => {
|
||
|
|
expect(roundToDifficulty(5)).toBe('hard')
|
||
|
|
expect(roundToDifficulty(6)).toBe('hard')
|
||
|
|
expect(roundToDifficulty(10)).toBe('hard')
|
||
|
|
expect(roundToDifficulty(20)).toBe('hard')
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('pickChallenge difficulty filtering', () => {
|
||
|
|
it('pickChallenge with roundNumber filters by difficulty when tagged prompts exist', () => {
|
||
|
|
// Pick many challenges at round 5 (hard) and check that the prompt pool was filtered
|
||
|
|
// We can't verify the internal filtering directly, but we can verify the function works
|
||
|
|
const usedTypes = new Set<string>()
|
||
|
|
for (let i = 0; i < 50; i++) {
|
||
|
|
const challenge = pickChallenge(usedTypes, null, undefined, 5)
|
||
|
|
expect(challenge).toBeDefined()
|
||
|
|
expect(challenge.prompt).toBeTruthy()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('pickChallenge without roundNumber does not filter by difficulty', () => {
|
||
|
|
const usedTypes = new Set<string>()
|
||
|
|
const challenge = pickChallenge(usedTypes, null)
|
||
|
|
expect(challenge).toBeDefined()
|
||
|
|
expect(challenge.prompt).toBeTruthy()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('pickChallenge at round 1 prefers easy prompts', () => {
|
||
|
|
const usedTypes = new Set<string>()
|
||
|
|
const challenge = pickChallenge(usedTypes, null, undefined, 1)
|
||
|
|
expect(challenge).toBeDefined()
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('difficulty distribution audit', () => {
|
||
|
|
it('counts prompts per difficulty level per challenge type', () => {
|
||
|
|
const templates = buildMergedTemplates()
|
||
|
|
const difficulties: PromptDifficulty[] = ['easy', 'medium', 'hard']
|
||
|
|
const matrix: Record<string, Record<string, number>> = {}
|
||
|
|
const typeTotals: Record<string, number> = {}
|
||
|
|
|
||
|
|
for (const t of templates) {
|
||
|
|
matrix[t.type] = { easy: 0, medium: 0, hard: 0, untagged: 0 }
|
||
|
|
typeTotals[t.type] = t.prompts.length
|
||
|
|
|
||
|
|
for (const p of t.prompts) {
|
||
|
|
if (p.difficulty && difficulties.includes(p.difficulty)) {
|
||
|
|
matrix[t.type][p.difficulty]++
|
||
|
|
} else {
|
||
|
|
matrix[t.type].untagged++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Console output
|
||
|
|
console.log('\n══════════════════════════════════════════════════════')
|
||
|
|
console.log(' DIFFICULTY DISTRIBUTION MATRIX')
|
||
|
|
console.log('══════════════════════════════════════════════════════')
|
||
|
|
const header = ' Type'.padEnd(24) + 'Easy'.padStart(8) + 'Medium'.padStart(8) + 'Hard'.padStart(8) + 'Untagged'.padStart(10) + 'Total'.padStart(8)
|
||
|
|
console.log(header)
|
||
|
|
console.log(' ' + '─'.repeat(62))
|
||
|
|
|
||
|
|
const sortedTypes = Object.keys(matrix).sort()
|
||
|
|
const globalCounts = { easy: 0, medium: 0, hard: 0, untagged: 0, total: 0 }
|
||
|
|
const typesLackingHard: string[] = []
|
||
|
|
const typesLackingMedium: string[] = []
|
||
|
|
|
||
|
|
for (const type of sortedTypes) {
|
||
|
|
const row = matrix[type]
|
||
|
|
const total = typeTotals[type]
|
||
|
|
const line = ` ${type}`.padEnd(24)
|
||
|
|
+ String(row.easy).padStart(8)
|
||
|
|
+ String(row.medium).padStart(8)
|
||
|
|
+ String(row.hard).padStart(8)
|
||
|
|
+ String(row.untagged).padStart(10)
|
||
|
|
+ String(total).padStart(8)
|
||
|
|
console.log(line)
|
||
|
|
|
||
|
|
globalCounts.easy += row.easy
|
||
|
|
globalCounts.medium += row.medium
|
||
|
|
globalCounts.hard += row.hard
|
||
|
|
globalCounts.untagged += row.untagged
|
||
|
|
globalCounts.total += total
|
||
|
|
|
||
|
|
if (row.hard === 0) typesLackingHard.push(type)
|
||
|
|
if (row.medium === 0) typesLackingMedium.push(type)
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(' ' + '─'.repeat(62))
|
||
|
|
console.log(` ${'TOTAL'.padEnd(22)}${String(globalCounts.easy).padStart(8)}${String(globalCounts.medium).padStart(8)}${String(globalCounts.hard).padStart(8)}${String(globalCounts.untagged).padStart(10)}${String(globalCounts.total).padStart(8)}`)
|
||
|
|
console.log('')
|
||
|
|
console.log(` Types lacking hard prompts: ${typesLackingHard.length > 0 ? typesLackingHard.join(', ') : 'none'}`)
|
||
|
|
console.log(` Types lacking medium prompts: ${typesLackingMedium.length > 0 ? typesLackingMedium.join(', ') : 'none'}`)
|
||
|
|
console.log('══════════════════════════════════════════════════════\n')
|
||
|
|
|
||
|
|
// Write markdown report
|
||
|
|
const __filename = fileURLToPath(import.meta.url)
|
||
|
|
const projectRoot = resolve(dirname(__filename), '..', '..', '..')
|
||
|
|
const loopDir = resolve(projectRoot, 'loop')
|
||
|
|
if (!existsSync(loopDir)) mkdirSync(loopDir, { recursive: true })
|
||
|
|
|
||
|
|
let md = `# Difficulty Distribution\n\nGenerated: ${new Date().toISOString().slice(0, 10)}\n\n`
|
||
|
|
md += `## Summary\n\n`
|
||
|
|
md += `- **Total prompts**: ${globalCounts.total}\n`
|
||
|
|
md += `- **Tagged**: ${globalCounts.easy + globalCounts.medium + globalCounts.hard} (${((globalCounts.easy + globalCounts.medium + globalCounts.hard) / globalCounts.total * 100).toFixed(1)}%)\n`
|
||
|
|
md += `- **Untagged**: ${globalCounts.untagged} (${(globalCounts.untagged / globalCounts.total * 100).toFixed(1)}%)\n\n`
|
||
|
|
md += `## Distribution\n\n`
|
||
|
|
md += `| Type | Easy | Medium | Hard | Untagged | Total |\n`
|
||
|
|
md += `|------|-----:|-------:|-----:|---------:|------:|\n`
|
||
|
|
for (const type of sortedTypes) {
|
||
|
|
const row = matrix[type]
|
||
|
|
md += `| ${type} | ${row.easy} | ${row.medium} | ${row.hard} | ${row.untagged} | ${typeTotals[type]} |\n`
|
||
|
|
}
|
||
|
|
md += `| **TOTAL** | **${globalCounts.easy}** | **${globalCounts.medium}** | **${globalCounts.hard}** | **${globalCounts.untagged}** | **${globalCounts.total}** |\n\n`
|
||
|
|
md += `## Gaps\n\n`
|
||
|
|
md += `- Types lacking hard prompts: ${typesLackingHard.length > 0 ? typesLackingHard.join(', ') : 'none'}\n`
|
||
|
|
md += `- Types lacking medium prompts: ${typesLackingMedium.length > 0 ? typesLackingMedium.join(', ') : 'none'}\n`
|
||
|
|
|
||
|
|
writeFileSync(resolve(loopDir, 'difficulty-distribution.md'), md)
|
||
|
|
console.log(` Report written to loop/difficulty-distribution.md`)
|
||
|
|
|
||
|
|
// Assertions
|
||
|
|
expect(globalCounts.total).toBeGreaterThan(800)
|
||
|
|
})
|
||
|
|
})
|