130 lines
11 KiB
Markdown
130 lines
11 KiB
Markdown
# 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. |
|