Files
geograsim/App/sims/logistik/balance-matrix.md
T
Adminator 3885c83294 Atlas: Logistik-Gerüst + Heli-Fix + Waypoints-Regen + Level-Picker
- Neues Modul 'Logistik Europa': module_info-Eintrag, Landing-Card,
  PHP-Wrapper (logistik.php, modul-logistik.php), Engine-Skelett mit
  Enums + Helpern (travelCost, latePenalty, Bonus, Dijkstra,
  Polyline-Interpolation), Test-Harness, Kompetenzen-Draft,
  5 Seed-Dateien (locations, vehicle-types, cargo-types, railnet,
  contract-templates), 3 Level-Einträge in game_levels, Phase 0 durch
  die Logistik-Instanz geliefert und Atlas-Review bestanden
- Heli-Fix: heli-game.php mit BASE_PATH (production-sicher) +
  Asset-Pfad-Injection, geo_waypoints-Tabelle per Seed-Script
  auffindbar gemacht
- Waypoints-Regen-Tool: regen-waypoints-sql.php synchronisiert
  waypoints.sql aus DB (70 Einträge)
- Design-System: .ggs-level-grid / .ggs-level-card als Standard
  gepromotet (Klimas Muster, iPad-hover-safe)
- Inbox-Nachrichten: Kickoff-Briefings für Heli und Logistik,
  Asset-Map für Heli, DALL-E-Pipeline-Anleitung, Koordinations-
  nachrichten an Fluss, Klima, Glossar, Lehrplan

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:43:48 +02:00

18 KiB
Raw Blame History

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

{
  "level_1": {
    "startBudget": 8000,
    "maxActiveContracts": 1,
    "availableVehicleTypes": ["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
  },
  "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

Vorbehalt: Schema-Annahmen unten basieren auf dem Atlas-Briefing (game_levels.params als JSON). Falls Spaltennamen abweichen, bitte in der Atlas-Review korrigieren — ich schiebe dann nach.

-- Logistik Europa — 3 Level-Konfigurationen (Phase 0)
-- Voraussetzung: module_info-Eintrag 'logistik' existiert (Atlas hat angelegt)

INSERT INTO `game_levels`
  (`game_id`, `sort_order`, `level_name`, `level_name_easy`, `params`, `created_at`)
VALUES
(
  'logistik', 1, 'Lernen', 'Erster Auftrag',
  JSON_OBJECT(
    '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
  ),
  NOW()
),
(
  'logistik', 2, 'Übung', 'Drei Aufträge',
  JSON_OBJECT(
    '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
  ),
  NOW()
),
(
  'logistik', 3, 'Profi', 'Profi-Disposition',
  JSON_OBJECT(
    '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
  ),
  NOW()
);

Wenn das Schema andere Spalten hat (z.B. module_id statt game_id, oder zusätzliche description/is_active-Spalten), bitte mit Korrektur-Snippet melden — ich übernehme das hier.


7. Konsistenz-Checks für Atlas

Bitte beim Review verifizieren:

  1. Bahnnetz-Distanzen: lg-railnet.json enthält 5 Knoten und (laut Gerüst-Mail) 5 Kanten. Im Briefing waren nur 3 Kanten genannt (WienMünchen 400, MünchenHamburg 800, HamburgRotterdam 500). Welche Distanzen haben Paris↔Rotterdam und Paris↔München in deiner Seed-Datei? Trag in §2 ein und in headless-runner.js ggf. die Test-Erwartungen anpassen.
  2. Event-Wahrsch. L1=0: Bewusste Abweichung von Atlas-Vorschlag (PH 65.5: Unfall 5 %, Schnee 10 %, Hafen 8 % → Atlas-Briefing halbiert für L1 → ich setze auf 0). Bitte freigeben oder Gegenvorschlag.
  3. game_levels-Schema: Spaltennamen aus §6 INSERT prüfen.
  4. level_name_easy: Existiert eine Spalte für Leichte-Sprache- Variante des Level-Namens? Falls nicht, fliegt sie aus dem INSERT.

8. Änderungshistorie

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