273 lines
9.3 KiB
TypeScript
273 lines
9.3 KiB
TypeScript
/**
|
||
* 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<readonly [number, number, number]> = [
|
||
[-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<number>(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;
|
||
}
|