From 6268ea66934eef38536607e985b8aa0ff72a821c Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 22 Jun 2026 11:38:41 +0200 Subject: [PATCH] Cleanup: KI-spezifische, veraltete und experimentelle Dateien entfernt Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/coverage.yml | 90 ------ .github/workflows/tests.yml | 116 ------- CLAUDE.md | 287 ------------------ CLEANUP_GUIDE.md | 269 ----------------- SCROLL_IMPROVEMENTS.md | 146 --------- claude_start_prompt.txt | 5 - test_app.py | 235 --------------- test_metronome_velocity.py | 536 --------------------------------- 8 files changed, 1684 deletions(-) delete mode 100644 .github/workflows/coverage.yml delete mode 100644 .github/workflows/tests.yml delete mode 100644 CLAUDE.md delete mode 100644 CLEANUP_GUIDE.md delete mode 100644 SCROLL_IMPROVEMENTS.md delete mode 100644 claude_start_prompt.txt delete mode 100644 test_app.py delete mode 100644 test_metronome_velocity.py diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index a0fd13a..0000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -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 - }); diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 8261a49..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index a72b57e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -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/` - 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/` diff --git a/CLEANUP_GUIDE.md b/CLEANUP_GUIDE.md deleted file mode 100644 index 8e901b6..0000000 --- a/CLEANUP_GUIDE.md +++ /dev/null @@ -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 # Nicht empfohlen! -# Besser: Ownership prĂŒfen -ls -la -``` - -### 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 -``` diff --git a/SCROLL_IMPROVEMENTS.md b/SCROLL_IMPROVEMENTS.md deleted file mode 100644 index c2c3e0d..0000000 --- a/SCROLL_IMPROVEMENTS.md +++ /dev/null @@ -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 diff --git a/claude_start_prompt.txt b/claude_start_prompt.txt deleted file mode 100644 index fa21bd4..0000000 --- a/claude_start_prompt.txt +++ /dev/null @@ -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. diff --git a/test_app.py b/test_app.py deleted file mode 100644 index 394e505..0000000 --- a/test_app.py +++ /dev/null @@ -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() diff --git a/test_metronome_velocity.py b/test_metronome_velocity.py deleted file mode 100644 index 6723b1d..0000000 --- a/test_metronome_velocity.py +++ /dev/null @@ -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)