# 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)
