f22c5ebbfe
- 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>
482 lines
16 KiB
TypeScript
482 lines
16 KiB
TypeScript
/**
|
||
* 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
|
||
}
|
||
}
|