Files
Habegger_loader_game/docs/ANALYSE.md
T

13 KiB
Raw Blame History

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.