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>
189 lines
6.2 KiB
TypeScript
189 lines
6.2 KiB
TypeScript
/**
|
|
* SIM-09: Energiemix-Simulator — LOGIK
|
|
*
|
|
* Modell:
|
|
* - Schüler*innen stellen einen Energiemix zusammen
|
|
* - Drei Zielgrößen: CO₂-Ausstoß, Kosten, Versorgungssicherheit
|
|
* - Jede Energiequelle hat spezifische Werte für alle drei Dimensionen
|
|
* - Zielkonflikt sichtbar machen: billiger = dreckiger, sauber = teurer/unsicherer
|
|
*
|
|
* Didaktik:
|
|
* - Keine "richtige" Antwort — es geht um das Abwägen
|
|
* - Urteilskompetenz: verschiedene Perspektiven einnehmen
|
|
* - Gamification: Highscore für besten Kompromiss
|
|
*/
|
|
|
|
import { Simulation, SimulationMeta } from '@core/simulation'
|
|
|
|
const META: SimulationMeta = {
|
|
id: 'sim-09',
|
|
name: 'Energiemix-Simulator',
|
|
educationLevels: [5, 6, 7, 8, 9, 10],
|
|
primaryLevel: 6,
|
|
kompetenzbereich: 'Nachhaltiger Umgang mit Energie und Ressourcen',
|
|
lernziele: [
|
|
'Erneuerbare und nicht erneuerbare Energieträger vergleichen',
|
|
'Zielkonflikte zwischen Kosten, Umwelt und Versorgungssicherheit erkennen',
|
|
'Eigene Position zu Energiepolitik bilden und begründen',
|
|
],
|
|
basiskonzepte: ['Leistungserstellung und Nachhaltigkeit', 'Ökonomische Prinzipien und Entscheidungsfindung'],
|
|
dpiMinuten: 25,
|
|
typ: 'sachsimulation',
|
|
tier: 1,
|
|
requiresReading: true,
|
|
}
|
|
|
|
export interface EnergySource {
|
|
id: string
|
|
name: string
|
|
emoji: string
|
|
color: string
|
|
co2PerGWh: number // Tonnen CO₂ pro GWh (Lifecycle)
|
|
costPerMWh: number // EUR pro MWh
|
|
reliability: number // 0-1 (1 = immer verfügbar)
|
|
maxShare: number // maximaler realistischer Anteil (0-1)
|
|
renewable: boolean
|
|
description: string
|
|
}
|
|
|
|
export const ENERGY_SOURCES: EnergySource[] = [
|
|
{
|
|
id: 'coal', name: 'Kohle', emoji: '🪨', color: '#4a4a4a',
|
|
co2PerGWh: 820, costPerMWh: 65, reliability: 0.85, maxShare: 1,
|
|
renewable: false, description: 'Billig aber sehr CO₂-intensiv'
|
|
},
|
|
{
|
|
id: 'gas', name: 'Erdgas', emoji: '🔥', color: '#c4a35a',
|
|
co2PerGWh: 490, costPerMWh: 55, reliability: 0.87, maxShare: 0.8,
|
|
renewable: false, description: 'Hälfte des CO₂ von Kohle, flexibel'
|
|
},
|
|
{
|
|
id: 'nuclear', name: 'Atomkraft', emoji: '⚛️', color: '#8a5caa',
|
|
co2PerGWh: 12, costPerMWh: 90, reliability: 0.92, maxShare: 0.6,
|
|
renewable: false, description: 'Kaum CO₂, aber teuer und Atommüll'
|
|
},
|
|
{
|
|
id: 'wind', name: 'Windkraft', emoji: '🌬️', color: '#5a9aaa',
|
|
co2PerGWh: 11, costPerMWh: 45, reliability: 0.35, maxShare: 0.5,
|
|
renewable: true, description: 'Günstig und sauber, aber wetterabhängig'
|
|
},
|
|
{
|
|
id: 'solar', name: 'Solarenergie', emoji: '☀️', color: '#e8c84a',
|
|
co2PerGWh: 45, costPerMWh: 40, reliability: 0.25, maxShare: 0.4,
|
|
renewable: true, description: 'Billigste Quelle, aber nur tagsüber'
|
|
},
|
|
{
|
|
id: 'hydro', name: 'Wasserkraft', emoji: '💧', color: '#4a7c8a',
|
|
co2PerGWh: 24, costPerMWh: 50, reliability: 0.55, maxShare: 0.3,
|
|
renewable: true, description: 'Zuverlässig, aber Standort-begrenzt'
|
|
},
|
|
]
|
|
|
|
export interface MixResult {
|
|
totalCO2: number // Tonnen CO₂ pro GWh (gewichteter Durchschnitt)
|
|
totalCost: number // EUR pro MWh
|
|
totalReliability: number // 0-1
|
|
renewableShare: number // 0-100%
|
|
score: number // Gesamtbewertung 0-100
|
|
rating: string // z.B. "Gut balanciert"
|
|
}
|
|
|
|
/**
|
|
* Berechnet die Ergebnisse eines Energiemixes
|
|
* @param mix Map von SourceID → Anteil (0-100, Summe sollte 100 sein)
|
|
*/
|
|
export function computeMixResult(mix: Record<string, number>): MixResult {
|
|
let totalCO2 = 0
|
|
let totalCost = 0
|
|
let totalReliability = 0
|
|
let renewableShare = 0
|
|
let totalShare = 0
|
|
|
|
for (const source of ENERGY_SOURCES) {
|
|
const share = (mix[source.id] || 0) / 100
|
|
totalShare += share
|
|
totalCO2 += source.co2PerGWh * share
|
|
totalCost += source.costPerMWh * share
|
|
totalReliability += source.reliability * share
|
|
if (source.renewable) renewableShare += share * 100
|
|
}
|
|
|
|
// Normalisieren falls Summe ≠ 100
|
|
if (totalShare > 0 && Math.abs(totalShare - 1) > 0.01) {
|
|
totalCO2 /= totalShare
|
|
totalCost /= totalShare
|
|
totalReliability /= totalShare
|
|
renewableShare /= totalShare
|
|
}
|
|
|
|
// Score: Multi-Kriterien-Bewertung
|
|
const co2Score = Math.max(0, 100 - totalCO2 / 8) // 0 CO₂ = 100, 800 = 0
|
|
const costScore = Math.max(0, 100 - (totalCost - 30) / 0.7) // 30€ = 100, 100€ = 0
|
|
const reliScore = totalReliability * 100
|
|
const score = Math.round((co2Score * 0.4 + costScore * 0.3 + reliScore * 0.3))
|
|
|
|
let rating = 'Experimentell'
|
|
if (score >= 80) rating = 'Exzellent! 🌟'
|
|
else if (score >= 65) rating = 'Gut balanciert 👍'
|
|
else if (score >= 50) rating = 'Solide Basis'
|
|
else if (score >= 35) rating = 'Verbesserungswürdig'
|
|
|
|
return {
|
|
totalCO2: Math.round(totalCO2),
|
|
totalCost: Math.round(totalCost),
|
|
totalReliability: Math.round(totalReliability * 100) / 100,
|
|
renewableShare: Math.round(renewableShare),
|
|
score,
|
|
rating,
|
|
}
|
|
}
|
|
|
|
export class EnergiemixSimulation extends Simulation {
|
|
constructor() {
|
|
super(META)
|
|
// Startwerte: aktueller europäischer Mix (circa)
|
|
this.state.variables = {
|
|
coal: 15, gas: 20, nuclear: 20,
|
|
wind: 18, solar: 12, hydro: 15,
|
|
}
|
|
}
|
|
|
|
getVariableRanges() {
|
|
const ranges: Record<string, { min: number; max: number; default: number; unit: string; label: string }> = {}
|
|
for (const source of ENERGY_SOURCES) {
|
|
ranges[source.id] = {
|
|
min: 0,
|
|
max: Math.round(source.maxShare * 100),
|
|
default: this.state.variables[source.id] || 0,
|
|
unit: '%',
|
|
label: `${source.emoji} ${source.name}`,
|
|
}
|
|
}
|
|
return ranges
|
|
}
|
|
|
|
/** Normalisiert den Mix auf 100% */
|
|
normalizeMix(): void {
|
|
const total = ENERGY_SOURCES.reduce((s, src) => s + this.getVariable(src.id), 0)
|
|
if (total > 0) {
|
|
for (const src of ENERGY_SOURCES) {
|
|
this.state.variables[src.id] = Math.round(this.getVariable(src.id) / total * 100)
|
|
}
|
|
}
|
|
}
|
|
|
|
compute() {
|
|
const mix: Record<string, number> = {}
|
|
for (const src of ENERGY_SOURCES) {
|
|
mix[src.id] = this.getVariable(src.id)
|
|
}
|
|
const result = computeMixResult(mix)
|
|
this.state.results = result
|
|
return result as unknown as Record<string, number>
|
|
}
|
|
|
|
protected onVariableChange(): void {
|
|
this.compute()
|
|
}
|
|
}
|