Files
Zauberwald/.planning/phases/01-grundger-st-tippmechanik/01-04-PLAN.md
T

16 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-grundger-st-tippmechanik 04 tdd 3
01-02
01-03
src/game/typing.ts
src/game/typing.test.ts
src/styles/main.css
true
TYPE-01
TYPE-02
TYPE-03
TYPE-04
LEVL-02
LEVL-03
truths artifacts key_links
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
path provides exports
src/game/typing.ts Core typing exercise engine
createTypingExercise
TypingExercise
path provides min_lines
src/game/typing.test.ts TDD tests for typing logic 50
from to via pattern
src/game/typing.ts src/game/levels.ts gets key set for current level getKeysUpToLevel|getLevelByNumber
from to via pattern
src/game/typing.ts src/game/keyboard.ts highlights target key, shows press animation highlightKey|pressKey
from to via pattern
src/game/typing.ts src/storage/db.ts saves progress after lesson completion 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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) 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:

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

<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>
After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-04-SUMMARY.md`