Files
geograsim/App/sims/logistik/balance-matrix.md
T
Adminator 79ac9dad84 Atlas: Deploy-Buendel vor Staustufen-Sprint
- Logistik: Musik-Player Playlist-Modus (5 Gruppen, 20 Tracks)
- Heli: End-Screen auf .ggs-endscreen, Mission-Bilder, Voice-Lines, Briefing-Audio
- Logistik: Layout-Tausch Auftraege+Fahrzeuge links, Karte rechts
- Logistik: Tier-1-Staedte ausgebaut, Auto-Timescale-Badge
- Logistik: Roadnet/Railnet Dijkstra-Routing, Balance-Updates
- Atlas: 5 Atlas-Inbox-Nachrichten (Cards-Pattern, DALL-E-Key, Musik-Playlists)
- Status-Updates Heli + Logistik

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:05:13 +02:00

376 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 1315-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 € | 13 | distanceKm/60 × 1.5 |
| Eilauftrag | 1.500 € | 12 | 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** (15) 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)