feat: morse data library with validation and challenge selection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
MORSE,
|
||||
NOVICE_LETTERS,
|
||||
OPERATOR_WORDS,
|
||||
wpmToTiming,
|
||||
validatePhrase,
|
||||
pickChallenge,
|
||||
} from './morse'
|
||||
|
||||
describe('MORSE map', () => {
|
||||
it('has all 26 letters', () => {
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
letters.forEach(l => expect(MORSE[l]).toBeDefined())
|
||||
})
|
||||
it('A is .-', () => expect(MORSE['A']).toBe('.-'))
|
||||
it('S is ...', () => expect(MORSE['S']).toBe('...'))
|
||||
it('O is ---', () => expect(MORSE['O']).toBe('---'))
|
||||
})
|
||||
|
||||
describe('NOVICE_LETTERS', () => {
|
||||
it('has 12 letters', () => expect(NOVICE_LETTERS).toHaveLength(12))
|
||||
it('includes A E T I N S H R D L U O', () => {
|
||||
;['A','E','T','I','N','S','H','R','D','L','U','O'].forEach(l =>
|
||||
expect(NOVICE_LETTERS).toContain(l)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('OPERATOR_WORDS', () => {
|
||||
it('has at least 20 words', () => expect(OPERATOR_WORDS.length).toBeGreaterThanOrEqual(20))
|
||||
it('all words use only Novice letters', () => {
|
||||
const novice = new Set(['A','E','T','I','N','S','H','R','D','L','U','O'])
|
||||
OPERATOR_WORDS.forEach(word => {
|
||||
word.split('').forEach(l => expect(novice.has(l)).toBe(true))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('wpmToTiming', () => {
|
||||
it('10 WPM gives dot=120ms', () => {
|
||||
expect(wpmToTiming(10).dot).toBe(120)
|
||||
})
|
||||
it('dash is 3x dot', () => {
|
||||
const t = wpmToTiming(10)
|
||||
expect(t.dash).toBe(t.dot * 3)
|
||||
})
|
||||
it('20 WPM gives dot=60ms', () => {
|
||||
expect(wpmToTiming(20).dot).toBe(60)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validatePhrase', () => {
|
||||
it('accepts valid phrase for E (.)', () => {
|
||||
expect(validatePhrase('go', 'E')).toEqual({ valid: true })
|
||||
})
|
||||
it('accepts valid phrase for A (.-)', () => {
|
||||
expect(validatePhrase('go WALKING', 'A')).toEqual({ valid: true })
|
||||
})
|
||||
it('accepts valid phrase for S (...)', () => {
|
||||
expect(validatePhrase('go go go', 'S')).toEqual({ valid: true })
|
||||
})
|
||||
it('rejects wrong word count', () => {
|
||||
const r = validatePhrase('go go', 'S')
|
||||
expect(r.valid).toBe(false)
|
||||
expect(r.message).toContain('3')
|
||||
})
|
||||
it('rejects long word for dot position', () => {
|
||||
const r = validatePhrase('WALKING go go', 'S')
|
||||
expect(r.valid).toBe(false)
|
||||
})
|
||||
it('rejects short word for dash position', () => {
|
||||
const r = validatePhrase('go go', 'A')
|
||||
expect(r.valid).toBe(false)
|
||||
})
|
||||
it('rejects unknown letter', () => {
|
||||
expect(validatePhrase('test', '1')).toEqual({ valid: false, message: 'Unknown letter' })
|
||||
})
|
||||
it('handles empty phrase', () => {
|
||||
expect(validatePhrase('', 'E')).toEqual({ valid: false, message: expect.stringContaining('1') })
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickChallenge', () => {
|
||||
it('always returns item from pool', () => {
|
||||
const pool = ['A', 'E', 'T', 'I']
|
||||
const result = pickChallenge(pool, {}, [])
|
||||
expect(pool).toContain(result)
|
||||
})
|
||||
it('avoids last 3 items when pool is large enough', () => {
|
||||
const pool = ['A', 'E', 'T', 'I', 'N', 'S']
|
||||
const history = ['A', 'E', 'T']
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const result = pickChallenge(pool, {}, history)
|
||||
expect(['A','E','T']).not.toContain(result)
|
||||
}
|
||||
})
|
||||
it('prefers items with lower accuracy', () => {
|
||||
const pool = ['A', 'B']
|
||||
const stats = {
|
||||
A: { correct: 9, attempts: 10 },
|
||||
B: { correct: 1, attempts: 10 },
|
||||
}
|
||||
let bCount = 0
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (pickChallenge(pool, stats, []) === 'B') bCount++
|
||||
}
|
||||
expect(bCount).toBeGreaterThan(60)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user