341 lines
14 KiB
Markdown
341 lines
14 KiB
Markdown
---
|
|||
|
|
phase: 03-komplettes-spielerlebnis
|
||
|
|
plan: 02
|
||
|
|
type: execute
|
||
|
|
wave: 2
|
||
|
|
depends_on: [03-01]
|
||
|
|
files_modified:
|
||
|
|
- src/game/typing.ts
|
||
|
|
- src/game/typing.test.ts
|
||
|
|
- src/ui/screens.ts
|
||
|
|
- index.html
|
||
|
|
autonomous: true
|
||
|
|
requirements: [TYPE-05, TYPE-06, TYPE-07, LEVL-05, LEVL-06]
|
||
|
|
must_haves:
|
||
|
|
truths:
|
||
|
|
- "Entdecken phase shows companion text and highlights the new key on keyboard"
|
||
|
|
- "Woerter phase displays whole words with active letter highlighted"
|
||
|
|
- "Lesson flows through discover -> practice -> words automatically"
|
||
|
|
- "Every 3rd session is a review mixing letters from last 2-3 levels"
|
||
|
|
- "After 2x same level, companion shows encouragement with curiosity trigger"
|
||
|
|
artifacts:
|
||
|
|
- path: "src/game/typing.ts"
|
||
|
|
provides: "Extended createExerciseState with mode='letters'|'words', generateWordLetters()"
|
||
|
|
exports: ["createExerciseState", "generateWordLetters"]
|
||
|
|
- path: "src/ui/screens.ts"
|
||
|
|
provides: "Lesson sub-phase state machine: discover -> practice -> words"
|
||
|
|
contains: "LessonPhase"
|
||
|
|
key_links:
|
||
|
|
- from: "src/ui/screens.ts"
|
||
|
|
to: "src/game/typing.ts"
|
||
|
|
via: "createExerciseState for practice and word phases"
|
||
|
|
pattern: "createExerciseState"
|
||
|
|
- from: "src/ui/screens.ts"
|
||
|
|
to: "src/companion/companion.ts"
|
||
|
|
via: "getLetterIntro for discover phase"
|
||
|
|
pattern: "getLetterIntro"
|
||
|
|
- from: "src/ui/screens.ts"
|
||
|
|
to: "src/game/words.ts"
|
||
|
|
via: "getWordsForLevel for word phase"
|
||
|
|
pattern: "getWordsForLevel"
|
||
|
|
---
|
||
|
|
|
||
|
|
<objective>
|
||
|
|
Build the complete lesson flow with 3 sub-phases (Entdecken -> Ueben -> Woerter) as an internal state machine, plus review mechanic and repetition protection.
|
||
|
|
|
||
|
|
Purpose: This is the core game loop — the child experiences discover, practice, then words in one fluid session.
|
||
|
|
Output: Extended typing engine with word mode, lesson screen with 3-phase state machine, review and repetition logic.
|
||
|
|
</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/ROADMAP.md
|
||
|
|
@.planning/STATE.md
|
||
|
|
@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md
|
||
|
|
@src/game/typing.ts
|
||
|
|
@src/ui/screens.ts
|
||
|
|
@src/companion/companion.ts
|
||
|
|
@src/game/words.ts
|
||
|
|
@src/game/levels.ts
|
||
|
|
@src/types.ts
|
||
|
|
@index.html
|
||
|
|
|
||
|
|
<interfaces>
|
||
|
|
<!-- From src/types.ts (after Plan 01): -->
|
||
|
|
export type LessonPhase = 'discover' | 'practice' | 'words';
|
||
|
|
export interface Progress {
|
||
|
|
id: 1;
|
||
|
|
currentLevel: number;
|
||
|
|
completedLevels: number[];
|
||
|
|
totalSessions: number;
|
||
|
|
sessionDates: string[];
|
||
|
|
selectedCharacter: CompanionType;
|
||
|
|
selectedLayout: KeyboardLayout;
|
||
|
|
totalCorrect: number;
|
||
|
|
totalErrors: number;
|
||
|
|
errorKeyCounts: Record<string, number>;
|
||
|
|
lastPlayedLevels: number[];
|
||
|
|
}
|
||
|
|
|
||
|
|
<!-- From src/game/words.ts (after Plan 01): -->
|
||
|
|
export const wordsByLevel: Record<number, string[]>;
|
||
|
|
export function getWordsForLevel(level: number, count: number): string[];
|
||
|
|
|
||
|
|
<!-- From src/companion/companion.ts (existing): -->
|
||
|
|
export async function getLetterIntro(characterType: CompanionType, letter: string, fingerDescription: string, db: IDBDatabase): Promise<string>;
|
||
|
|
|
||
|
|
<!-- From src/game/typing.ts (existing): -->
|
||
|
|
export function createExerciseState(level: number): TypingExerciseState;
|
||
|
|
export function handleKeyPress(state: TypingExerciseState, pressedKey: string): KeyPressResult;
|
||
|
|
export function getCurrentLetter(state: TypingExerciseState): string | null;
|
||
|
|
|
||
|
|
<!-- From src/game/levels.ts (existing): -->
|
||
|
|
export function getLevelByNumber(n: number): Level | undefined;
|
||
|
|
export function getKeysUpToLevel(n: number): string[];
|
||
|
|
</interfaces>
|
||
|
|
</context>
|
||
|
|
|
||
|
|
<tasks>
|
||
|
|
|
||
|
|
<task type="auto" tdd="true">
|
||
|
|
<name>Task 1: Extend typing engine with word mode</name>
|
||
|
|
<files>src/game/typing.ts, src/game/typing.test.ts</files>
|
||
|
|
<read_first>src/game/typing.ts, src/game/words.ts, src/types.ts</read_first>
|
||
|
|
<behavior>
|
||
|
|
- Test: createExerciseState(level) still works as before (letters mode, backwards compatible)
|
||
|
|
- Test: createWordExerciseState(level, words) creates state with letters from the words joined, e.g. words=["eis","die"] -> letters=["e","i","s","d","i","e"]
|
||
|
|
- Test: createWordExerciseState stores words and wordBoundaries in state for UI rendering
|
||
|
|
- Test: handleKeyPress works the same way for word-mode state
|
||
|
|
- Test: getWordAtIndex(state, letterIndex) returns which word the letter belongs to
|
||
|
|
</behavior>
|
||
|
|
<action>
|
||
|
|
Extend `src/game/typing.ts` (per D-03):
|
||
|
|
|
||
|
|
1. Add `ExerciseMode` type: `'letters' | 'words'`
|
||
|
|
|
||
|
|
2. Extend `TypingExerciseState` interface (in types.ts) with optional word fields:
|
||
|
|
```typescript
|
||
|
|
export interface TypingExerciseState {
|
||
|
|
currentLetterIndex: number;
|
||
|
|
letters: string[];
|
||
|
|
intervalMs: number;
|
||
|
|
correctStreak: number;
|
||
|
|
totalCorrect: number;
|
||
|
|
totalErrors: number;
|
||
|
|
mode: 'letters' | 'words';
|
||
|
|
words?: string[]; // original words for display
|
||
|
|
wordBoundaries?: number[]; // indices where each word starts in letters[]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
3. Keep existing `createExerciseState(level)` unchanged but set `mode: 'letters'`, `words: undefined`, `wordBoundaries: undefined`.
|
||
|
|
|
||
|
|
4. Add new function:
|
||
|
|
```typescript
|
||
|
|
export function createWordExerciseState(words: string[]): TypingExerciseState {
|
||
|
|
const letters: string[] = [];
|
||
|
|
const wordBoundaries: number[] = [];
|
||
|
|
for (const word of words) {
|
||
|
|
wordBoundaries.push(letters.length);
|
||
|
|
for (const ch of word) {
|
||
|
|
letters.push(ch);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
currentLetterIndex: 0,
|
||
|
|
letters,
|
||
|
|
intervalMs: 0, // no falling in word mode
|
||
|
|
correctStreak: 0,
|
||
|
|
totalCorrect: 0,
|
||
|
|
totalErrors: 0,
|
||
|
|
mode: 'words',
|
||
|
|
words,
|
||
|
|
wordBoundaries,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
5. Add helper: `getCurrentWordIndex(state: TypingExerciseState): number` — returns which word index (0-based) the currentLetterIndex is in, using wordBoundaries.
|
||
|
|
|
||
|
|
6. `handleKeyPress` already works generically — no changes needed.
|
||
|
|
|
||
|
|
Create `src/game/typing.test.ts` (or extend if exists) with vitest tests for the new functions plus backwards compatibility.
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>npx vitest run src/game/typing.test.ts</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- grep "createWordExerciseState" src/game/typing.ts returns export
|
||
|
|
- grep "getCurrentWordIndex" src/game/typing.ts returns export
|
||
|
|
- grep "mode.*letters.*words" src/types.ts returns match showing mode field
|
||
|
|
- npx vitest run src/game/typing.test.ts exits 0
|
||
|
|
</acceptance_criteria>
|
||
|
|
<done>Typing engine supports word mode with word boundaries tracking. Backwards compatible with letter mode. Tests pass.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
<task type="auto">
|
||
|
|
<name>Task 2: Lesson screen 3-phase state machine with review and repetition</name>
|
||
|
|
<files>src/ui/screens.ts, index.html</files>
|
||
|
|
<read_first>src/ui/screens.ts, src/game/typing.ts, src/companion/companion.ts, src/game/words.ts, src/game/levels.ts, src/types.ts, index.html</read_first>
|
||
|
|
<action>
|
||
|
|
Rewrite `initLessonScreen` in `src/ui/screens.ts` as a 3-phase state machine (per D-01, D-02, D-03):
|
||
|
|
|
||
|
|
**Add to index.html** inside `#screen-lesson .lesson`:
|
||
|
|
```html
|
||
|
|
<div class="lesson__phase-indicator" id="lesson-phase-indicator"></div>
|
||
|
|
```
|
||
|
|
This shows which phase is active (Entdecken / Ueben / Woerter). Add above the letter-area.
|
||
|
|
|
||
|
|
**Signature change:**
|
||
|
|
```typescript
|
||
|
|
export function initLessonScreen(
|
||
|
|
db: IDBDatabase,
|
||
|
|
level: number,
|
||
|
|
layout: KeyboardLayout,
|
||
|
|
onComplete: (level: number, stats: { totalCorrect: number; totalErrors: number; errorKeys: Record<string, number> }) => void,
|
||
|
|
onCancel?: () => void,
|
||
|
|
): () => void
|
||
|
|
```
|
||
|
|
The `onComplete` callback now receives exercise stats (per D-16).
|
||
|
|
|
||
|
|
**State machine flow:**
|
||
|
|
|
||
|
|
1. **DISCOVER phase** (per D-02, TYPE-05):
|
||
|
|
- Show phase indicator: "Entdecken"
|
||
|
|
- Get the first new key of this level via `getLevelByNumber(level).newKeys[0]`
|
||
|
|
- Fetch companion letter intro via `getLetterIntro(characterType, letter, fingerDescription, db)`
|
||
|
|
- Display intro text in `#lesson-companion-text`
|
||
|
|
- Highlight the target key on keyboard (already have `highlightKey()`)
|
||
|
|
- Make key pulse (add class `keyboard__key--discover-pulse` to the key)
|
||
|
|
- Wait for child to press the correct key ONCE to proceed
|
||
|
|
- If level has a second newKey, repeat discover for that key too
|
||
|
|
- Then transition to PRACTICE
|
||
|
|
|
||
|
|
2. **PRACTICE phase** (existing behavior, TYPE-01-04):
|
||
|
|
- Show phase indicator: "Ueben"
|
||
|
|
- Run existing letter exercise with `createExerciseState(level)`
|
||
|
|
- On complete, accumulate stats, transition to WORDS
|
||
|
|
|
||
|
|
3. **WORDS phase** (per D-03, TYPE-06):
|
||
|
|
- Show phase indicator: "Woerter"
|
||
|
|
- Get 3-4 random words via `getWordsForLevel(level, 4)` (fallback to 3 if < 4 available)
|
||
|
|
- Create word exercise via `createWordExerciseState(words)`
|
||
|
|
- Display current word fully, highlight active letter:
|
||
|
|
```html
|
||
|
|
<div class="word-display">
|
||
|
|
<span class="word-display__letter word-display__letter--done">e</span>
|
||
|
|
<span class="word-display__letter word-display__letter--active">i</span>
|
||
|
|
<span class="word-display__letter">s</span>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
- When word complete, show next word. After all words done, call `onComplete(level, stats)`.
|
||
|
|
|
||
|
|
**Review mechanic (per D-05, LEVL-05):**
|
||
|
|
Add `isReviewSession` parameter to `initLessonScreen`. When `true`:
|
||
|
|
- Discover phase: skip (reviews don't introduce new letters)
|
||
|
|
- Practice phase: use mixed letters from last 2-3 levels (combine allKeys from current and previous 1-2 levels)
|
||
|
|
- Words phase: pick words from last 2-3 levels randomly
|
||
|
|
|
||
|
|
The review trigger logic (`totalSessions % 3 === 0`) is handled in `app.ts` (Plan 04), not here. This function just needs to accept the `isReview: boolean` flag.
|
||
|
|
|
||
|
|
**Repetition protection (per D-06, LEVL-06):**
|
||
|
|
Add `showEncouragement` parameter. When `true`:
|
||
|
|
- Before discover phase, show companion encouragement text: a hardcoded array of 3-4 messages like "Magst du sehen, was als Naechstes kommt? Im Wald gibt es noch so viel zu entdecken!"
|
||
|
|
- Show for 3 seconds, then proceed to discover (or practice if review).
|
||
|
|
|
||
|
|
The trigger logic (checking `lastPlayedLevels` for 2x same) is handled in `app.ts` (Plan 04).
|
||
|
|
|
||
|
|
**Cumulative stats across phases:**
|
||
|
|
Track `totalCorrect`, `totalErrors`, `errorKeys: Record<string, number>` across all 3 phases. In handleKeyPress wrapper, if `!result.correct`, increment `errorKeys[expectedKey]`. Pass accumulated stats to `onComplete`.
|
||
|
|
|
||
|
|
**CSS for phase indicator** — add styles in action:
|
||
|
|
```css
|
||
|
|
.lesson__phase-indicator {
|
||
|
|
text-align: center;
|
||
|
|
font-family: var(--font-heading);
|
||
|
|
font-size: 1rem;
|
||
|
|
color: var(--text-warm);
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
opacity: 0.7;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**CSS for word display:**
|
||
|
|
```css
|
||
|
|
.word-display {
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 0.25rem;
|
||
|
|
font-family: var(--font-heading);
|
||
|
|
font-size: 2.5rem;
|
||
|
|
margin: 2rem 0;
|
||
|
|
}
|
||
|
|
.word-display__letter {
|
||
|
|
color: var(--text-light);
|
||
|
|
transition: color 0.3s;
|
||
|
|
}
|
||
|
|
.word-display__letter--active {
|
||
|
|
color: var(--accent-gold);
|
||
|
|
font-weight: 700;
|
||
|
|
}
|
||
|
|
.word-display__letter--done {
|
||
|
|
color: var(--accent-green);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Add CSS for discover pulse:
|
||
|
|
```css
|
||
|
|
.keyboard__key--discover-pulse {
|
||
|
|
animation: discover-pulse 1.5s ease-in-out infinite;
|
||
|
|
}
|
||
|
|
@keyframes discover-pulse {
|
||
|
|
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 var(--accent-gold); }
|
||
|
|
50% { transform: scale(1.1); box-shadow: 0 0 12px 4px var(--accent-gold); }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>npx tsc --noEmit && npx vitest run</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- grep "lesson-phase-indicator" index.html returns a match
|
||
|
|
- grep "discover.*practice.*words" src/ui/screens.ts shows state machine phases
|
||
|
|
- grep "getLetterIntro" src/ui/screens.ts shows companion integration in discover
|
||
|
|
- grep "createWordExerciseState" src/ui/screens.ts shows word mode usage
|
||
|
|
- grep "getWordsForLevel" src/ui/screens.ts shows word list integration
|
||
|
|
- grep "isReview" src/ui/screens.ts shows review parameter
|
||
|
|
- grep "showEncouragement" src/ui/screens.ts shows repetition protection parameter
|
||
|
|
- grep "word-display" src/styles/main.css returns CSS matches
|
||
|
|
- grep "discover-pulse" src/styles/main.css returns CSS matches
|
||
|
|
- npx tsc --noEmit exits 0
|
||
|
|
</acceptance_criteria>
|
||
|
|
<done>Lesson screen runs 3 phases (discover->practice->words) with companion letter intro, word display UI, review flag support, and repetition encouragement. Stats accumulated across phases and passed to onComplete callback.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
</tasks>
|
||
|
|
|
||
|
|
<verification>
|
||
|
|
- `npx tsc --noEmit` — no type errors
|
||
|
|
- `npx vitest run` — all tests pass
|
||
|
|
- Manual: lesson screen shows phase indicator, discover phase displays companion text and highlights key, words phase shows word with active letter
|
||
|
|
</verification>
|
||
|
|
|
||
|
|
<success_criteria>
|
||
|
|
1. Typing engine has createWordExerciseState() and getCurrentWordIndex()
|
||
|
|
2. Lesson screen flows: discover -> practice -> words -> onComplete(level, stats)
|
||
|
|
3. Discover phase: companion introduces letter, key pulses, child presses once to proceed
|
||
|
|
4. Words phase: 3-4 words displayed with active letter highlighted, typed letter by letter
|
||
|
|
5. Review flag skips discover and mixes letters/words from recent levels
|
||
|
|
6. Encouragement shown when showEncouragement=true
|
||
|
|
7. Stats (correct, errors, errorKeys) accumulated and passed to onComplete
|
||
|
|
</success_criteria>
|
||
|
|
|
||
|
|
<output>
|
||
|
|
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-02-SUMMARY.md`
|
||
|
|
</output>
|