Files
geograsim/App/src/sims/sim-08-energiemix/game.ts
T
Adminator f22c5ebbfe 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>
2026-04-13 16:43:42 +02:00

482 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
}
}