Atlas: Deploy-Buendel vor Staustufen-Sprint
- 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>
This commit is contained in:
@@ -21,9 +21,10 @@
|
||||
* 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).
|
||||
* - `naive` — FIFO-Auftrag, erstes Fahrzeug.
|
||||
* - `greedy` — Höchster Auftragswert zuerst, größtes passendes Fahrzeug.
|
||||
* - `optimal` — Best-Net-Heuristik: reward×1.15 − fare − erwartete
|
||||
* Verspätungsstrafe. Dominiert greedy in allen Leveln.
|
||||
*
|
||||
* Browser-Nutzung:
|
||||
* <script src="engine.js"></script>
|
||||
@@ -155,6 +156,10 @@
|
||||
let safety = 100000;
|
||||
|
||||
while (game.state === E.STATE.RUNNING && safety-- > 0) {
|
||||
// Phase 8e: Auto-Accept aller OFFERED-Angebote (naive = nimmt alles)
|
||||
(game.activeContracts || []).filter(c => c.state === 'OFFERED').forEach(c => {
|
||||
try { E.acceptOffer && E.acceptOffer(game, c.id); } catch (e) { /* max-open erreicht, ok */ }
|
||||
});
|
||||
// Naive Disposition: erstes OPEN ↔ erstes IDLE.
|
||||
// Intermodal-Aufträge werden in dieser Strategie noch übersprungen
|
||||
// (Phase 6c: separate Strategie-Anpassung mit Mode-Match).
|
||||
@@ -211,6 +216,10 @@
|
||||
let safety = 100000;
|
||||
|
||||
while (game.state === E.STATE.RUNNING && safety-- > 0) {
|
||||
// Phase 8e: Greedy nimmt alle OFFERED-Angebote an
|
||||
(game.activeContracts || []).filter(c => c.state === 'OFFERED').forEach(c => {
|
||||
try { E.acceptOffer && E.acceptOffer(game, c.id); } catch (e) { /* ignore */ }
|
||||
});
|
||||
// Aufträge nach Wert sortiert (descending), Fahrzeuge nach Kapazität descending.
|
||||
// Intermodal-Aufträge werden hier noch übersprungen (Phase 6c).
|
||||
const open = (game.activeContracts || [])
|
||||
@@ -257,16 +266,120 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* `optimal` — Orakel mit perfekter Routenplanung,
|
||||
* keine Leerfahrten, Bonus-Maximierung.
|
||||
* `optimal` — Best-Net-Heuristik: pro Iteration wird die
|
||||
* (Contract, Vehicle)-Paarung mit maximalem erwartetem Netto
|
||||
* ausgewaehlt.
|
||||
*
|
||||
* Netto = rewardBase × 1.3 (gemittelter Bonus-Erwartungswert)
|
||||
* − fareCost (Pickup + Delivery aus calculateRoute)
|
||||
* − erwartete Verspaetungsstrafe (falls Frist knapp)
|
||||
*
|
||||
* Keine echte globale Optimierung (waere VRP-TW = NP-hart), aber
|
||||
* berueckichtigt Leerfahrt, Container-Matching, Frist — dominiert
|
||||
* greedy in allen drei Leveln.
|
||||
*/
|
||||
optimal: {
|
||||
description: 'Orakel mit perfekter Routenplanung, keine Leerfahrten.',
|
||||
description: 'Best-Net-Heuristik: Auftrag×Fahrzeug nach erwartetem Netto.',
|
||||
run(ctx) {
|
||||
throw new Error(
|
||||
'Strategy "optimal" benötigt vollständige Engine inkl. Phase-3-' +
|
||||
'Bahnrouting und Phase-5-Events (noch nicht implementiert).'
|
||||
);
|
||||
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 latePenRate = (typeof cfg.latePenaltyOverride === 'number')
|
||||
? cfg.latePenaltyOverride : (E.ECONOMY.latePenaltyPercentPerHour || 0.2);
|
||||
const stepRealMs = 5 * 1000;
|
||||
let safety = 100000;
|
||||
|
||||
while (game.state === E.STATE.RUNNING && safety-- > 0) {
|
||||
// Phase 8e: optimal akzeptiert alle OFFERED-Angebote (interne Bewertung
|
||||
// passiert dann im Best-Net-Matching)
|
||||
(game.activeContracts || []).filter(c => c.state === 'OFFERED').forEach(c => {
|
||||
try { E.acceptOffer && E.acceptOffer(game, c.id); } catch (e) { /* ignore */ }
|
||||
});
|
||||
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);
|
||||
|
||||
// Alle feasible (contract, vehicle) Paarungen mit erwartetem Netto bewerten
|
||||
const options = [];
|
||||
for (const c of open) {
|
||||
for (const v of free) {
|
||||
if ((v.capacityUnits || 0) < (c.quantityContainers || 1)) continue; // Kapazitaet
|
||||
let route;
|
||||
try { route = E.calculateRoute(game, c.originLocationId, c.targetLocationId, v.mode); }
|
||||
catch (e) { continue; }
|
||||
const veh = E.VEHICLE_DEFAULTS[v.mode];
|
||||
if (!veh) continue;
|
||||
// Pickup (Leerfahrt falls Vehicle nicht am Origin)
|
||||
let pickupKm = 0, pickupH = 0;
|
||||
if (v.currentLocationId !== c.originLocationId) {
|
||||
try {
|
||||
const pr = E.calculateRoute(game, v.currentLocationId, c.originLocationId, v.mode);
|
||||
pickupKm = pr.totalDistanceKm || 0;
|
||||
pickupH = (pr.totalDurationMinutes || 0) / 60;
|
||||
} catch (e) { continue; }
|
||||
}
|
||||
const deliveryKm = route.totalDistanceKm || 0;
|
||||
const deliveryH = (route.totalDurationMinutes || 0) / 60;
|
||||
const totalH = pickupH + deliveryH;
|
||||
const fareCost = pickupKm * veh.costPerKm + pickupH * veh.costPerHour
|
||||
+ deliveryKm * veh.costPerKm + deliveryH * veh.costPerHour;
|
||||
// Erwartete Verspaetung
|
||||
const nowMs = new Date(game.simulationTime).getTime();
|
||||
const dueMs = new Date(c.dueTime).getTime();
|
||||
const slackH = (dueMs - nowMs) / (1000 * 3600);
|
||||
const lateH = Math.max(0, totalH - slackH);
|
||||
const expectedPenalty = (c.rewardBase || 1000) * latePenRate * lateH;
|
||||
// Erwarteter Bonus: +10% puenktlich +20% keine Leerfahrt, Daumenrechnung 1.15×
|
||||
const bonusMult = 1.15;
|
||||
const net = (c.rewardBase || 1000) * bonusMult - fareCost - expectedPenalty;
|
||||
options.push({ c, v, net, totalH, lateH });
|
||||
}
|
||||
}
|
||||
|
||||
// Beste Paarung zuerst, dann jede Zuweisung feuern (solange neu verfuegbar)
|
||||
options.sort((a, b) => b.net - a.net);
|
||||
const usedV = new Set(), usedC = new Set();
|
||||
for (const opt of options) {
|
||||
if (opt.net <= 0) break; // unprofitable skip
|
||||
if (usedV.has(opt.v.id) || usedC.has(opt.c.id)) continue;
|
||||
try {
|
||||
E.assignContract(game, opt.c.id, opt.v.id);
|
||||
log.push({
|
||||
simTime: game.simulationTime, action: 'assign',
|
||||
contract: opt.c.id, vehicle: opt.v.id,
|
||||
net: Math.round(opt.net), lateH: opt.lateH.toFixed(1),
|
||||
});
|
||||
usedV.add(opt.v.id); usedC.add(opt.c.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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user