Files
Habegger_loader_game/docs/ANALYSE.md
T

188 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
## 11. Controller wurde erkannt, reagierte aber nicht (v1.6)
Symptom auf dem MacBook: Im Start-Log stand `Controller: Xbox Series X Controller`,
aber keine Taste löste im Spiel etwas aus – mit Kabel und via Bluetooth.
Ursache: `connected_controllers()` öffnete den Joystick nur, um den Namen
auszulesen, und gab das Python-Objekt danach wieder frei. Damit schloss pygame
das Gerät, und SDL liefert für geschlossene Geräte **keine** Joystick-Events
mehr – die Tastatur funktionierte weiter, der Controller war stumm.
Behoben in `input_map.py`:
* Die Joystick-Objekte werden in `_OPEN_CONTROLLERS` offen gehalten
(`open_controllers()`), aufgerufen beim Start und bei
`JOYDEVICEADDED` / `JOYDEVICEREMOVED` (Hotplug).
* `open_controllers()` initialisiert das Joystick-Subsystem bei Bedarf selbst
und schluckt fehlende Geräte, statt abzubrechen.
* Neues Diagnose-Tool `tools/controller_check.py`: zeigt Name, Tasten-/
Achsen-/Hat-Anzahl, GUID und jedes eingehende Event samt abgeleiteter
Spiel-Aktion.
* Tests für die Tastenbelegung (Tasten 0–3, D-Pad als Hat, Stick mit
Deadzone), 62 Tests insgesamt.
## 12. Controller-Diagnose vom MacBook (v1.7)
Die Ausgabe von `tools/controller_check.py` auf dem MacBook (Xbox Series X
Controller, Bluetooth, pygame 2.6.1 / SDL 2.28.4):
* Der Controller meldet sich **erst nach dem Start** an (`Gefundene Controller:
keiner`, dann `Controller-Wechsel: …`) – Hotplug greift.
* Tasten 0 (A) und 2 (X) kommen als `JOYBUTTONDOWN` an.
* **Das D-Pad kommt nicht als Hat, sondern als Tasten 11–14**
(11 = hoch, 12 = runter, 13 = links, 14 = rechts).
* Der linke Stick kommt als Achsen 0/1, aufwärts = negativ.
Daraus: `BUTTON_ACTIONS` um 11–14 erweitert, damit D-Pad im Menü und im Spiel
funktioniert. Test ergänzt (63 Tests).
Ausserdem in `MAC-START.md` notiert: blinkt der Controller am USB-Kabel nur,
ist in der Regel ein reines Ladekabel ohne Datenleitungen schuld – Prüfbefehl
`system_profiler SPUSBDataType | grep -i -A 6 xbox`.