Kleine Lebenssimulation in JavaScript

Kleine Lebenssimulation in JavaScript

Eine prima Fingerübung, geblieben vom viel zu frühen Wachwerden heute. Conway's Game of Life ist ein zellulärer Automat, der auf einem zweidimensionalen Gitter arbeitet. Jede Zelle kann zwei Zustände haben: lebend (1) oder tot (0). Es folgt das eingebettete in deinem Browser lauffähige Programm, hiernach die Mathematik dahinter, gefolgt vom JavaScript-Code und dessen Erklärung


YOGAVUS Lebenssimulation

Eine einfaches Conway's Game of Life – komplett lokal im Browser.

    

Die zugrundeliegende Mathematik

Die Regeln von Conway's Game of Life kompakt dargelegt: Der Zustand einer Zelle in der nächsten Generation wird bestimmt durch:

N_{t+1}(i,j) =
{
1 falls k = 3
N_t(i,j) falls k = 2
0 sonst
}

Erklärung der Variablen

  • t : Zeitschritt bzw. Generation (t = 0 ist der Anfangszustand)
  • i, j : Zeilen- und Spaltenindex der Zelle im zweidimensionalen Gitter
  • N_t(i,j) : Zustand der Zelle an Position (i,j) zur Zeit t
    (1 = lebend, 0 = tot)
  • N_{t+1}(i,j): Zustand der Zelle an Position (i,j) in der nächsten Generation
  • k : Anzahl der lebenden Nachbarn der Zelle
    (Moore-Nachbarschaft: die bis zu 8 direkt angrenzenden Zellen)

Und in Worten

  • Eine Zelle wird genau dann lebend, wenn sie genau 3 lebende Nachbarn hat.
  • Eine bereits lebende Zelle bleibt nur lebend, wenn sie genau 2 oder 3 lebende Nachbarn hat.
  • In allen anderen Fällen ist die Zelle in der nächsten Generation tot.

Der zugrundliegende JavaScript-Code

  • Kapselung in IIFE (Immediately Invoked Function Expression) gekapselt, daher keine globalen Variablen, keine Konflikte mit anderen Skripten.
  • Beim Laden der Seite werden alle HTML-Elemente per ID geholt.
  • createGrid() baut eine HTML-Tabelle mit <td>-Zellen auf, initialisiert zufällige Zustände und speichert alle Zellen im cells-Array.
  • nextGeneration() berechnet simultan die nächste Generation nach den klassischen Conway-Regeln und aktualisiert nur die Hintergrundfarben, bliebt hübsch schnell.
  • setTimeout sorgt für die wiederholte Ausführung (Animationsschleife).
  • Die Buttons und Eingabefelder steuern alles: Start/Pause, Neustart, Ändern von Größe/Geschwindigkeit.
(function() {
  // 1. Elemente aus dem HTML holen
  const gridDiv = document.getElementById('grid');
  const startBtn = document.getElementById('start');
  const pauseBtn = document.getElementById('pause');
  const resetBtn = document.getElementById('reset');
  const applyBtn = document.getElementById('apply');
  const sizeInput = document.getElementById('size');
  const speedInput = document.getElementById('speed');

  // 2. Variablen für die Simulation
  let rows = 30;
  let cols = 30;
  let speed = 200;              // Geschwindigkeit in Millisekunden
  let cells = [];               // Array mit allen <td>-Elementen (für schnelles Updaten)
  let grid = [];                // 2D-Array mit dem aktuellen Zustand (0 = tot, 1 = lebend)
  let running = false;          // Läuft die Simulation gerade?
  let timer = null;             // Speichert den setTimeout für die Schleife

  // 3. Funktion: Erstellt ein neues Gitter und zeichnet es
  function createGrid() {
    grid = [];
    cells = [];
    gridDiv.innerHTML = '';                       // Alten Inhalt löschen

    const table = document.createElement('table');
    table.style.borderCollapse = 'collapse';

    for (let i = 0; i < rows; i++) {
      grid[i] = [];
      const tr = document.createElement('tr');
      for (let j = 0; j < cols; j++) {
        grid[i][j] = Math.random() < 0.3 ? 1 : 0;  // ~30% lebende Zellen zufällig

        const td = document.createElement('td');
        td.style.width = '10px';
        td.style.height = '10px';
        td.style.border = '1px solid #ddd';
        td.style.backgroundColor = grid[i][j] ? '#000' : '#fff';

        // Klick auf Zelle toggelt Zustand (nur wenn nicht läuft)
        td.addEventListener('click', () => {
          if (!running) {
            grid[i][j] = 1 - grid[i][j];
            td.style.backgroundColor = grid[i][j] ? '#000' : '#fff';
          }
        });

        cells.push(td);               // Zelle merken für späteres schnelles Updaten
        tr.appendChild(td);
      }
      table.appendChild(tr);
    }
    gridDiv.appendChild(table);
  }

  // 4. Nachbarn zählen (Moore-Nachbarschaft: 8 umliegende Zellen)
  function countNeighbors(x, y) {
    let count = 0;
    for (let dx = -1; dx <= 1; dx++) {
      for (let dy = -1; dy <= 1; dy++) {
        if (dx === 0 && dy === 0) continue;               // eigene Zelle überspringen
        const nx = x + dx;
        const ny = y + dy;
        if (nx >= 0 && nx < rows && ny >= 0 && ny < cols && grid[nx][ny]) count++;
      }
    }
    return count;
  }

  // 5. Nächste Generation berechnen
  function nextGeneration() {
    const newGrid = [];
    for (let i = 0; i < rows; i++) {
      newGrid[i] = [];
      for (let j = 0; j < cols; j++) {
        const neighbors = countNeighbors(i, j);
        if (grid[i][j]) {
          // Lebend → überlebt nur bei 2 oder 3 Nachbarn
          newGrid[i][j] = (neighbors === 2 || neighbors === 3) ? 1 : 0;
        } else {
          // Tot → wird nur bei genau 3 Nachbarn lebend
          newGrid[i][j] = (neighbors === 3) ? 1 : 0;
        }
      }
    }
    grid = newGrid;

    // Nur Farben aktualisieren (schneller als komplett neu zeichnen)
    let idx = 0;
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < cols; j++) {
        cells[idx].style.backgroundColor = grid[i][j] ? '#000' : '#fff';
        idx++;
      }
    }
  }

  // 6. Spielschleife
  function loop() {
    nextGeneration();
    if (running) timer = setTimeout(loop, speed);
  }

  // 7. Event-Listener für die Buttons
  startBtn.addEventListener('click', () => {
    if (!running) {
      running = true;
      loop();
    }
  });

  pauseBtn.addEventListener('click', () => {
    running = false;
    clearTimeout(timer);
  });

  resetBtn.addEventListener('click', () => {
    running = false;
    clearTimeout(timer);
    createGrid();
  });

  applyBtn.addEventListener('click', () => {
    const newSize = parseInt(sizeInput.value);
    const newSpeed = parseInt(speedInput.value);

    if (newSize < 10 || newSize > 80 || isNaN(newSize)) {
      alert('Grid-Größe bitte zwischen 10 und 80');
      return;
    }
    if (newSpeed < 50 || newSpeed > 1000 || isNaN(newSpeed)) {
      alert('Geschwindigkeit bitte zwischen 50 und 1000 ms');
      return;
    }

    rows = newSize;
    cols = newSize;
    speed = newSpeed;
    running = false;
    clearTimeout(timer);
    createGrid();
  });

  // 8. Start: Initiales Gitter erzeugen
  createGrid();
})();

Read more