16 KiB
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 |
|
|
true |
|
|
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;
**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).
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; }
}
```
<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>