import { describe, it, expect } from 'vitest' import { pickChallenge, pickRankedChallenge, getAllChallengeTypes, getAnswerPool, roundToDifficulty } from './challenges.js' describe('pickChallenge', () => { it('returns a valid challenge', () => { const c = pickChallenge(new Set(), null) expect(c).toBeTruthy() expect(c.type).toBeTruthy() expect(c.label).toBeTruthy() expect(c.prompt).toBeTruthy() expect(c.timeout_ms).toBeGreaterThan(0) expect(c.baseDamage).toBeGreaterThan(0) expect(['factual', 'creative']).toContain(c.scoring) }) it('avoids used types when possible', () => { const types = getAllChallengeTypes() // Use all types except one const used = new Set(types.slice(0, -1)) const remaining = types[types.length - 1] // With most types used, should pick from the remaining // Run multiple times to account for randomness let gotRemaining = false for (let i = 0; i < 50; i++) { const c = pickChallenge(used, null) if (c.type === remaining) gotRemaining = true } expect(gotRemaining).toBe(true) }) it('falls back to all types when all used', () => { const allUsed = new Set(getAllChallengeTypes()) const c = pickChallenge(allUsed, null) // Should still return something expect(c).toBeTruthy() expect(c.prompt).toBeTruthy() }) it('factual challenges have answers', () => { for (let i = 0; i < 100; i++) { const c = pickChallenge(new Set(), null) if (c.scoring === 'factual') { expect(c.answers).toBeTruthy() expect(c.answers!.length).toBeGreaterThan(0) } } }) it('creative challenges have no answers', () => { for (let i = 0; i < 100; i++) { const c = pickChallenge(new Set(), null) if (c.scoring === 'creative') { expect(!c.answers || c.answers.length === 0).toBe(true) } } }) it('factual challenges with choices have shuffled choices', () => { const orders = new Set() for (let i = 0; i < 50; i++) { const c = pickChallenge(new Set(), null) if (c.scoring === 'factual' && c.choices) { orders.add(c.choices.join(',')) } } // With 50 tries, choices should appear in more than one order expect(orders.size).toBeGreaterThan(1) }) it('distribution: ~70% factual, ~30% creative over many picks', () => { let factual = 0 let creative = 0 const runs = 1000 for (let i = 0; i < runs; i++) { const c = pickChallenge(new Set(), null) if (c.scoring === 'factual') factual++ else creative++ } const factualPct = factual / runs // Allow ±10% tolerance due to randomness expect(factualPct).toBeGreaterThan(0.55) expect(factualPct).toBeLessThan(0.85) }) it('True/False auto-generation for boolean answers', () => { // Pick many challenges, find ones with answers = ['true'] or ['false'] let foundTFWithChoices = false for (let i = 0; i < 500; i++) { const c = pickChallenge(new Set(), null) if (c.answers?.length === 1 && ['true', 'false'].includes(c.answers[0].toLowerCase())) { expect(c.choices).toBeTruthy() expect(c.choices!.length).toBe(2) expect(c.choices!.sort()).toEqual(['False', 'True']) foundTFWithChoices = true } } expect(foundTFWithChoices).toBe(true) }) }) describe('pickRankedChallenge', () => { it('never returns choices', () => { for (let i = 0; i < 100; i++) { const c = pickRankedChallenge(new Set()) expect(c.choices).toBeUndefined() } }) it('returns valid challenge structure', () => { const c = pickRankedChallenge(new Set()) expect(c.type).toBeTruthy() expect(c.prompt).toBeTruthy() expect(c.timeout_ms).toBeGreaterThan(0) expect(c.baseDamage).toBeGreaterThan(0) }) it('avoids used types', () => { const types = getAllChallengeTypes() const used = new Set(types.slice(0, -1)) let gotRemaining = false for (let i = 0; i < 50; i++) { const c = pickRankedChallenge(used) if (c.type === types[types.length - 1]) gotRemaining = true } expect(gotRemaining).toBe(true) }) }) describe('getAllChallengeTypes', () => { it('returns 16 types', () => { const types = getAllChallengeTypes() expect(types.length).toBe(16) }) it('types are unique', () => { const types = getAllChallengeTypes() expect(new Set(types).size).toBe(types.length) }) it('includes known types', () => { const types = getAllChallengeTypes() expect(types).toContain('speed_blitz') expect(types).toContain('roast_battle') expect(types).toContain('riddle') expect(types).toContain('math_blitz') }) }) describe('getAnswerPool', () => { it('returns answers for factual types', () => { const pool = getAnswerPool('speed_blitz') expect(pool.length).toBeGreaterThan(0) }) it('returns empty array for creative types', () => { const pool = getAnswerPool('roast_battle') expect(pool.length).toBe(0) }) it('returns empty for unknown type', () => { const pool = getAnswerPool('nonexistent_type') expect(pool.length).toBe(0) }) it('answers are unique within pool', () => { const types = getAllChallengeTypes() for (const type of types) { const pool = getAnswerPool(type) const unique = new Set(pool) expect(unique.size).toBe(pool.length) } }) }) 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 const seenPrompts = new Set() for (let i = 0; i < 500; i++) { const c = pickChallenge(new Set(), null) seenPrompts.add(c.prompt) } // Should have seen a good variety expect(seenPrompts.size).toBeGreaterThan(50) }) it('no empty prompts', () => { for (let i = 0; i < 200; i++) { const c = pickChallenge(new Set(), null) expect(c.prompt.trim().length).toBeGreaterThan(5) } }) it('factual prompts have non-empty answers', () => { for (let i = 0; i < 200; i++) { const c = pickChallenge(new Set(), null) if (c.scoring === 'factual' && c.answers) { for (const a of c.answers) { expect(a.trim().length).toBeGreaterThan(0) } } } }) })