# Logistik Europa — Balance-Matrix v0.2 **Stand:** 2026-04-20 (Phase 0 — Atlas-Review angefordert) **Quellen:** Pflichtenheft Kap 65 + Atlas-Kickoff-Briefing §5.1 + eigene Schätzungen (markiert) > Diese Matrix ist der **verbindliche Startpunkt** für alle Level-Konfigurationen. > Werte werden in `game_levels.params` (JSON) persistiert (siehe §6 INSERT-SQL), > zur Laufzeit erreichbar über `window.LOGISTIK_LEVELS`. Einzeln änderbar von > Lehrkräften über das Admin-Tool (Phase 7). > > **Akzeptanzkorridor (Atlas-Vorgabe, hartes Kriterium):** > - Level 1: ≥ 90 % der 13–15-jährigen schaffen Level beim **ersten** Versuch > - Level 3: ~50 % schaffen es beim ersten / ~90 % beim dritten Versuch > > Messung über Headless-Runner (`headless-runner.js`) mit drei Strategien > (`naive`, `greedy`, `optimal`) plus `noop` als Lebenszeichen-Strategie. --- ## 1. Level-Parameter mit Rationale Tabelle pro Parameter. **Quelle = A** (Atlas-Briefing §5.1), **PH** (Pflichtenheft Kap), **S** (Schätzung Logistik-Instanz). **„=A"** in Rationale heißt: Atlas-Wert übernommen, keine Abweichung. Abweichungen sind ausführlich begründet. | Parameter | L1 | L2 | L3 | Quelle | Rationale | |-----------------------------------|------------------|-----------------|-----------------|--------|-----------| | **Startbudget (€)** | 8.000 | 5.000 | 2.500 | A | =A. Werte erlauben bei TRUCK_SMALL Wien→Salzburg (~472 € Fahrkosten) genau 1 Standardauftrag (1.000 €) als Tutorialgewinn auf L1; auf L3 ist jede Fehlentscheidung sichtbar. | | **Aufträge parallel (max)** | 1 | 3 | 6 | A / PH 13.3 | =A. PH 13.3 nennt nur „mehrere", Atlas konkretisiert. 1/3/6 ist saubere Verdoppelung+Steigerung, didaktisch sortierbar. | | **Fahrzeuge verfügbar** | 1 | 3 | 5 | A / PH 13.3 | =A bzgl. Anzahl. Typen-Mix konkretisiert: L1=`TRUCK_SMALL`; L2=`TRUCK_SMALL+TRUCK_LARGE+TRAIN` (alle Typen, je 1); L3=variabel (2× SMALL, 2× LARGE, 1× TRAIN). | | **Fristlänge-Multiplikator** | × 1.5 | × 1.2 | × 1.0 | A / PH 65.6 | =A. PH 65.6 definiert nur Standardauftrag-Frist als `Distanz/60 × 1.5`. Atlas reduziert pro Level — gut, bleibt. | | **Hilfestufe (default)** | `BLINK_EXACT` | `SHOW_REGION` | `NONE` | A / PH 10.3 | =A. PH 10.3 nennt 5 Stufen, Atlas wählt die didaktisch sinnvollste Spreizung (volle Hilfe → mittlere → keine). `SHOW_COUNTRY` zwischen L1 und L2 als künftige Lehrkraft-Option. | | **Event-Wahrscheinlichkeit (Multiplikator)** | **0** (deaktiviert) | 0.5 | 1.0 | **S abweichend** | **Abweichung von A** (war 2 %/5 %/10 %): Setze L1 auf **0** statt halbiert. Begründung: PH 4.2 verlangt „motivierend, nicht frustrierend". Bei einem einzigen Auftrag im Tutorial ist ein Unfall-Event statistisch zwar selten, aber wenn er trifft, ist der Effekt katastrophal (Speed×0.5 bei einer einzigen 4.5h-Fahrt = +2.25h Verspätung = 450 € Strafe = halber Erlös weg). Bei 0 % wird das Modell durchsichtig. Events kommen ab L2. | | **Verspätungsstrafe (% Wert/h)** | 10 % | 20 % | 30 % | A / PH 65.2 | A weicht selbst von PH ab: PH 65.2 nennt fix „20 %", Atlas staffelt 10/20/30. Übernehme Atlas, weil pädagogisch sinnvoller (Schonung auf L1, Schmerz auf L3). Im JSON-Export als `latePenaltyOverride`, default bleibt `ECONOMY.latePenaltyPercentPerHour = 0.2`. | | **Miete Fahrzeug (€/Tag)** | 0 | 200 | 500 | A | =A. PH erwähnt Miete nur lose (Kap 10.7 „Miete ab bestimmten Levels"). 0/200/500 macht Fahrzeugauswahl strategisch ab L2 (großer LKW = teurer, aber 3× Kapazität → lohnt nur bei Großauftrag). | | **Standkosten Fahrzeug (€/h)** | 0 | 5 | 5 | PH 65.2 | =PH (5 €/h). L1 ausgesetzt, weil Tutorial — sonst wirkt jede Pause wie eine Strafe. Ab L2 normal aktiv. | | **Standkosten Container (€/h Hafen)** | n/a | n/a | 10 | PH 65.2 | =PH (10 €/h). Erst L3 relevant, weil Häfen erst L3 aktiv. | | **Min Ziel-Erlös (€)** | 3.000 | 10.000 | 20.000 | A / S | =A. Mein Sanity-Check: L3 mit 6 parallelen Aufträgen × ~1.500 € durchschnittlich × 2 Auftragsrunden in 72h = 18.000 € Brutto. 20.000 € Min-Ziel ist also „muss Bonus mitnehmen, sonst knapp". Passt zu „Profi". | | **Zeitlimit (Sim-Stunden)** | 24 | 48 | 72 | A / S | =A. 24h L1 ist genug für 1 Auftrag (4.5h Wien→Salzburg + Puffer). 48h L2 für 3 Aufträge mit Disposition. 72h L3 für 6+intermodal+Events. | | **Initial timeScale** | 1× | 1× | 1× | PH 65.4 | Standard. Bearbeiter:in wechselt in der UI auf 4× oder 8× nach Bedarf. Kein Auto-Skip auf höhere Levels — bewusste Entscheidung sollen sie selbst treffen. | | **Bahn aktiv?** | nein | nein | ja | A / PH 13.3 | =A. PH 13.3 sagt L3 „volle Grundlogik" inkl. Bahn. L1+L2 ohne Bahn vereinfacht Routing-Entscheidung erheblich (kein Mode-Choice). | | **Häfen aktiv?** | nein | nein | ja | A / PH 13.3 | =A. Begründung wie Bahn — Schiffsankünfte als Auftragsquelle braucht didaktischen Boden, der erst auf L3 da ist. | | **Intermodale Aufträge?** | nein | nein | ja | A / PH 13.3 | =A. Voraussetzung: Bahn + Häfen aktiv. | | **Minigames?** | nein | optional | optional | PH 11.4 | =PH (optional ab L2). L1 nichts, weil schon mit Hauptmechanik beschäftigt. „Optional" heißt: per Lehrkraft-Config schaltbar (Phase 7). | | **Sichtbare Locations** | nur `visible_from_level=1` (Hauptstädte) | bis Level 2 (+ Großstädte) | alle | PH 9.1 + Seed | Seed-Daten haben pro Location ein `visibleFromLevel`-Feld. Filter rendert nur sichtbare Marker — reduziert kognitive Last auf L1. Ab L3 alles sichtbar. | ### Begründungen je Level (Erzählebene) **Level 1 — „Erster Auftrag":** Tutorial. Ein einziger Auftrag (Wien → Salzburg, 300 km), ein einziger TRUCK_SMALL. `BLINK_EXACT` zeigt Start- und Zielort blinkend auf der Karte. Events deaktiviert (Abweichung von Atlas — siehe Tabelle, Zeile Event-Wahrsch.). Kontingent: 8.000 € + 1.000 € Auftrag − ~472 € Fahrkosten = klarer positiver Abschluss bei Pünktlichkeit. Bonus „pünktlich" (+10 %) und „keine Leerfahrt" (+20 %) spürbar, aber nicht überlebenswichtig. **Ziel:** Vertrauen aufbauen, Mechanik verstehen, „ich kann das"-Erlebnis. **Level 2 — „Drei Aufträge gleichzeitig":** Erste echte Disposition. 3 parallele Aufträge in Mitteleuropa, alle 3 Fahrzeugtypen verfügbar. `SHOW_REGION` zeigt nur die Region des Zielorts — Bearbeiter:in muss überlegen, welcher Ort innerhalb dieser Region passt. Events bei 50 % der PH-Werte (Unfall 2.5 %/h, Schnee 5 %/h, Hafen 4 %/h). Knappe 5.000 € Startbudget zwingt zu Effizienz. Zwei dumme Leerfahrten machen die Bilanz schon negativ. **Ziel:** Strategie-Bewusstsein, Fahrzeug-zu-Auftrag-Match lernen. **Level 3 — „Profi-Disposition Europa":** Volles Modell. 6 parallele Aufträge, intermodale Ketten (LKW → Zug → LKW), Häfen mit Schiffsankünften (= zusätzliche Container, neue Aufträge), Bahn-Dijkstra über 5 Knoten. `NONE` — alle Orte selbst suchen. Events bei 100 % der PH-Werte. 2.500 € Startbudget verzeiht keinen Patzer. **Ziel:** Wer hier mit positivem Kontostand abschließt, hat das Modell verstanden — auch wirtschaftlich. --- ## 2. Verbindliche Konstanten (aus Kap 65 — gleich für alle Level) > Über `LogistikEngine.VEHICLE_DEFAULTS`, `.ECONOMY`, `.EVENT_RULES` > bereits implementiert. **Nicht überschreiben pro Level**, nur via > Tuning-Iteration mit Atlas-Approval. ### Fahrzeuge | Typ | Speed | Kapazität | €/km | €/h | Lade min | Entlade min | |--------------|---------|-----------|------|------|----------|-------------| | TRUCK_SMALL | 70 km/h | 1 Cont. | 1.20 | 25 | 5 | 5 | | TRUCK_LARGE | 60 km/h | 3 Cont. | 2.50 | 45 | 8 | 8 | | TRAIN | 90 km/h | 20 Cont. | 8.00 | 150 | 20/Block | 20 | ### Aufträge | Typ | Basiswert | Container | Frist (h) | |------------------|-----------|-----------|------------------------| | Standardauftrag | 1.000 € | 1–3 | distanceKm/60 × 1.5 | | Eilauftrag | 1.500 € | 1–2 | distanceKm/70 | ### Bonus / Malus - Pünktlich: **+10 %** - Optimal (keine Leerfahrt): **+20 %** - Strafe pro Stunde Verspätung: **20 % Auftragswert** (per Level via `latePenaltyOverride` anpassbar) ### Events (pro Stunde, Basis-Werte) | Event | Basis-Wahrsch./h | Wirkung | |-----------------|---------------------|-----------------------| | TRAFFIC_ACCIDENT| 5 % | Speed × 0.5 für Event-Dauer | | SNOW (Alpen) | 10 % | Speed × 0.7 | | PORT_DELAY | 8 % | Ladezeit × 1.5 | Pro Level multipliziert mit `eventProbabilityMultiplier` (0/0.5/1.0). ### Zeit - 1 Tick = 250 ms Echtzeit - 1× = 1 Sim-Min/Echt-Sek - 4× = 4 Sim-Min/Echt-Sek - 8× = 8 Sim-Min/Echt-Sek (je nach Stabilität) ### Bahnnetz (aus `lg-railnet.json`, von Atlas bestätigt 01:45) Knoten: Wien, München, Hamburg, Rotterdam, Paris (5) Kanten: - Wien ↔ München: 400 km - München ↔ Hamburg: 800 km - Hamburg ↔ Rotterdam: 500 km - Paris ↔ Rotterdam: 520 km (durationMinutes: 347) - Paris ↔ München: 820 km (durationMinutes: 547) Test-Pfade für Dijkstra: - Wien → Hamburg via München = 400 + 800 = **1.200 km** - Wien → Paris via München = 400 + 820 = **1.220 km** - Wien → Rotterdam (kürzer): via München+Hamburg = 400+800+500 = **1.700 km** (vs. via Paris = 400+820+520 = 1.740 km) --- ## 3. Strategie-Profile (für Headless-Runner) | Strategie | Verhalten | Erwartung L1 | Erwartung L2 | Erwartung L3 | |-----------|-----------|--------------|--------------|--------------| | `noop` | Keine Aktion. Zeit läuft ab, kein Erlös. | fail | fail | fail | | `naive` | Erstes Fahrzeug, nächster Auftrag, FIFO. | **muss** schaffen (≥ 90 %) | knapp / fail | fail (sicher) | | `greedy` | Höchster Auftragswert zuerst, größtes Fahrzeug. | schafft locker | schafft (~ 70 %) | fail / knapp | | `optimal` | Orakel — perfekte Routenplanung, keine Leerfahrten. | schafft perfekt | schafft mit Reserve | **muss** schaffen (~ 90 %) | `noop` ist Test-Strategie für Phase 0: Beweist, dass Runner-Vertrag, Seed-RNG, Ergebnisstruktur und Loop-Steuerung funktionieren — ohne dass `engine.tick` implementiert sein muss. ### Akzeptanzkorridor (formal) ``` runLevel(1, *, 'naive') → success: true, endBalance ≥ +1.000 € runLevel(1, *, 'greedy') → success: true, endBalance ≥ +2.000 € runLevel(1, *, 'optimal') → success: true, endBalance ≥ +3.000 € runLevel(2, *, 'naive') → success: false (oder negativ) runLevel(2, *, 'greedy') → success: true, endBalance ≥ 0 runLevel(2, *, 'optimal') → success: true, endBalance ≥ +5.000 € runLevel(3, *, 'naive') → success: false (sicher) runLevel(3, *, 'greedy') → success: false oder knapp negativ runLevel(3, *, 'optimal') → success: true, endBalance ≥ 0 ``` Pro Level werden **5 Seeds** (1–5) gespielt. Kriterium gilt für **Median**, nicht Worst-Case (sonst kein Spielraum für Pech). Ergebnisse landen in `headless-runner.html`-Tabelle. --- ## 4. Tuning-Protokoll Wenn nach Phase 2/3 die Akzeptanz-Korridore nicht erreicht werden: 1. **Erst** prüfen, ob die Strategie wirklich „naive"/„optimal" ist — schlechte Strategien lassen Level-Werte gut aussehen. 2. **Dann** an *einem* Parameter pro Iteration drehen. Nie mehrere gleichzeitig — sonst nicht messbar. 3. Jede Tuning-Änderung mit kurzem Eintrag im §8 Änderungslog dokumentieren. 4. Bei Strukturveränderungen (Hilfestufe oder Fahrzeuge) Atlas-Review anfragen, weil dann Lehrplan-Anker möglicherweise nicht mehr passen. --- ## 5. JSON-Repräsentation für `game_levels.params` ```json { "level_1": { "startBudget": 8000, "maxActiveContracts": 1, "availableVehicleTypes": ["TRUCK_SMALL"], "vehicleCount": 1, "deadlineMultiplier": 1.5, "defaultHintMode": "BLINK_EXACT", "eventProbabilityMultiplier": 0, "latePenaltyOverride": 0.10, "rentalCostPerDay": 500, "idleCostPerHour": 0, "containerStandCostPerHour": 0, "minTargetEarnings": 3000, "timeLimitHours": 72, "initialTimeScale": 1, "railEnabled": false, "portsEnabled": false, "intermodalEnabled": false, "minigamesEnabled": false, "visibleLocationLevel": 1 }, "level_2": { "startBudget": 5000, "maxActiveContracts": 3, "availableVehicleTypes": ["TRUCK_SMALL", "TRUCK_LARGE", "TRAIN"], "vehicleCount": 3, "deadlineMultiplier": 1.2, "defaultHintMode": "SHOW_REGION", "eventProbabilityMultiplier": 0.5, "latePenaltyOverride": 0.20, "rentalCostPerDay": 200, "idleCostPerHour": 5, "containerStandCostPerHour": 0, "minTargetEarnings": 10000, "timeLimitHours": 48, "initialTimeScale": 1, "railEnabled": false, "portsEnabled": false, "intermodalEnabled": false, "minigamesEnabled": true, "visibleLocationLevel": 2 }, "level_3": { "startBudget": 2500, "maxActiveContracts": 6, "availableVehicleTypes": ["TRUCK_SMALL", "TRUCK_LARGE", "TRAIN"], "vehicleCount": 5, "deadlineMultiplier": 1.0, "defaultHintMode": "NONE", "eventProbabilityMultiplier": 1.0, "latePenaltyOverride": 0.30, "rentalCostPerDay": 500, "idleCostPerHour": 5, "containerStandCostPerHour": 10, "minTargetEarnings": 20000, "timeLimitHours": 72, "initialTimeScale": 1, "railEnabled": true, "portsEnabled": true, "intermodalEnabled": true, "minigamesEnabled": true, "visibleLocationLevel": 3 } } ``` --- ## 6. INSERT-SQL für `game_levels` **Schema (von Atlas bestätigt 01:45):** `id, game_id, level_name, scenario, params, sort_order, created_at, updated_at`. Keine eigene Spalte `level_name_easy` — die Leichte-Sprache-Variante wandert ins `params`-JSON als `levelNameEasy` (UI liest beide Felder, `pickText()` wählt). > **Hinweis:** Atlas hat die drei Logistik-Level am 2026-04-20 01:45 > bereits eingespielt. Diese SQLs dienen als Referenz / für Reset. ```sql -- Logistik Europa — 3 Level-Konfigurationen (Phase 0) -- Voraussetzung: module_info-Eintrag 'logistik' existiert -- created_at / updated_at werden von MySQL-Default gesetzt INSERT INTO `game_levels` (`game_id`, `sort_order`, `level_name`, `scenario`, `params`) VALUES ( 'logistik', 1, 'Lernen', 'logistik_level_1', JSON_OBJECT( 'levelNameEasy', 'Erster Auftrag', 'startBudget', 8000, 'maxActiveContracts', 1, 'availableVehicleTypes', JSON_ARRAY('TRUCK_SMALL'), 'vehicleCount', 1, 'deadlineMultiplier', 1.5, 'defaultHintMode', 'BLINK_EXACT', 'eventProbabilityMultiplier', 0, 'latePenaltyOverride', 0.10, 'rentalCostPerDay', 0, 'idleCostPerHour', 0, 'containerStandCostPerHour', 0, 'minTargetEarnings', 3000, 'timeLimitHours', 24, 'initialTimeScale', 1, 'railEnabled', FALSE, 'portsEnabled', FALSE, 'intermodalEnabled', FALSE, 'minigamesEnabled', FALSE, 'visibleLocationLevel', 1 ) ), ( 'logistik', 2, 'Übung', 'logistik_level_2', JSON_OBJECT( 'levelNameEasy', 'Drei Aufträge', 'startBudget', 5000, 'maxActiveContracts', 3, 'availableVehicleTypes', JSON_ARRAY('TRUCK_SMALL', 'TRUCK_LARGE', 'TRAIN'), 'vehicleCount', 3, 'deadlineMultiplier', 1.2, 'defaultHintMode', 'SHOW_REGION', 'eventProbabilityMultiplier', 0.5, 'latePenaltyOverride', 0.20, 'rentalCostPerDay', 200, 'idleCostPerHour', 5, 'containerStandCostPerHour', 0, 'minTargetEarnings', 10000, 'timeLimitHours', 48, 'initialTimeScale', 1, 'railEnabled', FALSE, 'portsEnabled', FALSE, 'intermodalEnabled', FALSE, 'minigamesEnabled', TRUE, 'visibleLocationLevel', 2 ) ), ( 'logistik', 3, 'Profi', 'logistik_level_3', JSON_OBJECT( 'levelNameEasy', 'Profi-Disposition', 'startBudget', 2500, 'maxActiveContracts', 6, 'availableVehicleTypes', JSON_ARRAY('TRUCK_SMALL', 'TRUCK_LARGE', 'TRAIN'), 'vehicleCount', 5, 'deadlineMultiplier', 1.0, 'defaultHintMode', 'NONE', 'eventProbabilityMultiplier', 1.0, 'latePenaltyOverride', 0.30, 'rentalCostPerDay', 500, 'idleCostPerHour', 5, 'containerStandCostPerHour', 10, 'minTargetEarnings', 20000, 'timeLimitHours', 72, 'initialTimeScale', 1, 'railEnabled', TRUE, 'portsEnabled', TRUE, 'intermodalEnabled', TRUE, 'minigamesEnabled', TRUE, 'visibleLocationLevel', 3 ) ); ``` --- ## 7. Konsistenz-Checks (durch Atlas 01:45 abgeschlossen) 1. ✅ **Bahnnetz-Distanzen:** Paris↔Rotterdam=520, Paris↔München=820 (bestätigt, in §2 eingearbeitet) 2. ✅ **Event-Wahrsch. L1=0:** Genehmigt 3. ✅ **`game_levels`-Schema:** Spalten geklärt, INSERT in §6 angepasst 4. ✅ **`level_name_easy`:** Existiert nicht, wandert ins params-JSON --- ## 8. Änderungshistorie - **2026-04-20 01:50** v0.3 — Atlas-Doku-Korrekturen: Bahnnetz-Distanzen Paris↔Rotterdam=520/Paris↔München=820 ergänzt, INSERT-Schema angepasst (`scenario` statt `level_name_easy`, `levelNameEasy` ins params-JSON), Konsistenz-Checks abgehakt - **2026-04-20** v0.2 — Rationale-Spalte je Parameter, INSERT-SQLs, Konsistenz-Checks an Atlas, `eventProbabilityMultiplier` als Multiplikator umstrukturiert, Container-Standkosten ergänzt, `noop`- Strategie zur Strategieliste - **2026-04-20** v0.1 — Erste Fassung (Logistik)