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

15 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 03 execute 2
01-01
src/game/levels.ts
src/game/keyboard.ts
src/game/levels.test.ts
src/styles/main.css
true
KYBD-01
KYBD-02
KYBD-03
KYBD-04
KYBD-05
LEVL-01
truths artifacts key_links
Bildschirmtastatur zeigt vollstaendiges QWERTZ-Layout mit allen Tasten
Gelernte Tasten leuchten in ihrer Fingerfarbe, ungelernte sind grau
Eine aktive Taste pulsiert sanft (CSS-Animation)
Tastendruck zeigt press-Animation (scale down)
Stufen 1-6 sind als Daten definiert mit korrekten Tastenzuordnungen
Beide Layouts DE/CH sind als Daten hinterlegt
path provides exports
src/game/levels.ts Level definitions 1-6 with key sets and finger mappings
levels
getLevelByNumber
getKeysUpToLevel
path provides exports
src/game/keyboard.ts QWERTZ keyboard renderer with finger colors
renderKeyboard
highlightKey
pressKey
updateKeyboardForLevel
from to via pattern
src/game/keyboard.ts src/game/levels.ts reads level data to determine which keys are active import.*levels
from to via pattern
src/game/keyboard.ts src/styles/main.css uses finger color CSS variables --finger-
Create the QWERTZ keyboard component with finger-colored keys and the level definitions (stages 1-6). The keyboard is the primary visual feedback during typing exercises.

Purpose: The keyboard component is needed by the typing exercise (Plan 04) and the forest overview (Plan 05). Level definitions drive all gameplay. Output: Renderable keyboard with finger colors, pulse/press animations, and complete level 1-6 data.

<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 @SPEC.md (sections 4.1, 8.1, 8.2, 8.3) From src/types.ts: ```typescript export type KeyboardLayout = 'de' | 'ch';

export interface Level { level: number; newKeys: string[]; allKeys: string[]; fingerMap: Record<string, string>; }

</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Level definitions with key sets and finger mappings</name>
  <files>
    src/game/levels.ts, src/game/levels.test.ts
  </files>
  <read_first>
    src/types.ts
    SPEC.md (section 4.1 for progression table, section 8.3 for finger assignments)
  </read_first>
  <behavior>
    - Test: levels array has exactly 6 entries
    - Test: Level 1 newKeys = ['f', 'j', ' '] (F, J, Space)
    - Test: Level 2 newKeys = ['d', 'k']
    - Test: Level 3 newKeys = ['s', 'l']
    - Test: Level 4 newKeys = ['a', 'ö']
    - Test: Level 5 newKeys = ['g', 'h']
    - Test: Level 6 newKeys = ['e', 'i']
    - Test: Level 3 allKeys includes all keys from levels 1-3
    - Test: getLevelByNumber(1) returns level 1 object
    - Test: getKeysUpToLevel(3) returns cumulative keys for levels 1-3
    - Test: Each key in allKeys has a fingerMap entry
  </behavior>
  <action>
    Create src/game/levels.ts with level definitions per spec section 4.1 and finger assignments per section 8.3:

    ```typescript
    import type { Level } from '../types';

    // Finger color CSS variable mapping per spec section 8.3
    // Left hand: pinky=Q/A/Y/1, ring=W/S/X/2, mid=E/D/C/3, index=R/F/V/T/G/B/4/5
    // Right hand: index=U/J/M/Z/H/N/6/7, mid=I/K/,/8, ring=O/L/./9, pinky=P/Ö/Ä/Ü/0/ß
    // Thumb: Space

    const fingerMap: Record<string, string> = {
      // Left pinky
      'q': '--finger-l-pinky', 'a': '--finger-l-pinky', 'y': '--finger-l-pinky',
      // Left ring
      'w': '--finger-l-ring', 's': '--finger-l-ring', 'x': '--finger-l-ring',
      // Left middle
      'e': '--finger-l-mid', 'd': '--finger-l-mid', 'c': '--finger-l-mid',
      // Left index
      'r': '--finger-l-index', 'f': '--finger-l-index', 'v': '--finger-l-index',
      't': '--finger-l-index', 'g': '--finger-l-index', 'b': '--finger-l-index',
      // Right index
      'u': '--finger-r-index', 'j': '--finger-r-index', 'm': '--finger-r-index',
      'z': '--finger-r-index', 'h': '--finger-r-index', 'n': '--finger-r-index',
      // Right middle
      'i': '--finger-r-mid', 'k': '--finger-r-mid',
      // Right ring
      'o': '--finger-r-ring', 'l': '--finger-r-ring',
      // Right pinky
      'p': '--finger-r-pinky', 'ö': '--finger-r-pinky', 'ä': '--finger-r-pinky', 'ü': '--finger-r-pinky',
      // Thumb
      ' ': '--finger-thumb',
    };

    export const levels: Level[] = [
      { level: 1, newKeys: ['f', 'j', ' '],           allKeys: ['f', 'j', ' '],                              fingerMap },
      { level: 2, newKeys: ['d', 'k'],                 allKeys: ['f', 'j', ' ', 'd', 'k'],                    fingerMap },
      { level: 3, newKeys: ['s', 'l'],                 allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l'],          fingerMap },
      { level: 4, newKeys: ['a', 'ö'],                 allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö'], fingerMap },
      { level: 5, newKeys: ['g', 'h'],                 allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö', 'g', 'h'], fingerMap },
      { level: 6, newKeys: ['e', 'i'],                 allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö', 'g', 'h', 'e', 'i'], fingerMap },
    ];

    export function getLevelByNumber(n: number): Level | undefined {
      return levels.find(l => l.level === n);
    }

    export function getKeysUpToLevel(n: number): string[] {
      const level = getLevelByNumber(n);
      return level ? level.allKeys : [];
    }
    ```

    Create src/game/levels.test.ts testing all behaviors listed above.
  </action>
  <verify>
    <automated>cd /home/dev/workspace/zauberwald && npx vitest run src/game/levels.test.ts</automated>
  </verify>
  <acceptance_criteria>
    - src/game/levels.ts exports `levels` (array of 6), `getLevelByNumber`, `getKeysUpToLevel`
    - levels[0].newKeys deep equals ['f', 'j', ' ']
    - levels[5].newKeys deep equals ['e', 'i']
    - levels[5].allKeys contains 13 keys (f,j,space,d,k,s,l,a,ö,g,h,e,i)
    - fingerMap maps 'f' to '--finger-l-index' and 'j' to '--finger-r-index'
    - fingerMap maps ' ' to '--finger-thumb'
    - `npx vitest run src/game/levels.test.ts` exits 0
  </acceptance_criteria>
  <done>Levels 1-6 defined with correct key sets per spec progression table, finger mapping matches spec section 8.3, all tests pass</done>
</task>

<task type="auto">
  <name>Task 2: QWERTZ keyboard renderer with finger colors and animations</name>
  <files>
    src/game/keyboard.ts, src/styles/main.css
  </files>
  <read_first>
    src/game/levels.ts
    src/types.ts
    src/styles/main.css
    SPEC.md (section 8.1 for keyboard layout, section 8.2 for DE/CH differences)
  </read_first>
  <action>
    1. Create src/game/keyboard.ts:

    ```typescript
    import type { KeyboardLayout } from '../types';
    import { getLevelByNumber } from './levels';

    // QWERTZ rows — each row is an array of key labels
    // For DE and CH layouts (identical for letters, differ on special chars above level 14)
    const KEYBOARD_ROWS_DE = [
      ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'ß'],
      ['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü'],
      ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'],
      ['y', 'x', 'c', 'v', 'b', 'n', 'm'],
      [' '],  // Spacebar
    ];

    const KEYBOARD_ROWS_CH = [
      ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
      ['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü'],
      ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'],
      ['y', 'x', 'c', 'v', 'b', 'n', 'm'],
      [' '],
    ];

    // Finger color mapping (shared with levels.ts fingerMap)
    const fingerColorMap: Record<string, string> = {
      'q': '--finger-l-pinky', 'a': '--finger-l-pinky', 'y': '--finger-l-pinky', '1': '--finger-l-pinky',
      'w': '--finger-l-ring', 's': '--finger-l-ring', 'x': '--finger-l-ring', '2': '--finger-l-ring',
      'e': '--finger-l-mid', 'd': '--finger-l-mid', 'c': '--finger-l-mid', '3': '--finger-l-mid',
      'r': '--finger-l-index', 'f': '--finger-l-index', 'v': '--finger-l-index', '4': '--finger-l-index',
      't': '--finger-l-index', 'g': '--finger-l-index', 'b': '--finger-l-index', '5': '--finger-l-index',
      'z': '--finger-r-index', 'h': '--finger-r-index', 'n': '--finger-r-index', '6': '--finger-r-index',
      'u': '--finger-r-index', 'j': '--finger-r-index', 'm': '--finger-r-index', '7': '--finger-r-index',
      'i': '--finger-r-mid', 'k': '--finger-r-mid', '8': '--finger-r-mid',
      'o': '--finger-r-ring', 'l': '--finger-r-ring', '9': '--finger-r-ring',
      'p': '--finger-r-pinky', 'ö': '--finger-r-pinky', 'ä': '--finger-r-pinky',
      'ü': '--finger-r-pinky', '0': '--finger-r-pinky', 'ß': '--finger-r-pinky',
      ' ': '--finger-thumb',
    };

    let keyboardContainer: HTMLElement | null = null;
    let keyElements: Map<string, HTMLElement> = new Map();

    export function renderKeyboard(
      container: HTMLElement,
      layout: KeyboardLayout,
      currentLevel: number
    ): void {
      keyboardContainer = container;
      keyElements.clear();
      container.innerHTML = '';
      container.className = 'keyboard';

      const rows = layout === 'ch' ? KEYBOARD_ROWS_CH : KEYBOARD_ROWS_DE;
      const level = getLevelByNumber(currentLevel);
      const activeKeys = level ? level.allKeys : [];

      rows.forEach((row, rowIndex) => {
        const rowEl = document.createElement('div');
        rowEl.className = `keyboard__row keyboard__row--${rowIndex}`;

        row.forEach(key => {
          const keyEl = document.createElement('div');
          const isActive = activeKeys.includes(key);
          const colorVar = fingerColorMap[key];

          keyEl.className = 'keyboard__key';
          if (key === ' ') keyEl.classList.add('keyboard__key--space');
          if (isActive && colorVar) {
            keyEl.style.backgroundColor = `var(${colorVar})`;
            keyEl.classList.add('keyboard__key--active');
          } else {
            keyEl.classList.add('keyboard__key--inactive');
          }

          keyEl.textContent = key === ' ' ? '' : key.toUpperCase();
          keyEl.dataset.key = key;
          keyElements.set(key, keyEl);
          rowEl.appendChild(keyEl);
        });

        container.appendChild(rowEl);
      });
    }

    export function highlightKey(key: string): void {
      // Remove previous highlights
      keyElements.forEach(el => el.classList.remove('keyboard__key--highlight'));
      const el = keyElements.get(key.toLowerCase());
      if (el) el.classList.add('keyboard__key--highlight');
    }

    export function pressKey(key: string): void {
      const el = keyElements.get(key.toLowerCase());
      if (el) {
        el.classList.add('keyboard__key--pressed');
        setTimeout(() => el.classList.remove('keyboard__key--pressed'), 150);
      }
    }

    export function updateKeyboardForLevel(currentLevel: number): void {
      const level = getLevelByNumber(currentLevel);
      const activeKeys = level ? level.allKeys : [];
      keyElements.forEach((el, key) => {
        const colorVar = fingerColorMap[key];
        if (activeKeys.includes(key) && colorVar) {
          el.style.backgroundColor = `var(${colorVar})`;
          el.classList.remove('keyboard__key--inactive');
          el.classList.add('keyboard__key--active');
        } else {
          el.style.backgroundColor = '';
          el.classList.remove('keyboard__key--active');
          el.classList.add('keyboard__key--inactive');
        }
      });
    }
    ```

    2. Add keyboard CSS to src/styles/main.css:

    ```css
    /* Keyboard */
    .keyboard {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 4px;
      padding: 1rem;
      background: rgba(255, 255, 255, 0.6);
      border-radius: 12px;
      user-select: none;
    }

    .keyboard__row {
      display: flex;
      gap: 4px;
    }

    .keyboard__key {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 44px;
      height: 44px;
      border-radius: 8px;
      font-family: 'Quicksand', sans-serif;
      font-weight: 700;
      font-size: 1rem;
      color: var(--text-dark);
      background: #e0e0e0;
      transition: transform 0.1s ease, background-color 0.2s ease, box-shadow 0.2s ease;
      cursor: default;
    }

    .keyboard__key--space {
      width: 280px;
    }

    .keyboard__key--inactive {
      background: #e0e0e0;
      color: var(--text-light);
      opacity: 0.5;
    }

    .keyboard__key--active {
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    /* Pulse animation for the target key (KYBD-03) */
    .keyboard__key--highlight {
      animation: key-pulse 1.2s ease-in-out infinite;
      box-shadow: 0 0 12px rgba(232, 184, 75, 0.6);
    }

    @keyframes key-pulse {
      0%, 100% { transform: scale(1); }
      50% { transform: scale(1.08); }
    }

    /* Press animation (KYBD-03) */
    .keyboard__key--pressed {
      transform: scale(0.9) !important;
      animation: none;
    }
    ```
  </action>
  <verify>
    <automated>cd /home/dev/workspace/zauberwald && npx tsc --noEmit && echo "PASS"</automated>
  </verify>
  <acceptance_criteria>
    - src/game/keyboard.ts exports `renderKeyboard`, `highlightKey`, `pressKey`, `updateKeyboardForLevel`
    - src/game/keyboard.ts contains KEYBOARD_ROWS_DE with 5 rows (numbers, top, home, bottom, space)
    - src/game/keyboard.ts contains KEYBOARD_ROWS_CH with 5 rows
    - src/game/keyboard.ts contains fingerColorMap with entries for 'f' -> '--finger-l-index', 'j' -> '--finger-r-index', ' ' -> '--finger-thumb'
    - src/styles/main.css contains `.keyboard__key--highlight` with `animation: key-pulse`
    - src/styles/main.css contains `@keyframes key-pulse`
    - src/styles/main.css contains `.keyboard__key--pressed` with `transform: scale(0.9)`
    - src/styles/main.css contains `.keyboard__key--inactive` with `opacity: 0.5`
    - `npx tsc --noEmit` exits 0
  </acceptance_criteria>
  <done>Full QWERTZ keyboard renders with finger colors for active keys, grayed inactive keys, pulse animation on target key, press animation on keydown. Both DE and CH layouts supported.</done>
</task>

</tasks>

<verification>
- `npx vitest run src/game/levels.test.ts` passes
- `npx tsc --noEmit` passes
- Keyboard renders correctly when manually instantiated (verified in Plan 05 integration)
- Level data is correct per spec progression table
</verification>

<success_criteria>
- Levels 1-6 defined with correct key progressions matching spec section 4.1
- Keyboard renders full QWERTZ layout with finger colors from spec section 8.3
- Pulse and press animations work via CSS classes
- Both DE and CH layout data exist
</success_criteria>

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