Conway's Game of Life with Dynamic Environmental Noise
Castle Walls: A Custom Twist on Conway’s Game of Life
This is a generative animation built with Processing.
The resulting visuals evoke a sense of continuous transformation—almost like ancient stone walls being consumed by fire and then slowly rebuilding themselves.
Larger Cell Grid Example
I wanted to experiment with custom rules for Conway’s Game of Life. Specifically, I wondered: What if the underlying rules of the system evolved over time, much like a changing physical environment?
To implement this, I introduced 2D Perlin noise to act as a global "environmental factor" that continuously alters the conditions for a cell’s birth and death. Shifting climates, sudden wildfires, droughts, or fertile rainfall—these environmental waves dynamically seal the fate of every cell on the grid!
Processing Source Code
Please feel free to use this Processing example code under the terms of the GPL.
I’d be honored and thrilled to see any works you create based on this code!
Note: This script renders frames directly to the /frames directory rather than relying only on real-time playback. You can stitch these image files together to create a smooth, high-resolution video animation.
/**
* Castle Walls.
* an animation with the game of life of my own rule.
*
* @author @deconbatch
* @version 0.1
* Processing 3.2.1
* created : 2019/09/23
*/
void setup() {
size(720, 720);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
smooth();
noStroke();
noLoop();
}
void draw() {
int frmMax = 24 * 16; // for 24fps x 16s animation
float cellSize = 4.0;
int canvasW = floor(width / cellSize);
int canvasH = floor(height / cellSize);
Cell[][] cells = new Cell[canvasW][canvasH];
// cells constraction
for (int x = 0; x < canvasW; x++) {
for (int y = 0; y < canvasH; y++) {
cells[x][y] = new Cell();
cells[x][y].setLife(random(90.0));
}
}
// set neighbor cells
for (int x = 0; x < canvasW; x++) {
for (int y = 0; y < canvasH; y++) {
int mX = getMinus(x, canvasW);
int mY = getMinus(y, canvasH);
int pX = getPlus(x, canvasW);
int pY = getPlus(y, canvasH);
cells[x][y].setNeighbor(new Cell[] {
// 8 directions
cells[mX][mY],
cells[pX][mY],
cells[mX][pY],
cells[pX][pY],
cells[x][mY],
cells[x][pY],
cells[mX][y],
cells[pX][y]
});
}
}
for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
background(0.0, 0.0, 100.0, 100.0);
float frmRatio = map(frmCnt, 0, frmMax, 0.0, 1.0);
// game of life calculation
for (int x = 0; x < canvasW; x++) {
for (int y = 0; y < canvasH; y++) {
cells[x][y].sumLife();
}
}
for (int x = 0; x < canvasW; x++) {
for (int y = 0; y < canvasH; y++) {
float envFactor = noise(frmRatio * 2.5, x * 0.05, y * 0.05);
// game of life calculation
cells[x][y].liveOrDie(envFactor);
// draw cells
float eHue = map(envFactor, 0.0, 1.0, 300.0, 360.0 + 300.0) % 360.0;
float eSat = cells[x][y].getLife() * 40.0;
float eBri = 100.0 - cells[x][y].getLife();
float eSiz = cellSize * (100.0 - cells[x][y].getLife() * 0.5) * 0.01;
fill(eHue, eSat, eBri, 100.0);
ellipse(x * cellSize, y * cellSize, eSiz, eSiz);
}
}
saveFrame("frames/" + String.format("%04d", frmCnt) + ".png");
}
exit();
}
/**
* getMinus
* calculate the coordinates of the point. take overflow into account.
* @param _a : coordinates of the point, x or y
* @param _border : canvas width or height
*/
private int getMinus(int _a, int _border) {
int ret = _a - 1;
if (ret < 0) {
ret = _border - 1;
}
return ret;
}
/**
* getPlus
* calculate the coordinates of the point. take overflow into account.
* @param _a : coordinates of the point, x or y
* @param _border : canvas width or height
*/
private int getPlus(int _a, int _border) {
int ret = _a + 1;
if (ret >= _border) {
ret = 0;
}
return ret;
}
/**
* Cell
* manage one cell.
*/
private class Cell {
private float myLife;
private float neighborLives;
private Cell[] neighbor;
Cell() {
myLife = 0.0;
}
/**
* setNeighbor
* set the neighbor cells.
* @param _neighbor : the neighbor cells array
*/
public void setNeighbor(Cell[] _neighbor) {
neighbor = new Cell[_neighbor.length];
for (int i = 0; i < _neighbor.length; i++) {
neighbor[i] = _neighbor[i];
}
}
/**
* setLife
* set the life value of this cell.
* @param _life : life value
*/
public void setLife(float _life) {
myLife = _life;
}
/**
* getLife
* get the life value of this cell.
* @return : life value 0.0 - 100.0
*/
public float getLife() {
return constrain(myLife, 0.0, 100.0);
}
/**
* sumLife
* sum up the life value of neighbor cells.
*/
public void sumLife() {
neighborLives = 0.0;
for (int i = 0; i < neighbor.length; i++) {
neighborLives += neighbor[i].getLife();
}
}
/**
* liveOrDie
* calculate the life value of this cell with deconbatch's game of life rule.
* @param _environment : the value that have an impact on calculation
*/
public void liveOrDie(float _environment) {
float lifeBorder = 50.0;
float paramA = map(_environment, 0.0, 1.0, 2.5, 3.5);
float paramB = map(_environment, 0.0, 1.0, 3.5, 4.5);
float paramC = map(_environment, 0.0, 1.0, 1.5, 0.5);
if (myLife < lifeBorder) {
// I'm dead
if (neighborLives > lifeBorder * paramA && neighborLives < lifeBorder * paramB) {
// inject vitality
myLife += 15.0;
} else {
// suck
myLife -= 15.0;
}
} else {
// I'm alive
if (neighborLives < lifeBorder * paramC || neighborLives > lifeBorder * paramB) {
// suck
myLife -= 20.0;
}
}
myLife = constrain(myLife, 0.0, 100.0);
}
}
/*
Copyright (C) 2019- deconbatch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
