life_game_screenshot (2).png

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

Building on last week’s work, I added more visual elements and mouse click interactions to the code. I’ll go over these in detail in the following sections.

sketch.JS

let grid;
const SIZE = 15;
const RADIUS = 6;
const THICKNESS = 5;

function setup() {
  createCanvas(windowWidth, windowHeight);
  grid = new Grid(floor(width / SIZE), floor(height / SIZE), SIZE, true);
  grid.randomize(0.5); // Randomly generate initial pattern
  colorMode(HSB);
  noStroke();
  fill(210);
}
function draw() {
  if (frameCount % 2 == 0) {
    background(210);
    grid.step();
    grid.draw();
  }
}

function mousePressed() {
  const gx = floor(mouseX / grid.s);
  const gy = floor(mouseY / grid.s);
  stampRing(gx, gy, RADIUS, THICKNESS);
}

function stampRing(cx, cy, r = 3, t = 1) {
  const rMin = r - 0.5;
  const rMax = r + t - 0.5;
  for (let dy = -r - t; dy <= r + t; dy++) {
    for (let dx = -r - t; dx <= r + t; dx++) {
      const d = dist(0, 0, dx, dy);
      if (d >= rMin && d <= rMax) {
        stamp(cx + dx, cy + dy);
      }
    }
  }
}

function stamp(x, y) {
  grid.cells[y][x].setAlive(true);
}

function keyPressed() {
  if (key === "s" || key === "S") {
    saveCanvas("life_game_screenshot", "png");
  }
}

Life.JS

class Cell {
  constructor(x, y, s, alive = false) {
    this.x = x;
    this.y = y;
    this.s = s;
    this.alive = alive;
    this.age = 0;
    this.fType = random([1, 2, 3]);
  }

  setAlive(v) {
   
    if (v && !this.alive) {
      //born
      this.age = 0;
    }
    if (!v && this.alive) {
      //died
      this.age = -1;
    }
    if (!v && !this.alive) {
      //died
      this.age = 0;
    }

    this.alive = v;
  }

  draw() {
    // if (!this.alive) {
    //   return;
    // }

    if (this.age == -1) {
      rectMode(CENTER);
      push();
      translate(this.x * this.s, this.y * this.s);
      rotate(PI / 4);
      fill(20);
      rect(0, 0, 2, 10);
      rect(0, 0, 10, 2);
      pop();
      // console.log("draw age0");
      return;
    }

    if (this.age >= 1 && this.age <= this.s) {
      fill(98, 69, 49);
      circle(this.x * this.s, this.y * this.s, constrain(this.age, 12, this.s));
      fill(240);
      circle(
        this.x * this.s,
        this.y * this.s,
        constrain(this.age, 10, this.s) - 2
      );
      fill(98, 69, 49);
      circle(
        this.x * this.s,
        this.y * this.s,
        constrain(this.age, 10, this.s) - 4
      );
      return;
    }

    if (this.age > this.s) {
      if (this.fType == 1) {
        push();
        translate(this.x * this.s, this.y * this.s);
        noStroke();
        for (let i = 0; i < 7; i++) {
          push();
          rotate((i * PI) / 7);
          fill(341, 80, 100);
          ellipse(0, 0, 5, this.s);
          pop();
        }
        fill(50, 100, 100);
        ellipse(0, 0, this.s / 2 - 2, this.s / 2 - 2);
        pop();
        return;
      }

      if (this.fType == 2) {
        push();
        translate(this.x * this.s, this.y * this.s);
        stroke(221, 77, 100);
        strokeWeight(1);
        for (let i = 0; i < 18; i++) {
          rotate(TWO_PI / 18);
          line(0, 0, this.s / 2, 0);
        }
        noStroke();
        fill(98, 28, 100);
        ellipse(0, 0, this.s / 3);
        pop();
        return;
      }

      if (this.fType == 3) {
        push();
        translate(this.x * this.s, this.y * this.s);
        rectMode(CENTER);
        fill(40, 100, 100);
        rect(-this.s / 4, this.s / 4, this.s / 2 - 2);
        rect(this.s / 4, -this.s / 4, this.s / 2 - 2);
        rect(-this.s / 4, -this.s / 4, this.s / 2 - 2);
        rect(this.s / 4, this.s / 4, this.s / 2 - 2);
        rotate(PI / 4);
        fill(22, 100, 100);
        rect(0, 0, this.s / 2 +1);
        pop();
        return;
      }
    }
  }
}

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 c = this.cells[y][x];
        const n = this.countNeighbors(x, y);
        c.lastNeighbors = n;
        this.buffer[y][x] =
          (c.alive && (n === 2 || n === 3)) || (!c.alive && n === 3);
      }
    }
    for (let y = 0; y < this.rows; y++) {
      for (let x = 0; x < this.cols; x++) {
        const c = this.cells[y][x];
        const nextAlive = this.buffer[y][x];
        c.setAlive(nextAlive);
        if (c.alive) c.age = min(c.age + 0.5, this.s + 1);
      }
    }
  }

  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);
  }
}

First, I introduced a new variable - age. If a cell survives, it will change. First, it will slowly grow larger, then it will become one of the three patterns. If it fails to survive, it will leave a trace, but that trace will disappear in the next frame.

Cell growth trajectory

Cell growth trajectory

The code is implemented as follows: in a step function in the grid class, I should have set all cells on the entire canvas to be alive by looping through them. After that, I added a new function to increase the age of the cells set to be alive by 1, so that they are 1 year old when they are born, and all subsequent changes are based on the age value.

  for (let y = 0; y < this.rows; y++) {
      for (let x = 0; x < this.cols; x++) {
        const c = this.cells[y][x];
        const nextAlive = this.buffer[y][x];
        c.setAlive(nextAlive);
        if (c.alive) c.age = min(c.age + 0.5, this.s + 1);

The corresponding increase in code size in the cell class resulted in a significant increase in the amount of code. My idea was to create different cell poses based on their age. This was achieved by modifying the setAlive and draw functions in the cell class. The setAlive function assigns an age of 0 to each newly born cell, -1 to newly dead cells, and 0 to non-living cells. However, newly born cells receive an age of 1 in the grid step, so they start out with an age of 1.

 setAlive(v) {
   
    if (v && !this.alive) {
      //born
      this.age = 0;
    }
    if (!v && this.alive) {
      //died
      this.age = -1;
    }
    if (!v && !this.alive) {
      //died
      this.age = 0;
    }

    this.alive = v;
  }

The draw function classifies cells according to their different ages and draws different patterns.

draw() {
    // if (!this.alive) {
    //   return;
    // }

    if (this.age == -1) {
      rectMode(CENTER);
      push();
      translate(this.x * this.s, this.y * this.s);
      rotate(PI / 4);
      fill(20);
      rect(0, 0, 2, 10);
      rect(0, 0, 10, 2);
      pop();
      // console.log("draw age0");
      return;
    }

    if (this.age >= 1 && this.age <= this.s) {
      fill(98, 69, 49);
      circle(this.x * this.s, this.y * this.s, constrain(this.age, 12, this.s));
      fill(240);
      circle(
        this.x * this.s,
        this.y * this.s,
        constrain(this.age, 10, this.s) - 2
      );
      fill(98, 69, 49);
      circle(
        this.x * this.s,
        this.y * this.s,
        constrain(this.age, 10, this.s) - 4
      );
      return;
    }

    if (this.age > this.s) {
      if (this.fType == 1) {
        push();
        translate(this.x * this.s, this.y * this.s);
        noStroke();
        for (let i = 0; i < 7; i++) {
          push();
          rotate((i * PI) / 7);
          fill(341, 80, 100);
          ellipse(0, 0, 5, this.s);
          pop();
        }
        fill(50, 100, 100);
        ellipse(0, 0, this.s / 2 - 2, this.s / 2 - 2);
        pop();
        return;
      }

      if (this.fType == 2) {
        push();
        translate(this.x * this.s, this.y * this.s);
        stroke(221, 77, 100);
        strokeWeight(1);
        for (let i = 0; i < 18; i++) {
          rotate(TWO_PI / 18);
          line(0, 0, this.s / 2, 0);
        }
        noStroke();
        fill(98, 28, 100);
        ellipse(0, 0, this.s / 3);
        pop();
        return;
      }

      if (this.fType == 3) {
        push();
        translate(this.x * this.s, this.y * this.s);
        rectMode(CENTER);
        fill(40, 100, 100);
        rect(-this.s / 4, this.s / 4, this.s / 2 - 2);
        rect(this.s / 4, -this.s / 4, this.s / 2 - 2);
        rect(-this.s / 4, -this.s / 4, this.s / 2 - 2);
        rect(this.s / 4, this.s / 4, this.s / 2 - 2);
        rotate(PI / 4);
        fill(22, 100, 100);
        rect(0, 0, this.s / 2 +1);
        pop();
        return;
      }
    }
  }

In fact, the final pattern of each cell is determined in its constructor when the class is instantiated. I use the fType variable to control it.

Next comes the mouse interaction. I chose the simplest mouse interaction: clicking. This will manually set the survival state of cells in a circle around the center of the mouse to birth. We can then observe how these artificially placed cells interact with the cells already on the canvas. This is also a manifestation of the concept of "emergence" in the Game of Life.

function mousePressed() {
  const gx = floor(mouseX / grid.s);
  const gy = floor(mouseY / grid.s);
  stampRing(gx, gy, RADIUS, THICKNESS);
}

function stampRing(cx, cy, r = 3, t = 1) {
  const rMin = r - 0.5;
  const rMax = r + t - 0.5;
  for (let dy = -r - t; dy <= r + t; dy++) {
    for (let dx = -r - t; dx <= r + t; dx++) {
      const d = dist(0, 0, dx, dy);
      if (d >= rMin && d <= rMax) {
        stamp(cx + dx, cy + dy);
      }
    }
  }
}

function stamp(x, y) {
  grid.cells[y][x].setAlive(true);
}

The codes mousePressed and stamp are too simple. I will explain how to draw this circle. In fact, the principle is not complicated. It is to calculate the maximum radius and minimum half-pound of the circle, draw a square of different sizes, and then traverse all the elements in the square, calculate their distance from the center cell clicked by the mouse, and if it is less than a certain value, add them to the setAlive lidfad'fa