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:
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
AUSTRIA, GERMANY_BAYERN, SWITZERLAND,
|
||||
getLocalName, getPrimaryStages, requiresReading,
|
||||
getReadingLevel, getSubjectName
|
||||
} from '../../src/core/education-levels'
|
||||
|
||||
describe('Education Levels — Österreich', () => {
|
||||
|
||||
it('AT: 1. Klasse MS = Schulstufe 5', () => {
|
||||
expect(getLocalName(AUSTRIA, 5)).toBe('1. Klasse')
|
||||
})
|
||||
|
||||
it('AT: 4. Klasse MS = Schulstufe 8', () => {
|
||||
expect(getLocalName(AUSTRIA, 8)).toBe('4. Klasse')
|
||||
})
|
||||
|
||||
it('AT: Volksschule = Schulstufe 1–4', () => {
|
||||
expect(getLocalName(AUSTRIA, 1)).toBe('1. Klasse Volksschule')
|
||||
expect(getLocalName(AUSTRIA, 4)).toBe('4. Klasse Volksschule')
|
||||
})
|
||||
|
||||
it('AT: Hauptfokus = Stufe 5–8 (4 Stufen)', () => {
|
||||
const primary = getPrimaryStages(AUSTRIA)
|
||||
expect(primary).toHaveLength(4)
|
||||
expect(primary[0].level).toBe(5)
|
||||
expect(primary[3].level).toBe(8)
|
||||
})
|
||||
|
||||
it('AT: Fach heißt "Geografie und wirtschaftliche Bildung" ab Stufe 5', () => {
|
||||
expect(getSubjectName(AUSTRIA, 5)).toBe('Geografie und wirtschaftliche Bildung')
|
||||
})
|
||||
|
||||
it('AT: Fach heißt "Sachunterricht" in der Volksschule', () => {
|
||||
expect(getSubjectName(AUSTRIA, 3)).toBe('Sachunterricht')
|
||||
})
|
||||
|
||||
it('AT: Stundentafel korrekt (2-1-2-2)', () => {
|
||||
const primary = getPrimaryStages(AUSTRIA)
|
||||
expect(primary.map(s => s.hoursPerWeek)).toEqual([2, 1, 2, 2])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Education Levels — Deutschland Bayern', () => {
|
||||
|
||||
it('DE-BY: Stufe 5 = "5. Jahrgangsstufe"', () => {
|
||||
expect(getLocalName(GERMANY_BAYERN, 5)).toBe('5. Jahrgangsstufe')
|
||||
})
|
||||
|
||||
it('DE-BY: Kein Geo in Stufe 6 und 9 (0 Stunden)', () => {
|
||||
const stages = getPrimaryStages(GERMANY_BAYERN)
|
||||
const stage6 = stages.find(s => s.level === 6)
|
||||
const stage9 = stages.find(s => s.level === 9)
|
||||
expect(stage6?.hoursPerWeek).toBe(0)
|
||||
expect(stage9?.hoursPerWeek).toBe(0)
|
||||
})
|
||||
|
||||
it('DE-BY: Fach heißt "Geographie"', () => {
|
||||
expect(getSubjectName(GERMANY_BAYERN, 7)).toBe('Geographie')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Education Levels — Schweiz', () => {
|
||||
|
||||
it('CH: Stufe 7 = "1. Oberstufe"', () => {
|
||||
expect(getLocalName(SWITZERLAND, 7)).toBe('1. Oberstufe')
|
||||
})
|
||||
|
||||
it('CH: Hauptfokus = Stufe 7–9 (Zyklus 3)', () => {
|
||||
const primary = getPrimaryStages(SWITZERLAND)
|
||||
expect(primary).toHaveLength(3)
|
||||
expect(primary[0].level).toBe(7)
|
||||
})
|
||||
|
||||
it('CH: Fach heißt "Räume, Zeiten, Gesellschaften" in Zyklus 3', () => {
|
||||
expect(getSubjectName(SWITZERLAND, 8)).toBe('Räume, Zeiten, Gesellschaften')
|
||||
})
|
||||
|
||||
it('CH: Fach heißt "Natur, Mensch, Gesellschaft" in Zyklus 2', () => {
|
||||
expect(getSubjectName(SWITZERLAND, 5)).toBe('Natur, Mensch, Gesellschaft')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Education Levels — Lesekompetenz', () => {
|
||||
|
||||
it('Stufe 1 (6-jährige): keine Lesekompetenz', () => {
|
||||
expect(getReadingLevel(1)).toBe('none')
|
||||
expect(requiresReading(1)).toBe(false)
|
||||
})
|
||||
|
||||
it('Stufe 2–3 (7-9 Jahre): basale Lesekompetenz', () => {
|
||||
expect(getReadingLevel(2)).toBe('basic')
|
||||
expect(getReadingLevel(3)).toBe('basic')
|
||||
})
|
||||
|
||||
it('Stufe 4+ (ab 9 Jahre): fließende Lesekompetenz', () => {
|
||||
expect(getReadingLevel(4)).toBe('fluent')
|
||||
expect(getReadingLevel(7)).toBe('fluent')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Klimawächter — Mechanik-Trace
|
||||
*
|
||||
* Spielt das Klimawächter-Spiel rein rechnerisch über alle 75 Jahre für
|
||||
* verschiedene Strategien durch und gibt die Schlüssel-Variablen als
|
||||
* ASCII-Tabellen aus. Dient zur Analyse der Spielmechanik:
|
||||
* - Steigt CO₂ ohne Maßnahmen?
|
||||
* - Wie reagiert die Temperatur auf verschiedene Strategien?
|
||||
* - Wann wird die Stadt überflutet?
|
||||
* - Sind die Maßnahmen sinnvoll dimensioniert?
|
||||
*
|
||||
* Wird mit `npx vitest run sim-05-trace` aufgerufen.
|
||||
* Test schlägt nie fehl — er druckt nur.
|
||||
*/
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
import { KlimawaechterGame, MEASURES } from '../../src/sims/sim-05-treibhaus/game'
|
||||
|
||||
interface TraceRow {
|
||||
tick: number
|
||||
co2: number
|
||||
temp: number
|
||||
sea: number
|
||||
flooded: number
|
||||
budget: number
|
||||
pop: number
|
||||
measures: Record<string, number>
|
||||
}
|
||||
|
||||
interface Strategy {
|
||||
name: string
|
||||
buy: (g: KlimawaechterGame, tick: number) => void
|
||||
}
|
||||
|
||||
function runStrategy(strat: Strategy): TraceRow[] {
|
||||
const game = new KlimawaechterGame()
|
||||
// Tutorial überspringen, in 'playing' wechseln
|
||||
;(game as any).state = 'playing'
|
||||
;(game as any).tutorialStep = (game as any).tutorialSteps.length
|
||||
const rows: TraceRow[] = []
|
||||
|
||||
// Startzustand auch erfassen
|
||||
const snapshot0 = (): TraceRow => ({
|
||||
tick: 0,
|
||||
co2: game.getResource('co2'),
|
||||
temp: game.getResource('temperature'),
|
||||
sea: game.getResource('sealevel'),
|
||||
flooded: game.getResource('flooded'),
|
||||
budget: game.getResource('budget'),
|
||||
pop: game.getResource('population'),
|
||||
measures: countMeasures(game),
|
||||
})
|
||||
rows.push(snapshot0())
|
||||
|
||||
// 75 Ticks (= 75 Jahre)
|
||||
for (let i = 1; i <= 75; i++) {
|
||||
strat.buy(game, i)
|
||||
// simulateTick ist protected → cast
|
||||
;(game as any).simulateTick()
|
||||
;(game as any).tick = i
|
||||
rows.push({
|
||||
tick: i,
|
||||
co2: game.getResource('co2'),
|
||||
temp: game.getResource('temperature'),
|
||||
sea: game.getResource('sealevel'),
|
||||
flooded: game.getResource('flooded'),
|
||||
budget: game.getResource('budget'),
|
||||
pop: game.getResource('population'),
|
||||
measures: countMeasures(game),
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
function countMeasures(game: KlimawaechterGame): Record<string, number> {
|
||||
const result: Record<string, number> = {}
|
||||
for (const m of MEASURES) {
|
||||
result[m.id] = game.getMeasureCount(m.id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function fmtRow(r: TraceRow): string {
|
||||
const m = Object.entries(r.measures).filter(([_, c]) => c > 0).map(([k, c]) => `${k}:${c}`).join(',') || '-'
|
||||
return [
|
||||
String(2025 + r.tick).padStart(4),
|
||||
r.co2.toFixed(0).padStart(5),
|
||||
r.temp.toFixed(2).padStart(6),
|
||||
r.sea.toFixed(1).padStart(6),
|
||||
r.flooded.toFixed(0).padStart(4),
|
||||
r.budget.toFixed(0).padStart(6),
|
||||
String(r.pop).padStart(6),
|
||||
m.padStart(20),
|
||||
].join(' │ ')
|
||||
}
|
||||
|
||||
function printTrace(name: string, rows: TraceRow[]) {
|
||||
console.log('\n═══════════════════════════════════════════════════════════════════════════════════')
|
||||
console.log(`STRATEGIE: ${name}`)
|
||||
console.log('═══════════════════════════════════════════════════════════════════════════════════')
|
||||
console.log(' Jahr │ CO₂ │ Temp │ Meer │ Flut │ Budget │ Bev │ Maßnahmen')
|
||||
console.log('──────┼───────┼────────┼───────┼──────┼────────┼────────┼──────────────────────')
|
||||
// Alle 5 Jahre + erstes + letztes
|
||||
for (const r of rows) {
|
||||
if (r.tick === 0 || r.tick === 75 || r.tick % 5 === 0) {
|
||||
console.log(fmtRow(r))
|
||||
}
|
||||
}
|
||||
// Endbewertung
|
||||
const last = rows[rows.length - 1]
|
||||
const won =
|
||||
last.tick >= 75 &&
|
||||
last.temp < 17 &&
|
||||
last.budget > 0 &&
|
||||
last.flooded < 30
|
||||
console.log('──────┴───────┴────────┴───────┴──────┴────────┴────────┴──────────────────────')
|
||||
console.log(` Status: ${won ? '✅ GEWONNEN' : '❌ VERLOREN'}`)
|
||||
}
|
||||
|
||||
// === Strategien ===
|
||||
const strategies: Strategy[] = [
|
||||
{
|
||||
name: 'Nichts tun (Baseline)',
|
||||
buy: () => {},
|
||||
},
|
||||
{
|
||||
name: 'Nur Wälder (alle 3 Jahre einer)',
|
||||
buy: (g, t) => { if (t % 3 === 0) g.buyMeasure('forest') },
|
||||
},
|
||||
{
|
||||
name: 'Nur Solar (sobald leistbar)',
|
||||
buy: (g) => {
|
||||
while (g.getResource('budget') >= 200) {
|
||||
if (!g.buyMeasure('solar')) break
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Nur Wind (sobald leistbar)',
|
||||
buy: (g) => {
|
||||
while (g.getResource('budget') >= 400) {
|
||||
if (!g.buyMeasure('wind')) break
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Nur Deiche (gegen Überflutung)',
|
||||
buy: (g) => {
|
||||
while (g.getResource('budget') >= 300) {
|
||||
if (!g.buyMeasure('dike')) break
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Wälder + Solar gemischt',
|
||||
buy: (g, t) => {
|
||||
if (t % 4 === 0 && g.getResource('budget') >= 200) g.buyMeasure('solar')
|
||||
else if (g.getResource('budget') >= 50) g.buyMeasure('forest')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Optimal: Solar + Wind + 1 Deich',
|
||||
buy: (g, t) => {
|
||||
if (t === 5 && g.getResource('budget') >= 300) g.buyMeasure('dike')
|
||||
if (g.getResource('budget') >= 400) g.buyMeasure('wind')
|
||||
else if (g.getResource('budget') >= 200) g.buyMeasure('solar')
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe('Klimawächter — Mechanik-Trace über 75 Jahre', () => {
|
||||
for (const strat of strategies) {
|
||||
it(`Trace: ${strat.name}`, () => {
|
||||
const rows = runStrategy(strat)
|
||||
printTrace(strat.name, rows)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { computeTemperature, computeEffects, TreibhausSimulation } from '../../src/sims/sim-05-treibhaus/logic'
|
||||
|
||||
describe('Treibhauseffekt — Physik-Modell', () => {
|
||||
|
||||
it('berechnet korrekte Temperatur ohne Treibhauseffekt (Albedo 0.3)', () => {
|
||||
// Ohne CO₂ (theoretisch 0 ppm → ln(0) ist undefiniert, daher sehr niedrig)
|
||||
// Bei 1 ppm sollte Temp nahe -18°C + 33°C - großer negativer Wert sein
|
||||
// Besser: Prüfen, dass 280 ppm → ~15°C ergibt
|
||||
const temp = computeTemperature(280, 0.3)
|
||||
expect(temp).toBeGreaterThan(13.5)
|
||||
expect(temp).toBeLessThan(16) // ~14-15°C, vereinfachtes Modell
|
||||
})
|
||||
|
||||
it('ergibt ~16°C bei aktuellem CO₂-Niveau (425 ppm)', () => {
|
||||
const temp = computeTemperature(425, 0.3)
|
||||
expect(temp).toBeGreaterThan(15.5)
|
||||
expect(temp).toBeLessThan(17)
|
||||
})
|
||||
|
||||
it('Verdoppelung von CO₂ erhöht Temperatur um ~3°C', () => {
|
||||
const temp280 = computeTemperature(280, 0.3)
|
||||
const temp560 = computeTemperature(560, 0.3)
|
||||
const delta = temp560 - temp280
|
||||
expect(delta).toBeCloseTo(3.0, 0.5)
|
||||
})
|
||||
|
||||
it('höhere Albedo → kältere Temperatur', () => {
|
||||
const tempLow = computeTemperature(400, 0.2)
|
||||
const tempHigh = computeTemperature(400, 0.5)
|
||||
expect(tempLow).toBeGreaterThan(tempHigh)
|
||||
})
|
||||
|
||||
it('steigende CO₂ → steigende Temperatur (monoton)', () => {
|
||||
const temps = [200, 300, 400, 600, 800, 1000].map(co2 => computeTemperature(co2, 0.3))
|
||||
for (let i = 1; i < temps.length; i++) {
|
||||
expect(temps[i]).toBeGreaterThan(temps[i - 1])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Treibhauseffekt — Folgen-Berechnung', () => {
|
||||
|
||||
it('vorindustrielle Temperatur → kein Meeresspiegelanstieg', () => {
|
||||
const effects = computeEffects(15)
|
||||
expect(effects.seaLevelRise).toBe(0)
|
||||
})
|
||||
|
||||
it('+2°C → messbarer Meeresspiegelanstieg', () => {
|
||||
const effects = computeEffects(17)
|
||||
expect(effects.seaLevelRise).toBeGreaterThan(20)
|
||||
})
|
||||
|
||||
it('arktisches Eis sinkt mit steigender Temperatur', () => {
|
||||
const ice15 = computeEffects(15).arcticIce
|
||||
const ice18 = computeEffects(18).arcticIce
|
||||
expect(ice15).toBeGreaterThan(ice18)
|
||||
})
|
||||
|
||||
it('Extremereignisse steigen mit Temperatur', () => {
|
||||
const events15 = computeEffects(15).extremeEvents
|
||||
const events20 = computeEffects(20).extremeEvents
|
||||
expect(events20).toBeGreaterThan(events15)
|
||||
})
|
||||
})
|
||||
|
||||
describe('TreibhausSimulation — Klasse', () => {
|
||||
|
||||
it('hat korrekte Metadaten', () => {
|
||||
const sim = new TreibhausSimulation()
|
||||
expect(sim.meta.id).toBe('sim-05')
|
||||
expect(sim.meta.primaryLevel).toBe(5)
|
||||
expect(sim.meta.educationLevels).toContain(5)
|
||||
expect(sim.meta.tier).toBe(1)
|
||||
expect(sim.meta.dpiMinuten).toBe(20)
|
||||
})
|
||||
|
||||
it('startet mit Standardwerten', () => {
|
||||
const sim = new TreibhausSimulation()
|
||||
expect(sim.getVariable('co2')).toBe(425)
|
||||
expect(sim.getVariable('albedo')).toBe(0.3)
|
||||
})
|
||||
|
||||
it('berechnet Ergebnisse nach Variable-Änderung', () => {
|
||||
const sim = new TreibhausSimulation()
|
||||
sim.setVariable('co2', 560)
|
||||
const results = sim.compute()
|
||||
expect(results.temperature).toBeGreaterThan(17)
|
||||
})
|
||||
|
||||
it('loggt Variablenänderungen im Assessment', () => {
|
||||
const sim = new TreibhausSimulation()
|
||||
sim.setVariable('co2', 300)
|
||||
sim.setVariable('co2', 600)
|
||||
const assessment = sim.getAssessmentData()
|
||||
expect(assessment.processLog.length).toBeGreaterThanOrEqual(2)
|
||||
expect(assessment.processLog.some(l => l.action === 'set-variable')).toBe(true)
|
||||
})
|
||||
|
||||
it('Predict-Observe-Explain Workflow funktioniert', () => {
|
||||
const sim = new TreibhausSimulation()
|
||||
|
||||
// Predict
|
||||
sim.nextPhase() // intro → predict
|
||||
sim.setPrediction('temp_at_800ppm', 'Ich glaube über 20°C')
|
||||
|
||||
// Simulate
|
||||
sim.nextPhase() // predict → simulate
|
||||
sim.setVariable('co2', 800)
|
||||
|
||||
// Observe
|
||||
sim.nextPhase() // simulate → observe
|
||||
const results = sim.compute()
|
||||
expect(results.temperature).toBeDefined()
|
||||
|
||||
// Reflect
|
||||
sim.nextPhase() // observe → reflect
|
||||
sim.addReflection('Die Temperatur ist höher als ich dachte')
|
||||
|
||||
const assessment = sim.getAssessmentData()
|
||||
expect(assessment.predictions['temp_at_800ppm']).toBeDefined()
|
||||
expect(assessment.reflections.length).toBe(1)
|
||||
expect(assessment.completedPhases).toContain('reflect')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { computeMagnitude, computeCityImpact, ErdbebenSimulation } from '../../src/sims/sim-07-erdbeben/logic'
|
||||
|
||||
describe('Erdbeben — Magnitude-Berechnung', () => {
|
||||
|
||||
it('geringe Spannung → niedrige Magnitude', () => {
|
||||
const mag = computeMagnitude(1, 0.5)
|
||||
expect(mag).toBeLessThan(4)
|
||||
})
|
||||
|
||||
it('hohe Spannung → hohe Magnitude', () => {
|
||||
const mag = computeMagnitude(5000, 1.5)
|
||||
expect(mag).toBeGreaterThan(5)
|
||||
})
|
||||
|
||||
it('Magnitude nie über 9.5', () => {
|
||||
const mag = computeMagnitude(10000, 10)
|
||||
expect(mag).toBeLessThanOrEqual(9.5)
|
||||
})
|
||||
|
||||
it('härteres Gestein → stärkeres Beben bei gleicher Spannung', () => {
|
||||
const soft = computeMagnitude(10, 0.5)
|
||||
const hard = computeMagnitude(10, 1.5)
|
||||
expect(hard).toBeGreaterThan(soft)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Erdbeben — Stadtauswirkungen', () => {
|
||||
|
||||
it('hohe Bauqualität → weniger Schaden', () => {
|
||||
const rich = computeCityImpact(7, 30, 0.9)
|
||||
const poor = computeCityImpact(7, 30, 0.1)
|
||||
expect(rich.damage).toBeLessThan(poor.damage)
|
||||
})
|
||||
|
||||
it('gleiche Magnitude, gleiche Distanz, verschiedene Bauqualität → verschiedene Opferzahlen', () => {
|
||||
const rich = computeCityImpact(7, 30, 0.9)
|
||||
const poor = computeCityImpact(7, 30, 0.1)
|
||||
expect(poor.casualties).toBeGreaterThan(rich.casualties)
|
||||
})
|
||||
|
||||
it('größere Entfernung → weniger Schaden', () => {
|
||||
const near = computeCityImpact(7, 10, 0.5)
|
||||
const far = computeCityImpact(7, 300, 0.5)
|
||||
expect(far.damage).toBeLessThan(near.damage)
|
||||
})
|
||||
|
||||
it('schwaches Beben → wenig Schaden auch bei schlechter Bauqualität', () => {
|
||||
const impact = computeCityImpact(3, 30, 0.1)
|
||||
expect(impact.damage).toBeLessThan(20)
|
||||
})
|
||||
|
||||
it('starkes Beben + schlechte Bauqualität → hoher Gebäudekollaps', () => {
|
||||
const impact = computeCityImpact(8, 20, 0.1)
|
||||
expect(impact.buildingCollapse).toBeGreaterThan(30)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ErdbebenSimulation — Klasse', () => {
|
||||
|
||||
it('hat korrekte Metadaten', () => {
|
||||
const sim = new ErdbebenSimulation()
|
||||
expect(sim.meta.id).toBe('sim-07')
|
||||
expect(sim.meta.primaryLevel).toBe(5)
|
||||
expect(sim.meta.tier).toBe(1)
|
||||
})
|
||||
|
||||
it('Spannung baut sich über Ticks auf', () => {
|
||||
const sim = new ErdbebenSimulation()
|
||||
sim.tick()
|
||||
sim.tick()
|
||||
sim.tick()
|
||||
expect(sim.getStress()).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('irgendwann kommt ein Erdbeben', () => {
|
||||
const sim = new ErdbebenSimulation()
|
||||
sim.setVariable('plateSpeed', 15)
|
||||
let quake = null
|
||||
for (let i = 0; i < 100 && !quake; i++) {
|
||||
quake = sim.tick()
|
||||
}
|
||||
expect(quake).not.toBeNull()
|
||||
expect(quake!.magnitude).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('Vergleich der Stadtauswirkungen funktioniert', () => {
|
||||
const sim = new ErdbebenSimulation()
|
||||
const { cityA, cityB } = sim.compareImpact(7)
|
||||
expect(cityA.type).toBe('rich')
|
||||
expect(cityB.type).toBe('poor')
|
||||
expect(cityB.damage).toBeGreaterThan(cityA.damage)
|
||||
})
|
||||
|
||||
it('schnellere Platten → häufigere Beben', () => {
|
||||
const slow = new ErdbebenSimulation()
|
||||
slow.setVariable('plateSpeed', 2)
|
||||
const fast = new ErdbebenSimulation()
|
||||
fast.setVariable('plateSpeed', 15)
|
||||
|
||||
let slowQuakes = 0, fastQuakes = 0
|
||||
for (let i = 0; i < 200; i++) {
|
||||
if (slow.tick()) slowQuakes++
|
||||
if (fast.tick()) fastQuakes++
|
||||
}
|
||||
expect(fastQuakes).toBeGreaterThan(slowQuakes)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { computeMixResult, ENERGY_SOURCES, EnergiemixSimulation } from '../../src/sims/sim-09-energiemix/logic'
|
||||
|
||||
describe('Energiemix — Mix-Berechnung', () => {
|
||||
|
||||
it('100% Kohle → hoher CO₂-Ausstoß', () => {
|
||||
const result = computeMixResult({ coal: 100 })
|
||||
expect(result.totalCO2).toBeGreaterThan(700)
|
||||
expect(result.renewableShare).toBe(0)
|
||||
})
|
||||
|
||||
it('100% Wind → niedriger CO₂, niedrige Zuverlässigkeit', () => {
|
||||
const result = computeMixResult({ wind: 100 })
|
||||
expect(result.totalCO2).toBeLessThan(20)
|
||||
expect(result.totalReliability).toBeLessThan(0.5)
|
||||
})
|
||||
|
||||
it('100% Solar → billigste Option', () => {
|
||||
const solarCost = computeMixResult({ solar: 100 }).totalCost
|
||||
const coalCost = computeMixResult({ coal: 100 }).totalCost
|
||||
expect(solarCost).toBeLessThan(coalCost)
|
||||
})
|
||||
|
||||
it('gemischter Mix → Werte dazwischen', () => {
|
||||
const result = computeMixResult({ coal: 30, wind: 30, solar: 20, hydro: 20 })
|
||||
expect(result.totalCO2).toBeGreaterThan(50)
|
||||
expect(result.totalCO2).toBeLessThan(400)
|
||||
expect(result.renewableShare).toBe(70)
|
||||
})
|
||||
|
||||
it('Erneuerbare-Anteil wird korrekt berechnet', () => {
|
||||
const result = computeMixResult({ wind: 50, solar: 50 })
|
||||
expect(result.renewableShare).toBe(100)
|
||||
})
|
||||
|
||||
it('Score bevorzugt saubere + zuverlässige + günstige Mixes', () => {
|
||||
const dirty = computeMixResult({ coal: 100 })
|
||||
const balanced = computeMixResult({ nuclear: 30, wind: 30, hydro: 20, solar: 20 })
|
||||
expect(balanced.score).toBeGreaterThan(dirty.score)
|
||||
})
|
||||
|
||||
it('leerer Mix gibt Nullwerte', () => {
|
||||
const result = computeMixResult({})
|
||||
expect(result.totalCO2).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Energiemix — Quellenddaten', () => {
|
||||
|
||||
it('hat 6 Energiequellen', () => {
|
||||
expect(ENERGY_SOURCES).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('Wind und Solar sind erneuerbar', () => {
|
||||
const wind = ENERGY_SOURCES.find(s => s.id === 'wind')!
|
||||
const solar = ENERGY_SOURCES.find(s => s.id === 'solar')!
|
||||
expect(wind.renewable).toBe(true)
|
||||
expect(solar.renewable).toBe(true)
|
||||
})
|
||||
|
||||
it('Kohle hat den höchsten CO₂-Wert', () => {
|
||||
const coal = ENERGY_SOURCES.find(s => s.id === 'coal')!
|
||||
const maxCO2 = Math.max(...ENERGY_SOURCES.map(s => s.co2PerGWh))
|
||||
expect(coal.co2PerGWh).toBe(maxCO2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('EnergiemixSimulation — Klasse', () => {
|
||||
|
||||
it('hat korrekte Metadaten', () => {
|
||||
const sim = new EnergiemixSimulation()
|
||||
expect(sim.meta.id).toBe('sim-09')
|
||||
expect(sim.meta.primaryLevel).toBe(6)
|
||||
})
|
||||
|
||||
it('Startwerte summieren sich auf ~100%', () => {
|
||||
const sim = new EnergiemixSimulation()
|
||||
const total = ENERGY_SOURCES.reduce((s, src) => s + sim.getVariable(src.id), 0)
|
||||
expect(total).toBe(100)
|
||||
})
|
||||
|
||||
it('compute() gibt sinnvolle Werte', () => {
|
||||
const sim = new EnergiemixSimulation()
|
||||
const result = sim.compute()
|
||||
expect(result.totalCO2).toBeGreaterThan(0)
|
||||
expect(result.totalCost).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('Variablenänderung loggt im Assessment', () => {
|
||||
const sim = new EnergiemixSimulation()
|
||||
sim.setVariable('coal', 0)
|
||||
sim.setVariable('solar', 50)
|
||||
const assessment = sim.getAssessmentData()
|
||||
expect(assessment.processLog.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Tests fuer SIM-12: Flussmanagement
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
simulateRound, computeScore, computeCost, computeTotalCost,
|
||||
checkWinLose, checkFinalWin,
|
||||
LEVELS, CONTROL_META, STATE_META,
|
||||
type Controls, type State, type Conditions,
|
||||
} from '../../src/sims/sim-12-fluss/logic'
|
||||
import { FlussGame } from '../../src/sims/sim-12-fluss/game'
|
||||
|
||||
// Hilfsfunktion: Default-Zustand
|
||||
function defaultState(): State {
|
||||
return {
|
||||
floodLocal: 50, floodDownstream: 40, erosion: 30,
|
||||
soilFertility: 60, biodiversity: 60, groundwater: 55,
|
||||
usableLand: 50, economy: 50,
|
||||
}
|
||||
}
|
||||
|
||||
function emptyControls(): Controls {
|
||||
return {
|
||||
straightening: 0, levees: 0, dredging: 0,
|
||||
floodplainRelease: 0, renaturation: 0, irrigation: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function defaultConditions(): Conditions {
|
||||
return { rainfall: 60, extremeWeather: 30, slope: 40, populationPressure: 50, budget: 150 }
|
||||
}
|
||||
|
||||
describe('Fluss-Simulation Logik', () => {
|
||||
|
||||
describe('simulateRound', () => {
|
||||
it('gibt einen neuen State zurueck (Immutabilitaet)', () => {
|
||||
const state = defaultState()
|
||||
const result = simulateRound(state, emptyControls(), defaultConditions())
|
||||
expect(result).not.toBe(state) // Neues Objekt
|
||||
})
|
||||
|
||||
it('ohne Massnahmen aendert sich wenig', () => {
|
||||
const state = defaultState()
|
||||
const result = simulateRound(state, emptyControls(), defaultConditions())
|
||||
// Werte sollten nah am Ausgangszustand sein
|
||||
for (const key of Object.keys(state) as (keyof State)[]) {
|
||||
expect(Math.abs(result[key] - state[key])).toBeLessThan(10)
|
||||
}
|
||||
})
|
||||
|
||||
it('Deiche reduzieren lokales Hochwasser', () => {
|
||||
const state = defaultState()
|
||||
const controls = { ...emptyControls(), levees: 80 }
|
||||
const result = simulateRound(state, controls, defaultConditions())
|
||||
expect(result.floodLocal).toBeLessThan(state.floodLocal)
|
||||
})
|
||||
|
||||
it('Begradigung erhoeht Hochwasser flussabwaerts', () => {
|
||||
const state = defaultState()
|
||||
const controls = { ...emptyControls(), straightening: 80 }
|
||||
const result = simulateRound(state, controls, defaultConditions())
|
||||
expect(result.floodDownstream).toBeGreaterThan(state.floodDownstream)
|
||||
})
|
||||
|
||||
it('Renaturierung erhoeht Biodiversitaet', () => {
|
||||
const state = defaultState()
|
||||
const controls = { ...emptyControls(), renaturation: 70 }
|
||||
const result = simulateRound(state, controls, defaultConditions())
|
||||
expect(result.biodiversity).toBeGreaterThan(state.biodiversity)
|
||||
})
|
||||
|
||||
it('Bewaesserung erhoeht Bodenfruchtbarkeit', () => {
|
||||
const state = defaultState()
|
||||
const controls = { ...emptyControls(), irrigation: 60 }
|
||||
const result = simulateRound(state, controls, defaultConditions())
|
||||
expect(result.soilFertility).toBeGreaterThan(state.soilFertility)
|
||||
})
|
||||
|
||||
it('Auen freigeben reduziert Hochwasser lokal und flussabwaerts', () => {
|
||||
const state = defaultState()
|
||||
const controls = { ...emptyControls(), floodplainRelease: 70 }
|
||||
const result = simulateRound(state, controls, defaultConditions())
|
||||
expect(result.floodLocal).toBeLessThan(state.floodLocal)
|
||||
expect(result.floodDownstream).toBeLessThan(state.floodDownstream)
|
||||
})
|
||||
|
||||
it('alle Werte bleiben zwischen 0 und 100', () => {
|
||||
// Extreme Kontrollen
|
||||
const controls: Controls = {
|
||||
straightening: 100, levees: 100, dredging: 100,
|
||||
floodplainRelease: 100, renaturation: 100, irrigation: 100,
|
||||
}
|
||||
const result = simulateRound(defaultState(), controls, defaultConditions())
|
||||
for (const key of Object.keys(result) as (keyof State)[]) {
|
||||
expect(result[key]).toBeGreaterThanOrEqual(0)
|
||||
expect(result[key]).toBeLessThanOrEqual(100)
|
||||
}
|
||||
})
|
||||
|
||||
it('Hochwasser-Event erhoeht Floodwerte', () => {
|
||||
const state = defaultState()
|
||||
const event = { round: 1, type: 'flood_event' as const, intensity: 80 }
|
||||
const result = simulateRound(state, emptyControls(), defaultConditions(), event)
|
||||
expect(result.floodLocal).toBeGreaterThan(state.floodLocal)
|
||||
})
|
||||
|
||||
it('Duerre-Event reduziert Grundwasser', () => {
|
||||
const state = defaultState()
|
||||
const event = { round: 1, type: 'drought' as const, intensity: 70 }
|
||||
const result = simulateRound(state, emptyControls(), defaultConditions(), event)
|
||||
expect(result.groundwater).toBeLessThan(state.groundwater)
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeScore', () => {
|
||||
it('berechnet alle Scores zwischen 0 und 100', () => {
|
||||
const score = computeScore(defaultState(), { safety: 0.25, ecology: 0.25, agriculture: 0.25, economy: 0.25 })
|
||||
expect(score.safety).toBeGreaterThanOrEqual(0)
|
||||
expect(score.safety).toBeLessThanOrEqual(100)
|
||||
expect(score.ecology).toBeGreaterThanOrEqual(0)
|
||||
expect(score.total).toBeGreaterThanOrEqual(0)
|
||||
expect(score.total).toBeLessThanOrEqual(100)
|
||||
})
|
||||
|
||||
it('Gewichtung beeinflusst Gesamtscore', () => {
|
||||
const state: State = { ...defaultState(), economy: 90 }
|
||||
const scoreEco = computeScore(state, { safety: 0, ecology: 0, agriculture: 0, economy: 1 })
|
||||
const scoreSafe = computeScore(state, { safety: 1, ecology: 0, agriculture: 0, economy: 0 })
|
||||
expect(scoreEco.total).not.toEqual(scoreSafe.total)
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeCost', () => {
|
||||
it('0 Intensitaet kostet 0', () => {
|
||||
expect(computeCost('levees', 0)).toBe(0)
|
||||
})
|
||||
|
||||
it('hoehere Intensitaet kostet mehr (ueberproportional)', () => {
|
||||
const cost50 = computeCost('levees', 50)
|
||||
const cost100 = computeCost('levees', 100)
|
||||
expect(cost100).toBeGreaterThan(cost50 * 1.5) // ueberproportional
|
||||
})
|
||||
})
|
||||
|
||||
describe('Levels', () => {
|
||||
it('hat 5 Levels', () => {
|
||||
expect(LEVELS).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('jedes Level hat gueltige Struktur', () => {
|
||||
for (const l of LEVELS) {
|
||||
expect(l.id).toBeTruthy()
|
||||
expect(l.title).toBeTruthy()
|
||||
expect(l.rounds).toBeGreaterThan(0)
|
||||
expect(l.allowedControls.length).toBeGreaterThan(0)
|
||||
expect(l.conditions.budget).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('keine dominante Strategie: extreme Massnahme hat Nachteile', () => {
|
||||
// Teste ob maximale Begradigung nicht alles verbessert
|
||||
const state = LEVELS[3].initialState // Level 4 (Gleichgewicht)
|
||||
const conditions = LEVELS[3].conditions
|
||||
const extremeControls = { ...emptyControls(), straightening: 100 }
|
||||
const result = simulateRound({ ...state }, extremeControls, conditions)
|
||||
|
||||
// Begradigung muss MINDESTENS einen Nachteil haben
|
||||
const hasDownside =
|
||||
result.biodiversity < state.biodiversity ||
|
||||
result.floodDownstream > state.floodDownstream ||
|
||||
result.groundwater < state.groundwater ||
|
||||
result.erosion > state.erosion
|
||||
expect(hasDownside).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CONTROL_META / STATE_META', () => {
|
||||
it('hat Metadaten fuer alle 6 Massnahmen', () => {
|
||||
expect(Object.keys(CONTROL_META)).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('hat Metadaten fuer alle 8 Zustandsparameter', () => {
|
||||
expect(Object.keys(STATE_META)).toHaveLength(8)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('FlussGame', () => {
|
||||
it('startet mit level-select Phase', () => {
|
||||
const game = new FlussGame()
|
||||
expect(game.phase).toBe('level-select')
|
||||
})
|
||||
|
||||
it('selectLevel setzt korrekt auf', () => {
|
||||
const game = new FlussGame()
|
||||
game.selectLevel('L1')
|
||||
expect(game.level.id).toBe('L1')
|
||||
expect(game.phase).toBe('intro')
|
||||
expect(game.round).toBe(0)
|
||||
expect(game.budgetRemaining).toBe(120) // L1 budget
|
||||
})
|
||||
|
||||
it('serialize / deserialize roundtrip', () => {
|
||||
const game = new FlussGame()
|
||||
game.selectLevel('L2')
|
||||
game.startPlaying()
|
||||
game.setControl('levees', 40)
|
||||
game.executeRound()
|
||||
|
||||
const json = game.serialize()
|
||||
const game2 = new FlussGame()
|
||||
expect(game2.deserialize(json)).toBe(true)
|
||||
expect(game2.level.id).toBe('L2')
|
||||
expect(game2.round).toBe(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user