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>
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user