feat(onboarding): anchor mnemonic to target letter
First word of each suggestion starts with the letter being learned (e.g. "age WALK" for A). Validation requires at least one word to start with the target letter. Added O/U/N/D/H words to fill gaps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,14 @@ describe('suggestPhrases', () => {
|
||||
expect(phrase.length).toBeLessThanOrEqual(3)
|
||||
})
|
||||
})
|
||||
it('first word starts with the target letter', () => {
|
||||
for (const letter of ['A','E','T','S','N','H','R','D','L','I','U','O']) {
|
||||
const s = suggestPhrases(letter, 5)
|
||||
s.forEach(phrase => {
|
||||
expect(phrase[0].toUpperCase()).toBe(letter)
|
||||
})
|
||||
}
|
||||
})
|
||||
it('returns empty array for unknown letter', () => {
|
||||
expect(suggestPhrases('$', 5)).toEqual([])
|
||||
})
|
||||
@@ -108,40 +116,52 @@ describe('suggestPhrases', () => {
|
||||
|
||||
describe('validatePhrase', () => {
|
||||
it('accepts valid phrase for E (.)', () => {
|
||||
expect(validatePhrase('go', 'E')).toEqual({ valid: true })
|
||||
expect(validatePhrase('eat', 'E')).toEqual({ valid: true })
|
||||
})
|
||||
it('accepts 3-letter dot word', () => {
|
||||
expect(validatePhrase('run', 'E')).toEqual({ valid: true })
|
||||
expect(validatePhrase('eat', 'E')).toEqual({ valid: true })
|
||||
})
|
||||
it('accepts valid phrase for A (.-)', () => {
|
||||
expect(validatePhrase('go WALKING', 'A')).toEqual({ valid: true })
|
||||
// A = .- → dot + dash; "age" starts with A, "WALKING" is 4+
|
||||
expect(validatePhrase('age WALKING', 'A')).toEqual({ valid: true })
|
||||
})
|
||||
it('accepts valid phrase for S (...)', () => {
|
||||
expect(validatePhrase('go go go', 'S')).toEqual({ valid: true })
|
||||
// S = ... → three dot words; "sun" starts with S
|
||||
expect(validatePhrase('sun go go', 'S')).toEqual({ valid: true })
|
||||
})
|
||||
it('rejects 4-letter word in dot position', () => {
|
||||
const r = validatePhrase('walk go go', 'S')
|
||||
const r = validatePhrase('walk sun go', 'S')
|
||||
expect(r.valid).toBe(false)
|
||||
})
|
||||
it('accepts 3-letter word in dot position', () => {
|
||||
expect(validatePhrase('run', 'E')).toEqual({ valid: true })
|
||||
expect(validatePhrase('eat', 'E')).toEqual({ valid: true })
|
||||
})
|
||||
it('rejects 3-letter word in dash position', () => {
|
||||
const r = validatePhrase('go go', 'A') // A=.- so second word must be 4+ letters
|
||||
const r = validatePhrase('age go', 'A') // A=.- so second word must be 4+ letters
|
||||
expect(r.valid).toBe(false)
|
||||
})
|
||||
it('accepts 4-letter word in dash position', () => {
|
||||
expect(validatePhrase('go walk', 'A')).toEqual({ valid: true })
|
||||
expect(validatePhrase('aim also', 'A')).toEqual({ valid: true })
|
||||
})
|
||||
it('rejects wrong word count', () => {
|
||||
const r = validatePhrase('go go', 'S')
|
||||
const r = validatePhrase('sun 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')
|
||||
const r = validatePhrase('SAILING go go', 'S')
|
||||
expect(r.valid).toBe(false)
|
||||
})
|
||||
it('rejects phrase without anchor letter', () => {
|
||||
// A = .- but no word starts with A
|
||||
const r = validatePhrase('go walk', 'A')
|
||||
expect(r.valid).toBe(false)
|
||||
expect(r.message).toContain('"A"')
|
||||
})
|
||||
it('accepts anchor letter in any position', () => {
|
||||
// A = .- → "go ALSO" — "ALSO" starts with A
|
||||
expect(validatePhrase('go ALSO', 'A')).toEqual({ valid: true })
|
||||
})
|
||||
it('rejects unknown letter', () => {
|
||||
expect(validatePhrase('test', '1')).toEqual({ valid: false, message: 'Unknown letter' })
|
||||
})
|
||||
|
||||
+19
-2
@@ -54,6 +54,8 @@ export const DOT_WORDS = [
|
||||
'pay','put','raw','sad','shy','son','tap','tin','toe','toy','two','use',
|
||||
'way','you','aim','air','ask','bit','cut','dry','fun','god','had','hit',
|
||||
'its','law','log','lot','mad','mix','net','not','odd','pet','row','she',
|
||||
'up','urn','us','ore','our','oar','oh','ow','ox','nod','nap','nil',
|
||||
'nor','dip','dig','dug','den','did','dam','hub','hug','hum','hen',
|
||||
]
|
||||
|
||||
export const DASH_WORDS = [
|
||||
@@ -88,19 +90,29 @@ export const DASH_WORDS = [
|
||||
'weed','week','well','went','wide','wife','wild','will','wind','wine',
|
||||
'wing','wire','wise','wish','wolf','wood','wool','word','work','worn',
|
||||
'yarn','yell','zone','zoom',
|
||||
'oath','once','only','open','ours','over','oven',
|
||||
'unit','upon','urge','used',
|
||||
]
|
||||
|
||||
function pickRandom<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
|
||||
export function suggestPhrases(letter: string, count: number): string[] {
|
||||
const upper = letter.toUpperCase()
|
||||
const pattern = MORSE[upper]
|
||||
if (!pattern) return []
|
||||
const elements = pattern.split('')
|
||||
// First word should start with the letter
|
||||
const firstPool = elements[0] === '.' ? DOT_WORDS : DASH_WORDS
|
||||
const anchorWords = firstPool.filter(w => w[0].toUpperCase() === upper)
|
||||
const results: string[] = []
|
||||
const seen = new Set<string>()
|
||||
for (let attempt = 0; attempt < count * 30 && results.length < count; attempt++) {
|
||||
const words = elements.map(el => {
|
||||
const words = elements.map((el, i) => {
|
||||
if (i === 0 && anchorWords.length > 0) return pickRandom(anchorWords)
|
||||
const pool = el === '.' ? DOT_WORDS : DASH_WORDS
|
||||
return pool[Math.floor(Math.random() * pool.length)]
|
||||
return pickRandom(pool)
|
||||
})
|
||||
const phrase = words.join(' ')
|
||||
if (!seen.has(phrase)) { seen.add(phrase); results.push(phrase) }
|
||||
@@ -134,6 +146,11 @@ export function validatePhrase(phrase: string, letter: string): ValidationResult
|
||||
}
|
||||
}
|
||||
|
||||
const hasAnchor = words.some(w => w[0].toUpperCase() === upper)
|
||||
if (!hasAnchor) {
|
||||
return { valid: false, message: `At least one word should start with "${upper}"` }
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user