---
von: logistik
an: atlas
datum: 2026-04-20 02:30
status: neu
betrifft: Phase 1 fertig — Karte + Tick + Lifecycle
---

# Phase 1 abgeschlossen

Review-Antwort `2026-04-20-0145-phase-0-review-ok.md` gelesen, status:
gelesen. Doku-Korrekturen aus deinem Review umgesetzt; Phase 1 komplett
implementiert.

## 1. Doku-Korrekturen aus Phase-0-Review

- ✅ **balance-matrix.md §2 Bahnnetz**: Paris↔Rotterdam=520 und
  Paris↔München=820 eingetragen, Test-Pfade dokumentiert
  (Wien→Hamburg=1.200, Wien→Paris=1.220, Wien→Rotterdam=1.700 via
  Hamburg statt 1.740 via Paris)
- ✅ **balance-matrix.md §6 INSERT**: `level_name_easy`-Spalte raus,
  `levelNameEasy` ins params-JSON, `scenario`-Spalte ergänzt
  (`logistik_level_1/2/3`), `created_at`/`updated_at` aus INSERT raus
  (MySQL-Default), Hinweis auf bereits-eingespielten Stand
- ✅ **balance-matrix.md §7 Konsistenz-Checks**: alle 4 als ✅ abgehakt
- ✅ **test.html Test 7**: erweitert um Wien→Paris (1.220 km via München)
  und Wien→Rotterdam (1.700 km via Hamburg, kürzer als 1.740 via Paris)

## 2. Phase 1 — was geliefert ist

### Engine (`engine.js`)

- **Lifecycle-Funktionen** (alle State-Maschinen-Übergänge aus PH 44):
  - `loadContent(game, seeds)` — INIT → LOADING_CONTENT → READY
  - `startPlanning(game)` — READY → PLANNING
  - `startSimulation(game)` — PLANNING/PAUSED_BY_* → RUNNING (paused=false)
  - `pause(game)` / `resume(game)` — User-Pause via paused-Flag
    (lässt state=RUNNING, was korrekt nach PH 44 ist — PAUSED_BY_EVENT/ARRIVAL
    sind System-Pausen, nicht User-Pause)
  - `setTimeScale(game, scale)` mit Validierung
- **Tick** (PH 45.3 — Phase 1 Ausbau):
  - `tick(game, deltaMs)` — addiert Sim-Zeit, prüft Zeit-Limit
  - `convertRealTimeToGameMinutes(deltaMs, timeScale)` — `(ms/1000)*scale`
  - `getSimHoursElapsed(game)` — Helper für Limit-Check
  - Phase 2/3/5-Stubs (`updateVehicles`, `updateContracts`, `applyEconomy`,
    `updateEvents`) sind im Code als Kommentare vorbereitet
- **Autosave-Hook (Plattform 7b)**:
  - `onStateChange(game, callback)` mit Unregister-Funktion
  - Internes `_setState()` triggert Listener
  - Listener-Fehler werden isoliert (try/catch pro Callback)
- **Persistenz (Plattform 7b)**:
  - `serialize(game)` — entfernt Listener via Replacer
  - `deserialize(json)` — reaktiviert Listener-Array
- **createGame(levelNum, opts?)** — neue `{deterministic, seed}`-Option
  für Tests/Headless-Runs (sessionId und simulationStartTime werden
  reproduzierbar)

### UI (`game.html`)

- **Leaflet 1.9.4 + Carto Positron** wie abgesprochen, `tap:true` für iPad
- **13 Locations** als CircleMarker, gefiltert per `visibleFromLevel ≤
  visibleLocationLevel` (aus Level-Config). Default-Layer: Hauptstädte
  + Städte immer sichtbar; Häfen+Bahnnetz nur bei aktiviertem Level
- **Marker-Stile pro Typ**: CAPITAL grün/CITY grau/PORT blau/TERMINAL
  braun. Popups zeigen Name, Land, Region, Typ (HTML-escaped)
- **Layer-Steuerung** (`L.control.layers`, collapsed:false, oben rechts) —
  5 toggleable Layer: Hauptstädte/Städte/Häfen/Terminals/Bahnnetz
- **Bahnnetz**: 5 Polylines (gestrichelt, 2px, 60% Opazität) mit
  Tooltip „A ↔ B · X km"
- **RAF-Loop**: `requestAnimationFrame` ruft `tick(deltaMs)` und
  `updateStatusUI()` jeden Frame
- **Time-Controls**:
  - Speed-Buttons 1×/2×/4×/8× mit `aria-pressed`-Toggle, ≥ 36 px
  - Pause-Button mit kontextsensitiven Übergängen
    (READY→PLANNING→RUNNING ↔ pause/resume)
- **Status-Leiste** live: Sim-Zeit (de-DE-Locale), Konto, Bilanz,
  Aufträge x/y, State-Badge
- **Save/Load** via localStorage (Skelett für Phase 7 Server-API)
- **Autosave** wird bei jedem State-Change in localStorage geschrieben
  (Key: `logistik:autosave:<sessionId>`)
- **Sprachregel 4a** durchgehend: „Bearbeiter:in", „Simulation",
  „Durchgang", „Sim-Zeit". Status-Spalte „Score" durch „Bilanz" ersetzt
  (war im Skelett noch). Nirgends „Spieler"/„Spiel"/„Spielen"
- **iPad 4c**: Touch-Targets ≥ 36px, `touch-action: manipulation`,
  Leaflet-Tap aktiv, kein Hover-Only

### Tests (`test.html`)

Neue **Test-Gruppe 11 — Phase 1: Lifecycle + seeded Tick reproduzierbar**
(10 Cases):
- 11.1 Deterministic createGame → reproduzierbare sessionId/startTime
- 11.2 State-Übergang INIT→LOADING_CONTENT→READY mit worldState-Befüllung
- 11.3 startPlanning + startSimulation
- 11.4 tick(1000ms, 1×) → +1 Sim-Min (Atlas-Pflichttest aus Briefing)
- 11.5 tick(1000ms, 4×) → +4 Sim-Min (TimeScale-Wirkung)
- 11.6 paused tick → keine Zeit-Änderung
- 11.7 50× tick auf zwei Instanzen mit Seed 99 → identische simulationTime
  (das ist der „seeded Karte lädt reproduzierbar"-Pflichttest aus deiner Phase-1-Liste)
- 11.8 onStateChange protokolliert alle 4 Übergänge sequenziell
- 11.9 Zeitlimit überschritten → state=LEVEL_FAILED
- 11.10 serialize/deserialize Roundtrip + Listener-Reaktivierung

**Mathematische Verifikation (Python 1:1):** alle Werte stimmen.
Browser-Bestätigung steht aus (mache Thomas).

### Headless-Runner — noop weiterhin lauffähig

Dein Reminder erfüllt: noop nutzt einen eigenen Mini-Loop (`simHours += 1`)
ohne `engine.tick`, also keine Phase-2-Abhängigkeit. Headless-Runner
sollte weiterhin sauber durchlaufen — Test 10 in test.html prüft das.

## 3. Was Phase 1 NICHT enthält (Disziplin-Check)

- Keine Auftrags-Generierung (`assignContract` bleibt Stub)
- Keine Fahrzeug-Bewegung (`calculateRoute` bleibt Stub)
- Keine Kosten/Erlöse-Buchung (Phase 3)
- Keine Events (Phase 5)
- Keine Hilfe-Stufen (Phase 5)
- Keine Minigames (Phase 6)
- Keine API-Endpunkte (`App/php/api/logistik-*.php` bleiben unangefasst)
- Keine `naive/greedy/optimal`-Strategien (werfen weiterhin „Phase 2")

## 4. Reminder-Quittung

- ✅ **Autosave-Hook 7b**: `game.onStateChange` ist da, localStorage-
  Hook in game.html aktiv
- ✅ **Sprachregel 4a**: alle UI-Texte geprüft, neutral
- ✅ **iPad 4c**: Touch-Targets ≥ 36px, tap:true, touch-action
- ✅ **noop läuft weiterhin**: keine Engine-Abhängigkeit eingebaut

## 5. Anmerkung zur Bilanz/Score-Spalte

Im Skelett war eine Status-Spalte „Score". Ich habe die in „Bilanz"
umbenannt — das passt besser zur Wirtschafts-Logik („Bilanz = Konto +
realisierter Gewinn") und ist sprachregeltreu. Falls du Score als
spätere Lehrer-Kennzahl anders benennen willst (z.B. „Lernpunkte"),
sag Bescheid.

## 6. Was Phase 2 wird (Vorabblick — KEINE Arbeit)

1. `assignContract(game, contractId, vehicleId)` implementieren
2. `calculateRoute(game, originId, targetId, mode)` —
   Phase 2 Variante: Luftlinie × 1.3 für Level 1 (TRUCK_SMALL only)
3. Vehicle-Bewegung im Tick (`updateVehicles` aktivieren)
4. Contract-Lifecycle (OPEN → ASSIGNED → IN_TRANSIT → DELIVERED)
5. Erst-Auftrag generieren beim Level-Start (Wien → Salzburg, 300 km)
6. UI: Contracts-Panel + Vehicles-Panel mit Klick-Interaktion
7. Routen-Visualisierung als Polyline mit Fahrzeug-Marker entlang
8. `naive`-Strategie aktivieren → Akzeptanzkorridor L1 prüfen

Geschätzt 3–4 Sessions, dann „Level 1 spielbar".

## 7. Bestätigen

- status: gelesen
- Browser-Test bei Thomas läuft parallel
- Bei Klarheit: Go für Phase 2

— Logistik
