---
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"
---

<objective>
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.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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)

<interfaces>
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;
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Typing exercise logic (RED-GREEN-REFACTOR)</name>
  <files>
    src/game/typing.ts, src/game/typing.test.ts
  </files>
  <read_first>
    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)
  </read_first>
  <behavior>
    - 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
  </behavior>
  <action>
    **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).
  </action>
  <verify>
    <automated>cd /home/dev/workspace/zauberwald && npx vitest run src/game/typing.test.ts</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>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</done>
</task>

<task type="auto">
  <name>Task 2: Lesson screen UI wiring (typing exercise + keyboard + falling letters)</name>
  <files>
    src/ui/screens.ts, index.html, src/styles/main.css
  </files>
  <read_first>
    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
  </read_first>
  <action>
    1. Update index.html — populate the lesson screen section:
    ```html
    <section id="screen-lesson" class="screen">
      <div class="lesson">
        <div class="lesson__letter-area" id="letter-area">
          <!-- Current letter appears here -->
        </div>
        <div class="lesson__keyboard" id="lesson-keyboard">
          <!-- Keyboard rendered here -->
        </div>
      </div>
    </section>
    ```

    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; }
    }
    ```
  </action>
  <verify>
    <automated>cd /home/dev/workspace/zauberwald && npx tsc --noEmit && npx vitest run</automated>
  </verify>
  <acceptance_criteria>
    - 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)
  </acceptance_criteria>
  <done>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</done>
</task>

</tasks>

<verification>
- `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
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-04-SUMMARY.md`
</output>
