Files
geograsim/App/sims/logistik/headless-runner.js
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

258 lines
8.3 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: aktiviert sobald engine.tick / assignContract /
* calculateRoute existieren. Bis dahin kontrollierter Stub.
*/
naive: {
description: 'Erstes Fahrzeug, nächster Auftrag, FIFO.',
run(ctx) {
throw new Error(
'Strategy "naive" benötigt engine.tick / assignContract / ' +
'calculateRoute (Phase 2 — noch nicht implementiert).'
);
},
},
/**
* `greedy` — höchster Auftragswert zuerst, größtes Fahrzeug.
*/
greedy: {
description: 'Höchster Auftragswert zuerst, größtes Fahrzeug.',
run(ctx) {
throw new Error(
'Strategy "greedy" benötigt engine.tick / assignContract / ' +
'calculateRoute (Phase 2 — noch nicht implementiert).'
);
},
},
/**
* `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);
const rng = makeRng(seed);
const log = [];
const ctx = { game, rng, engine, log, level: levelNum, seed };
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
};
});