Stand 2026-04-13: PHP/MySQL Infrastruktur, Flussmanagement, Stadt-Prototyp

- PHP/MySQL Backend (XAMPP + Produktionsserver)
- Front-Controller, API-Endpunkte, Session-Management
- Flussmanagement-Simulation (Echtzeit, Punkt-basierter Fluss)
- Stadt & Raumplanung (Prototyp, Top-Down Kachelsystem)
- Klimawaechter 3D: Deiche kleiner, Baeume kippen, Budget angepasst
- persistence.ts: Dualer Speicher (localStorage + Server-API)
- 6 Unit-Test-Dateien fuer bestehende Simulationen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 16:43:42 +02:00
commit f22c5ebbfe
83 changed files with 22709 additions and 0 deletions
+481
View File
@@ -0,0 +1,481 @@
/**
* SIM-08: Energiewende-Planer*in — Spielbare Energiemix-Simulation
*
* Du übernimmst 2025 als Energieplaner*in eine Region mit ca. 200.000
* Einwohnern. Aktuell kommt der Strom größtenteils aus fossilen Quellen.
* Bis 2050 (25 Jahre) musst du den Energiemix umbauen:
*
* - Die Nachfrage steigt (E-Autos, Wärmepumpen, Digitalisierung).
* - CO₂ muss runter — Klimaziel.
* - Blackouts dürfen nicht passieren — Versorgungssicherheit.
* - Du hast ein knappes Budget.
*
* Fachlich korrekt:
* - Kapazitätsfaktoren echt (Wind ~25%, Solar ~12%, Wasser ~45%, Kernkraft ~90%)
* - CO₂-Emissionen in gCO₂/kWh basieren auf IPCC-Median-Werten
* - Erneuerbare brauchen Speicher oder backup-fähige Partner (Gas)
*
* Kernbotschaft: Es gibt keinen Königsweg. Jede Technologie hat Stärken und
* Schwächen. Die Transformation ist ein Balance-Akt zwischen drei Zielen:
* CO₂, Kosten und Versorgungssicherheit.
*/
import { GameEngine, type GameMeta } from '@core/game-engine'
const META: GameMeta = {
id: 'sim-08',
title: 'Energiewende-Planer*in',
description: 'Führe deine Region bis 2050 in eine CO₂-neutrale, sichere Stromversorgung.',
msPerTick: 4000,
tickUnit: 'Jahr',
maxTicks: 25,
tutorialSteps: 4,
}
const START_YEAR = 2025
export interface PlantType {
id: string
name: string
emoji: string
description: string
cost: number // Baukosten in Mio. €
capacityMW: number // Nennleistung
capacityFactor: number // Anteil der Nennleistung im Jahresmittel (0..1)
co2PerKWh: number // gCO₂/kWh (IPCC Median)
upkeep: number // Mio. €/Jahr
renewable: boolean
flexible: boolean // Kann bei Bedarf hochgefahren werden (für Backup)
storage?: number // MW Speicherkapazität (reduziert Blackout-Risiko)
}
export const PLANT_TYPES: PlantType[] = [
{
id: 'coal',
name: 'Kohlekraftwerk',
emoji: '🏭',
description: 'Billig, zuverlässig, aber höchste CO₂-Emission. Gesellschaftlich umstritten.',
cost: 80,
capacityMW: 300,
capacityFactor: 0.75,
co2PerKWh: 820,
upkeep: 8,
renewable: false,
flexible: true,
},
{
id: 'gas',
name: 'Gaskraftwerk',
emoji: '⛽',
description: 'Flexibler Backup — schnell regelbar. Halbe CO₂-Emission wie Kohle.',
cost: 110,
capacityMW: 250,
capacityFactor: 0.55,
co2PerKWh: 490,
upkeep: 9,
renewable: false,
flexible: true,
},
{
id: 'hydro',
name: 'Wasserkraftwerk',
emoji: '💧',
description: 'CO₂-frei und zuverlässig. Standortgebunden, bei Dürre weniger Leistung.',
cost: 280,
capacityMW: 180,
capacityFactor: 0.45,
co2PerKWh: 24,
upkeep: 4,
renewable: true,
flexible: true,
},
{
id: 'wind',
name: 'Windpark',
emoji: '💨',
description: 'Günstig, CO₂-arm. Wetterabhängig: liefert nur bei Wind (~25% der Zeit).',
cost: 120,
capacityMW: 200,
capacityFactor: 0.28,
co2PerKWh: 11,
upkeep: 5,
renewable: true,
flexible: false,
},
{
id: 'solar',
name: 'Solarpark',
emoji: '☀️',
description: 'Sehr günstig, CO₂-arm. Nur tagsüber, bei Bewölkung weniger.',
cost: 70,
capacityMW: 150,
capacityFactor: 0.13,
co2PerKWh: 48,
upkeep: 3,
renewable: true,
flexible: false,
},
{
id: 'biomass',
name: 'Biomasse-Heizkraftwerk',
emoji: '🌿',
description: 'Aus Holz und Agrarresten. Flexibel, nahezu CO₂-neutral.',
cost: 140,
capacityMW: 80,
capacityFactor: 0.65,
co2PerKWh: 230,
upkeep: 7,
renewable: true,
flexible: true,
},
{
id: 'nuclear',
name: 'Kernkraftwerk',
emoji: '☢️',
description: 'CO₂-frei, extrem hohe Grundlast-Leistung. Hohe Baukosten, politisch umstritten.',
cost: 600,
capacityMW: 1000,
capacityFactor: 0.90,
co2PerKWh: 12,
upkeep: 18,
renewable: false,
flexible: false,
},
{
id: 'battery',
name: 'Batteriespeicher',
emoji: '🔋',
description: 'Erzeugt keinen Strom, glättet aber Schwankungen und verhindert Blackouts.',
cost: 150,
capacityMW: 0,
capacityFactor: 0,
co2PerKWh: 0,
upkeep: 4,
renewable: true,
flexible: true,
storage: 100,
},
]
interface OwnedPlant {
typeId: string
count: number
builtTick: number
}
export class EnergiemixGame extends GameEngine {
private plants: OwnedPlant[] = []
private firedEvents = new Set<string>()
// Wetter-Modifikator dieses Jahres (ca. 0.7..1.2)
private weatherWindFactor = 1.0
private weatherSolarFactor = 1.0
private weatherHydroFactor = 1.0
constructor() {
super(META)
this.setupResources()
this.setupGoals()
this.setupTutorial()
this.setupStartingPlants()
this.recalc()
}
private setupResources(): void {
this.addResource({
id: 'budget',
name: 'Budget',
icon: '💰',
initial: 300,
unit: 'Mio €',
format: (v) => `${Math.round(v)} Mio €`,
})
this.addResource({
id: 'demand',
name: 'Strombedarf',
icon: '🔌',
initial: 700,
unit: 'MW',
format: (v) => `${Math.round(v)} MW`,
})
this.addResource({
id: 'supply',
name: 'Erzeugung (Ø)',
icon: '⚡',
initial: 0,
unit: 'MW',
format: (v) => `${Math.round(v)} MW`,
})
this.addResource({
id: 'co2',
name: 'CO₂-Ausstoß',
icon: '🌫',
initial: 0,
unit: 'kt/J',
format: (v) => `${Math.round(v)} kt/J`,
})
this.addResource({
id: 'renewable',
name: 'Erneuerbar',
icon: '🌱',
initial: 0,
unit: '%',
format: (v) => `${Math.round(v)} %`,
})
this.addResource({
id: 'blackouts',
name: 'Blackouts',
icon: '🕯',
initial: 0,
unit: '',
format: (v) => `${Math.round(v)}`,
})
}
private setupGoals(): void {
this.addGoal({
id: 'survive',
title: 'Bis 2050 planen',
description: '25 Jahre Energiewende begleiten.',
check: (g) => (g as EnergiemixGame).tick >= 25,
progress: (g) => Math.min(100, ((g as EnergiemixGame).tick / 25) * 100),
required: true,
})
this.addGoal({
id: 'renewable',
title: '80 % Erneuerbare Energie',
description: 'Mindestens 80 % des Strombedarfs aus erneuerbaren Quellen.',
check: (g) => g.getResource('renewable') >= 80,
progress: (g) => Math.min(100, (g.getResource('renewable') / 80) * 100),
required: true,
})
this.addGoal({
id: 'co2',
title: 'CO₂ unter 400 kt/Jahr',
description: 'Die Emissionen müssen deutlich sinken.',
check: (g) => g.getResource('co2') < 400,
progress: (g) => {
const co2 = g.getResource('co2')
return Math.max(0, Math.min(100, 100 - ((co2 - 400) / 10)))
},
required: true,
})
this.addGoal({
id: 'reliable',
title: 'Maximal 3 Blackouts',
description: 'Die Versorgung muss sicher bleiben.',
check: (g) => g.getResource('blackouts') <= 3,
progress: (g) => Math.max(0, Math.min(100, 100 - g.getResource('blackouts') * 25)),
required: true,
})
this.addGoal({
id: 'budget',
title: 'Nicht pleite gehen',
description: 'Budget muss positiv bleiben.',
check: (g) => g.getResource('budget') > 0,
required: true,
})
}
private setupTutorial(): void {
this.setTutorial([
{
triggerTick: 0,
title: 'Willkommen, Energieplaner*in!',
text: '2025. Du übernimmst die Energieplanung für eine Region mit ca. 200.000 Einwohner*innen.\n\nDein Auftrag: Bis 2050 (in 25 Jahren) muss der Strom CO₂-neutral, bezahlbar und zuverlässig sein.\n\nAktuell dominiert noch fossiler Strom. Der Umbau kostet Geld — aber nichts zu tun kostet das Klima.',
unlocks: ['budget'],
},
{
triggerTick: 0,
title: 'Der Energie-Mix',
text: 'Es gibt keine Lösung, die alles kann:\n\n🏭 Kohle: billig, aber hoher CO₂-Ausstoß\n⛽ Gas: flexibel, mittleres CO₂\n💧 Wasser: sauber, aber abhängig vom Niederschlag\n💨 Wind: günstig, nur bei Wind\n☀️ Solar: sehr günstig, nur bei Sonne\n🌿 Biomasse: klimaneutral, begrenzte Verfügbarkeit\n☢️ Kernkraft: CO₂-frei, teuer, umstritten\n🔋 Speicher: glättet Schwankungen',
unlocks: ['shop'],
},
{
triggerTick: 0,
title: 'Versorgungssicherheit',
text: 'Wind und Sonne liefern nicht immer.\n\nDer sogenannte Kapazitätsfaktor beschreibt, wieviel % der Nennleistung im Jahresmittel wirklich anfällt:\n- Wind ≈ 28 %\n- Solar ≈ 13 %\n- Wasser ≈ 45 %\n- Kernkraft ≈ 90 %\n\nFehlt Strom, drohen Blackouts. Backup-Kraftwerke (Gas, Biomasse) oder Speicher helfen, Lücken zu überbrücken.',
unlocks: ['supply'],
},
{
triggerTick: 0,
title: 'Der Zielkonflikt',
text: 'Du musst drei Ziele unter einen Hut bringen:\n\n🌱 CO₂: auf unter 400 kt/Jahr senken\n⚡ Versorgung: maximal 3 Blackouts bis 2050\n💰 Budget: nicht pleite gehen\n\nPro Jahr bekommst du Einnahmen aus Stromverkauf. Bau klug aus — Kraftwerke brauchen mehrere Jahre, bis sie sich rechnen.\n\nViel Erfolg! ⚡',
unlocks: ['controls'],
},
])
}
private setupStartingPlants(): void {
// Startzustand: fossil dominiert
this.plants = [
{ typeId: 'coal', count: 2, builtTick: -10 },
{ typeId: 'gas', count: 1, builtTick: -5 },
{ typeId: 'hydro', count: 1, builtTick: -20 },
]
}
private recalc(): void {
let totalMW = 0
let renewableMW = 0
let co2 = 0
let upkeep = 0
let storageMW = 0
let flexibleMW = 0
for (const p of this.plants) {
const t = PLANT_TYPES.find(x => x.id === p.typeId)
if (!t) continue
// Wetter-Modifikator anwenden
let cf = t.capacityFactor
if (t.id === 'wind') cf *= this.weatherWindFactor
else if (t.id === 'solar') cf *= this.weatherSolarFactor
else if (t.id === 'hydro') cf *= this.weatherHydroFactor
const avgMW = t.capacityMW * cf * p.count
totalMW += avgMW
if (t.renewable) renewableMW += avgMW
// CO₂: gCO₂/kWh × MW × 8760h/Jahr = gCO₂/J → kt/J
co2 += (avgMW * 8760 * t.co2PerKWh) / 1e9
upkeep += t.upkeep * p.count
if (t.storage) storageMW += t.storage * p.count
if (t.flexible) flexibleMW += t.capacityMW * p.count
}
this.setResource('supply', totalMW)
this.setResource('co2', co2)
const demand = this.getResource('demand')
this.setResource('renewable', demand > 0 ? Math.min(100, (renewableMW / demand) * 100) : 0)
this.setVariable('upkeepTotal', upkeep)
this.setVariable('storageMW', storageMW)
this.setVariable('flexibleMW', flexibleMW)
}
buyPlant(typeId: string): boolean {
const t = PLANT_TYPES.find(x => x.id === typeId)
if (!t) return false
const budget = this.getResource('budget')
if (budget < t.cost) {
this.addEvent('error', `Nicht genug Budget für ${t.name}`, 'warning')
return false
}
this.changeResource('budget', -t.cost)
const existing = this.plants.find(p => p.typeId === typeId)
if (existing) existing.count++
else this.plants.push({ typeId, count: 1, builtTick: this.tick })
this.recalc()
this.addEvent('build', `${t.emoji} ${t.name} gebaut (-${t.cost} Mio €)`, 'success')
this.notify()
return true
}
getOwnedPlants(): OwnedPlant[] { return this.plants }
getPlantCount(id: string): number {
return this.plants.find(p => p.typeId === id)?.count ?? 0
}
getStartYear(): number { return START_YEAR }
getWeatherFactors() {
return {
wind: this.weatherWindFactor,
solar: this.weatherSolarFactor,
hydro: this.weatherHydroFactor,
}
}
protected simulateTick(): void {
// 1. Nachfrage wächst (E-Mobilität, Wärmepumpen, Digitalisierung)
// Ca. 1.8 % pro Jahr
const demand = this.getResource('demand')
this.setResource('demand', demand * 1.018)
// 2. Wetter des Jahres ziehen
this.weatherWindFactor = 0.75 + Math.random() * 0.5 // 0.75..1.25
this.weatherSolarFactor = 0.85 + Math.random() * 0.3 // 0.85..1.15
this.weatherHydroFactor = 0.70 + Math.random() * 0.55 // 0.70..1.25
// 3. Neu berechnen mit aktuellem Wetter
this.recalc()
// 4. Versorgungs-Check
const supply = this.getResource('supply')
const newDemand = this.getResource('demand')
const storageMW = this.getVariable('storageMW')
const flexibleMW = this.getVariable('flexibleMW')
// Spitzenlast ist höher als Durchschnitt (Faktor ~1.35)
const peakDemand = newDemand * 1.35
// Gesicherte Leistung = flexible Kraftwerke + Speicher
const firmCapacity = flexibleMW + storageMW
if (supply < newDemand * 0.9) {
// Unterversorgung im Jahresmittel
this.changeResource('blackouts', 1)
this.addEvent('blackout', `🕯 Blackout! Stromangebot (${Math.round(supply)} MW) deckt die Nachfrage nicht.`, 'danger')
} else if (firmCapacity < peakDemand * 0.7) {
// Nicht genug gesicherte Leistung für Spitzenlast
if (Math.random() < 0.35) {
this.changeResource('blackouts', 1)
this.addEvent('peak-blackout', `⚠️ Spitzenlast-Blackout: zu wenig steuerbare Leistung im Netz.`, 'warning')
}
}
// 5. Einnahmen aus Stromverkauf (nur was wirklich verkauft wird)
const soldMW = Math.min(supply, newDemand)
const revenue = Math.round(soldMW * 0.08) // ~0.08 Mio €/MW/Jahr
this.changeResource('budget', revenue)
// 6. Wartungskosten
const upkeep = this.getVariable('upkeepTotal')
this.changeResource('budget', -upkeep)
// 7. CO₂-Strafe ab 2030 (EU-ETS-Preise steigen)
if (this.tick >= 5) {
const co2 = this.getResource('co2')
const penalty = Math.round(co2 * 0.03 * Math.min(3, (this.tick - 4) / 5))
this.changeResource('budget', -penalty)
if (penalty > 0 && this.tick === 5 && !this.firedEvent('ets-start')) {
this.addEvent('ets', `📜 EU-CO₂-Bepreisung greift: Emissionen kosten jetzt Geld.`, 'warning')
}
}
// 8. Events
if (this.weatherWindFactor < 0.85 && this.weatherSolarFactor < 0.95 && this.tick > 2) {
this.addEvent('dunkelflaute', `🌫 Dunkelflaute: wenig Wind, wenig Sonne. Versorgung knapp.`, 'warning')
}
if (this.weatherHydroFactor < 0.8) {
this.addEvent('drought', `☀️ Trockenes Jahr: Wasserkraft liefert weniger.`, 'info')
}
if (this.tick === 3 && !this.firedEvent('ev-boom')) {
this.addEvent('ev-boom', `🔋 E-Mobilitäts-Boom: Strombedarf steigt schneller als erwartet.`, 'info')
}
if (this.tick === 10 && !this.firedEvent('heat-pumps')) {
this.addEvent('heat-pumps', `🔥 Wärmepumpen-Förderung: Heizen wird elektrisch.`, 'info')
}
if (this.tick === 15 && !this.firedEvent('coal-exit')) {
const coalCount = this.getPlantCount('coal')
if (coalCount > 0) {
this.addEvent('coal-exit', `📢 Kohleausstieg beschlossen — alte Kohlekraftwerke werden unrentabel.`, 'warning')
}
}
if (this.getResource('renewable') >= 50 && !this.firedEvent('milestone-50')) {
this.addEvent('milestone-50', `🌱 Meilenstein: 50 % erneuerbare Energie erreicht!`, 'success')
}
if (this.getResource('renewable') >= 80 && !this.firedEvent('milestone-80')) {
this.addEvent('milestone-80', `🎉 80 % erneuerbar — Klimaziel erreicht!`, 'success')
}
}
private firedEvent(id: string): boolean {
if (this.firedEvents.has(id)) return true
this.firedEvents.add(id)
return false
}
protected checkLossCondition(): boolean {
if (this.getResource('budget') < -200) return true
if (this.getResource('blackouts') > 8) return true
return false
}
}