import { describe, expect, it } from 'vitest'; import { TrafficSimulation } from '@/domain/simulation/simulation'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { isGreenAt } from '@/domain/plan/aspects'; import { createStandardIntersectionProject } from '@/domain/model/factory'; const FIXED_DATE = new Date('2026-01-01T00:00:00Z'); function setup(): { simulation: TrafficSimulation; plan: ReturnType } { const project = createStandardIntersectionProject('Simulation', FIXED_DATE); const plan = buildSignalPlan(project); return { simulation: new TrafficSimulation(plan, project, { timeStep: 0.5 }), plan }; } describe('Verkehrsablaufsimulation', () => { it('laesst sich mit den Projektdaten starten', () => { const { simulation } = setup(); expect(simulation.runnable).toBe(true); expect(simulation.snapshot().groups.length).toBeGreaterThan(0); }); it('haelt die Fahrzeugbilanz geschlossen', () => { const { simulation } = setup(); simulation.advance(60); simulation.advance(60); simulation.advance(60); for (const group of simulation.snapshot().groups) { // Der Altbestand liess Fahrzeuge verschwinden, weil der Abfluss nicht // gegen die Warteschlangenlaenge begrenzt war. expect(group.arrived).toBe(group.departed + group.queue); expect(group.queue).toBeGreaterThanOrEqual(0); } }); it('laesst nur bei Freigabe Fahrzeuge abfliessen', () => { const { simulation, plan } = setup(); let lastDeparted = new Map(); for (let i = 0; i < 400; i += 1) { const before = simulation.snapshot(); simulation.advance(0.5); const after = simulation.snapshot(); for (const group of after.groups) { const previous = lastDeparted.get(group.groupId) ?? 0; if (group.departed > previous) { const planned = plan.groups.find((g) => g.groupId === group.groupId)!; // Innerhalb des Schritts muss die Gruppe Freigabe gehabt haben. const hadGreen = isGreenAt(planned, before.cyclePosition, plan.cycleTime) || isGreenAt(planned, after.cyclePosition, plan.cycleTime); expect(hadGreen, `${group.name} floss ohne Freigabe ab`).toBe(true); } } lastDeparted = new Map(after.groups.map((g) => [g.groupId, g.departed])); } }); it('liefert bei gleichem Startwert identische Ergebnisse', () => { const a = setup().simulation; const b = setup().simulation; a.advance(300); b.advance(300); expect(a.snapshot().groups.map((g) => g.arrived)).toEqual( b.snapshot().groups.map((g) => g.arrived), ); expect(a.snapshot().overallDelay).toBe(b.snapshot().overallDelay); }); it('setzt vollstaendig zurueck', () => { const { simulation } = setup(); simulation.advance(300); simulation.reset(); const snapshot = simulation.snapshot(); expect(snapshot.time).toBe(0); expect(snapshot.totalArrived).toBe(0); expect(snapshot.totalQueue).toBe(0); for (const group of snapshot.groups) { expect(group.maxQueue).toBe(0); expect(group.averageDelay).toBe(0); } }); it('bildet die Ankunftsrate ueber laengere Zeit richtig ab', () => { const { simulation } = setup(); simulation.advance(60); for (let i = 0; i < 59; i += 1) simulation.advance(60); const snapshot = simulation.snapshot(); for (const group of snapshot.groups) { // Nach einer Stunde muessen etwa so viele Fahrzeuge angekommen sein wie // die Verkehrsstaerke angibt. expect(group.arrived).toBeGreaterThan(group.demand * 0.97); expect(group.arrived).toBeLessThanOrEqual(group.demand + 1); } }); it('begrenzt einen ueberlangen Zeitsprung', () => { const { simulation } = setup(); simulation.advance(100000); // Hoechstens 60 s werden nachgerechnet, damit die Oberflaeche nicht einfriert. expect(simulation.snapshot().time).toBeLessThanOrEqual(60); }); it('meldet sich als nicht lauffaehig, wenn keine Verkehrsstaerken erfasst sind', () => { const project = createStandardIntersectionProject('Ohne Daten', FIXED_DATE); const plan = buildSignalPlan({ ...project, demands: [] }); const simulation = new TrafficSimulation(plan, { ...project, demands: [] }); expect(simulation.runnable).toBe(false); simulation.advance(60); expect(simulation.snapshot().totalArrived).toBe(0); }); });