From 5a6ba70dab307c2c69f575cec50b1b2f675ee534 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Sun, 29 Mar 2026 13:41:20 +0200 Subject: [PATCH] feat(03-01): implement word lists per level with getWordsForLevel - Static word lists for levels 1-6, 8-10 words each - Levels 1-2 use letter combos (no real words possible with F/J/D/K) - Levels 3-6 mix real German words with combos - All words validated: only use learned letters, max 5 chars - getWordsForLevel with/without replacement sampling - All 8 tests pass Co-Authored-By: Claude Opus 4.6 (1M context) --- src/game/words.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/game/words.ts diff --git a/src/game/words.ts b/src/game/words.ts new file mode 100644 index 0000000..1cf709a --- /dev/null +++ b/src/game/words.ts @@ -0,0 +1,64 @@ +import { getKeysUpToLevel } from "./levels"; + +/** + * Word lists per level. + * Levels 1-2: Only F/J/D/K available -- no real German words possible, use letter combos. + * Levels 3+: Mix of real German words and combos using only learned letters. + * All words max 5 characters, no spaces. + */ +export const wordsByLevel: Record = { + // Level 1: f, j + 1: ["ff", "jj", "fj", "jf", "fjf", "jfj", "fjfj", "jfjf", "ffj", "jjf"], + // Level 2: f, j, d, k + 2: ["fdk", "kdf", "djf", "fkd", "dkf", "jdk", "kfj", "dfjk", "kjdf", "fkjd"], + // Level 3: f, j, d, k, s, l + 3: ["sdf", "sdk", "lsd", "flsk", "sldf", "dsl", "kls", "flds", "jskl", "lkds"], + // Level 4: f, j, d, k, s, l, a, ö -- "aas", "das", "lass", "als" are real words + 4: ["als", "das", "aas", "lass", "fad", "dal", "lad", "alfa", "salö", "aöds"], + // Level 5: f, j, d, k, s, l, a, ö, g, h -- "lag", "sah", "glas" are real words + 5: ["glas", "lag", "gah", "hag", "gal", "sah", "hals", "has", "gash", "flags"], + // Level 6: f, j, d, k, s, l, a, ö, g, h, e, i -- "eis", "die", "sie", "lied", "fies" + 6: ["eis", "die", "sie", "lied", "fies", "dies", "sei", "geld", "held", "lieh"], +}; + +/** + * Returns `count` random words from the given level's word list. + * If count <= list length: sampling without replacement. + * If count > list length: sampling with replacement. + * Returns empty array for non-existent levels. + */ +export function getWordsForLevel(level: number, count: number): string[] { + const list = wordsByLevel[level]; + if (!list || list.length === 0) return []; + + if (count <= list.length) { + // Shuffle and take first `count` (without replacement) + const shuffled = [...list]; + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j]!, shuffled[i]!]; + } + return shuffled.slice(0, count); + } + + // With replacement + const result: string[] = []; + for (let i = 0; i < count; i++) { + result.push(list[Math.floor(Math.random() * list.length)]!); + } + return result; +} + +// Validation helper (used in tests) +export function validateWordList(): boolean { + for (const [levelStr, words] of Object.entries(wordsByLevel)) { + const level = Number(levelStr); + const allowedKeys = getKeysUpToLevel(level).filter((k) => k !== " "); + for (const word of words) { + for (const char of word) { + if (!allowedKeys.includes(char)) return false; + } + } + } + return true; +}