--- phase: 01-grundger-st-tippmechanik plan: 04 type: tdd wave: 3 depends_on: ["01-02", "01-03"] files_modified: - src/game/typing.ts - src/game/typing.test.ts - src/styles/main.css autonomous: true requirements: - TYPE-01 - TYPE-02 - TYPE-03 - TYPE-04 - LEVL-02 - LEVL-03 must_haves: truths: - "Buchstaben erscheinen einzeln auf dem Bildschirm" - "Richtiger Tastendruck loest den Buchstaben auf" - "Falscher Tastendruck tut nichts, richtige Taste blinkt auf Tastatur" - "Tempo passt sich an: schneller nach 2x richtig, langsamer nach Fehler" - "Nach 8-12 richtigen Buchstaben ist die Uebung abgeschlossen" - "Abgeschlossene Stufe schaltet naechste frei" artifacts: - path: "src/game/typing.ts" provides: "Core typing exercise engine" exports: ["createTypingExercise", "TypingExercise"] - path: "src/game/typing.test.ts" provides: "TDD tests for typing logic" min_lines: 50 key_links: - from: "src/game/typing.ts" to: "src/game/levels.ts" via: "gets key set for current level" pattern: "getKeysUpToLevel|getLevelByNumber" - from: "src/game/typing.ts" to: "src/game/keyboard.ts" via: "highlights target key, shows press animation" pattern: "highlightKey|pressKey" - from: "src/game/typing.ts" to: "src/storage/db.ts" via: "saves progress after lesson completion" pattern: "saveProgress" --- Implement the core typing exercise engine using TDD. This is the heart of the app: letters appear, the child types them, adaptive tempo adjusts, and the lesson completes after 8-12 correct keystrokes. Purpose: The typing exercise is the core gameplay loop. Getting the logic right (adaptive tempo, correct/wrong handling, completion) is critical and benefits from TDD. Output: Tested typing engine that manages letter generation, input handling, tempo adaptation, and lesson completion. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md @.planning/phases/01-grundger-st-tippmechanik/01-01-SUMMARY.md @.planning/phases/01-grundger-st-tippmechanik/01-03-SUMMARY.md @SPEC.md (section 3.1 phase 2 "Ueben", section 6.2 adaptive tempo) From src/types.ts: ```typescript export interface TypingExerciseState { currentLetterIndex: number; letters: string[]; intervalMs: number; correctStreak: number; totalCorrect: number; totalErrors: number; } export interface Progress { id: 1; currentLevel: number; completedLevels: number[]; totalSessions: number; sessionDates: string[]; selectedCharacter: CompanionType; selectedLayout: KeyboardLayout; } ``` From src/game/levels.ts: ```typescript export function getLevelByNumber(n: number): Level | undefined; export function getKeysUpToLevel(n: number): string[]; ``` From src/game/keyboard.ts: ```typescript export function highlightKey(key: string): void; export function pressKey(key: string): void; ``` Task 1: Typing exercise logic (RED-GREEN-REFACTOR) src/game/typing.ts, src/game/typing.test.ts src/types.ts src/game/levels.ts src/game/keyboard.ts SPEC.md (section 3.1 for exercise structure, section 6.2 for adaptive tempo rules) .planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md (D-03, D-04, D-08 for exercise scope and tempo rules) - Test: generateLetters(level=1) returns 8-12 letters, all from ['f', 'j', ' '] - Test: generateLetters(level=3) returns letters only from levels 1-3 key set - Test: Initial interval is 3000ms - Test: After 2 correct in sequence, interval decreases by 200ms (3000 -> 2800) - Test: After 1 error, interval increases by 500ms (2800 -> 3300) - Test: Interval never goes below 1500ms - Test: Interval never goes above 4000ms - Test: handleKeyPress with correct key increments currentLetterIndex and totalCorrect - Test: handleKeyPress with wrong key does NOT increment currentLetterIndex, increments totalErrors - Test: Exercise is complete when currentLetterIndex equals letters.length - Test: correctStreak resets to 0 on error - Test: correctStreak increments on correct key **RED phase:** Create src/game/typing.test.ts with all tests above. Tests import from typing.ts which does not exist yet. Run tests — they must fail. **GREEN phase:** Create src/game/typing.ts implementing: ```typescript import type { TypingExerciseState } from '../types'; import { getKeysUpToLevel } from './levels'; const MIN_LETTERS = 8; const MAX_LETTERS = 12; const INITIAL_INTERVAL = 3000; const INTERVAL_DECREASE = 200; const INTERVAL_INCREASE = 500; const MIN_INTERVAL = 1500; const MAX_INTERVAL = 4000; const STREAK_THRESHOLD = 2; export function generateLetters(level: number): string[] { const keys = getKeysUpToLevel(level); // Filter out space for letter-only exercise (space can appear but less frequently) const letterKeys = keys.filter(k => k !== ' '); const count = MIN_LETTERS + Math.floor(Math.random() * (MAX_LETTERS - MIN_LETTERS + 1)); const letters: string[] = []; for (let i = 0; i < count; i++) { // 80% letters, 20% chance of space if space is in key set if (keys.includes(' ') && Math.random() < 0.2) { letters.push(' '); } else { letters.push(letterKeys[Math.floor(Math.random() * letterKeys.length)]); } } return letters; } export function createExerciseState(level: number): TypingExerciseState { return { currentLetterIndex: 0, letters: generateLetters(level), intervalMs: INITIAL_INTERVAL, correctStreak: 0, totalCorrect: 0, totalErrors: 0, }; } export interface KeyPressResult { correct: boolean; exerciseComplete: boolean; newInterval: number; } export function handleKeyPress( state: TypingExerciseState, pressedKey: string ): KeyPressResult { const expectedKey = state.letters[state.currentLetterIndex]; const correct = pressedKey.toLowerCase() === expectedKey.toLowerCase(); if (correct) { state.currentLetterIndex++; state.totalCorrect++; state.correctStreak++; // Adaptive tempo: after STREAK_THRESHOLD correct in sequence, speed up if (state.correctStreak >= STREAK_THRESHOLD) { state.intervalMs = Math.max(MIN_INTERVAL, state.intervalMs - INTERVAL_DECREASE); state.correctStreak = 0; // Reset streak after speed adjustment } } else { state.totalErrors++; state.correctStreak = 0; // Slow down after error state.intervalMs = Math.min(MAX_INTERVAL, state.intervalMs + INTERVAL_INCREASE); } return { correct, exerciseComplete: state.currentLetterIndex >= state.letters.length, newInterval: state.intervalMs, }; } export function getCurrentLetter(state: TypingExerciseState): string | null { if (state.currentLetterIndex >= state.letters.length) return null; return state.letters[state.currentLetterIndex]; } export function isExerciseComplete(state: TypingExerciseState): boolean { return state.currentLetterIndex >= state.letters.length; } ``` **REFACTOR phase:** Clean up if needed, ensure all exports are clear and constants are well-named. Note on D-08: Interval starts at 3000ms, -200ms after 2 consecutive correct (min 1500ms), +500ms after 1 error (max 4000ms). Reset to 3000ms at start of each new lesson (handled by createExerciseState). cd /home/dev/workspace/zauberwald && npx vitest run src/game/typing.test.ts - src/game/typing.ts exports: generateLetters, createExerciseState, handleKeyPress, getCurrentLetter, isExerciseComplete - src/game/typing.ts contains `INITIAL_INTERVAL = 3000` - src/game/typing.ts contains `MIN_INTERVAL = 1500` - src/game/typing.ts contains `MAX_INTERVAL = 4000` - src/game/typing.ts contains `INTERVAL_DECREASE = 200` - src/game/typing.ts contains `INTERVAL_INCREASE = 500` - src/game/typing.test.ts contains at least 10 test cases - `npx vitest run src/game/typing.test.ts` exits 0 - handleKeyPress with correct key returns { correct: true } - handleKeyPress with wrong key returns { correct: false } and does not advance index Typing exercise engine tested and working: generates random letters from level key set, handles correct/wrong input, adapts tempo per D-08 rules, detects exercise completion Task 2: Lesson screen UI wiring (typing exercise + keyboard + falling letters) src/ui/screens.ts, index.html, src/styles/main.css src/ui/screens.ts src/game/typing.ts src/game/keyboard.ts src/game/levels.ts src/types.ts src/app.ts src/storage/db.ts index.html src/styles/main.css 1. Update index.html — populate the lesson screen section: ```html
``` 2. Add to src/ui/screens.ts — export a new function `initLessonScreen`: ```typescript import { createExerciseState, handleKeyPress, getCurrentLetter, isExerciseComplete } from '../game/typing'; import { renderKeyboard, highlightKey, pressKey } from '../game/keyboard'; import { getProgress, saveProgress } from '../storage/db'; import type { Progress, TypingExerciseState, KeyboardLayout } from '../types'; export function initLessonScreen( db: IDBDatabase, level: number, layout: KeyboardLayout, onComplete: (level: number) => void ): () => void { // Returns a cleanup function const letterArea = document.getElementById('letter-area')!; const keyboardContainer = document.getElementById('lesson-keyboard')!; const state = createExerciseState(level); // Render keyboard renderKeyboard(keyboardContainer, layout, level); // Show first letter function showCurrentLetter(): void { const letter = getCurrentLetter(state); if (!letter) return; letterArea.innerHTML = ''; const letterEl = document.createElement('div'); letterEl.className = 'falling-letter'; letterEl.textContent = letter === ' ' ? '␣' : letter.toUpperCase(); letterArea.appendChild(letterEl); highlightKey(letter); } showCurrentLetter(); // Keyboard handler function onKeyDown(e: KeyboardEvent): void { // Ignore modifier keys, function keys, etc. if (e.key.length > 1 && e.key !== ' ') return; e.preventDefault(); const result = handleKeyPress(state, e.key); pressKey(e.key); if (result.correct) { // Correct: dissolve letter with stardust animation const currentEl = letterArea.querySelector('.falling-letter'); if (currentEl) { currentEl.classList.add('falling-letter--dissolve'); } if (result.exerciseComplete) { // Lesson complete document.removeEventListener('keydown', onKeyDown); onComplete(level); } else { // Show next letter after brief delay setTimeout(() => showCurrentLetter(), 300); } } else { // Wrong: do nothing to letter (per D-03), keyboard already highlights correct key via highlightKey // The correct key is already highlighted from showCurrentLetter } } document.addEventListener('keydown', onKeyDown); // Return cleanup function return () => { document.removeEventListener('keydown', onKeyDown); letterArea.innerHTML = ''; }; } ``` 3. Update src/app.ts to handle lesson start and completion: - Add a `startLesson(level: number)` function that calls showScreen('lesson') then initLessonScreen - The `onComplete` callback should: a. Update progress: add level to completedLevels if not already there, increment totalSessions, add today's date to sessionDates if not already there, set currentLevel to level+1 if level was the current level (per LEVL-02, LEVL-03) b. Save progress to IndexedDB c. Navigate to forest screen (per D-07: skip reward in Phase 1, or show placeholder) 4. Add CSS for falling letter and dissolve animation to src/styles/main.css: ```css .lesson { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem; padding: 2rem; min-height: 100vh; background: var(--bg-cream); } .lesson__letter-area { display: flex; align-items: center; justify-content: center; width: 200px; height: 200px; } .falling-letter { font-family: 'Quicksand', sans-serif; font-size: 5rem; font-weight: 700; color: var(--accent-green); animation: letter-float 2s ease-in-out infinite; } @keyframes letter-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } .falling-letter--dissolve { animation: letter-dissolve 0.3s ease-out forwards; } @keyframes letter-dissolve { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(1.5); opacity: 0; } } ```
cd /home/dev/workspace/zauberwald && npx tsc --noEmit && npx vitest run - src/ui/screens.ts exports `initLessonScreen` - src/ui/screens.ts contains `handleKeyPress(state, e.key)` call - src/ui/screens.ts contains `renderKeyboard(keyboardContainer, layout, level)` call - src/ui/screens.ts contains `highlightKey(letter)` call in showCurrentLetter - src/ui/screens.ts contains `pressKey(e.key)` call in onKeyDown - src/ui/screens.ts contains `onComplete(level)` call when exercise is complete - src/app.ts contains function that updates completedLevels and saves progress on lesson complete - index.html contains `id="letter-area"` and `id="lesson-keyboard"` - src/styles/main.css contains `.falling-letter` with `font-size: 5rem` - src/styles/main.css contains `@keyframes letter-dissolve` - src/styles/main.css contains `.falling-letter--dissolve` - `npx tsc --noEmit` exits 0 - `npx vitest run` exits 0 (all existing tests still pass) Lesson screen shows letters one at a time, keyboard highlights target key, correct press dissolves letter and advances, wrong press does nothing, lesson completes after all letters typed, progress saved with level completion and next level unlocked
- `npx vitest run` passes all tests (typing engine + DB) - `npx tsc --noEmit` passes - In browser: can start a lesson, type letters, see keyboard highlights, complete lesson - After lesson: progress updated, next level unlocked - Typing exercise engine fully tested with TDD (RED-GREEN-REFACTOR) - Adaptive tempo works: 3s start, -200ms after 2x correct, +500ms after error, bounds 1.5s-4s - Lesson screen renders falling letter + keyboard, handles input correctly - Lesson completion updates progress and unlocks next level After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-04-SUMMARY.md`