feat: add python and web implementation
This commit is contained in:
1 parent
fabbc6d147
commit
df0950ef11
30 files changed
+5458
-8
No files matched your search
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in new issue
Block a user