life game.png

link:https://editor.p5js.org/StonesGate604/sketches/kgjoSqi_u

I've always been fascinated by Conway's Game of Life. I'd like to use this midterm assignment as an opportunity to write my own version of the Game of Life and rework it to make it more visually appealing. I plan to spend the first week studying the Game of Life code and the second week experimenting with and reworking it.

The Game of Life is a cellular automaton proposed by British mathematician John Conway in 1970. It exists on a two-dimensional grid of countless squares, each representing a "cell," which has only two states: alive or dead. In each round of evolution (generation), all cells are updated simultaneously according to a fixed rule:

If a cell has three live neighbors, it is born (B3);

If a live cell has two or three live neighbors, it continues to live (S23);

In all other cases, it dies.

So, on this page, I'll focus on understanding and learning the core code of the Game of Life. I didn't write all the code myself, but mainly used this video as a reference: https://www.youtube.com/watch?v=jGTCwCLRCrE. With the help of AI, I was able to write two classes and put them in a separate js file, and call these two classes in the main Sketch code.

index

index

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Game of Life</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <script src="<https://cdn.jsdelivr.net/npm/[email protected]/lib/p5.min.js>"></script>
  <script src="life.js"></script>
  <script src="sketch.js"></script>
  <style>body{margin:0;background:#121212;}</style>
</head>
<body></body>
</html>

Nested structure of classes

Nested structure of classes

The structure of the entire file is to call the class in lifegame in sketch, where cell is the state of the cell on each grid. Cell is very simple, with only three parameters: x, y (position) and life state (dead/alive).

class Cell {
  constructor(x, y, s, alive = false) {
    this.x = x; this.y = y; this.s = s;
    this.alive = alive;
  }
  
  setAlive(v) { this.alive = v; }
  
  draw() {
    if (!this.alive) return;
    fill(0);
    circle(this.x * this.s, this.y * this.s, this.s);
  }
}

The grid class is very complex. It is like a control center that detects whether there are other cells near each cell and initializes the canvas at the beginning of the program.

The grid will generate cells based on the calculated number of rows and columns when it is created. Therefore, there is no need to instantiate the cell class in the main sketch.

class Cell {
  constructor(x, y, s, alive = false) {
    this.x = x;
    this.y = y;
    this.s = s;
    this.alive = alive;
  }

  setAlive(v) {
    this.alive = v;
  }

  draw() {
    if (!this.alive) return;
    fill(0);
    circle(this.x * this.s, this.y * this.s, this.s);
  }
}

class Grid {
  constructor(cols, rows, s, wrapEdges = true) {
    this.cols = cols;
    this.rows = rows;
    this.s = s;
    this.wrapEdges = wrapEdges;
    this.cells = this.makeCells();//create the array that for current 
    this.buffer = this.makeBool2D(false);//create array for the next rendering
  }

  makeCells() {
    const arr = [];
    for (let y = 0; y < this.rows; y++) {
      const row = [];
      for (let x = 0; x < this.cols; x++)
        row.push(new Cell(x, y, this.s, false));
      arr.push(row);
    }
    return arr;
  }

  makeBool2D(v) {
    let arr = [];
    for (let y = 0; y < this.rows; y++) {
      let row = [];
      for (let x = 0; x < this.cols; x++) {
        row.push(v); 
      }
      arr.push(row); 
    }
    return arr;
  }

  countNeighbors(x, y) {
    let n = 0;
    for (let j = -1; j <= 1; j++) {
      for (let i = -1; i <= 1; i++) {
        if (i === 0 && j === 0) continue;
        //Temporarily calculate the coordinates of neighbors
        let cx = x + i,
          cy = y + j;
        // Let the coordinates outside the boundary "return to the other side" to form a ring map
        if (this.wrapEdges) {
          cx = (cx + this.cols) % this.cols;
          cy = (cy + this.rows) % this.rows;
          if (this.cells[cy][cx].alive) n++;
        } else if (cx >= 0 && cx < this.cols && cy >= 0 && cy < this.rows) {
          if (this.cells[cy][cx].alive) n++;
        }
      }
    }
    return n;
  }

  step() {
    for (let y = 0; y < this.rows; y++) {
      for (let x = 0; x < this.cols; x++) {
        const alive = this.cells[y][x].alive;
        const n = this.countNeighbors(x, y);
        this.buffer[y][x] =
          (alive && (n === 2 || n === 3)) || (!alive && n === 3);
      }
    }
    for (let y = 0; y < this.rows; y++)
      for (let x = 0; x < this.cols; x++)
        this.cells[y][x].setAlive(this.buffer[y][x]);
  }

  draw() {
    for (let y = 0; y < this.rows; y++)
      for (let x = 0; x < this.cols; x++) this.cells[y][x].draw();
  }
  randomize(p = 0.25) {
    for (let y = 0; y < this.rows; y++)
      for (let x = 0; x < this.cols; x++)
        this.cells[y][x].setAlive(random() < p);
  }
}

Frame 12.png

The program's workflow is as follows: We know that each cell's state is stored in an array element. The program first reads each cell's state, then uses the step function to calculate the state of each cell for the next frame according to the core rules of the Game of Life. Finally, the state is stored in a buffer array and drawn. When the program enters the next loop, the previous buffer array becomes the current cell's state.