/** * Waldbrand als zellulärer Automat – Portierung von python/ue1.py. * * Gleiche Regeln wie in Python, aber ein anderer Zufallsgenerator (mulberry32): * Läufe mit demselben Seed sind reproduzierbar, stimmen aber nicht mit den Python-Läufen überein. */ export const WALD = 0; export const BRENNEND = 1; export const ABGEBRANNT = 2; export const GELOESCHT = 3; export const SCHNEISE = 4; export const ZUSTAENDE = [ { name: "Wald", farbe: "#008300" }, { name: "brennend", farbe: "#e34948" }, { name: "abgebrannt", farbe: "#3a3a38" }, { name: "gelöscht", farbe: "#86b6ef" }, { name: "Brandschneise", farbe: "#d9d4c7" }, ] as const; // Moore-Nachbarschaft: [Zeilenversatz, Spaltenversatz, Distanzgewicht] export const NACHBARN: ReadonlyArray = [ [-1, -1, 0.7], [-1, 0, 1.0], [-1, 1, 0.7], [0, -1, 1.0], [0, 1, 1.0], [1, -1, 0.7], [1, 0, 1.0], [1, 1, 0.7], ]; export type Zelle = readonly [zeile: number, spalte: number]; export interface Params { breite: number; hoehe: number; /** Anteil natürlicher Brandschneisen (z.B. Wasser, Fels), zufällig verteilt */ schneiseAnteil: number; /** Künstliche Brandschneisen als volle Zeilen bzw. Spalten */ schneisenZeilen: readonly number[]; schneisenSpalten: readonly number[]; /** Brandfaktor ("Trockenheit") je Waldzelle, gleichverteilt */ brandfaktorMin: number; brandfaktorMax: number; /** Restbrenndauer je Waldzelle, ganzzahlig gleichverteilt (inklusive Grenzen) */ brenndauerMin: number; brenndauerMax: number; /** (x, y): x zeigt nach Osten, y nach Norden; Richtung, in die der Wind weht */ windRichtung: readonly [number, number]; windStaerke: number; loeschWahrscheinlichkeit: number; /** Startzelle, null = zufällige Waldzelle */ start: Zelle | null; maxSchritte: number; seed: number; } export const STANDARD_PARAMS: Params = { breite: 100, hoehe: 100, schneiseAnteil: 0.05, schneisenZeilen: [], schneisenSpalten: [], brandfaktorMin: 0.1, brandfaktorMax: 1.0, brenndauerMin: 2, brenndauerMax: 5, windRichtung: [1, 0], windStaerke: 0, loeschWahrscheinlichkeit: 0.05, start: null, maxSchritte: 1000, seed: 0, }; export interface Gitter { breite: number; hoehe: number; zustand: Uint8Array; brandfaktor: Float32Array; restbrenndauer: Uint16Array; } export type Zufall = () => number; /** mulberry32: kleiner, schneller Zufallsgenerator mit Seed, liefert Zahlen in [0, 1) */ export function zufallsgenerator(seed: number): Zufall { let a = seed >>> 0; return () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /** 1 + Windstärke * cos(Winkel zwischen Windvektor und Vektor zur Nachbarzelle) */ export function windfaktor(dr: number, dc: number, [wx, wy]: readonly [number, number], staerke: number): number { const normWind = Math.hypot(wx, wy); if (staerke === 0 || normWind === 0) return 1; // Zeilenindex wächst nach unten, y zeigt nach oben const nx = dc; const ny = -dr; return 1 + (staerke * (wx * nx + wy * ny)) / (normWind * Math.hypot(nx, ny)); } /** Erzeugt Zustand, Brandfaktor und Restbrenndauer und entzündet die Startzelle. */ export function initGitter(params: Params, zufall: Zufall): Gitter { const { breite, hoehe } = params; const n = breite * hoehe; const zustand = new Uint8Array(n); for (let i = 0; i < n; i++) zustand[i] = zufall() < params.schneiseAnteil ? SCHNEISE : WALD; if (params.start) { // Am vorgegebenen Brandherd wächst immer Wald, auch wenn dort zufällig eine natürliche Schneise läge zustand[params.start[0] * breite + params.start[1]] = WALD; } for (const zeile of params.schneisenZeilen) zustand.fill(SCHNEISE, zeile * breite, (zeile + 1) * breite); for (const spalte of params.schneisenSpalten) { for (let r = 0; r < hoehe; r++) zustand[r * breite + spalte] = SCHNEISE; } // Rand: die äußerste Zeile bzw. Spalte auf jeder Seite ist Brandschneise zustand.fill(SCHNEISE, 0, breite); zustand.fill(SCHNEISE, (hoehe - 1) * breite, n); for (let r = 0; r < hoehe; r++) { zustand[r * breite] = SCHNEISE; zustand[r * breite + breite - 1] = SCHNEISE; } let start: number; if (params.start === null) { // Zufällige Waldzelle; hängt nur vom Seed und den Zelltypen ab, daher bei gleichem Seed // in allen Varianten dieselbe, solange diese die Zelltypen nicht verändern const waldzellen: number[] = []; for (let i = 0; i < n; i++) if (zustand[i] === WALD) waldzellen.push(i); if (waldzellen.length === 0) throw new Error("Keine Waldzelle für den Brandherd vorhanden"); start = waldzellen[Math.floor(zufall() * waldzellen.length)]; } else { start = params.start[0] * breite + params.start[1]; if (zustand[start] !== WALD) { throw new Error(`Startzelle (${params.start.join(", ")}) liegt auf einer künstlichen Brandschneise oder am Rand`); } } const brandfaktor = new Float32Array(n); const restbrenndauer = new Uint16Array(n); const spanne = params.brandfaktorMax - params.brandfaktorMin; const dauern = params.brenndauerMax - params.brenndauerMin + 1; for (let i = 0; i < n; i++) { // Für jede Zelle ziehen, damit Varianten mit gleichem Seed denselben Wald erhalten const bf = params.brandfaktorMin + zufall() * spanne; const dauer = params.brenndauerMin + Math.floor(zufall() * dauern); if (zustand[i] !== SCHNEISE) { brandfaktor[i] = bf; restbrenndauer[i] = dauer; } } zustand[start] = BRENNEND; return { breite, hoehe, zustand, brandfaktor, restbrenndauer }; } /** Ein synchroner Zeitschritt: liest nur das alte Gitter und gibt ein neues zurück. */ export function schritt(gitter: Gitter, params: Params, zufall: Zufall): Gitter { const { breite, hoehe, zustand, brandfaktor } = gitter; const neu: Gitter = { breite, hoehe, zustand: zustand.slice(), brandfaktor: brandfaktor.slice(), restbrenndauer: gitter.restbrenndauer.slice(), }; const windfaktoren = NACHBARN.map(([dr, dc]) => windfaktor(dr, dc, params.windRichtung, params.windStaerke)); for (let r = 0; r < hoehe; r++) { for (let c = 0; c < breite; c++) { const i = r * breite + c; if (zustand[i] !== BRENNEND) continue; if (zufall() < params.loeschWahrscheinlichkeit) { neu.zustand[i] = GELOESCHT; neu.brandfaktor[i] = 0; neu.restbrenndauer[i] = 0; continue; } for (let k = 0; k < NACHBARN.length; k++) { const [dr, dc, gewicht] = NACHBARN[k]; const nr = r + dr; const nc = c + dc; if (nr < 0 || nr >= hoehe || nc < 0 || nc >= breite) continue; const j = nr * breite + nc; if (zustand[j] !== WALD) continue; if (zufall() < 0.5 * gewicht * brandfaktor[j] * windfaktoren[k]) neu.zustand[j] = BRENNEND; } neu.restbrenndauer[i] -= 1; if (neu.restbrenndauer[i] === 0) { neu.zustand[i] = ABGEBRANNT; neu.brandfaktor[i] = 0; } } } return neu; } export type Anzahl = number[]; export function zaehle(zustand: Uint8Array): Anzahl { const anzahl = new Array(ZUSTAENDE.length).fill(0); for (const z of zustand) anzahl[z]++; return anzahl; } /** Zellen, die irgendwann gebrannt haben */ export function betroffen(anzahl: Anzahl): number { return anzahl[BRENNEND] + anzahl[ABGEBRANNT] + anzahl[GELOESCHT]; } /** Jede Zelle, die keine Brandschneise ist, war zu Beginn Wald */ export function waldflaeche(anzahl: Anzahl): number { return anzahl.reduce((a, b) => a + b, 0) - anzahl[SCHNEISE]; } export interface Ergebnis { breite: number; hoehe: number; endzustand: Uint8Array; /** Anzahl Zellen je Zustand und Zeitschritt, anzahl[0] = Anfangszustand */ anzahl: Anzahl[]; dauer: number; betroffenAnteil: number; } /** Simuliert, bis keine Zelle mehr brennt oder maxSchritte erreicht ist. */ export function simuliere(params: Params): Ergebnis { const zufall = zufallsgenerator(params.seed); let gitter = initGitter(params, zufall); const anzahl = [zaehle(gitter.zustand)]; while (anzahl[anzahl.length - 1][BRENNEND] > 0 && anzahl.length <= params.maxSchritte) { gitter = schritt(gitter, params, zufall); anzahl.push(zaehle(gitter.zustand)); } const letzte = anzahl[anzahl.length - 1]; return { breite: gitter.breite, hoehe: gitter.hoehe, endzustand: gitter.zustand, anzahl, dauer: anzahl.length - 1, betroffenAnteil: betroffen(letzte) / waldflaeche(letzte), }; } function imInneren(gitter: Gitter, [r, c]: Zelle): boolean { return r > 0 && r < gitter.hoehe - 1 && c > 0 && c < gitter.breite - 1; } /** Macht eine Waldzelle zur Brandschneise (interaktives Zeichnen). Verändert das Gitter direkt. */ export function setzeSchneise(gitter: Gitter, zelle: Zelle): boolean { const i = zelle[0] * gitter.breite + zelle[1]; if (!imInneren(gitter, zelle) || gitter.zustand[i] !== WALD) return false; gitter.zustand[i] = SCHNEISE; gitter.brandfaktor[i] = 0; gitter.restbrenndauer[i] = 0; return true; } /** Entzündet eine Waldzelle (interaktiv). Verändert das Gitter direkt. */ export function entzuende(gitter: Gitter, zelle: Zelle): boolean { const i = zelle[0] * gitter.breite + zelle[1]; if (!imInneren(gitter, zelle) || gitter.zustand[i] !== WALD) return false; gitter.zustand[i] = BRENNEND; return true; }