# Analyse Altstand (01.11.2026) Grundlage: `old_version/loader_game.py` (656 Zeilen), `old_version/qr_scan.py` (188 Zeilen), `old_version/qr_scan_old.py` (160 Zeilen), `old_version/case_sequences.txt` (6 Sequenzen). ## 1. Wie der Altstand funktioniert hat ``` qr_scan.py ──► last_user.txt ──► subprocess: python loader_game.py │ │ └ Kamera + pyzbar + HTTP-API └ pygame-Spiel, ein Durchgang, dann sys.exit() ``` * **`qr_scan.py`**: öffnet die Webcam, liest QR-Codes, fragt eine externe API (`hag.putschgi.ch/.../qr.php?action=verify_hash`) nach Gültigkeit und Name, schreibt `mail`/`name` in `last_user.txt` und startet das Spiel als Unterprozess. Danach wieder Scanner – so kam der nächste Spieler dran. * **`loader_game.py`**: ein Durchgang. Ein Koffer läuft von rechts ein; der Spieler kann ihn an die Barriere snappen (Space/A), drehen (←/X) oder auf einen anderen Koffer stapeln (↑/Y). Kollidiert er ungebremst, wird er mit einer Lücke eingereiht. Ist die Barriere hinter dem Heck, ist Schluss. Punktzahl = belegte Fläche im Auflieger, wird per `curl` an `submit_points.php` geschickt. * **`case_sequences.txt`**: 6 Koffersequenzen, Zeilen `breite,höhe,rotiert`, Sequenzen durch Leerzeilen getrennt. Das Spiel zieht sich zufällig eine. ## 2. Gefundene Fehler und Risiken | # | Fund | Wirkung | Status im Neustand | | --- | --- | --- | --- | | 1 | **Secrets im Quellcode**: `API_SECRET = "ddlK…"` in `qr_scan.py`, `api_key=hag-trailer-8051` in `loader_game.py` | Zugangsdaten liegen in der Git-Historie | entfernt; Endpunkt + Schlüssel nur noch per `HAG_SCORE_URL` / `HAG_SCORE_API_KEY` aus der Umgebung → **alte Schlüssel rotieren** | | 2 | `pygame.event.set_grab(True)` | Maus/Tastatur werden gefangen; auf macOS unangenehm, Esc kommt nicht mehr durch | entfernt | | 3 | Feste 1920×1080-Vollbildannahme | MacBook (Retina/1440×900) → Fenster grösser als der Bildschirm | virtuelle Auflösung + `pygame.SCALED`, Fenster/`--fullscreen`, F11 | | 4 | Windows-Annahmen: `subprocess.run(["python", …])`, `pygetwindow`/`pyautogui`, `python.exe` in der Install-Doku | läuft so nicht auf macOS | kein Subprozess mehr, Scanner gestrichen | | 5 | **Doppelte Drehlogik**: `Case.rotated` **und** getauschte Masse; `draw()` skaliert bei `rotated` auf `(height, width)` und dreht zusätzlich um 90° | verzerrte/falsch gedrehte Koffer, schwer nachvollziehbar | nur noch `units_w`/`units_h`; Grafik wird einmalig passend erzeugt | | 6 | Zufällige Zusatzdrehung in `Case.__init__` (`random.choice([True, False])`) | die „Next"-Vorschau zeigt etwas anderes als der Koffer, der wirklich kommt | Zufallsdrehung entfernt, Vorschau stimmt jetzt | | 7 | `can_place_on_top()` und `animate_place_on_top()` prüfen **unterschiedliche** Regeln (Barriere-Ausrichtung fehlt in der Animation) | „OnTop möglich!" wird angekündigt, dann landet der Koffer woanders oder gar nicht | eine einzige Regel: `Trailer.find_platform()` | | 8 | Zwei verschiedene Fehlschlag-Pfade (`fail_snap` einmal mit `sound_place`, einmal mit `sound_fail`) | inkonsistentes Feedback, verwirrende Punktevergabe | ein Pfad (`_stack_with_gap`), definierter Sound + Shake | | 9 | Blockierende Animationsschleifen (`for` + `pygame.time.delay` mitten im Event-Loop) | Events stauen sich, keine `dt`-Basis, Ruckler; 120-fps-Ziel war so nicht erreichbar | Animationen laufen über `elapsed/duration` im Update, 60 fps | | 10 | `Case.move()` startet den Roll-Sound und kehrt zurück; Sound hängt an `Channel(1)` | erster Frame ohne Bewegung; Kanal-Kollisionen mit anderen Sounds | Roll-Sound beim Spawn, Stopp beim Platzieren, eigener Kanal | | 11 | Blankes `except:` an 6 Stellen (Bilder, Sequenzen, Punkte-Upload) | Fehler bleiben unsichtbar, z. B. Tippfehler in `case_sequences.txt` fallen still auf die Notfall-Sequenz zurück | gezielte Exceptions + Warnungen beim Start | | 12 | `last_user.txt` wird mit `utf-8-sig` geschrieben und mit `utf-8` gelesen | BOM landet im Spielernamen; Datei liegt im Programmordner | `runtime/last_player.json`, Ordner ist in `.gitignore` | | 13 | `calculate_load_percent()` summiert die Fläche **aller** Koffer, auch der aufgesetzten | Beladung ist „gefühlt" zu hoch; `min(100, …)` verdeckt das nur | bewusst beibehalten (Spielbalance), aber dokumentiert und getestet | | 14 | Toter/doppelter Code: `global current_case` zweimal in `draw_game`, `can_use_on_top` in `spawn_case`, `transition_counter`, `state`/`PLAYING`, `entry_angle` (immer 0), `frame_color`, `game_started` | unnötige Verwirrung | entfernt | | 15 | Kein Neustart möglich: Spiel endet mit `sys.exit()` | jeder Spieler braucht einen neuen Prozess (Aufgabe des Scanners) | Spiel ist eine Klasse; Aufrufer entscheidet (Neustart/Beenden) | | 16 | `cap = cv2.VideoCapture(0)` in der Endlosschleife, `cv2.namedWindow()` pro Frame | Kamerahandle-Leak, Flackern | mit dem QR-Scanner entfallen (pyzbar/opencv nicht mehr nötig) | | 17 | Keine `requirements.txt`, kein venv, Installation nur als Windows-Befehlskette | auf dem Mac nicht reproduzierbar | `requirements.txt` + `run.sh` (venv + Auto-Install) | | 18 | Spieler kam nur über die API, ohne Server kein Spielstart | Abhängigkeit von einem fremden Server | Name kommt künftig vom Controller (Schritt 2) | ## 3. Was der Neustand anders macht * **Struktur statt Monolith**: `main.py` + Paket `loader_game/` mit je einem Modul pro Aufgabe (siehe `loader_game/__init__.py`). * **Logik ohne pygame**: `case.py`, `trailer.py`, `sequences.py` lassen sich ohne Fenster testen (35 Tests). * **Zeitbasiert**: alles rechnet in Sekunden (`dt`), nicht in Frames. * **Eine Wahrheit für Regeln**: Stapelregeln leben ausschliesslich in `Trailer`, Zeichnen ausschliesslich in `scene.py`. * **Konfiguration an einer Stelle**: `config.py` (Masse, Tempi, Farben, Texte). * **Eingabe abstrahiert**: `input_map.py` bildet Tastatur **und** Controller auf `Action.PRIMARY/LEFT/RIGHT/UP/DOWN/BACK` ab – die Basis für die Controller-Namenseingabe. * **Nur noch eine Abhängigkeit**: `pygame` (kein numpy, kein opencv, kein pyzbar, kein requests, kein pyautogui). ## 4. Bewusst *nicht* geändert * Spielmechanik und Balance (Tempo 720/240 px/s entspricht den alten 6/2 px pro Frame bei 120 fps; Snap-Schwelle, Lücken-Logik, Beladungsformel). * `case_sequences.txt` (Datenbestand bleibt unverändert). * Grafiken, Sounds, Fonts. * Der Punkte-Upload existiert weiter, ist aber ohne konfigurierten Endpunkt stumm geschaltet. ## 5. Offene Punkte (nächste Schritte) 1. **Controller-Namenseingabe**: Buchstabe für Buchstabe per D-Pad/Stick, alternative Eingabe über die Tastatur. Ersetzt `last_user.txt`/QR-Scanner. 2. **Mehrere Durchgänge ohne Prozess-Neustart** (Rückkehr zur Namenseingabe statt Beenden). 3. Entscheiden, ob der Punkte-Upload wieder aktiv genutzt wird – und wohin. 4. `images/*.png` haben ein defektes sRGB-Profil (libpng-Warnung beim Laden, rein kosmetisch). ## 6. Änderungen nach dem ersten Test (v1.1) Rückmeldungen aus dem ersten Durchlauf auf dem MacBook und was daraus wurde: | Rückmeldung | Umsetzung | | --- | --- | | „läuft viel zu schnell" | Stufen **Easy / Normal / Hard** (`difficulty.py`), Hard entspricht exakt dem Altstand-Tempo (720/240 px pro Sekunde). Auswahl im Startmenü mit `←`/`→` und A/Space; das Tempo steht jetzt an einer Stelle. | | „beim Tippen plötzlich andere Textur" | Der Koffer merkt sich in `Case.texture` die Orientierung, in der er eingelaufen ist. Gedreht wird nur die Grafik (90°), nie gegen eine andere Bilddatei getauscht. | | „Spieler klingt doof" | Name und Punkteliste komplett entfernt (`storage.py`, `net.py`, `--name` gelöscht). Angezeigt werden nur noch Stufe und Beladung in Prozent. | | „soll nicht enden, sondern fragen: nochmals?" | Neue Phasenkette in `main.py`: Tutorial → Menü → Ampel → Runde → Ergebnis → Menü → … Beendet wird nur mit `Esc`/B. `Game.handle()` liefert dafür `HandleResult.CONTINUE / NEW_ROUND / QUIT`. | | „lädst du von einer Map/CSV?" | Ja: `case_sequences.txt` im Projektwurzelverzeichnis, 6 Sequenzen als `breite,höhe,rotiert`. Pro Runde wird zufällig eine Sequenz gezogen. | ## 7. Änderungen nach dem zweiten Test (v1.2) | Rückmeldung | Umsetzung | | --- | --- | | „zeig im Log, welche Map gewählt wurde" | Beim Start jeder Runde: `Runde n: Sequenz x/6 (m Koffer, Start: …), Stufe …`. Zusätzlich wird dieselbe Sequenz nie zweimal direkt hintereinander gezogen (`Game._pick_sequence`). | | „Reihenfolge: Anleitung → Moduswahl → Sequenz" | Phasenkette korrigiert. Die Anleitung kommt jetzt **jede** Runde zuerst, danach die Stufenauswahl, dann Ampel/Reissverschluss, dann das Spiel. | | „kein Roll-Sound bis zum ersten Klick" | Ursache: der Roll-Sound lief auf einem Kanal, den die automatische Kanalvergabe für Einzelklänge (Ampel, Reissverschluss) belegen konnte; war er belegt, wurde der Roll übersprungen. Jetzt: `pygame.mixer.set_reserved(2)`, eigener Kanal 0 für den Roll, `unpause()` nach dem Laden und kein „skip when busy" mehr. Lautstärke 0.3 → 0.5. Beim Start steht `Audio: …` im Log. | | „Verlangsamen und Snap-Fenster früher" | `FAST_TO_SLOW_DISTANCE` 120 → 260 px, neues `SNAP_WINDOW` 160 px (vorher 60). Die Strafe bei einer Kollision bleibt unverändert bei `FAIL_GAP` 60 px. Snap-Animation dauert jetzt entfernungsabhängig 0.08–0.30 s. | | „Cooldown am Ende" | `RESULT_COOLDOWN = 10` s. Ergebnis bleibt stehen, zeigt „Neue Runde in n s …", A wird erst danach angenommen. Esc beendet jederzeit. | | „lustige Sprüche" | Neues Modul `sayings.py` mit Stufen (100 % = Vertrag-im-Lager-Spruch, 85+, 70+, 55+, 40+, 20+, darunter). Bewusst jugendtauglich: frech, aber nicht beleidigend – ein Test prüft das. | ## 8. Ablauf-Korrektur (v1.3) Der Ablauf war doppelt verdrahtet: die Klasse `Intro` enthielt sowohl das Anleitungsbild als auch die Ampel-/Reissverschluss-Sequenz. Wer die Anleitung bestätigte, startete damit ungewollt die Startsequenz – sie lief also vor der Moduswahl und danach noch einmal. Jetzt sind es zwei getrennte Bausteine (`intro.py`): `Tutorial` (nur Bild, wartet auf A/Space, keine Töne) und `StartSequence` (Ampel + Reissverschluss). Die Phasenkette im Hauptprogramm ist damit eindeutig und gilt für **jede** Runde: ``` ANLEITUNG → MODUSWAHL → STARTSEQUENZ → SPIEL → ABSCHLUSS (10 s) → ANLEITUNG → … ``` ## 9. Tempo nachgezogen (v1.4) | Rückmeldung | Umsetzung | | --- | --- | | „Normal und Hard sind zu einfach, aber die Zeit für die Befehle soll bleiben" | Tempo erhöht: Normal 570 → **740** px/s (+30 %), Hard 720 → **1080** px/s (+50 %), Easy unverändert 420. Gebremst entsprechend 250 bzw. 360 px/s. Damit die Reaktionszeit nicht mitschrumpft, wurde das Snap-Fenster von 160 auf **200 px** vergrössert: rund 1,4 s (Easy), 0,8 s (Normal) und 0,55 s (Hard) zwischen „Befehl möglich" und Barriere. | ## 10. Python-3.9-Kompatibilität (v1.5) Auf dem MacBook läuft `python3` aus den Xcode Command Line Tools: **3.9.6**. Der Code nutzte `@dataclass(..., slots=True)` – das gibt es erst ab Python 3.10, also brach der Start mit `TypeError: dataclass() got an unexpected keyword argument 'slots'` ab. * `slots=True` in `sequences.py` und `difficulty.py` entfernt (der Speichervorteil ist hier irrelevant). * `run.sh` prüft jetzt die Python-Version und gibt bei < 3.9 eine verständliche Meldung mit Lösungshinweis aus, statt mit einem Traceback abzubrechen. * Beim Start stehen `Python x.y.z, pygame a.b.c` im Log – hilfreich bei Support-Fällen. * Verifiziert mit echtem Python 3.9.25 + pygame 2.6.1: 57 Tests grün, Selbsttest läuft durch.