Files
geograsim/App/sims/logistik/headless-runner.js
T
Adminator fc8996f064 Atlas: DSGVO-Tile-Proxy + Logistik-Phase-bis-8 + Sounds-Anleitung
Platform:
- OSM-DE als dritter Tile-Proxy-Provider (deutsche Beschriftungen)
- logistik.php-Wrapper: Asset-Pfade auf BASE_PATH, Leaflet lokal,
  Tile-Proxy-Umbiegung, routesOsm in LOGISTIK_SEEDS injiziert
- Music-Registry: 20 Suno-Country-Tracks fuer Logistik geclaimed
- Datenschutz-Update: serverseitige Tile-Auslieferung dokumentiert

Logistik (Instanz-Lieferungen, parallel):
- Phase 0 bis 8 inkl. Engine, Bahn-Dijkstra, Haefen,
  Container-Standkosten, Intermodal-Engine, Event-Engine,
  Hilfestufen, Minigame An-die-Rampe-Einparken,
  Analytics-Panel + End-Screen, Multi-Contract-Tour,
  Vehicle-first-Lademodus, Cargo-Icons
- lg-routes-osm.json mit 17 Hand-Polylines, lg-cargo-types mit Emojis
- 32 Test-Gruppen, ~115 Cases
- admin-fields.json fuer Admin-UI-Integration

Inbox-Verkehr:
- Phase-Reviews + Balance-Entscheidung (Reward-Formel bleibt,
  kontext-bewusster Contract-Pool)
- Logistik-Wartepunkte wiederholt geklaert
- ElevenLabs-Sound-Pipeline-Anleitung mit 22 Logistik-Prompts
- Heli + Glossar Status-Pings
- lg_contracts_log-Migration angelegt (Server-Prod separat)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:15:00 +02:00

362 lines
13 KiB
JavaScript
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 — Headless-Runner (Phase 0)
* ----------------------------------------------------------------
* Deterministische Level-Durchläufe für die Balance-Verifikation
* gemäß `balance-matrix.md` §3.
*
* Vertrag (aus Kickoff-Briefing §5.4):
* runLevel(levelNum, seed, strategy) → Promise<{
* successful: boolean,
* endBalance: number,
* completedContracts: number,
* lateDeliveries: number,
* durationHours: number,
* hintUsages: number,
* log: Array,
* strategy: string,
* level: number,
* seed: number
* }>
*
* Verfügbare Strategien:
* - `noop` — funktionierende Demo: keine Aktion, Zeit läuft ab,
* success=false. Beweist Runner-Vertrag in Phase 0.
* - `naive` — Skelett, wirft `Phase 2` (FIFO-Auftrag, erstes Fahrzeug).
* - `greedy` — Skelett, wirft `Phase 2` (höchster Wert zuerst).
* - `optimal` — Skelett, wirft `Phase 2` (Orakel mit Bonus-Maximierung).
*
* Browser-Nutzung:
* <script src="engine.js"></script>
* <script src="headless-runner.js"></script>
* <script>
* const r = await LogistikRunner.runLevel(1, 1, 'noop');
* console.log(r);
* </script>
*
* Node-Nutzung (sobald engine.js Node-Export bekommt):
* const Runner = require('./headless-runner');
* const Engine = require('./engine');
* const r = await Runner.runLevel(1, 1, 'noop', { engine: Engine });
*/
(function (root, factory) {
'use strict';
const api = factory();
if (typeof module !== 'undefined' && module.exports) {
module.exports = api; // Node / CommonJS
}
if (typeof root !== 'undefined') {
root.LogistikRunner = api; // Browser global
}
})(typeof window !== 'undefined' ? window : globalThis, function () {
'use strict';
/* ============================================================
SEED-RNG (Mulberry32 — deterministisch, ausreichend für Tests)
Gleicher Seed → identische Sequenz auf jeder Plattform.
============================================================ */
function makeRng(seed) {
let s = (seed >>> 0) || 1;
return function () {
s = (s + 0x6D2B79F5) >>> 0;
let t = s;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/* ============================================================
ENGINE-RESOLVE (Browser via window, Node via opts.engine)
============================================================ */
function resolveEngine(opts) {
if (opts && opts.engine) return opts.engine;
if (typeof window !== 'undefined' && window.LogistikEngine) {
return window.LogistikEngine;
}
throw new Error(
'LogistikEngine nicht verfügbar. Im Browser: <script src="engine.js"> ' +
'vor diesem Skript einbinden. In Node: opts.engine übergeben.'
);
}
/* ============================================================
STRATEGIEN
Jede Strategie ist ein Objekt mit { description, run(ctx) }.
`ctx` enthält game, rng, engine, log, level, seed.
run() gibt das Result-Objekt zurück (gleicher Vertrag wie runLevel).
============================================================ */
const STRATEGIES = {
/**
* `noop` — funktionierende Demo-Strategie für Phase 0.
* Beweist, dass Runner-Vertrag, Seed-RNG, Loop-Steuerung und
* Ergebnisstruktur korrekt sind, OHNE engine.tick zu callen.
*
* Verhalten: keine Aktion, Sim-Zeit läuft bis Limit, kein Erlös,
* Bilanz bleibt = Startbudget, success=false (Min-Ziel-Erlös
* nicht erreicht).
*/
noop: {
description: 'Keine Aktion — Zeit läuft ab, kein Erlös.',
run(ctx) {
const { game, log } = ctx;
const cfg = game._config || {};
const limitHours = cfg.timeLimitHours || 24;
const stepHours = 1;
let simHours = 0;
let safety = 100000;
while (simHours < limitHours && safety-- > 0) {
simHours += stepHours;
log.push({ simHours, action: 'idle', balance: game.balance });
}
// noop schließt definitionsgemäß KEINE Aufträge ab → immer fail.
// (Auch wenn Min-Ziel-Erlös 0 wäre — die Strategie ist explizit
// dafür gedacht, „kein Lebenszeichen" zu beweisen.)
return {
successful: false,
endBalance: game.balance,
completedContracts: 0,
lateDeliveries: 0,
durationHours: simHours,
hintUsages: 0,
log,
};
},
},
/**
* `naive` — erstes Fahrzeug, nächster Auftrag, FIFO-Reihenfolge.
* Phase 2 (aktiv): nimmt den ersten OPEN-Auftrag, weist ihn dem
* ersten IDLE-Fahrzeug zu, lässt dann ticken bis Limit oder
* alle Aufträge fertig.
*/
naive: {
description: 'Erstes Fahrzeug, nächster Auftrag, FIFO.',
run(ctx) {
const { game, engine, log, seeds } = ctx;
const cfg = game._config || {};
const limitHours = cfg.timeLimitHours || 24;
const E = engine;
// Seeds + Lifecycle
E.loadContent(game, seeds);
E.seedInitialVehicles(game);
E.seedInitialContracts(game);
E.startPlanning(game);
E.startSimulation(game);
// Phase-2-Tick-Step: 5 Min Sim-Zeit pro Iteration (entspricht
// ~5000 ms real bei 1×; im Headless egal, da deterministisch)
const stepMinutes = 5;
const stepRealMs = stepMinutes * 1000; // (1×: 1 sec real = 1 min sim → 5 min = 5000 ms)
let safety = 100000;
while (game.state === E.STATE.RUNNING && safety-- > 0) {
// Naive Disposition: erstes OPEN ↔ erstes IDLE.
// Intermodal-Aufträge werden in dieser Strategie noch übersprungen
// (Phase 6c: separate Strategie-Anpassung mit Mode-Match).
const open = (game.activeContracts || []).filter(c => c.state === E.CONTRACT_STATE.OPEN && !c.intermodal);
const free = (game.vehicles || []).filter(v => v.state === E.VEHICLE_STATE.IDLE);
if (open.length && free.length) {
try {
E.assignContract(game, open[0].id, free[0].id);
log.push({ simTime: game.simulationTime, action: 'assign', contract: open[0].id, vehicle: free[0].id });
} catch (e) {
log.push({ simTime: game.simulationTime, action: 'assign_failed', error: e.message });
}
}
E.tick(game, stepRealMs);
if (game.state !== E.STATE.RUNNING) break;
if (E.getSimHoursElapsed(game) >= limitHours) break;
}
const startBudget = cfg.startBudget || 5000;
const profit = game.balance - startBudget;
const minTarget = cfg.minTargetEarnings || 0;
return {
successful: profit >= minTarget,
endBalance: game.balance,
completedContracts: (game.completedContracts || []).length,
lateDeliveries: (game.analytics && game.analytics.lateDeliveries) || 0,
durationHours: E.getSimHoursElapsed(game),
hintUsages: 0,
log,
};
},
},
/**
* `greedy` — höchster Auftragswert zuerst, fähigstes verfügbares
* Fahrzeug. „Fähig" = Kapazität ≥ Container-Bedarf, sonst egal.
* Phase 3 aktiv.
*/
greedy: {
description: 'Höchster Auftragswert zuerst, größtes passendes Fahrzeug.',
run(ctx) {
const { game, engine, log, seeds } = ctx;
const cfg = game._config || {};
const limitHours = cfg.timeLimitHours || 24;
const E = engine;
E.loadContent(game, seeds);
E.seedInitialVehicles(game);
E.seedInitialContracts(game);
E.startPlanning(game);
E.startSimulation(game);
const stepRealMs = 5 * 1000; // 5 min sim
let safety = 100000;
while (game.state === E.STATE.RUNNING && safety-- > 0) {
// Aufträge nach Wert sortiert (descending), Fahrzeuge nach Kapazität descending.
// Intermodal-Aufträge werden hier noch übersprungen (Phase 6c).
const open = (game.activeContracts || [])
.filter(c => c.state === E.CONTRACT_STATE.OPEN && !c.intermodal)
.slice()
.sort((a, b) => (b.rewardBase || 0) - (a.rewardBase || 0));
const free = (game.vehicles || [])
.filter(v => v.state === E.VEHICLE_STATE.IDLE)
.slice()
.sort((a, b) => (b.capacityUnits || 0) - (a.capacityUnits || 0));
// Pro Auftrag das passendste Fahrzeug nehmen
for (const c of open) {
if (!free.length) break;
// Erstes free, das genug Kapazität hat (wenn keines, das größte)
let pick = free.find(v => (v.capacityUnits || 0) >= (c.quantityContainers || 1));
if (!pick) pick = free[0];
try {
E.assignContract(game, c.id, pick.id);
log.push({ simTime: game.simulationTime, action: 'assign', contract: c.id, vehicle: pick.id });
const idx = free.indexOf(pick);
if (idx >= 0) free.splice(idx, 1);
} catch (e) {
log.push({ simTime: game.simulationTime, action: 'assign_failed', error: e.message });
}
}
E.tick(game, stepRealMs);
if (game.state !== E.STATE.RUNNING) break;
if (E.getSimHoursElapsed(game) >= limitHours) break;
}
const startBudget = cfg.startBudget || 5000;
const profit = game.balance - startBudget;
const minTarget = cfg.minTargetEarnings || 0;
return {
successful: profit >= minTarget,
endBalance: game.balance,
completedContracts: (game.completedContracts || []).length,
lateDeliveries: (game.analytics && game.analytics.lateDeliveries) || 0,
durationHours: E.getSimHoursElapsed(game),
hintUsages: 0,
log,
};
},
},
/**
* `optimal` — Orakel mit perfekter Routenplanung,
* keine Leerfahrten, Bonus-Maximierung.
*/
optimal: {
description: 'Orakel mit perfekter Routenplanung, keine Leerfahrten.',
run(ctx) {
throw new Error(
'Strategy "optimal" benötigt vollständige Engine inkl. Phase-3-' +
'Bahnrouting und Phase-5-Events (noch nicht implementiert).'
);
},
},
};
/* ============================================================
CORE — runLevel
============================================================ */
/**
* Spielt einen Level deterministisch durch und liefert Kennzahlen.
*
* @param {number} levelNum
* @param {number} seed Mulberry32-Seed
* @param {'noop'|'naive'|'greedy'|'optimal'} strategyName
* @param {Object} [opts]
* @param {Object} [opts.engine] LogistikEngine-Override (für Node)
* @returns {Promise<Object>}
*/
async function runLevel(levelNum, seed, strategyName, opts) {
const engine = resolveEngine(opts);
const strategy = STRATEGIES[strategyName];
if (!strategy) {
throw new Error('Unbekannte Strategie: ' + strategyName +
' (verfügbar: ' + Object.keys(STRATEGIES).join(', ') + ')');
}
const game = engine.createGame(levelNum, { deterministic: true, seed });
const rng = makeRng(seed);
const log = [];
const ctx = {
game, rng, engine, log, level: levelNum, seed,
seeds: (opts && opts.seeds) || null, // optional override
};
const result = await strategy.run(ctx);
// Vertrag-Anreicherung — alle Felder garantieren
return Object.assign({
successful: false,
endBalance: 0,
completedContracts: 0,
lateDeliveries: 0,
durationHours: 0,
hintUsages: 0,
log: [],
}, result, {
strategy: strategyName,
level: levelNum,
seed,
});
}
/**
* Bequemer Wrapper: alle Kombinationen Level × Strategie × Seeds.
* @returns {Promise<Array>}
*/
async function runMatrix(opts) {
const o = opts || {};
const levels = o.levels || [1, 2, 3];
const strategies = o.strategies || ['noop', 'naive', 'greedy', 'optimal'];
const seeds = o.seeds || [1, 2, 3, 4, 5];
const tolerateErrors = o.tolerateErrors !== false;
const out = [];
for (const lv of levels) {
for (const st of strategies) {
for (const sd of seeds) {
try {
out.push(await runLevel(lv, sd, st, o));
} catch (e) {
if (!tolerateErrors) throw e;
out.push({
level: lv, strategy: st, seed: sd,
successful: null, _error: e.message,
endBalance: null, completedContracts: 0,
lateDeliveries: 0, durationHours: 0, hintUsages: 0,
log: [],
});
}
}
}
}
return out;
}
return {
runLevel,
runMatrix,
STRATEGIES,
makeRng,
_resolveEngine: resolveEngine, // exposed für Tests
};
});