79ac9dad84
- 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>
247 lines
11 KiB
HTML
247 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Logistik Europa — Headless-Runner</title>
|
||
<style>
|
||
body { font-family: 'Segoe UI', system-ui, sans-serif; max-width: 1100px; margin: 24px auto; padding: 0 16px; color: #222; background: #f7f5ef; }
|
||
h1 { font-size: 1.4rem; margin-bottom: 8px; }
|
||
h2 { font-size: 1.05rem; color: #1f4b37; margin-top: 24px; }
|
||
.meta { color: #666; font-size: 0.9rem; margin-bottom: 20px; }
|
||
.panel { background: #fff; border: 1px solid #e2ddd1; border-radius: 8px; padding: 14px 18px; margin: 14px 0; }
|
||
table { border-collapse: collapse; width: 100%; font-size: 0.88rem; }
|
||
th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid #eee; }
|
||
th { background: #f0ece3; font-weight: 600; }
|
||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||
.ok { color: #2d6d3d; font-weight: 600; }
|
||
.fail { color: #b32d2d; font-weight: 600; }
|
||
.skip { color: #8a7a4f; font-style: italic; }
|
||
.err { color: #b32d2d; font-style: italic; }
|
||
button { background: #1f4b37; color: #fff; border: 0; border-radius: 4px; padding: 8px 14px; cursor: pointer; font-size: 0.95rem; margin-right: 8px; }
|
||
button:hover { background: #2d6d4d; }
|
||
pre { background: #2b2b2b; color: #e6e6e6; padding: 12px; border-radius: 6px; font-size: 0.85rem; overflow-x: auto; }
|
||
.legend { font-size: 0.82rem; color: #666; margin-top: 6px; }
|
||
details > summary { cursor: pointer; color: #1f4b37; font-weight: 600; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<h1>Logistik Europa — Headless-Runner</h1>
|
||
<p class="meta">
|
||
Deterministische Level-Durchläufe für die Balance-Verifikation aus
|
||
<code>balance-matrix.md</code> §3. Logik liegt komplett in
|
||
<code>headless-runner.js</code> — diese Seite ist nur die UI-Hülle.
|
||
</p>
|
||
|
||
<div class="panel">
|
||
<p>
|
||
<button id="run-noop">Nur <code>noop</code> (3 Level × 5 Seeds, läuft)</button>
|
||
<button id="run-all">Alle Strategien (Stubs werfen)</button>
|
||
<button id="run-corridor" style="background:#b45900;">Balance-Korridor-Check (R-1/R-2/R-3)</button>
|
||
</p>
|
||
<div id="results"><p class="legend">Klick auf einen Button.</p></div>
|
||
</div>
|
||
|
||
<h2>Akzeptanzkorridor (aus balance-matrix.md §3)</h2>
|
||
<table>
|
||
<thead><tr><th>Level</th><th>noop</th><th>naive</th><th>greedy</th><th>optimal</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>1</td><td>fail</td><td>success ≥+1.000 €</td><td>success ≥+2.000 €</td><td>success ≥+3.000 €</td></tr>
|
||
<tr><td>2</td><td>fail</td><td>fail / negativ</td><td>success ≥0 €</td><td>success ≥+5.000 €</td></tr>
|
||
<tr><td>3</td><td>fail</td><td>fail (sicher)</td><td>fail / knapp</td><td>success ≥0 €</td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h2>Strategie-Definitionen</h2>
|
||
<ul id="strat-list"><!-- gefüllt aus LogistikRunner.STRATEGIES --></ul>
|
||
|
||
<h2>API</h2>
|
||
<pre>const r = await LogistikRunner.runLevel(1, 1, 'noop');
|
||
// { successful, endBalance, completedContracts, lateDeliveries,
|
||
// durationHours, hintUsages, log, strategy, level, seed }
|
||
|
||
const matrix = await LogistikRunner.runMatrix({
|
||
levels: [1,2,3], strategies: ['noop','naive','greedy','optimal'],
|
||
seeds: [1,2,3,4,5], tolerateErrors: true
|
||
});</pre>
|
||
|
||
<script src="engine.js"></script>
|
||
<script src="headless-runner.js"></script>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
|
||
// Strategie-Liste rendern
|
||
const stratList = document.getElementById('strat-list');
|
||
Object.entries(LogistikRunner.STRATEGIES).forEach(([name, def]) => {
|
||
const li = document.createElement('li');
|
||
li.innerHTML = '<strong>' + name + '</strong> — ' + def.description;
|
||
stratList.appendChild(li);
|
||
});
|
||
|
||
function renderTable(rows) {
|
||
let html = '<table><thead><tr>'
|
||
+ '<th>Lvl</th><th>Strategie</th><th>Seed</th>'
|
||
+ '<th>Status</th><th class="num">Balance</th>'
|
||
+ '<th class="num">Aufträge</th><th class="num">Verspätet</th>'
|
||
+ '<th class="num">Dauer (h)</th><th>Notiz</th></tr></thead><tbody>';
|
||
rows.forEach(r => {
|
||
let cls, label, note = '';
|
||
if (r._error) {
|
||
cls = 'err'; label = 'ERROR'; note = r._error;
|
||
} else if (r.successful === null) {
|
||
cls = 'skip'; label = 'SKIP';
|
||
} else if (r.successful) {
|
||
cls = 'ok'; label = 'success';
|
||
} else {
|
||
cls = 'fail'; label = 'fail';
|
||
}
|
||
html += '<tr>'
|
||
+ '<td>' + r.level + '</td>'
|
||
+ '<td><code>' + r.strategy + '</code></td>'
|
||
+ '<td>' + r.seed + '</td>'
|
||
+ '<td class="' + cls + '">' + label + '</td>'
|
||
+ '<td class="num">' + (r.endBalance ?? '—') + '</td>'
|
||
+ '<td class="num">' + (r.completedContracts ?? 0) + '</td>'
|
||
+ '<td class="num">' + (r.lateDeliveries ?? 0) + '</td>'
|
||
+ '<td class="num">' + (r.durationHours ?? 0) + '</td>'
|
||
+ '<td>' + note + '</td>'
|
||
+ '</tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
return html;
|
||
}
|
||
|
||
async function runNoop() {
|
||
const root = document.getElementById('results');
|
||
root.innerHTML = '<p class="legend">…läuft…</p>';
|
||
const rows = await LogistikRunner.runMatrix({
|
||
strategies: ['noop'], tolerateErrors: true,
|
||
});
|
||
root.innerHTML = renderTable(rows)
|
||
+ '<p class="legend">' + rows.length + ' Durchläufe — '
|
||
+ '<code>noop</code> ist die Phase-0-Demo-Strategie. '
|
||
+ 'Alle Level → success=false ist erwartet.</p>';
|
||
}
|
||
|
||
async function runAll() {
|
||
const root = document.getElementById('results');
|
||
root.innerHTML = '<p class="legend">…läuft…</p>';
|
||
const rows = await LogistikRunner.runMatrix({ tolerateErrors: true });
|
||
root.innerHTML = renderTable(rows)
|
||
+ '<p class="legend">' + rows.length + ' Durchläufe — '
|
||
+ '<code>naive/greedy/optimal</code> werfen kontrolliert „Phase 2", '
|
||
+ '<code>noop</code> läuft sauber durch.</p>';
|
||
}
|
||
|
||
document.getElementById('run-noop').addEventListener('click', runNoop);
|
||
document.getElementById('run-all').addEventListener('click', runAll);
|
||
|
||
/* ---- Balance-Korridor-Check (siehe balance-corridor.md) ----
|
||
R-1 (Runway): mathematisch, ohne Run noetig.
|
||
R-2 (Winnable): pro Level mindestens eine Strategie schafft success.
|
||
R-3 (Loseable): noop schlaegt 100% fehl; (per Level) auch naive (L2) und greedy (L3).
|
||
*/
|
||
function r1Check(cfg) {
|
||
const vc = cfg.vehicleCount || 1;
|
||
const rental = cfg.rentalCostPerDay || 0;
|
||
const idle = cfg.idleCostPerHour || 0;
|
||
const daily = rental * vc + idle * vc * 12;
|
||
const needed = 1.5 * daily;
|
||
const have = cfg.startBudget || 0;
|
||
return { pass: have >= needed, have, needed, daily };
|
||
}
|
||
|
||
async function runBalanceCorridor() {
|
||
const root = document.getElementById('results');
|
||
root.innerHTML = '<p class="legend">…laufe Korridor-Check (3 Level × 3 Strategien × 5 Seeds = 45 Durchläufe)…</p>';
|
||
|
||
// R-1: aus level-config (deterministisch, kein Run noetig)
|
||
const levels = (window.LOGISTIK_LEVELS || []).reduce((m, l) => { m[l.level] = l.params; return m; }, {});
|
||
const r1 = {};
|
||
[1,2,3].forEach(lvl => { if (levels[lvl]) r1[lvl] = r1Check(levels[lvl]); });
|
||
|
||
// R-2 / R-3: per runMatrix (mit optimal fuer R-2-Hard-Check)
|
||
const rows = await LogistikRunner.runMatrix({
|
||
levels: [1,2,3], strategies: ['noop','naive','greedy','optimal'], seeds: [1,2,3,4,5], tolerateErrors: true,
|
||
});
|
||
|
||
// Aggregate per (level, strategy): wieviel von 5 Seeds erfolgreich
|
||
const agg = {};
|
||
rows.forEach(r => {
|
||
const key = r.level + ':' + r.strategy;
|
||
if (!agg[key]) agg[key] = { total: 0, success: 0, errors: 0, sumBalance: 0 };
|
||
agg[key].total++;
|
||
if (r._error) agg[key].errors++;
|
||
else if (r.successful) { agg[key].success++; agg[key].sumBalance += r.endBalance || 0; }
|
||
else agg[key].sumBalance += r.endBalance || 0;
|
||
});
|
||
|
||
// Korridor-Verdikte
|
||
const verdicts = [];
|
||
[1,2,3].forEach(lvl => {
|
||
// R-1
|
||
const r1v = r1[lvl];
|
||
verdicts.push({
|
||
rule: 'R-1', level: lvl, pass: r1v && r1v.pass,
|
||
detail: r1v ? ('startBudget=' + r1v.have + '€ vs benoetigt ' + Math.ceil(r1v.needed) + '€ (Tagesfix ' + r1v.daily + '€)') : 'config fehlt',
|
||
});
|
||
// R-2 (Winnable): mind. eine Strategie schafft Median-success.
|
||
const optimal = agg[lvl + ':optimal'] || { success: 0, total: 5 };
|
||
const greedy = agg[lvl + ':greedy' ] || { success: 0, total: 5 };
|
||
const naive = agg[lvl + ':naive' ] || { success: 0, total: 5 };
|
||
const winnable = optimal.success >= 3 || greedy.success >= 3 || (lvl <= 1 && naive.success >= 3);
|
||
verdicts.push({
|
||
rule: 'R-2', level: lvl, pass: winnable,
|
||
detail: 'naive ' + naive.success + '/' + naive.total
|
||
+ ' · greedy ' + greedy.success + '/' + greedy.total
|
||
+ ' · optimal ' + optimal.success + '/' + optimal.total,
|
||
});
|
||
// R-3 (Loseable): noop schlaegt zuverlaessig fehl, zusaetzlich level-spezifisch.
|
||
const noop = agg[lvl + ':noop'] || { success: 0, total: 5 };
|
||
let loseable = noop.success === 0;
|
||
let loseDetail = 'noop ' + noop.success + '/' + noop.total;
|
||
if (lvl >= 2) {
|
||
// L2: naive soll median-fail
|
||
loseable = loseable && naive.success <= 2;
|
||
loseDetail += ' · naive ' + naive.success + '/' + naive.total + ' (darf fast-fail)';
|
||
}
|
||
if (lvl === 3) {
|
||
// L3: greedy soll auch nicht zuverlaessig gewinnen
|
||
loseable = loseable && greedy.success <= 2;
|
||
loseDetail += ' · greedy ' + greedy.success + '/' + greedy.total + ' (darf median-fail)';
|
||
}
|
||
verdicts.push({
|
||
rule: 'R-3', level: lvl, pass: loseable,
|
||
detail: loseDetail,
|
||
});
|
||
});
|
||
|
||
// Render
|
||
let html = '<h3 style="margin:14px 0 6px;">Korridor-Verdikte</h3>'
|
||
+ '<table><thead><tr><th>Regel</th><th>Level</th><th>Status</th><th>Detail</th></tr></thead><tbody>';
|
||
verdicts.forEach(v => {
|
||
const cls = v.pass ? 'ok' : 'fail';
|
||
const lbl = v.pass ? 'PASS' : 'FAIL';
|
||
html += '<tr><td><code>' + v.rule + '</code></td><td>' + v.level + '</td>'
|
||
+ '<td class="' + cls + '">' + lbl + '</td>'
|
||
+ '<td style="font-size:0.85rem;color:#555;">' + v.detail + '</td></tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
|
||
html += '<details style="margin-top:14px;"><summary>Roh-Daten (' + rows.length + ' Runs)</summary>'
|
||
+ renderTable(rows) + '</details>';
|
||
|
||
const allPass = verdicts.every(v => v.pass);
|
||
html += '<p class="legend"><strong>'
|
||
+ (allPass ? '✅ Alle Korridor-Regeln erfüllt.' : '⚠ Mindestens eine Regel verletzt.')
|
||
+ '</strong> R-2 prüft jetzt mit <code>optimal</code> (Best-Net-Heuristik).</p>';
|
||
|
||
root.innerHTML = html;
|
||
}
|
||
document.getElementById('run-corridor').addEventListener('click', runBalanceCorridor);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|