Cleanup: KI-spezifische, veraltete und experimentelle Dateien entfernt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-22 11:38:41 +02:00
commit 6268ea6693
8 changed files with 0 additions and 1684 deletions

View file

@ -1,90 +0,0 @@
name: Coverage
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
jobs:
coverage:
name: Test Coverage
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install coverage
- name: Run Tests with Coverage
run: |
coverage run -m unittest discover -s tests -p "test_*.py"
coverage report -m
coverage html
- name: Generate Coverage Badge
run: |
COVERAGE=$(coverage report | grep TOTAL | awk '{print $4}' | sed 's/%//')
echo "Coverage: $COVERAGE%"
# Create badge color
if (( $(echo "$COVERAGE >= 90" | bc -l) )); then
COLOR="brightgreen"
elif (( $(echo "$COVERAGE >= 75" | bc -l) )); then
COLOR="green"
elif (( $(echo "$COVERAGE >= 60" | bc -l) )); then
COLOR="yellow"
else
COLOR="red"
fi
echo "COVERAGE=$COVERAGE" >> $GITHUB_ENV
echo "COLOR=$COLOR" >> $GITHUB_ENV
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov/
retention-days: 30
- name: Coverage Summary
run: |
echo "# Coverage Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "![Coverage](https://img.shields.io/badge/coverage-${{ env.COVERAGE }}%25-${{ env.COLOR }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Detailed Coverage" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
coverage report >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Full HTML report available as artifact." >> $GITHUB_STEP_SUMMARY
- name: Comment Coverage on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const coverage = process.env.COVERAGE;
const color = process.env.COLOR;
const body = `## 📊 Coverage Report\n\n![Coverage](https://img.shields.io/badge/coverage-${coverage}%25-${color})\n\nDetailed report available in artifacts.`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});

View file

@ -1,116 +0,0 @@
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Unit Tests
run: |
python -m unittest tests.test_lesson_service tests.test_statistics_service tests.test_settings_service -v
- name: Run Integration Tests
run: |
python -m unittest tests.test_integration_progress tests.test_integration_statistics -v
- name: Run All Tests
run: |
python -m unittest discover -s tests -p "test_*.py" -v
test-summary:
name: Test Summary
runs-on: ubuntu-latest
needs: test
if: always()
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Generate Test Summary
run: |
echo "# Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Test Execution" >> $GITHUB_STEP_SUMMARY
python -m unittest discover -s tests -p "test_*.py" -v 2>&1 | tee test_output.txt
# Parse test results
TESTS_RUN=$(grep -o "Ran [0-9]* test" test_output.txt | grep -o "[0-9]*" || echo "0")
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Tests Run:** $TESTS_RUN" >> $GITHUB_STEP_SUMMARY
if grep -q "OK" test_output.txt; then
echo "**Status:** ✅ All tests passed!" >> $GITHUB_STEP_SUMMARY
else
echo "**Status:** ❌ Some tests failed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Test Details" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -20 test_output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
lint:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install flake8
run: |
python -m pip install --upgrade pip
pip install flake8
- name: Lint with flake8
run: |
# Stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# Exit-zero treats all errors as warnings
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
continue-on-error: true

287
CLAUDE.md
View file

@ -1,287 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Typewriter Tutor (Tipptrainer) is a web-based typing tutor application built with Flask that helps users learn touch typing. The application features lesson-based training, real-time statistics, progress tracking, and an adaptive metronome system to guide typing speed.
**Important Context**: This is a minimal practice project created to learn Aider (an AI coding tool). The codebase is in German, and this is intentional.
## Technology Stack
- **Backend**: Python 3 with Flask
- **ORM**: SQLAlchemy with SQLite database
- **Frontend**: HTML, Bootstrap 5, JavaScript
- **Charts**: Chart.js for statistics visualization
- **Icons**: Lucide Icons
## Development Commands
### Running the Application
```bash
# Start the Flask development server
python app.py
```
The application runs on `http://localhost:5000` by default.
### Database Setup
**WICHTIG**: Die Anwendung verwendet Alembic für Datenbank-Migrationen.
**Erste Installation**:
```bash
# Setup-Script ausführen (empfohlen)
python setup_database.py
# ODER manuell:
alembic upgrade head
```
**Bei Schema-Änderungen**:
```bash
# Neue Migration erstellen
alembic revision --autogenerate -m "Beschreibung der Änderung"
# Migration anwenden
alembic upgrade head
```
Siehe `MIGRATIONS.md` für detaillierte Informationen.
### Dependencies
Install required packages:
```bash
pip install flask flask-sqlalchemy
```
For production or development environment:
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# or
venv\Scripts\activate # Windows
pip install flask flask-sqlalchemy
```
### Testing Endpoints
Debug endpoint for checking lesson loading and configuration:
```bash
curl http://localhost:5000/debug
```
### Creating Lessons from TXT Files
The project includes a CLI tool to convert TXT files into lessons.json format:
```bash
# Convert TXT files (order matters!)
python3 convert_txt_files_to_lessons_json.py 01.1_*.txt 01.2_*.txt 01.3_*.txt
# With custom output file
python3 convert_txt_files_to_lessons_json.py -o data/lessons.json *.txt
# Show help
python3 convert_txt_files_to_lessons_json.py --help
```
**TXT File Format:**
```
Titel: 01.1 Erarbeitung der Grundstellung
Untertitel: Optional subtitle (ignored)
Anweisung: Task/instruction for the user
ffff jjjj ffff jjjj
# Comments starting with # are ignored
...
```
**Filename Format:** `XX.Y_Description.txt` (e.g., `01.1_Grundstellung.txt`)
- Number becomes `lesson` as string (e.g., `01.1``"1.1"`)
- Parameter order determines JSON order
- Supports both decimal (1.1) and integer (2) lesson numbers
**Output Format:**
```json
{
"lesson": "1.1",
"title": "01.1 Erarbeitung der Grundstellung",
"text": "The practice text...",
"task": "Instruction for the user"
}
```
## Architecture Overview
### Request Flow
1. **Browser → Flask Routes** (`app.py`) → **Templates** (`templates/`) with server-side rendered data
2. **JavaScript** (`static/js/script.js`) handles user input, cursor management, and real-time statistics
3. **AJAX calls** to Flask endpoints for progress updates and lesson changes
4. **SQLAlchemy ORM** (`models.py`) manages database operations
### Key Components
**Session State Management**: The application uses a hybrid approach:
- **Flask Session**: Stores temporary state like metronome settings, elapsed time, keystroke count
- **SQLite Database**: Persists progress (current lesson, cursor position, user input) and statistics
**Lesson Loading System**:
- Lessons are loaded from `data/lessons.json` at startup
- Includes automatic JSON repair functionality if the file is corrupted
- Falls back to hardcoded `FALLBACK_LESSONS` if JSON loading fails
- JSON repair endpoint available at `/repair_json` (POST)
**Direct Text Input**: The application uses a unique approach where users type directly in the template text field rather than a separate input field. The cursor position is tracked and visualized with a blinking underscore.
### Database Models (`models.py`)
**Progress**: Tracks the current typing session state
- `current_text_index`: Which lesson is currently active
- `current_position`: Cursor position within the text
- `last_text`: User's typed input (allows resuming sessions)
**Statistic**: General typing statistics (currently not heavily used)
**LessonStatistic**: Historical performance data per lesson
- `lesson_index`: References the lesson in `lessons.json`
- `chars_per_minute`: Typing speed (ZPM in German)
- `error_rate`: Error percentage
- `wpm`: Words per minute (characters/5 per minute)
- `created_at`: Timestamp for tracking progress over time
### Frontend Architecture
**Modular JavaScript (ES6 Modules)**: Das Frontend ist in separate Module aufgeteilt für bessere Wartbarkeit:
- **`theme.js`**: Theme Management (Light/Dark Mode)
- **`metronome.js`**: Adaptive Metronome-Klasse und Audio-Player
- **`statistics.js`**: Statistik-Berechnungen und Anzeige
- **`textDisplay.js`**: Text-Rendering und Highlighting
- **`keyboard.js`**: Keyboard-Event-Handling
- **`session.js`**: Session Management und Fortschritt-Speicherung
- **`ui.js`**: UI-Interaktionen (Buttons, Modals, Toggles)
- **`main.js`**: Hauptorchestration und Initialisierung
**Theme System**: Light/Dark mode toggle using Bootstrap's `data-bs-theme` attribute, stored in localStorage.
**Adaptive Metronome**:
- Adjusts typing speed guidance based on user accuracy
- Target accuracy: 96% (configurable)
- Tracks last 50 keystrokes to calculate rolling accuracy
- Speed increases when accuracy is high, decreases when low
- Maximum speed configurable (default: 100 BPM) to prevent overwhelming users
- Runs independently with precise timing (Web Audio API, not setTimeout)
- Implementiert in `metronome.js` mit zwei Klassen:
- `AdaptiveMetronome`: Geschwindigkeitsanpassungslogik mit `maxSpeed` option
- `MetronomePlayer`: Audio-Wiedergabe und präzises Timing
**Visual Feedback**:
- Green highlighting for correct characters
- Red highlighting with orange overlay for incorrect characters
- Blinking cursor shows current typing position
**UI Design**:
- Minimalistic statistics display without colorful badges
- Right-aligned values with consistent spacing
- Tooltips on all icons for better user guidance (v0.4.5)
- Color-coded whitespace characters (v0.4.5)
- Precision display (100 - error_rate) instead of error rate for positive feedback
- Uniform button colors: Blue (primary actions), Green (continue), Red (end)
### Data Storage
**Lessons**: `data/lessons.json`
- Each lesson has: `lesson` (number), `title`, `text`
- Text can contain newlines for structured practice
- Maximum 5000 characters per lesson (enforced by recent commit)
**Database**: `instance/typewriter.db`
- Auto-created on first run
- Includes migration logic for schema updates (see `chars_per_second``chars_per_minute` migration in `app.py:144-172`)
## Important Routes
### Main Routes
- `/` - Main typing interface
- `/statistics` - Statistics page with charts
- `/settings` - Settings page for metronome configuration
### API Endpoints
- `POST /update_progress` - Updates typing progress and returns real-time statistics
- `POST /next_text` - Loads next lesson
- `POST /set_lesson` - Switches to a specific lesson by index
- `POST /save_lesson_statistics` - Saves completed lesson statistics
- `GET /get_lesson_statistics/<lesson_index>` - Returns historical stats for a lesson
- `POST /end_session` - Ends current typing session
- `POST /save_settings` - Saves metronome and display settings to session
- `POST /set_metronome` - Updates metronome configuration
### Debug/Utility Routes
- `/debug` - Shows lesson loading status and configuration
- `POST /repair_json` - Attempts to repair corrupted lessons.json
- `POST /recreate_lessons` - Creates sample lessons file
## Statistics Calculation
The application calculates several metrics based on real-time typing:
- **Zeichen pro Minute (ZPM)**: `(total_chars / elapsed_time_seconds) * 60`
- **Wörter pro Minute (WPM)**: `(correct_chars / 5) / (elapsed_time_seconds / 60)`
- **Fehlerrate**: `(incorrect_chars / total_chars) * 100`
- **Genauigkeit (Accuracy)**: `(correct_chars / total_chars) * 100`
Time measurement starts with the first keystroke and runs continuously. All keystrokes are counted including backspace and arrow keys.
## Configuration & Settings
**Metronome Settings** (stored persistently in UserSettings database table):
- `metronome_enabled`: Boolean for on/off
- `metronome_bpm`: Beats per minute
- `metronome_sound`: Sound type ('beep')
- `metronome_mode`: 'automatic' or manual
- `metronome_speed`: Base speed setting
- `target_error_rate`: Target error percentage (default: 5%)
- `max_bpm_speed`: Maximum speed for adaptive metronome (default: 100 BPM) - prevents overwhelming users
- `speed_display`: 'zpm' or 'wpm' for display preference
**Version Display**: Version number from VERSION file is displayed in the header using `APP_VERSION` template variable.
**Secret Key**: Update `app.secret_key` in `app.py:10` for production use.
## Database Schema Changes
The application includes automatic migration logic. When adding new columns:
1. Update the model in `models.py`
2. Add migration logic in `app.py` within the `with app.app_context():` block
3. Use SQLAlchemy's `inspect` to check existing schema
4. Create temporary table, migrate data, drop old table, rename new table
See example migration at `app.py:144-172` for converting `chars_per_second` to `chars_per_minute`.
## Anti-Cheat Features
The application includes measures to prevent cheating:
- Copy-paste is disabled
- Direct keyboard input only
- All keystrokes are counted including corrections
## Common Pitfalls
1. **Lessons not loading**: Check `data/lessons.json` exists and is valid JSON. Use `/debug` endpoint to diagnose.
2. **Session state lost**: Flask sessions require a valid `secret_key` and cookies enabled.
3. **Database schema issues**: The app includes automatic migration for some schema changes, but manual intervention may be needed for complex changes.
4. **Character encoding**: Lessons use UTF-8 encoding for German characters (ä, ö, ü, ß). Ensure files are saved with UTF-8.
## File Structure Notes
- Loose files in root directory (e.g., `Metronome_app.py`, `adative_speed.py`, `*.js` files) appear to be experimental/prototype code not integrated into the main application
- The active codebase is: `app.py`, `models.py`, `data/`, `static/`, `templates/`

View file

@ -1,269 +0,0 @@
# 🧹 Aufräum-Guide für Typewriter Tutor
## Schnell-Übersicht
```bash
./cleanup.sh # Interaktives Aufräumen (empfohlen)
```
---
## 📋 Was kann aufgeräumt werden?
### 🔴 Immer sicher zu löschen
| Was | Wo | Warum | Wie |
|-----|-----|-------|-----|
| Python-Cache | `__pycache__/` | Wird automatisch neu erstellt | `find . -name "__pycache__" -type d -exec rm -rf {} +` |
| .pyc Dateien | `*.pyc` | Kompilierte Python-Dateien | `find . -name "*.pyc" -delete` |
| Temporäre Dateien | `*.tmp`, `*.temp` | Nicht benötigt | `find . -name "*.tmp" -delete` |
| Backup-Dateien | `*.bak`, `*~` | Alte Backups | `find . -name "*.bak" -delete` |
### 🟡 Optional löschbar
| Was | Wo | Beachten | Wie |
|-----|-----|----------|-----|
| Aider-History | `.aider.*` | Nur wenn Sie Aider nicht mehr verwenden | `rm -rf .aider.*` |
| Claude Code Cache | `.claude/` | Cache wird neu erstellt | `rm -rf .claude/` |
| Log-Dateien | `*.log` | Falls nicht für Debugging benötigt | `find . -name "*.log" -delete` |
### 🟠 Vorsicht beim Löschen
| Was | Wo | Beachten | Wiederherstellung |
|-----|-----|----------|-------------------|
| Datenbank | `instance/typewriter.db` | **Alle Fortschritte gehen verloren!** | `alembic upgrade head` |
| Virtual Environment | `venv/` | Muss neu installiert werden | `./setup.sh` oder `python -m venv venv` |
| .env Datei | `.env` | **SECRET_KEY geht verloren!** | Neu generieren lassen |
### ⛔ NIE löschen
| Was | Warum |
|-----|-------|
| `data/lessons.json` | Enthält alle Tipp-Lektionen |
| `alembic/versions/` | Datenbank-Migrations-Historie |
| `requirements.txt` | Dependencies-Liste |
| Alle `.py`, `.js`, `.html`, `.css` Dateien | Kern-Anwendung |
---
## 🚀 Aufräum-Szenarien
### Szenario 1: Leichtes Aufräumen
**Ziel**: Cache und temporäre Dateien entfernen
```bash
# Automatisch (empfohlen)
./cleanup.sh
# Bei allen Fragen "n" eingeben außer bei Cache
# Oder manuell
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null
find . -type f -name "*.pyc" -delete
find . -type f -name "*.tmp" -delete
git gc
```
**Ergebnis**: ~5-50 MB gespart, keine Neuinstallation nötig
---
### Szenario 2: Tiefes Aufräumen
**Ziel**: Alles außer Code und Daten zurücksetzen
```bash
./cleanup.sh
# Bei allen Fragen "j" eingeben
# Oder manuell
rm -rf __pycache__/ venv/ instance/ .aider.* .claude/
find . -name "*.pyc" -delete
```
**Dann neu aufsetzen**:
```bash
./setup.sh
```
**Ergebnis**: ~100-500 MB gespart, ~2 Minuten Neuinstallation
---
### Szenario 3: Kompletter Reset
**Ziel**: Projekt auf Auslieferungszustand zurücksetzen
```bash
# VORSICHT: Alle Daten gehen verloren!
# 1. Aufräumen
./cleanup.sh # Alle Fragen mit "j" beantworten
# 2. Git-Status prüfen
git status
# 3. Falls gewünscht: Uncommitted Changes löschen
git checkout .
git clean -fd
# 4. Neustart
./setup.sh
```
**Ergebnis**: Wie frisch geklont, alle Benutzer-Daten verloren
---
## 🎯 Empfohlene Routine
### Täglich
```bash
# Nicht nötig! Git-Ignore kümmert sich darum
```
### Wöchentlich
```bash
# Cache aufräumen
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null
```
### Monatlich
```bash
# Komplettes Aufräumen
./cleanup.sh
```
### Bei Problemen
```bash
# Reset und Neuinstallation
rm -rf venv/ instance/
./setup.sh
```
---
## 📊 Speicherplatz-Analyse
### Typische Größen
```bash
# Gesamtprojekt (frisch): ~10 MB
# Mit venv: ~60 MB
# Mit Cache: ~70 MB
# Mit Datenbank: ~71 MB
# Mit Aider-History: ~75+ MB
```
### Größte Verzeichnisse prüfen
```bash
du -h --max-depth=1 | sort -hr
```
**Typisches Ergebnis**:
```
75M .
50M ./venv
10M ./.aider.tags.cache.v4
3M ./.aider.chat.history.md
5M ./static
1M ./alembic
500K ./templates
```
---
## 🔧 Nützliche Befehle
### Alle Cache-Dateien finden
```bash
find . -type d -name "__pycache__"
find . -type f -name "*.pyc"
```
### Große Dateien finden
```bash
find . -type f -size +1M -exec ls -lh {} \;
```
### Alte Dateien finden (>30 Tage)
```bash
find . -type f -mtime +30
```
### Git-Größe optimieren
```bash
git gc --aggressive --prune=now
```
### Verzeichnis-Größen
```bash
du -sh */ | sort -hr
```
---
## ✅ Checkliste vor Aufräumen
- [ ] **Wichtige Daten gesichert?**
- [ ] Datenbank-Backup erstellt (`cp instance/typewriter.db backup.db`)
- [ ] Aktuelle Änderungen committed (`git status`)
- [ ] **Was wird gelöscht verstanden?**
- [ ] Liste oben durchgelesen
- [ ] Bei Unsicherheit: Nur Cache löschen
- [ ] **Neuinstallation vorbereitet?**
- [ ] `./setup.sh` verfügbar
- [ ] Internet-Verbindung vorhanden (für pip)
- [ ] **Zeit eingeplant?**
- [ ] Leichtes Aufräumen: 10 Sekunden
- [ ] Tiefes Aufräumen: 2-5 Minuten
- [ ] Kompletter Reset: 5-10 Minuten
---
## 🆘 Probleme und Lösungen
### "Permission denied" beim Löschen
```bash
sudo rm -rf <datei> # Nicht empfohlen!
# Besser: Ownership prüfen
ls -la <datei>
```
### Virtual Environment lässt sich nicht löschen
```bash
# Erst deaktivieren
deactivate
# Dann löschen
rm -rf venv/
```
### Datenbank lässt sich nicht löschen
```bash
# App läuft möglicherweise noch
pkill -f "python app.py"
# Oder
lsof instance/typewriter.db # Zeigt, wer die Datei verwendet
```
### "Command not found: ./cleanup.sh"
```bash
# Ausführbar machen
chmod +x cleanup.sh setup.sh start.sh
```
---
## 📚 Weiterführende Links
- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) - Projekt-Übersicht
- [MIGRATIONS.md](MIGRATIONS.md) - Datenbank-Migrationen
- [CLAUDE.md](CLAUDE.md) - Entwickler-Guide
---
**Tipp**: Starten Sie mit dem leichten Aufräumen und arbeiten Sie sich bei Bedarf hoch!
```bash
./cleanup.sh # Interaktiv und sicher
```

View file

@ -1,146 +0,0 @@
# Scroll-Verbesserungen
## Implementierte Verbesserungen
### 1. Cursor immer in der ersten sichtbaren Zeile
Der Eingabe-Cursor steht nun immer in der ersten sichtbaren Zeile des Vorlagen-Fensters. Dies verbessert die Lesbarkeit und reduziert Augenbelastung.
**Implementierung**:
- Neue Methode `scrollToCursorLine()` in `textDisplay.js`
- Berechnet Scroll-Position so, dass Cursor-Zeile = erste sichtbare Zeile
### 2. Automatisches Zeilenweise Scrollen
Wenn der Benutzer alle Zeichen einer Zeile eingetippt hat, scrollt das Programm automatisch um eine Zeile nach unten, sodass der Cursor wieder in der ersten sichtbaren Zeile steht.
**Mechanismus**:
```javascript
scrollToCursorLine(currentCharIndex) {
const currentLineIndex = this.getCurrentLineIndex(currentCharIndex);
const lineHeight = this.getLineHeight();
// Cursor-Zeile soll erste sichtbare Zeile sein
let desiredScrollTop = (currentLineIndex * lineHeight) - this.topPadding;
// Mit Grenzen-Prüfung
desiredScrollTop = Math.max(0, Math.min(desiredScrollTop, maxScrollTop));
this.targetElement.scrollTop = desiredScrollTop;
}
```
### 3. Genug Abstand für Overlay-Zeichen
Es gibt nun ausreichend Abstand (2.5em ≈ 48px) am oberen Rand, damit die falsch eingegebenen Zeichen (orange Overlay) vollständig lesbar angezeigt werden.
**CSS**:
```css
#target-text {
padding-top: 2.5em; /* Vergrößerter Abstand am oberen Rand */
}
```
**JavaScript**:
```javascript
this.topPadding = 48; // Entspricht dem CSS padding-top: 2.5em
```
### 4. Automatische Rückkehr nach manuellem Scrollen
Wenn der Benutzer mit dem Scrollbalken manuell scrollt, wird bei der nächsten Tastatureingabe sofort wieder zur Cursor-Position gescrollt.
**Implementierung**:
```javascript
updateDisplay(userInput, currentCharIndex, isKeystroke = false) {
// Reset user scroll flag on keystroke
if (isKeystroke && this.userScrolled) {
this.userScrolled = false;
}
// ... rendering ...
// Auto-scroll nur wenn nicht manuell gescrollt
if (!this.userScrolled) {
this.scrollToCursorLine(currentCharIndex);
}
}
```
## Geänderte Dateien
### 1. `static/js/textDisplay.js`
- Neue Eigenschaft: `topPadding` (48px)
- Neue Methode: `getLineHeight()`
- Neue Methode: `scrollToCursorLine(currentCharIndex)`
- Parameter hinzugefügt: `isKeystroke` zu `updateDisplay()`
- Automatisches Zurücksetzen von `userScrolled` bei Tastendruck
### 2. `static/js/main.js`
- Parameter `isKeystroke = true` bei allen Tastendruck-Aufrufen:
- `processKeystroke()`
- `processBackspace()`
- `processArrowLeft()`
- `processArrowRight()`
- `updateDisplay(isKeystroke)` Methode angepasst
## Benutzer-Erlebnis
### Vorher:
- Cursor konnte irgendwo im Fenster sein
- Benutzer musste oft nach oben/unten schauen
- Manuelles Scrollen wurde beibehalten
### Nachher:
- ✅ Cursor immer in der ersten Zeile sichtbar
- ✅ Automatisches Scrollen Zeile für Zeile
- ✅ Genug Platz für Fehler-Anzeige oben
- ✅ Bei Tastendruck sofortige Rückkehr zur Cursor-Position
- ✅ Optimale Lesbarkeit und Ergonomie
## Technische Details
### Scroll-Berechnung:
```
desiredScrollTop = (currentLineIndex × lineHeight) - topPadding
```
- `currentLineIndex`: Zeile, in der sich der Cursor befindet
- `lineHeight`: Höhe einer Textzeile in Pixel
- `topPadding`: 48px Abstand oben für Overlay-Zeichen
### Grenzen:
```javascript
desiredScrollTop = Math.max(0, Math.min(desiredScrollTop, maxScrollTop));
```
- Minimum: 0 (nicht über den Anfang hinaus)
- Maximum: `scrollHeight - containerHeight` (nicht über das Ende hinaus)
## Testing
Manuell testen:
1. Starte die Anwendung
2. Beginne zu tippen
3. Beobachte, dass der Cursor immer in der ersten Zeile bleibt
4. Scrolle manuell mit der Maus
5. Tippe ein Zeichen → Automatische Rückkehr zur Cursor-Position
6. Mache absichtlich Fehler → Orange Overlay sollte vollständig sichtbar sein
## Kompatibilität
- ✅ Funktioniert in allen modernen Browsern
- ✅ Responsive Design unverändert
- ✅ Dark/Light Mode kompatibel
- ✅ Keine Breaking Changes
## Performance
- Scroll-Berechnung: O(1)
- Timeout verwendet für DOM-Rendering (0ms)
- Sehr effizient, keine merkbare Verzögerung
---
**Datum**: 2025-10-27
**Version**: 1.1.0
**Autor**: Claude Code

View file

@ -1,5 +0,0 @@
Bitte arbeite nur mit den Dateien app.py und README.md. Verzichte auf das Laden des gesamten Repos.
Zeige alle Änderungen nur als diff im unified diff-Format, keine ganzen Dateien!
Batch-Edits zusammenfassen, unnötige Kontextdaten vermeiden.
Verwende vorhandene Informationen aus README.md und CLAUDE.md, lade keine Fremddateien.
Nutze /clear nach jedem Task und /compact für die History.

View file

@ -1,235 +0,0 @@
"""
Unit Tests für die Typewriter Tutor Anwendung
"""
import unittest
import json
from app import app, db, calculate_statistics, repair_json
from models import Progress, LessonStatistic
class TypewriterTestCase(unittest.TestCase):
"""Basis-Testklasse für Typewriter Tutor"""
def setUp(self):
"""Setup für jeden Test"""
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
app.config['WTF_CSRF_ENABLED'] = False # CSRF für Tests deaktivieren
self.client = app.test_client()
with app.app_context():
db.create_all()
def tearDown(self):
"""Cleanup nach jedem Test"""
with app.app_context():
db.session.remove()
db.drop_all()
class StatisticsCalculationTests(TypewriterTestCase):
"""Tests für Statistik-Berechnungen"""
def test_calculate_statistics_perfect_input(self):
"""Test: Perfekte Eingabe ohne Fehler"""
user_input = "hello world"
current_text = "hello world"
elapsed_time = 10.0 # 10 Sekunden
stats = calculate_statistics(user_input, current_text, elapsed_time)
self.assertEqual(stats['correct_chars'], 11)
self.assertEqual(stats['incorrect_chars'], 0)
self.assertEqual(stats['total_chars'], 11)
self.assertEqual(stats['error_rate'], 0.0)
self.assertEqual(stats['accuracy'], 100.0)
self.assertGreater(stats['typing_speed'], 0)
def test_calculate_statistics_with_errors(self):
"""Test: Eingabe mit Fehlern"""
user_input = "hallo world" # 'a' statt 'e'
current_text = "hello world"
elapsed_time = 10.0
stats = calculate_statistics(user_input, current_text, elapsed_time)
self.assertEqual(stats['correct_chars'], 10) # alle außer 'a'
self.assertEqual(stats['incorrect_chars'], 1)
self.assertEqual(stats['total_chars'], 11)
self.assertGreater(stats['error_rate'], 0)
self.assertLess(stats['accuracy'], 100.0)
def test_calculate_statistics_zero_time(self):
"""Test: Division durch Null vermeiden"""
user_input = "test"
current_text = "test"
elapsed_time = 0.0
stats = calculate_statistics(user_input, current_text, elapsed_time)
self.assertEqual(stats['typing_speed'], 0)
self.assertEqual(stats['words_per_minute'], 0)
def test_calculate_statistics_wpm_calculation(self):
"""Test: WPM-Berechnung (5 Zeichen = 1 Wort)"""
user_input = "12345" # genau 5 Zeichen = 1 Wort
current_text = "12345"
elapsed_time = 60.0 # 60 Sekunden = 1 Minute
stats = calculate_statistics(user_input, current_text, elapsed_time)
self.assertEqual(stats['words_per_minute'], 1.0)
class JSONRepairTests(TypewriterTestCase):
"""Tests für JSON-Reparatur"""
def test_repair_json_missing_comma(self):
"""Test: Fehlende Kommas zwischen Objekten"""
broken_json = '{"a": 1}\n{"b": 2}'
repaired = repair_json(broken_json)
# Sollte Komma hinzufügen
self.assertIn(',', repaired)
def test_repair_json_valid_json(self):
"""Test: Gültiges JSON bleibt unverändert (abgesehen von Regex-Änderungen)"""
valid_json = '[{"a": 1}, {"b": 2}]'
repaired = repair_json(valid_json)
# Sollte parsebar sein
try:
json.loads(repaired)
parsed = True
except json.JSONDecodeError:
parsed = False
self.assertTrue(parsed)
class APIEndpointTests(TypewriterTestCase):
"""Tests für API-Endpunkte"""
def test_index_page_loads(self):
"""Test: Hauptseite lädt erfolgreich"""
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertIn(b'Typewriter Trainer', response.data)
def test_statistics_page_loads(self):
"""Test: Statistik-Seite lädt"""
response = self.client.get('/statistics')
self.assertEqual(response.status_code, 200)
def test_settings_page_loads(self):
"""Test: Einstellungsseite lädt"""
response = self.client.get('/settings')
self.assertEqual(response.status_code, 200)
def test_update_progress_requires_data(self):
"""Test: update_progress benötigt Daten"""
response = self.client.post('/update_progress',
data=json.dumps({}),
content_type='application/json')
self.assertEqual(response.status_code, 400)
def test_save_lesson_statistics_validation(self):
"""Test: Input-Validierung bei save_lesson_statistics"""
# Negative Werte sollten abgelehnt werden
response = self.client.post('/save_lesson_statistics',
data=json.dumps({
'lesson_index': 0,
'chars_per_minute': -10,
'error_rate': 5,
'wpm': 2
}),
content_type='application/json')
self.assertEqual(response.status_code, 400)
def test_save_lesson_statistics_error_rate_validation(self):
"""Test: Fehlerrate über 100% wird abgelehnt"""
response = self.client.post('/save_lesson_statistics',
data=json.dumps({
'lesson_index': 0,
'chars_per_minute': 100,
'error_rate': 150, # über 100%
'wpm': 20
}),
content_type='application/json')
self.assertEqual(response.status_code, 400)
def test_debug_endpoint_disabled_in_production(self):
"""Test: Debug-Endpoint ist in Produktion deaktiviert"""
app.config['DEBUG'] = False
response = self.client.get('/debug')
self.assertEqual(response.status_code, 404)
app.config['DEBUG'] = True # Zurücksetzen
class DatabaseTests(TypewriterTestCase):
"""Tests für Datenbank-Operationen"""
def test_create_lesson_statistic(self):
"""Test: LessonStatistic kann erstellt werden"""
with app.app_context():
stat = LessonStatistic(
lesson_index=0,
chars_per_minute=100.0,
error_rate=5.0,
wpm=20.0
)
db.session.add(stat)
db.session.commit()
# Abrufen und prüfen
retrieved = LessonStatistic.query.first()
self.assertIsNotNone(retrieved)
self.assertEqual(retrieved.lesson_index, 0)
self.assertEqual(retrieved.chars_per_minute, 100.0)
def test_lesson_statistic_index_exists(self):
"""Test: Index auf lesson_index existiert"""
with app.app_context():
from sqlalchemy import inspect
inspector = inspect(db.engine)
indexes = inspector.get_indexes('lesson_statistic')
# Prüfe ob ein Index auf lesson_index existiert
has_index = any('lesson_index' in idx['column_names'] for idx in indexes)
self.assertTrue(has_index, "Index auf lesson_index sollte existieren")
class ExportTests(TypewriterTestCase):
"""Tests für Export-Funktionalität"""
def test_csv_export_empty_database(self):
"""Test: CSV-Export funktioniert auch mit leerer Datenbank"""
response = self.client.get('/export/statistics/csv')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers['Content-Type'], 'text/csv; charset=utf-8')
def test_json_export_empty_database(self):
"""Test: JSON-Export funktioniert auch mit leerer Datenbank"""
response = self.client.get('/export/statistics/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers['Content-Type'], 'application/json; charset=utf-8')
def test_csv_export_with_data(self):
"""Test: CSV-Export enthält Daten"""
with app.app_context():
stat = LessonStatistic(
lesson_index=0,
chars_per_minute=100.0,
error_rate=5.0,
wpm=20.0
)
db.session.add(stat)
db.session.commit()
response = self.client.get('/export/statistics/csv')
self.assertEqual(response.status_code, 200)
self.assertIn(b'100.0', response.data)
if __name__ == '__main__':
unittest.main()

View file

@ -1,536 +0,0 @@
"""
Umfangreiche Tests für die adaptive Metronom-Geschwindigkeitsanpassung.
Dieses Modul testet die JavaScript-Logik der AdaptiveMetronome-Klasse
durch eine Python-Reimplementierung und umfangreiche Testszenarien.
"""
import unittest
from typing import List
class AdaptiveMetronome:
"""
Python-Reimplementierung der JavaScript AdaptiveMetronome-Klasse.
Muss identisch zur JS-Version in static/js/script.js:81-137 sein.
"""
def __init__(self):
self.speed = 60.0 # Startgeschwindigkeit in BPM
self.accuracy_history: List[float] = [] # Letzte 50 Ergebnisse (1.0=korrekt, 0.0=falsch)
self.consecutive_correct = 0 # Aufeinanderfolgende korrekte Zeichen
self.target_accuracy = 0.96 # Zielgenauigkeit (96%)
def process_keystroke(self, is_correct: bool, current_difficulty: float = 1.0) -> float:
"""
Verarbeitet einen Tastendruck und passt die Geschwindigkeit an.
Args:
is_correct: Ob das eingegebene Zeichen korrekt war
current_difficulty: Schwierigkeitsfaktor (1.0 = normal)
Returns:
Neue BPM-Geschwindigkeit
"""
# 1. History aktualisieren
self.accuracy_history.append(1.0 if is_correct else 0.0)
if len(self.accuracy_history) > 50:
self.accuracy_history.pop(0)
# 2. Konsekutive korrekte Zeichen zählen
if is_correct:
self.consecutive_correct += 1
else:
self.consecutive_correct = 0
# 3. Geschwindigkeit anpassen
return self._adjust_speed(current_difficulty)
def _adjust_speed(self, difficulty: float) -> float:
"""
Passt die Geschwindigkeit basierend auf der aktuellen Genauigkeit an.
Args:
difficulty: Schwierigkeitsfaktor
Returns:
Neue BPM-Geschwindigkeit
"""
if len(self.accuracy_history) == 0:
return self.speed
# Berechne aktuelle Genauigkeit
current_accuracy = sum(self.accuracy_history) / len(self.accuracy_history)
adjusted_target = self.target_accuracy - (difficulty * 0.05)
# Mehrere Anpassungsstrategien kombinieren:
if self.consecutive_correct >= 15:
# Belohnung für längere Fehlerfreiheit
self.speed *= 1.03
elif current_accuracy > adjusted_target + 0.04:
# Zu einfach - stärker beschleunigen
self.speed *= 1.02
elif current_accuracy < adjusted_target - 0.06:
# Zu schwierig - stärker bremsen
self.speed *= 0.92
elif current_accuracy < adjusted_target:
# Leicht unter Ziel - sanft bremsen
self.speed *= 0.97
else:
# Im Zielbereich - sanft beschleunigen
self.speed *= 1.01
# Geschwindigkeitsgrenzen
self.speed = max(40.0, min(200.0, self.speed))
return self.speed
class TestAdaptiveMetronomeBasics(unittest.TestCase):
"""Basistests für die AdaptiveMetronome-Klasse"""
def setUp(self):
"""Wird vor jedem Test ausgeführt"""
self.metronome = AdaptiveMetronome()
def test_initial_values(self):
"""Test: Initialwerte sind korrekt gesetzt"""
self.assertEqual(self.metronome.speed, 60.0)
self.assertEqual(len(self.metronome.accuracy_history), 0)
self.assertEqual(self.metronome.consecutive_correct, 0)
self.assertEqual(self.metronome.target_accuracy, 0.96)
def test_single_correct_keystroke(self):
"""Test: Ein einzelner korrekter Tastendruck"""
speed = self.metronome.process_keystroke(True)
self.assertEqual(len(self.metronome.accuracy_history), 1)
self.assertEqual(self.metronome.accuracy_history[0], 1.0)
self.assertEqual(self.metronome.consecutive_correct, 1)
# Bei 100% Genauigkeit sollte beschleunigt werden (Regel #2: +2%)
self.assertAlmostEqual(speed, 61.2, places=1)
def test_single_incorrect_keystroke(self):
"""Test: Ein einzelner falscher Tastendruck"""
speed = self.metronome.process_keystroke(False)
self.assertEqual(len(self.metronome.accuracy_history), 1)
self.assertEqual(self.metronome.accuracy_history[0], 0.0)
self.assertEqual(self.metronome.consecutive_correct, 0)
# Bei 0% Genauigkeit sollte stark gebremst werden (Regel #3: -8%)
self.assertAlmostEqual(speed, 55.2, places=1)
def test_history_limit_50(self):
"""Test: History wird auf 50 Einträge begrenzt"""
# Gebe 60 korrekte Zeichen ein
for _ in range(60):
self.metronome.process_keystroke(True)
self.assertEqual(len(self.metronome.accuracy_history), 50)
def test_consecutive_correct_reset_on_error(self):
"""Test: Consecutive-Counter wird bei Fehler zurückgesetzt"""
# 5 korrekte Zeichen
for _ in range(5):
self.metronome.process_keystroke(True)
self.assertEqual(self.metronome.consecutive_correct, 5)
# Ein Fehler
self.metronome.process_keystroke(False)
self.assertEqual(self.metronome.consecutive_correct, 0)
class TestAdaptiveMetronomeSpeedAdjustment(unittest.TestCase):
"""Tests für die Geschwindigkeitsanpassung"""
def setUp(self):
"""Wird vor jedem Test ausgeführt"""
self.metronome = AdaptiveMetronome()
def test_rule_1_streak_bonus(self):
"""Test Regel #1: Streak-Bonus bei 15+ korrekten Zeichen"""
# 15 korrekte Zeichen eingeben
for i in range(15):
self.metronome.process_keystroke(True)
initial_speed = self.metronome.speed
# 16. korrektes Zeichen sollte Streak-Bonus auslösen (+3%)
speed = self.metronome.process_keystroke(True)
# Überprüfe dass Regel #1 angewendet wurde
expected_speed = initial_speed * 1.03
self.assertAlmostEqual(speed, expected_speed, places=1)
self.assertGreater(speed, initial_speed)
def test_rule_2_too_easy(self):
"""Test Regel #2: Zu einfach - starke Beschleunigung"""
# Erstelle 10 korrekte Eingaben (100% Genauigkeit)
for _ in range(10):
self.metronome.process_keystroke(True)
# Bei 100% Genauigkeit (> 95%) sollte Regel #2 greifen (+2%)
# Aber nicht Regel #1, da consecutive_correct nur 10 ist
self.assertEqual(self.metronome.consecutive_correct, 10)
# Letzter gespeicherter Speed vor nächstem Keystroke
speeds = [self.metronome.speed]
self.metronome.process_keystroke(True)
# Überprüfe Beschleunigung um ~2%
speed_increase = self.metronome.speed / speeds[0]
self.assertAlmostEqual(speed_increase, 1.02, places=2)
def test_rule_3_too_difficult(self):
"""Test Regel #3: Zu schwer - starkes Bremsen"""
# Erstelle Scenario: 2 korrekt, 8 falsch = 20% Genauigkeit
for _ in range(2):
self.metronome.process_keystroke(True)
for _ in range(8):
self.metronome.process_keystroke(False)
# 20% < (91% - 6%) → Regel #3 sollte greifen (-8%)
current_accuracy = sum(self.metronome.accuracy_history) / len(self.metronome.accuracy_history)
self.assertLess(current_accuracy, 0.85)
# Aber: Speed ist bereits auf Minimum (40 BPM) - kann nicht weiter sinken
# Daher testen wir mit höherem Ausgangspoint
self.metronome.speed = 100.0
initial_speed = self.metronome.speed
# Ein weiterer Fehler sollte Regel #3 auslösen
self.metronome.process_keystroke(False)
# Überprüfe starkes Bremsen um ~8%
speed_decrease = self.metronome.speed / initial_speed
self.assertAlmostEqual(speed_decrease, 0.92, places=2)
def test_rule_4_slightly_below_target(self):
"""Test Regel #4: Leicht unter Ziel - sanftes Bremsen"""
# Erstelle Scenario: 9 korrekt, 1 falsch = 90% Genauigkeit
# 90% liegt zwischen 85% und 91% → Regel #4 (-3%)
for _ in range(9):
self.metronome.process_keystroke(True)
self.metronome.process_keystroke(False)
initial_speed = self.metronome.speed
self.metronome.process_keystroke(True)
# Überprüfe sanftes Bremsen um ~3%
speed_decrease = self.metronome.speed / initial_speed
self.assertAlmostEqual(speed_decrease, 0.97, places=2)
def test_rule_5_in_target_range(self):
"""Test Regel #5: Im Zielbereich - sanfte Beschleunigung"""
# Problem: Nach vielen korrekten Eingaben erreicht Speed das Maximum (200 BPM)
# Daher: Starte mit niedrigerer Speed und baue Genauigkeit auf
# Setze Speed manuell niedriger
self.metronome.speed = 80.0
# Erstelle Scenario: 92% Genauigkeit (zwischen 91% und 95%)
# 46 korrekt, 4 falsch = 92%
for i in range(50):
is_correct = i < 46
self.metronome.process_keystroke(is_correct)
# Verhindere dass Speed zu hoch wird
if self.metronome.speed > 100:
self.metronome.speed = 85.0
# Setze finale Testbedingungen
self.metronome.speed = 85.0
self.metronome.consecutive_correct = 0 # Kein Streak-Bonus
# Accuracy sollte im Zielbereich sein
current_accuracy = sum(self.metronome.accuracy_history) / len(self.metronome.accuracy_history)
self.assertGreater(current_accuracy, 0.91)
self.assertLess(current_accuracy, 0.95)
initial_speed = self.metronome.speed
# Füge ein korrektes Zeichen hinzu (sollte Regel #5 auslösen)
self.metronome.process_keystroke(True)
# Überprüfe sanfte Beschleunigung um ~1%
speed_increase = self.metronome.speed / initial_speed
self.assertAlmostEqual(speed_increase, 1.01, places=2)
def test_speed_limits_minimum(self):
"""Test: Geschwindigkeit wird nicht unter 40 BPM fallen"""
# Setze Speed manuell nahe an Minimum
self.metronome.speed = 41.0
# Erzeuge sehr schlechte Performance
for _ in range(50):
self.metronome.process_keystroke(False)
# Speed sollte nicht unter 40 fallen
self.assertGreaterEqual(self.metronome.speed, 40.0)
def test_speed_limits_maximum(self):
"""Test: Geschwindigkeit wird nicht über 200 BPM steigen"""
# Setze Speed manuell nahe an Maximum
self.metronome.speed = 195.0
# Erzeuge perfekte Performance mit Streak
for _ in range(20):
self.metronome.process_keystroke(True)
# Speed sollte nicht über 200 steigen
self.assertLessEqual(self.metronome.speed, 200.0)
class TestAdaptiveMetronomeScenarios(unittest.TestCase):
"""Realistische Szenarien und Edge Cases"""
def setUp(self):
"""Wird vor jedem Test ausgeführt"""
self.metronome = AdaptiveMetronome()
def test_scenario_beginner(self):
"""Scenario: Anfänger mit 70% Genauigkeit"""
# Simuliere 100 Tasteneingaben mit 70% Genauigkeit
correct_count = 0
initial_speed = self.metronome.speed
pattern = [True] * 7 + [False] * 3 # 70% korrekt
for i in range(10): # 100 Eingaben
for keystroke in pattern:
self.metronome.process_keystroke(keystroke)
if keystroke:
correct_count += 1
# Bei 70% Genauigkeit sollte das Metronom deutlich langsamer werden
self.assertLess(self.metronome.speed, initial_speed)
self.assertGreaterEqual(self.metronome.speed, 40.0)
print(f"Beginner: {initial_speed:.1f} BPM → {self.metronome.speed:.1f} BPM")
def test_scenario_intermediate(self):
"""Scenario: Fortgeschrittener mit 92% Genauigkeit"""
# Simuliere 100 Tasteneingaben mit 92% Genauigkeit
initial_speed = self.metronome.speed
pattern = [True] * 23 + [False] * 2 # 92% korrekt
for i in range(4): # 100 Eingaben
for keystroke in pattern:
self.metronome.process_keystroke(keystroke)
# Bei 92% Genauigkeit (im Zielbereich) sollte sanft beschleunigt werden
self.assertGreater(self.metronome.speed, initial_speed)
print(f"Intermediate: {initial_speed:.1f} BPM → {self.metronome.speed:.1f} BPM")
def test_scenario_expert(self):
"""Scenario: Experte mit 98% Genauigkeit"""
# Simuliere 100 Tasteneingaben mit 98% Genauigkeit
initial_speed = self.metronome.speed
pattern = [True] * 49 + [False] * 1 # 98% korrekt
for i in range(2): # 100 Eingaben
for keystroke in pattern:
self.metronome.process_keystroke(keystroke)
# Bei 98% Genauigkeit sollte deutlich beschleunigt werden
self.assertGreater(self.metronome.speed, initial_speed * 1.5)
print(f"Expert: {initial_speed:.1f} BPM → {self.metronome.speed:.1f} BPM")
def test_scenario_recovery_after_errors(self):
"""Scenario: Erholung nach Fehlerphase"""
# Phase 1: Viele Fehler
for _ in range(20):
self.metronome.process_keystroke(False)
speed_after_errors = self.metronome.speed
self.assertLess(speed_after_errors, 60.0)
# Phase 2: Perfekte Eingaben
for _ in range(50):
self.metronome.process_keystroke(True)
# Speed sollte sich wieder erholen
self.assertGreater(self.metronome.speed, speed_after_errors)
print(f"Recovery: {speed_after_errors:.1f} BPM → {self.metronome.speed:.1f} BPM")
def test_scenario_alternating_performance(self):
"""Scenario: Wechselnde Performance"""
speeds = [60.0] # Startgeschwindigkeit
# 5 Zyklen: 10 korrekt, dann 5 falsch
for cycle in range(5):
for _ in range(10):
self.metronome.process_keystroke(True)
speeds.append(self.metronome.speed)
for _ in range(5):
self.metronome.process_keystroke(False)
speeds.append(self.metronome.speed)
print(f"Alternating: Start={speeds[0]:.1f}, End={speeds[-1]:.1f}")
print(f" Speed changes: {[f'{s:.1f}' for s in speeds]}")
# Nach mehreren Zyklen sollte sich ein Gleichgewicht einstellen
self.assertGreaterEqual(self.metronome.speed, 40.0)
self.assertLessEqual(self.metronome.speed, 200.0)
def test_difficulty_parameter_effect(self):
"""Test: Difficulty-Parameter beeinflusst Zielgenauigkeit"""
# Teste mit normalem difficulty (1.0)
metronome1 = AdaptiveMetronome()
for _ in range(50):
metronome1.process_keystroke(True, current_difficulty=1.0)
speed_normal = metronome1.speed
# Teste mit höherem difficulty (2.0)
metronome2 = AdaptiveMetronome()
for _ in range(50):
metronome2.process_keystroke(True, current_difficulty=2.0)
speed_difficult = metronome2.speed
# Bei höherer Schwierigkeit sollte adjusted_target niedriger sein
# → Gleiche Performance wird als "besser" bewertet → schneller
print(f"Difficulty effect: normal={speed_normal:.1f}, difficult={speed_difficult:.1f}")
# Beide sollten schneller sein als Start, aber möglicherweise unterschiedlich
self.assertGreater(speed_normal, 60.0)
self.assertGreater(speed_difficult, 60.0)
class TestAdaptiveMetronomeEdgeCases(unittest.TestCase):
"""Edge Cases und Grenzfälle"""
def test_empty_history(self):
"""Test: Verhalten bei leerer History"""
metronome = AdaptiveMetronome()
# Ohne Eingaben sollte Speed unverändert bleiben
speed = metronome._adjust_speed(1.0)
self.assertEqual(speed, 60.0)
def test_exactly_at_boundaries(self):
"""Test: Verhalten exakt an den Regel-Grenzen"""
metronome = AdaptiveMetronome()
# Erstelle exakt 95% Genauigkeit (Grenze zwischen Regel #2 und #5)
for _ in range(19):
metronome.process_keystroke(True)
metronome.process_keystroke(False)
accuracy = sum(metronome.accuracy_history) / len(metronome.accuracy_history)
self.assertEqual(accuracy, 0.95)
# Bei 95% Genauigkeit mit adjusted_target=0.91:
# 95% ist NICHT > (0.91 + 0.04) = 0.95, daher nicht Regel #2
# 95% ist >= 0.91, daher Regel #5 (+1%)
# ABER: consecutive_correct ist 19, daher KEINE Regel #1 (braucht >= 15)
# Warte... consecutive_correct wird bei Fehler auf 0 gesetzt!
# Also nach dem Fehler ist consecutive_correct = 0
initial_speed = metronome.speed
# Nächstes korrektes Zeichen: Genauigkeit 95.2%, consecutive=1
# 95.2% > 95% → Regel #2 sollte greifen (+2%)
metronome.process_keystroke(True)
self.assertAlmostEqual(metronome.speed / initial_speed, 1.02, places=2)
def test_streak_exactly_15(self):
"""Test: Streak-Bonus exakt bei 15"""
metronome = AdaptiveMetronome()
# 14 korrekte
for _ in range(14):
metronome.process_keystroke(True)
self.assertEqual(metronome.consecutive_correct, 14)
initial_speed = metronome.speed
# 15. korrekt - sollte noch nicht Streak-Bonus auslösen
metronome.process_keystroke(True)
# Streak ist jetzt 15, aber wird erst beim NÄCHSTEN Keystroke belohnt
speed_at_15 = metronome.speed
# 16. korrekt - JETZT sollte Streak-Bonus aktiv sein
metronome.process_keystroke(True)
# Jetzt sollte Regel #1 aktiv sein
self.assertGreater(metronome.speed / speed_at_15, 1.02)
def test_rapid_speed_changes(self):
"""Test: Schnelle Wechsel zwischen Erfolg und Misserfolg"""
metronome = AdaptiveMetronome()
speeds = []
# Alterniere zwischen korrekt und falsch
for i in range(20):
is_correct = (i % 2 == 0)
metronome.process_keystroke(is_correct)
speeds.append(metronome.speed)
# Speed sollte trotz Chaos in gültigen Grenzen bleiben
self.assertGreaterEqual(min(speeds), 40.0)
self.assertLessEqual(max(speeds), 200.0)
# Genauigkeit sollte bei ~50% liegen
accuracy = sum(metronome.accuracy_history) / len(metronome.accuracy_history)
self.assertAlmostEqual(accuracy, 0.5, places=1)
def run_detailed_simulation():
"""
Führt eine detaillierte Simulation durch und gibt die Ergebnisse aus.
Dies ist keine Unit-Test, sondern eine visuelle Demonstration.
"""
print("\n" + "=" * 80)
print("DETAILLIERTE SIMULATION: Adaptive Metronom-Geschwindigkeit")
print("=" * 80)
metronome = AdaptiveMetronome()
# Simuliere verschiedene Phasen
phases = [
("Anfangsphase (70% korrekt)", [True]*7 + [False]*3, 5),
("Verbesserung (85% korrekt)", [True]*17 + [False]*3, 5),
("Zielbereich (93% korrekt)", [True]*28 + [False]*2, 5),
("Exzellent (98% korrekt)", [True]*49 + [False]*1, 2),
("Fehlerphase (50% korrekt)", [True, False], 10),
("Erholung (95% korrekt)", [True]*19 + [False]*1, 5),
]
print(f"\nStartgeschwindigkeit: {metronome.speed:.1f} BPM\n")
total_keystrokes = 0
for phase_name, pattern, repetitions in phases:
phase_start_speed = metronome.speed
phase_keystrokes = 0
for _ in range(repetitions):
for is_correct in pattern:
metronome.process_keystroke(is_correct)
phase_keystrokes += 1
total_keystrokes += 1
accuracy = sum(metronome.accuracy_history[-phase_keystrokes:]) / phase_keystrokes * 100
print(f"{phase_name}:")
print(f" Eingaben: {phase_keystrokes} Tasten, Genauigkeit: {accuracy:.1f}%")
print(f" Speed: {phase_start_speed:.1f}{metronome.speed:.1f} BPM "
f"({metronome.speed - phase_start_speed:+.1f})")
print(f" Streak: {metronome.consecutive_correct} korrekte Zeichen")
print()
print(f"Gesamtstatistik:")
print(f" Gesamte Eingaben: {total_keystrokes}")
print(f" Endgeschwindigkeit: {metronome.speed:.1f} BPM")
overall_accuracy = sum(metronome.accuracy_history) / len(metronome.accuracy_history) * 100
print(f" Aktuelle Genauigkeit (letzte 50): {overall_accuracy:.1f}%")
print("=" * 80 + "\n")
if __name__ == '__main__':
# Führe zuerst die Simulation aus
run_detailed_simulation()
# Dann die Unit-Tests
print("\nFühre Unit-Tests aus...\n")
unittest.main(verbosity=2)