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>
217 lines
7.8 KiB
TypeScript
217 lines
7.8 KiB
TypeScript
/**
|
|
* 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)
|
|
})
|
|
})
|