Claude Code hat übernommen

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jamulix 2025-10-27 14:14:17 +01:00
commit 2a308d2d72
15 changed files with 1867 additions and 0 deletions

3
.stylelintrc.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "stylelint-config-standard"
}

199
CLAUDE.md Normal file
View file

@ -0,0 +1,199 @@
# 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
The database is automatically initialized when the application starts for the first time. Tables are created via SQLAlchemy in `app.py` using `db.create_all()`.
### 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
```
## 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
**Theme System**: Light/Dark mode toggle using Bootstrap's `data-bs-theme` attribute, stored in localStorage.
**Adaptive Metronome** (`script.js`):
- 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
**Visual Feedback**:
- Green highlighting for correct characters
- Red highlighting with orange overlay for incorrect characters
- Blinking cursor shows current typing position
### 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 in Flask session):
- `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%)
- `speed_display`: 'zpm' or 'wpm' for display preference
**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/`

108
MIGRATIONS.md Normal file
View file

@ -0,0 +1,108 @@
# Datenbank-Migrationen mit Alembic
Diese Anwendung verwendet Alembic für Datenbank-Migrationen.
## Setup
Alembic ist bereits konfiguriert und einsatzbereit.
## Wichtig für Deployment
**ACHTUNG:** Die App führt keine automatischen Schema-Migrationen mehr beim Start durch. Alle Datenbank-Änderungen müssen über Alembic verwaltet werden.
Vor dem Start der Anwendung in Production:
```bash
alembic upgrade head
```
## Häufige Befehle
### Neue Migration erstellen (nach Modelländerungen)
```bash
alembic revision --autogenerate -m "Beschreibung der Änderung"
```
### Migrationen anwenden
```bash
# Alle ausstehenden Migrationen anwenden
alembic upgrade head
# Eine bestimmte Migration anwenden
alembic upgrade <revision_id>
```
### Migrationen rückgängig machen
```bash
# Eine Migration zurücksetzen
alembic downgrade -1
# Zu einer bestimmten Revision zurücksetzen
alembic downgrade <revision_id>
```
### Migrations-Historie anzeigen
```bash
# Aktuelle Revision anzeigen
alembic current
# Alle Migrationen anzeigen
alembic history
# Verbose History
alembic history --verbose
```
## Workflow für Schema-Änderungen
1. **Modell ändern** in `models.py`
```python
class LessonStatistic(db.Model):
# Neue Spalte hinzufügen
new_field = db.Column(db.String(100))
```
2. **Migration erstellen**
```bash
alembic revision --autogenerate -m "Add new_field to LessonStatistic"
```
3. **Migration überprüfen**
- Öffne die generierte Datei in `alembic/versions/`
- Prüfe ob upgrade() und downgrade() korrekt sind
4. **Migration anwenden**
```bash
alembic upgrade head
```
## Best Practices
- Immer vor einer Migration ein Backup der Datenbank erstellen
- Migrations-Dateien ins Git aufnehmen
- Migrationen niemals nachträglich ändern (neue Migration erstellen stattdessen)
- Vor Production-Deployment Migrationen auf Staging testen
## Troubleshooting
### "Target database is not up to date"
```bash
alembic stamp head
```
### Migration zeigt keine Änderungen
- Stelle sicher, dass `models.py` importiert ist
- Prüfe ob `target_metadata` in `alembic/env.py` korrekt gesetzt ist
### Datenbank zurücksetzen (Entwicklung)
```bash
# Vorsicht: Löscht alle Daten!
rm instance/typewriter.db
alembic upgrade head
```

211
README_METRONOME_TESTS.md Normal file
View file

@ -0,0 +1,211 @@
# Metronom-Geschwindigkeitsanpassung - Test-Dokumentation
## Übersicht
`test_metronome_velocity.py` ist eine umfassende Test-Suite für die adaptive Metronom-Geschwindigkeitsanpassung des Typewriter Trainers. Das Programm implementiert die JavaScript-Logik in Python nach und testet alle Aspekte der Geschwindigkeitsanpassung.
## Installation und Ausführung
```bash
# Keine zusätzlichen Dependencies nötig (nur Python Standard Library)
python test_metronome_velocity.py
```
**Ausgabe:**
1. Detaillierte Simulation mit visueller Ausgabe
2. 22 Unit-Tests mit detaillierten Ergebnissen
## Test-Struktur
### 📊 **Test-Kategorien**
#### 1. **TestAdaptiveMetronomeBasics** (5 Tests)
Grundlegende Funktionalität:
- ✅ Initialwerte korrekt gesetzt
- ✅ Einzelner korrekter Tastendruck
- ✅ Einzelner falscher Tastendruck
- ✅ History-Limit auf 50 Einträge
- ✅ Consecutive-Counter Reset bei Fehler
#### 2. **TestAdaptiveMetronomeSpeedAdjustment** (7 Tests)
Alle 5 Anpassungsregeln:
- ✅ **Regel #1**: Streak-Bonus bei 15+ korrekten Zeichen (+3%)
- ✅ **Regel #2**: Zu einfach bei >95% Genauigkeit (+2%)
- ✅ **Regel #3**: Zu schwer bei <85% Genauigkeit (-8%)
- ✅ **Regel #4**: Leicht unter Ziel bei 85-91% (-3%)
- ✅ **Regel #5**: Im Zielbereich bei 91-95% (+1%)
- ✅ Minimum-Grenze (40 BPM)
- ✅ Maximum-Grenze (200 BPM)
#### 3. **TestAdaptiveMetronomeScenarios** (6 Tests)
Realistische Szenarien:
- ✅ **Anfänger**: 70% Genauigkeit → Speed sinkt
- ✅ **Fortgeschrittener**: 92% Genauigkeit → Speed steigt leicht
- ✅ **Experte**: 98% Genauigkeit → Speed steigt stark
- ✅ **Erholung**: Nach Fehlerphase wieder schneller werden
- ✅ **Wechselnde Performance**: Auf/Ab-Zyklen
- ✅ **Difficulty-Parameter**: Höhere Schwierigkeit = niedrigeres Ziel
#### 4. **TestAdaptiveMetronomeEdgeCases** (4 Tests)
Grenzfälle und Edge Cases:
- ✅ Leere History
- ✅ Exakt an Regelgrenzen
- ✅ Streak exakt bei 15
- ✅ Schnelle Wechsel (alternierend korrekt/falsch)
## 📈 **Detaillierte Simulation**
Die Simulation zeigt 6 typische Phasen:
```
Phase | Genauigkeit | Speed-Änderung
------------------------------|-------------|----------------
Anfangsphase (70% korrekt) | 70% | 60 → 40 BPM
Verbesserung (85% korrekt) | 85% | 40 → 40 BPM
Zielbereich (93% korrekt) | 93% | 40 → 200 BPM
Exzellent (98% korrekt) | 98% | 200 → 200 BPM
Fehlerphase (50% korrekt) | 50% | 200 → 92 BPM
Erholung (95% korrekt) | 95% | 92 → 107 BPM
```
## 🎯 **Wichtige Erkenntnisse aus den Tests**
### 1. **Schnelle Anpassung bei schlechter Performance**
- Bei 70% Genauigkeit: 60 → 40 BPM in 50 Eingaben
- System bremst aggressiv ab um Fehlerrate zu reduzieren
### 2. **Langsame Beschleunigung bei guter Performance**
- Bei 92% Genauigkeit: Langsame Steigerung (+1% pro Schritt)
- Verhindert Überforderung
### 3. **Streak-Bonus ist effektiv**
- 15+ fehlerfreie Zeichen: +3% Boost
- Motiviert zu fehlerfreiem Tippen
### 4. **Grenzen werden respektiert**
- Minimum 40 BPM: Auch bei 0% Genauigkeit
- Maximum 200 BPM: Auch bei perfekter Performance
### 5. **Sliding Window funktioniert**
- Letzte 50 Eingaben werden berücksichtigt
- Alte Fehler werden vergessen
- Erlaubt Erholung nach Fehlerphasen
## 🔍 **Test-Beispiele**
### Beispiel 1: Streak-Bonus
```python
metronome = AdaptiveMetronome()
# 15 korrekte Zeichen
for _ in range(15):
metronome.process_keystroke(True)
initial_speed = metronome.speed
# 16. korrektes Zeichen löst Bonus aus
speed = metronome.process_keystroke(True)
# Speed erhöht sich um 3%
assert speed == initial_speed * 1.03
```
### Beispiel 2: Zu schwierig → Bremsen
```python
metronome = AdaptiveMetronome()
metronome.speed = 100.0
# 20% Genauigkeit (2 korrekt, 8 falsch)
for _ in range(2):
metronome.process_keystroke(True)
for _ in range(8):
metronome.process_keystroke(False)
initial_speed = metronome.speed
metronome.process_keystroke(False)
# Speed reduziert sich um 8%
assert metronome.speed == initial_speed * 0.92
```
## 🐛 **Bekannte Einschränkungen**
1. **Difficulty-Parameter wird nicht genutzt**
- Aktuell immer `1.0` in der JavaScript-Implementation
- Könnte für schwierige Zeichenkombinationen verwendet werden
2. **Consecutive-Counter wird bei JEDEM Fehler resettet**
- Verhindert Streak-Bonus bei vereinzelten Fehlern
- Könnte toleranter sein (z.B. 1 Fehler pro 20 Zeichen erlauben)
3. **Speed kann bei Maximum "stecken bleiben"**
- Bei 200 BPM und perfekter Performance keine weitere Steigerung
- Tests müssen dies berücksichtigen
## 📊 **Test-Ergebnisse interpretieren**
### ✅ **Alle Tests bestanden (22/22)**
Das adaptive Metronom funktioniert wie designed:
- Alle 5 Anpassungsregeln arbeiten korrekt
- Grenzen werden eingehalten
- Realistische Szenarien werden korrekt verarbeitet
### ⚠️ **Mögliche Verbesserungen**
Basierend auf den Tests könnten folgende Anpassungen sinnvoll sein:
1. **Sanfteres Bremsen bei Anfängern**
- Aktuell: 70% Genauigkeit → Speed auf Minimum
- Besser: Graduelleres Abbremsen
2. **Toleranterer Streak-Counter**
- Aktuell: 1 Fehler → Streak auf 0
- Besser: Streak nur bei 2+ aufeinanderfolgenden Fehlern resetten
3. **Dynamische Ziel-Genauigkeit**
- Aktuell: Fest auf 96%
- Besser: Anpassbar in Settings (aus `target_error_rate` berechnen)
## 🚀 **Verwendung der Test-Suite**
### Regression Testing
```bash
# Nach Änderungen an der Metronom-Logik
python test_metronome_velocity.py
```
### Einzelne Tests ausführen
```bash
# Nur Regel #1 testen
python -m unittest test_metronome_velocity.TestAdaptiveMetronomeSpeedAdjustment.test_rule_1_streak_bonus -v
# Alle Scenario-Tests
python -m unittest test_metronome_velocity.TestAdaptiveMetronomeScenarios -v
```
### Neue Tests hinzufügen
```python
class TestAdaptiveMetronomeSpeedAdjustment(unittest.TestCase):
def test_my_new_scenario(self):
"""Test: Beschreibung"""
metronome = AdaptiveMetronome()
# Setup
# ...
# Execute
result = metronome.process_keystroke(True)
# Assert
self.assertEqual(result, expected_value)
```
## 📝 **Zusammenfassung**
Die Test-Suite bestätigt, dass die adaptive Metronom-Geschwindigkeit:
- ✅ Korrekt auf User-Performance reagiert
- ✅ Alle 5 Anpassungsregeln korrekt implementiert
- ✅ Grenzen (40-200 BPM) respektiert
- ✅ In realistischen Szenarien funktioniert
- ✅ Edge Cases korrekt behandelt
**Empfehlung:** Bei Änderungen an `static/js/script.js` (Zeilen 80-137) immer diese Tests ausführen, um Regressionen zu vermeiden.

147
alembic.ini Normal file
View file

@ -0,0 +1,147 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be installed by adding
# `alembic[tz]` to the pip requirements.
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = sqlite:///instance/typewriter.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
alembic/README Normal file
View file

@ -0,0 +1 @@
Generic single-database configuration.

82
alembic/env.py Normal file
View file

@ -0,0 +1,82 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# Import Models für Autogenerate
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from models import db
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

28
alembic/script.py.mako Normal file
View file

@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

View file

@ -0,0 +1,67 @@
"""Initial migration
Revision ID: 09de7cfb8b74
Revises:
Create Date: 2025-10-27 11:31:09.573690
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '09de7cfb8b74'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('lesson_statistic',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('lesson_index', sa.Integer(), nullable=False),
sa.Column('chars_per_minute', sa.Float(), nullable=True),
sa.Column('error_rate', sa.Float(), nullable=True),
sa.Column('wpm', sa.Float(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_lesson_created', 'lesson_statistic', ['lesson_index', 'created_at'], unique=False)
op.create_index(op.f('ix_lesson_statistic_created_at'), 'lesson_statistic', ['created_at'], unique=False)
op.create_index(op.f('ix_lesson_statistic_lesson_index'), 'lesson_statistic', ['lesson_index'], unique=False)
op.create_table('progress',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('current_text_index', sa.Integer(), nullable=True),
sa.Column('current_position', sa.Integer(), nullable=True),
sa.Column('last_text', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('statistic',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('total_chars', sa.Integer(), nullable=True),
sa.Column('correct_chars', sa.Integer(), nullable=True),
sa.Column('incorrect_chars', sa.Integer(), nullable=True),
sa.Column('error_rate', sa.Float(), nullable=True),
sa.Column('typing_speed', sa.Float(), nullable=True),
sa.Column('duration_per_char', sa.Float(), nullable=True),
sa.Column('accuracy', sa.Float(), nullable=True),
sa.Column('wpm', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('statistic')
op.drop_table('progress')
op.drop_index(op.f('ix_lesson_statistic_lesson_index'), table_name='lesson_statistic')
op.drop_index(op.f('ix_lesson_statistic_created_at'), table_name='lesson_statistic')
op.drop_index('idx_lesson_created', table_name='lesson_statistic')
op.drop_table('lesson_statistic')
# ### end Alembic commands ###

92
data/lessons.json.old Normal file
View file

@ -0,0 +1,92 @@
[
{
"lesson": 1.1,
"title": "01.1 Erarbeitung der Grundstellung",
"text": "ffff jjjj ffff jjjj ffff jjjj ffff jjjj ffff jjjj ffff jjjj\nffjj ffjj ffjj ffjj ffjj ffjj fjfj fjfj fjfj fjfj fjfj fjfj\n\ndddd kkkk dddd kkkk dddd kkkk dddd kkkk dddd kkkk dddd kkkk\nddkk ddkk ddkk ddkk ddkk ddkk ddkk ddkk ddkk ddkk ddkk ddkk\n\nssss llll ssss llll ssss llll ssss llll ssss llll ssss llll\nssll ssll ssll ssll ssll ssll slsl slsl slsl slsl slsl slsl\n\naaaa öööö aaaa öööö aaaa öööö aaaa öööö aaaa öööö aaaa öööö\naaöö aaöö aaöö aaöö aaöö aaöö aöaö aöaö aöaö aöaö aöaö aöaö\n\nasdf jklö asdf jklö asdf jklö asdf jklö asdf jklö asdf jklö\nfdsa ölkj fdsa ölkj fdsa ölkj fdsa ölkj fdsa ölkj fdsa ölkj\n\nafa aja afa aja ada aka ada aka asa ala asa ala aöa aöa asd\nöjö öfö öjö öfö ökö ödö ökö ödö ölö ösö ölö ösö öal ösk ölk\n\nöl ja öl ja öl ja öl öd da öd da öd da sö la sö la sö la sö\nlö aj lö aj lö aj lö dö ad dö ad dö ad ös al ös al ös al ös"
},
{
"lesson": 1.2,
"title": "01.2 Festigung durch Wortübungen",
"text": "all als all als all als all als all als all als all als all\ndas las das las das las das las das las das las das las das\n\ndas das das las las las als als als das las als das las als\nlas las las als als als das das das als las das als das las\n\nda all das las da all das las da all das da all das als las\nda all das las da all das las da all das da all das als las\n\nja lös das ja lös das ja lös das ja lös das ja lös da ja da\nja lös das ja lös das ja lös das ja lös das ja lös da ja da\n\nda ja öd fad da ja öd fad da ja öd fad da ja öd fad das las\nda ja öd fad da ja öd fad da ja öd fad da ja öd fad das las\n\nda ja öd da ja öd das lös fad das lös fad da ja öd da ja da\ndas las lös das las lös als das fad als das fad da öl da ja"
},
{
"lesson": 1.3,
"title": "01.3 Schreibsicherheit durch Griffübungen",
"text": "asdf jklö asdf jklö asdf jklö asdf jklö asdf jklö asdf jklö\njklö asdf jklö asdf jklö asdf jklö asdf jklö asdf jklö asdf\n\nölkj fdsa ölkj fdsa ölkj fdsa ölkj fdsa ölkj fdsa ölkj fdsa\nfdsa ölkj fdsa ölkj fdsa ölkj fdsa ölkj fdsa ölkj fdsa ölkj\n\nasdfjklö asdfjklö asdfjklö asdfjklö asdfjklö asdfjklö asdfj\njklöasdf jklöasdf jklöasdf jklöasdf jklöasdf jklöasdf jklöa\n\njfkdlsöa jfkdlsöa jfkdlsöa jfkdlsöa jfkdlsöa jfkdlsöa jfkdl\nöalskdjf öalskdjf öalskdjf öalskdjf öalskdjf öalskdjf öalsk\n\nafa aja ada aka asa ala aöa afa aja ada aka asa ala aöa afa\nöjö öfö ölö ösö ökö ödö öjö öfö ölö ösö ökö ödö öja öfa öka\n\naöaöjö ajaöfö adaölö akaösö asaökö alaödö aöaöjö afaöfö aöj\nöföafa öjöaöa ödöala ököasa ösöaka ölöada öföaja aöfjaö kfj\n\nsla sal sad sla sal sad sla sla sal sad sla sal sad sla sll\nals las das als las das als las das als las das als las das\n\nsad söl aj sad lla ad aj lö sad söl aj sad lla ad aj lö aja\ndas lös ja das all da ja öl das lös ja das all da ja öl das"
},
{
"lesson": 1.4,
"title": "01.4 Lernkontrolle durch Wortübungen",
"text": "ja das las ja das lös ja das las ja das lös ja das las da j\nja das las ja das lös ja das las ja das lös ja das las ja a\n\ndas lös ja all das lös ja all das lös ja all das lös da das\ndas lös ja all das lös ja all das lös ja all das lös da all\n\nja das fad ja das fad ja das fad ja das fad ja das fad da s\nja das fad ja das fad ja das fad ja das fad ja das fad ja f\n\ndas las als all lös fad öd öl da ja das las als all ja da s\ndas las als all lös fad öd öl da ja das las als all ja da l"
},
{
"lesson": 2.1,
"title": "02.1 Erarbeitung der Griffe e und i",
"text": "dede kiki dede kiki dede kiki dede kiki dede kiki dede kiki\ndeki kide deki kide deki kide deki kide deki kide deki kide\n\ndedf kikj deds kikl deda kikö dedf kikj deds kikl deda kikö\ndidf kekj dids kekl dida kekö didf kekj dids kekl dida kekö\n\ndeed kiik deed kiik deed kiik deed kiik deed kiik deed kiik\ndedi kike dedi kike dedi kike dedi kike dedi kike dedi kike\n\ndedkik dedkik dedkik dedkik dedkik dedkik dedkik dedkik ded\njedfik jedfik jedfik jedfik jedfik jedfik jedfik jedfik dfi\n\nella esöl esel edal lede ellaf ella esöl esel edal lede ell\nalle löse lese lade edel falle alle löse lese lade edel all\n\nesöl sella edal sella esel sella edö sella elde lla sad lla\nlöse alles lade alles lese alles öde alles edle all das all"
},
{
"lesson": 2.2,
"title": "02.2 Festigung durch Wortübungen",
"text": "lies dies leise dies sei leise lies dies leise dieses leide\nlies dies leise dies sei leise lies dies leise dieses leide\n\neile leise dieses alles sie lese dieses alles leise sei die\neile leise dieses alles sie lese dieses alles leise sei die\n\nfade öde löse alle falle lasse fade öde löse alle falle sie\nfade öde löse alle falle lasse fade öde löse alle falle sie\n\ndies sei lila fasse das alles löse dies alles dieses sei da\nalles eile jedes lies leise alles eile jedes lies leise all\n\nsei edel das sei edel diese sei edel dieses sei edel sei da\nseid leise dies sei ideal sie lief leise sie fiel leise die"
},
{
"lesson": 2.3,
"title": "Schreibsicherheit durch Griffübungen",
"text": "fdsajklö fdsajklö fdsajklö fdsajklö fdsajklö fdsajklö fdsaj\nölkjasdf ölkjasdf ölkjasdf ölkjasdf ölkjasdf ölkjasdf ölkja\n\nasdedfölkikj asdedfölkikj asdedfölkikj asdedfölkikj asdedfö\njkikölfdedsa jkikölfdedsa jkikölfdedsa jkikölfdedsa jkikölf\n\nadedf ökikj adedf ökikj adedf ökikj adedf ökikj adedf ökikj\njaded fökik jaded fökik jaded fökik jaded fökik jaded fökik\n\njiklö fedsa jiklö fedsa jiklö fedsa jiklö fedsa jiklö fedsa\nölijk asefd ölijk asefd ölijk asefd ölijk asefd ölijk asefd"
},
{
"lesson": 2.4,
"title": "02.4 Lernkontrolle durch Wortübungen",
"text": "seid seid esiel sella ellaf esiel seseid edö alil lede dies\nlies dies leise alles falle leise dieses öde lila edel seid\n\nelie esiel seseid sella eis esel seseid sella esiel eis ies\neile leise dieses alles sie lese dieses alles leise sie sei\n\nedeis ellaf ella esöl edö edaf essal ellaf ella esöl edö ed\nsiede falle alle löse öde fade lasse falle alle löse öde fa\n\nseid dies eile seid dies eile seid dies eile seid dieses ei\nseid dies eile seid dies eile seid dies eile seid dieses ei\n\nes sei dies alles leise es sei dies alles leise es sei dies\nes sei dies alles leise es sei dies alles leise es sei dies\n\nfiel feil fidele feil fiel lief fiel feil fidele feile fiel\nfiel feil fidele feil fiel lief fiel feil fidele feile fiel\n\nsei sie sei die eid die eid alle alles alle alles diese die\nsei sie sei die eid die eid alle alles alle alles diese die\n\nfiel fiel fiel feil feil feil fiel fiel fiel feil feil feil\nlief lief lief eile eile eile lief lief lief eile eile eile"
},
{
"lesson": 3.1,
"title": "03.1 Erarbeitung der beiden Umschalter",
"text": "öÖö öÖö aAa aAa lLl lLl sSs sSs kKk kKk dDd dDd jJj jJj fFf F\nöÖö öÖö aAa aAa lLl lLl sSs sSs kKk kKk dDd dDd jJj jJj fFf F\n\nJa Ja Fö Fö Ka Ka Dö Dö La La Sö Sö Öa Öa Aö Aö Ja Fö Ka Dö L\nJa Ja Fö Fö Ka Ka Dö Dö La La Sö Sö Öa Öa Aö Aö Ja Fö Ka Dö L\n\nöJ aJ aF öF öK aK aD öD öL aL aS öS öA aÖ sA lÖ kA dÖ jA fÖ a\nöJ aJ aF öF öK aK aD öD öL aL aS öS öA aÖ sA lÖ kA dÖ jA fÖ a\n\nJaf Föj Jad Fök Jas Föl Jaa Föö Jaö Föa Jal Fös Jak Föd Ja Fö\nJaf Föj Jad Fök Jas Föl Jaa Föö Jaö Föa Jal Fös Jak Föd Ja Fö\n\nöF aJ döF kaJ söF laJ aöF öaJ ööF aaJ löF saJ köF daJ jöF faJ\nöF aJ döF kaJ söF laJ aöF öaJ ööF aaJ löF saJ köF daJ jöF faJ"
},
{
"lesson": 3.2,
"title": "03.2 Festigung durch Wortübungen",
"text": "Aal Aal Aal Aas Aas Aas Aal Aas Aal Aas Aal Aas Aal Aas Aal A\nAal Aal Aal Aas Aas Aas Aal Aas Aal Aas Aal Aas Aal Aas Aal A\n\nÖle Öle Öle Öde Öde Öde Öle Öde Öle Öde Öle Öde Öle Öde Öle Ö\nÖle Öle Öle Öde Öde Öde Öle Öde Öle Öde Öle Öde Öle Öde Öle Ö\n\nEile Seide Seile Eile Seide Seile Eile Seide Seile Eile Seide\nEile Seide Seile Eile Seide Seile Eile Seide Seile Eile Seide\n\ndie Seide die Eile die Seide die Eile die Seide die Eile Aale\ndie Seide die Eile die Seide die Eile die Seide die Eile Aale\n\ndie Öle die Aale die Öle die Aale die Öle die Aale die Öle Öl\ndie Öle die Aale die Öle die Aale die Öle die Aale die Öle Öl"
},
{
"lesson": 3.3,
"title": "03.3 Schreibsicherheit durch Wortgruppen",
"text": "diese Seide alle Keile des Eides diese Löffel diese Seele\ndiese Seide alle Keile des Eides diese Löffel diese Seele\n\ndiese fidele Liesel dieses ideale Lied Liesa lies das leise\ndiese fidele Liesel dieses ideale Lied Liesa lies das leise\n\ndie Seide dieses Kleides diese Seide des Kleides ideales Ei\ndie Seide dieses Kleides diese Seide des Kleides ideales Ei\n\ndiese ideale Kasse dieses ideale Kleid diese Diele das Eis\ndiese ideale Kasse dieses ideale Kleid diese Diele das Eis\n\ndiese leise Kasse diese Eisdiele diese Eislöffel die Seele\ndiese leise Kasse diese Eisdiele diese Eislöffel die Seele"
},
{
"lesson": 3.4,
"title": "03.4 Lernkontrolle durch Satzübungen",
"text": "Fasse die Seide dieses Kleides Fasse diese Seide des Kleides\nFasse die Seide dieses Kleides Fasse diese Seide des Kleides\n\nLiesel löse leise diese Kaffeelöffel Ja alles ideale Löffel\nLiesel löse leise diese Kaffeelöffel Ja alles ideale Löffel\n\nLiesel lies leise dieses Lied Dieses Lied lies leise Liesel\nLiesel lies leise dieses Lied Dieses Lied lies leise Liesel\n\nLiesel fasse diese Eislöffel Ja Liesel fasse alle Eislöffel\nLiesel fasse diese Eislöffel Ja Liesel fasse alle Eislöffel"
},
{
"lesson": 4.1,
"title": "04.1 Erarbeitung der Griffe g und h",
"text": "asdfgfölkjhj asdfgfölkjhj asdfgfölkjhj asdfgfölkjh asdfgfölkj\nölkjhjasdfgf ölkjhjasdfgf ölkjhjasdfgf ölkjhjasdfg ölkjhasdfg\n\nfgf jhj gfd hjk gfs hjl gfa hjö fgf jhj gfd hjk gfs hjl gfa h\nöhj agf öhk agd öhl ags öhj agf öhk agd öhl ags öhj agf öhk g\n\nhHj gGf hHj gGf Hjh Gfg Hjh gFg Hkl Gds Hlö Gsa Hil Ges HH GG\nHeg Geh Hag Gih Hie Gei Hei Gie Hid Gid Hel Gel Hal Göl Halle"
},
{
"lesson": 4.2,
"title": "04.2 Festigung durch Wortübungen",
"text": "Jagd Jagd Jagd Jagd Jagd Jagd Jagd Jagd Jagd Jagd Jagd Jagd J\nLage Lage Lage Lage Lage Lage Lage Lage Lage Lage Lage Lage L\n\nJagd Lage Jagd Lage Jagd Lage Jagd Lage Jagd Lage Jagd Lage J\nSage Lage Jagd Sage Lage Jagd Sage Lage Jagd Sage Lage Jagd S\n\nhalf Haag sage Sieg sehe Glas gehe Gala sieh Sieg lies Affe H\nsage Agfa half Jagd hege Haff lies Hall gehe Hase sehe Hefe S"
},
{
"lesson": 4.3,
"title": "04.3 Festigung durch Wendungen",
"text": "das Jagdglas die Lage des Jagdglases die Lage dieses Glases\ndas Jagdglas die Lage des Jagdglases die Lage dieses Glases\n\ndie eisige See die eilige Hilfe die öde Höhle die Eisdiele\ndie eisige See die eilige Hilfe die öde Höhle die Eisdiele\n\ndie Lage des Haffes hege diese Aale diese heilige Halle Öl\ndie Lage des Haffes hege diese Aale diese heilige Halle Öl"
},
{
"lesson": 4.4,
"title": "04.4 Lernkontrolle durch Satzübungen",
"text": "Aha da lag ja das Jagdglas Gisela sah die eisige öde Höhle\nAha da lag ja das Jagdglas Gisela sah die eisige öde Höhle\n\nAls Adelheid diese eisige See sah da half sie eilig Gisela\nAls Adelheid diese eisige See sah da half sie eilig Gisela"
},
{
"lesson": 4.5,
"title": "04.5 Sicherheit durch Griffwiederholung = Fingergymnastik",
"text": "jhiikihh jhiikihh jhiikihh jhiikihh jhiikihh jhiikihh jhiikik\nfgeedegg fgeedegg fgeedegg fgeedegg fgeedegg fgeedegg fgeedeg\n\nöikk aedd öikk aedd öikk aedd öikk aedd öikk aedd öikk aedd ö\nlikk sedd likk sedd likk sedd likk sedd likk sedd likk sedd l\n\nkikk dedd kikk dedd kikk dedd kikk dedd kikk dedd kikk dedd k\njikk fedd jikk fedd jikk fedd jikk fedd jikk fedd jikk fedd j\n\naikö öeda aikö öeda aikö öeda aikö öeda aikö öeda aikö öeda a\nsikö leda sikö leda sikö leda sikö leda sikö leda sikö leda s\n\ndikö keda dikö keda dikö keda dikö keda dikö keda dikö keda d\nfikö jeda fikö jeda fikö jeda fikö jeda fikö jeda fikö jeda f\n\nöikklikkkikkjikkaikösiködiköfikö aeddsedddeddfeddöedaledakeda\nehesegalesahekieh ehesegalesahekie ehesegalesahekie ehesegale\n\nsadeleidellehhaseiSffaHsadegaL elhöHedöseidflahseeföHeidegalK\nellaHegiliehsessaHsedelaAeseid giliealesiGeisflahadhaseeSella"
},
{
"lesson": 4.6,
"title": "04.06 Lernkontrolle durch Wortübungen",
"text": "alle See sah das half sie Gisela eilige diese Aale des Hasses\nKlage die Höfe es half diese öde Höhle Lage dies Haff Sie sah\n\nsehe Halle sage Heike helle Höfe öde Höhle die Heide die Haie\nsehe Halle sage Heike helle Höfe öde Höhle die Heide die Haie\n\ngedeihe Klage heikel Heilige fehle Felge eilig Lage lege Igel\ngedeihe Klage heikel Heilige fehle Felge eilig Lage lege Igel"
}
]

118
data/lessons.json.very_old Normal file
View file

@ -0,0 +1,118 @@
[
{
"lesson": 1,
"title": "Grundreihe",
"text": "asdf asdf jklö asdf jklö as df jk lö asdf jklö asdf jklö as df jk lö fj as df jk lö asdf asdf jklö jklö asdf jklö as df jk lö asdf jklö"
},
{
"lesson": 2,
"title": "Kombinationen der Grundreihe",
"text": "as ja sa lö ölä da fj as ja sa lö ölä da fj kalal säda jöla aföl fall eska jass lösa dajf ölja lasa alla ösad fjöl fjöl assa lösa dajf salf jäda löfa söja läsa jäla fjod ölsa"
},
{
"lesson": 3,
"title": "Leichte Wörter",
"text": "Affe Tasche Leute Löwe Fels Saft Jäger Löcher Fassade Tasse Kaffee Löffel Straße Klasse Flosse Raster Sofa Falter Salz Wasser Lager Ölzug Liste Hälfte Saal Krater Jalousie Lösung Läufer Förster Trage Latte Falle Löffel Jalousie Zähler Torte Hölle Futter Hülse"
},
{
"lesson": 4,
"title": "Neue Buchstaben q w e r t z",
"text": "wertz wertz qwertz qwer tz rewe zetra freta treze werte zeter querträt treff werzt zerqt wertet wetzert\nqwertz rewetz werqzreet wertzet zerqwertz trewert zetra wertzqwert rewert zetrewer"
},
{
"lesson": 5,
"title": "Kurze Wörter",
"text": "der die das und ein ist auf zu mit von wie was wir sie du ich ja nein kein am im zum beim dem den des\nwir sind hier und tippen neue texte mit geduld und ruhe lernen wir schnell und sicher"
},
{
"lesson": 6,
"title": "Einfache Sätze",
"text": "Der Hund rennt schnell. Ich trinke gern Wasser. Wir lernen das Zehnfingersystem. Heute ist das Wetter schön. Sie sitzt am kleinen Tisch.\nMein Bruder schreibt eine Mail. Das Kind liest laut. Wir üben jeden Tag fleißig."
},
{
"lesson": 7,
"title": "Umlaute und ß",
"text": "ä ö ü ß Ä Ö Ü über Ölstraße läßt äußere Füße säße Käse, Öl, Maß und süße Früchte.\nEr hält die Tüte am äußeren Sitz. Äußere Räume sind größer. Löse die Aufgabe äußerst genau."
},
{
"lesson": 8,
"title": "Komplexe Wörter",
"text": "Arbeit Erfahrung Geschichte Schreiben Lernen Sprache Gefühl Tastatur Benutzer Geschwindigkeit Finger Training Übung Fehler\nProgrammieren erfordert Geduld und Präzision. Jede Zeile Code verbessert dein Tippgefühl."
},
{
"lesson": 9,
"title": "Kurze Textpassagen",
"text": "Das schnelle Tippen mit zehn Fingern spart viel Zeit. Mit Geduld und Übung wird jeder Tippfehler seltener. Eine gute Haltung und ruhige Atmung helfen beim Lernen.\nBleibe konzentriert, atme ruhig und halte den Blick auf dem Text."
},
{
"lesson": 10,
"title": "Fließtextrealismus",
"text": "Der Morgen begann mit einer Tasse Kaffee und einem neuen Tipptraining. Ich sah auf die Tastatur, legte die Finger auf asdf jklö und begann.\nMit jedem Satz wurde das Tippen flüssiger und leiser. Nach einer Stunde konnte ich meine Gedanken schreiben, ohne auf die Tasten zu sehen."
},
{
"lesson": 11,
"title": "Sonderzeichen und Zahlen",
"text": "@ € % & ( ) ? ! 1 2 3 4 5 6 7 8 9 0 # _ - + = < > [ ] { } / \\ | ^ ~\nMit 100% Einsatz klappt das Tippen bestimmt! Zahlen und Symbole erscheinen oft in Passwörtern und Codefragmenten.\nÜbe sie regelmäßig: (1+2)*3 = 9; printf(\"Summe: %d\", s);"
},
{
"lesson": 12,
"title": "Dialoge",
"text": "\"Guten Morgen!\", sagte Anna. \"Wie geht es dir?\" \"Sehr gut, danke.\" \"Ich übe gerade das Tippen!\"\n\"Das klingt prima!\", meinte Max und lachte. Gemeinsam tippten sie denselben Text. So wurde das Lernen leichter."
},
{
"lesson": 13,
"title": "Redewendungen",
"text": "Übung macht den Meister. Aller Anfang ist schwer. Wer schreibt, der bleibt. Viele Wege führen nach Rom. Steter Tropfen höhlt den Stein.\nLernen durch Wiederholung ist der Schlüssel. Je öfter du einen Text tippst, desto schneller wirst du."
},
{
"lesson": 14,
"title": "Thematische Texte",
"text": "Im Büro stehen mehrere Computer und Tastaturen. Die Mitarbeiter schreiben Berichte und E-Mails. Jeder achtet auf korrekte Groß- und Kleinschreibung.\nIn der Pause reden sie über neue Programme und lernen Tastenkombinationen auswendig."
},
{
"lesson": 15,
"title": "Abschlusstext",
"text": "Nach vielen Übungen kann ich nun schnell und sicher tippen. Fehler sind selten geworden, und das Schreiben macht Spaß.\nTäglich verbessere ich meine Genauigkeit und mache neue Fortschritte. Mit Ruhe, Technik und Konzentration wird jede Tastatur vertrauter."
},
{
"lesson": 16,
"title": "Programmierzeichen",
"text": "` ! @ # $ % ^ & * ( ) - _ = + [ ] { } ; : ' \" < > , . / ? \\ |\nTrainiere diese Zeichen in Verbindung mit Variablen und Klammern: (x+y)*z; var name = 'dev'; const data = { a:1, b:2 };"
},
{
"lesson": 17,
"title": "Python Basics",
"text": "def greet(name):\n\tprint('Hello,', name)\n\nfor i in range(5):\n\tgreet('World')\n\nif __name__ == '__main__':\n \tgreet('Programmer') # Fokus auf : ' ( ) und Einrückung"
},
{
"lesson": 18,
"title": "C-Style Syntax",
"text": "#include <stdio.h>\n\tint main() {\n\tfor(int i = 0; i < 10; i++) {\n\t\tprintf(\"%d\\n\", i);\n\t}\n\treturn 0;\n}// Konzentriere dich auf { } < > und ;"
},
{
"lesson": 19,
"title": "HTML / JavaScript",
"text": "<html>\n<head>\n<title>Test</title></head>\n<body>\n<script>\nfunction add(a, b) {\n\treturn a + b;\n}\nconsole.log(add(2, 3));\n</script>\n</body>\n</html>"
},
{
"lesson": 20,
"title": "JSON Parsing",
"text": "{\n\"user\": \"dev\",\n\"skills\": [\"Python\", \"C++\", \"JSON\"],\n\"active\": true,\n\"projects\": [{\n \"title\": \"Tipptrainer\",\n \"completed\": false\n }]\n} // Beachte Anführungszeichen und Doppelpunkte"
},
{
"lesson": 21,
"title": "Kurze Test Lektion",
"text": "Dies ist nur ein kurzer Text zum Testen."
},
{
"lesson": 22,
"title": "Extra lange Test Lektion",
"text": "Lagebild des Bundeskriminalamts\nBedrohung durch Organisierte Kriminalität bleibt hoch\n\nDrogenhandel, Geldwäsche, Cybercrime - das Ausmaß der Organisierten Kriminalität in Deutschland bleibt laut BKA auf hohem Niveau. Sorge bereitet Ermittlern, dass Kriminelle zunehmend versuchen, Minderjährige für Straftaten zu rekrutieren. Die Bedrohung durch die Organisierte Kriminalität bleibt in Deutschland auf anhaltend hohem Niveau. Das geht aus dem Lagebild für das vergangene Jahr hervor, welches das Bundeskriminalamt (BKA) veröffentlicht hat. Demnach wurde 2024 in insgesamt 647 Verfahren wegen Organisierter Kriminalität ermittelt. Die Zahl bleibt im Vergleich zum Vorjahr nahezu gleich, 2024 stieg sie um fünf Verfahren an. Allerdings ist es der zweithöchste Wert innerhalb von zehn Jahren. Am häufigsten wurde laut BKA in Fällen von Rauschgiftkriminalität ermittelt - mit fast 260 Verfahren machte dieser Bereich den Großteil der Taten der Organisierten Kriminalität aus. Es folgten Fälle von Wirtschaftskriminalität mit etwa 110 Verfahren, Steuer- und Zolldelikte mit rund 60 Verfahren. Ähnlich viele Verfahren wurden wegen Schleuser- und Eigentumskriminalität verfolgt.Einen deutlich geringeren Anteil machten mit vier Prozent aller Verfahren Ermittlungen gegen Cybercrime-Gruppierungen aus. Allerdings ist der durch diese Vergehen entstandene Schaden massiv: Dem Lagebild zufolge wird er auf rund 1,7 Milliarden Euro geschätzt, was mehr als die Hälfte aller durch Organisierte Kriminalität verursachten finanziellen Schäden ausmacht. Insgesamt bezifferte das BKA die Finanzschäden im vergangenen Jahr auf etwa 2,64 Milliarden Euro."
},
{
"lesson": 23,
"title": "Dummy: Leere Lektion",
"text": "Fast leer"
}
]

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
flask>=2.3.0
flask-sqlalchemy>=3.0.0
flask-wtf>=1.2.0
alembic>=1.13.0

36
static/favicon.svg Normal file
View file

@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<!-- Schreibmaschine Favicon -->
<rect x="10" y="30" width="80" height="50" rx="5" fill="#2c3e50" stroke="#34495e" stroke-width="2"/>
<!-- Papier -->
<rect x="25" y="15" width="50" height="25" rx="2" fill="#ecf0f1" stroke="#bdc3c7" stroke-width="1.5"/>
<!-- Papierlinien -->
<line x1="30" y1="22" x2="70" y2="22" stroke="#95a5a6" stroke-width="1"/>
<line x1="30" y1="27" x2="70" y2="27" stroke="#95a5a6" stroke-width="1"/>
<line x1="30" y1="32" x2="65" y2="32" stroke="#95a5a6" stroke-width="1"/>
<!-- Tastatur (3 Reihen) -->
<g fill="#34495e" stroke="#2c3e50" stroke-width="0.5">
<!-- Reihe 1 -->
<rect x="20" y="45" width="6" height="6" rx="1"/>
<rect x="28" y="45" width="6" height="6" rx="1"/>
<rect x="36" y="45" width="6" height="6" rx="1"/>
<rect x="44" y="45" width="6" height="6" rx="1"/>
<rect x="52" y="45" width="6" height="6" rx="1"/>
<rect x="60" y="45" width="6" height="6" rx="1"/>
<rect x="68" y="45" width="6" height="6" rx="1"/>
<rect x="76" y="45" width="6" height="6" rx="1"/>
<!-- Reihe 2 -->
<rect x="22" y="53" width="6" height="6" rx="1"/>
<rect x="30" y="53" width="6" height="6" rx="1"/>
<rect x="38" y="53" width="6" height="6" rx="1"/>
<rect x="46" y="53" width="6" height="6" rx="1"/>
<rect x="54" y="53" width="6" height="6" rx="1"/>
<rect x="62" y="53" width="6" height="6" rx="1"/>
<rect x="70" y="53" width="6" height="6" rx="1"/>
<!-- Reihe 3 (Leertaste) -->
<rect x="35" y="61" width="30" height="6" rx="1"/>
</g>
<!-- Walze oben -->
<rect x="20" y="35" width="60" height="4" rx="2" fill="#7f8c8d"/>
<!-- Return-Hebel -->
<path d="M 85 35 L 90 30 L 88 28 L 83 33" fill="#e74c3c" stroke="#c0392b" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

235
test_app.py Normal file
View file

@ -0,0 +1,235 @@
"""
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()

536
test_metronome_velocity.py Normal file
View file

@ -0,0 +1,536 @@
"""
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)