Rethinking Conway’s Game of Life: Dynamic Environments in Processing


I recently revisited one of my older Processing experiments—a custom Conway's Game of Life where the rules shift based on environmental conditions. After refining the mechanics, I managed to create much more compelling visual motion. Here is a look into my process, along with the complete, usable code.

[Japanese version / 日本語版はこちら]

What is Conway’s Game of Life?

Conway's Game of Life is a iconic cellular automaton devised by mathematician John Conway. From a set of simple rules, surprisingly complex, organic patterns emerge. If you'd like a quick refresher on the fundamentals, check out these resources:

• Conway's Game of Life: Wikipedia
https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
• Cellular Automaton: Wikipedia
https://en.wikipedia.org/wiki/Cellular_automaton

The Core Idea: Rule-Bending Environments

In the classic Game of Life, the rules determining whether a cell lives or dies are immutable. But in the real world, living organisms are constantly shaped by their surroundings. Temperature, climate, terrain—environment changes everything.

That inspired the core hypothesis behind this project: What if environmental parameters dynamic altered the survival rules over time?

Deconstructing the Old Implementation: The Good, the Bad, and the Ugly

Here is the original 2019 implementation for context:


Analyzing my previous code revealed a mix of solid concepts and significant shortcomings:

  • Continuous Health Values (Good): Instead of binary states (0 or 1), I assigned cells a health capacity ranging from 0 to 100, letting environmental rules increment or decrement this value smoothly.
  • Self-Aware Neighbor References (Good): Each cell object maintained references to its adjacent neighbors. This eliminated redundant coordinate lookups during rule evaluation—a neat architectural win.
  • Flawed Responsibility Distribution (Bad): Having cell instances modify their own health based on internal logic felt conceptually messy from an Object-Oriented standpoint.
  • Lackluster Visual Impact (Fatal): I used 3D Perlin noise (x, y, and time) to shift the environment, but the visual connection between the noise and cell behaviors was far too subtle. The resulting motion simply wasn't captivating.

The New Approach: Refactoring for Better Visual Dynamics

To overcome these issues, I introduced several structural and algorithmic refinements:

  • Direction-Aware Neighbor Mapping: Kept the self-contained neighbor approach, but utilized a HashMap to explicitly track directional orientation (N, S, E, W), allowing for dynamic corner-rounding during rendering.
  • Dedicated Field Controller: Introduced a overarching Field class to encapsulate global rules and state management, separating environment handling from individual cell data.
  • Global Environmental Shifts: Replaced local noise with a unified global environmental factor that evolves across the entire board over time, making the cause-and-effect visual relationship obvious.
  • Structured Initial Placement: Arranged starting cell health in deliberate mathematical patterns to produce rhythmic, intriguing animations.

Processing Source Code

Note: This sketch renders offline image frames into a /frames directory for high-quality video exporting rather than running real-time animation loops.

Click to view the source code

/**
 * Castle Walls
 * A Custom Twist on Conway’s Game of Life.
 * 
 * @author @deconbatch
 * @license GPL3
 * @version 0.2
 * Processing 4.3.3
 * updated : 2026/09/10
 * created : 2019/09/23
 */

import java.util.Map;

void setup() {
  size(720, 720);
  colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
  rectMode(CENTER);
  smooth();
  noLoop();
}

void draw() {
  int   frmMax    = 24 * 20;
  float cellSize  = 18.0;
  int   layoutMod = 13; // ex. common divisor of (width / cellSize - 1)
  float lifeFull  = 90.0;
  
  int canvasW = floor(width / cellSize);
  int canvasH = floor(height / cellSize);
  Field field = new Field(canvasW, canvasH, layoutMod, lifeFull);

  translate(cellSize * 0.5, cellSize * 0.5);
  for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {

    float envFactor = map(frmCnt, 0, frmMax - 1, 0.1, 0.8);

    blendMode(BLEND);
    background(map(envFactor, 0.0, 1.0, 120.0, 260.0), 80.0, 30.0, 100.0);
    field.calculateLife(envFactor);
    field.drawField(cellSize);

    saveFrame("frames/anim." + String.format("%04d", frmCnt) + ".png");
  }
  exit();
}

/**
 * Field
 * manage field of cells.
 */
private class Field {
  private Cell[][] cells;
  private int fieldW;
  private int fieldH;
  private int layoutMod;
  private float lifeFull;

  Field(int _w, int _h, int _layoutMod, float _lifeFull) {
    fieldW    = _w;
    fieldH    = _h;
    layoutMod = _layoutMod;
    lifeFull  = _lifeFull;
    initCells();
  }

  /**
   * initCells
   * initialize the cells on the field.
   */
  private void initCells() {
    cells = new Cell[fieldW][fieldH];
    // cells constraction
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        cells[x][y] = new Cell();
        cells[x][y].setLife(lifeFull * (((x * y) % layoutMod == 0) ? 0.6 : 0.9));
      }
    }

    // set 8 neighbor cells
    Map<String, Cell> nei = new HashMap();
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        int mX = getMinus(x, fieldW);
        int mY = getMinus(y, fieldH);
        int pX = getPlus(x, fieldW);
        int pY = getPlus(y, fieldH);

        nei.clear();
        nei.put("w",  cells[mX][y]);
        nei.put("nw", cells[mX][mY]);
        nei.put("n",  cells[x][mY]);
        nei.put("ne", cells[pX][mY]);
        nei.put("e",  cells[pX][y]);
        nei.put("se", cells[pX][pY]);
        nei.put("s",  cells[x][pY]);
        nei.put("sw", cells[mX][pY]);
        cells[x][y].setNeighbors(nei);
      }
    }
  }

  /**
   * calculateLife
   * calculate the life value of the cells with deconbatch's game of life rule.
   * @param  _env : 0.0 - 1.0 : the value that have an impact on calculation
   */
  private void calculateLife(float _env) {
    // calculate neighbors life
    // key of the code: neighbor life > cell life, not neighbor life > fixed value
    int neighborLife[][] = new int[fieldW][fieldH];
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        float cellLife = cells[x][y].getLife();
        neighborLife[x][y] = 0;
        for (Cell nei : cells[x][y].getNeighbors().values()) {
          if (nei.getLife() > cellLife) {
            neighborLife[x][y]++;
          }
        }
      }
    }

    // calculate and set cells life
    float lifeBorder = lifeFull * _env;
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        float cellLife = cells[x][y].getLife();
        if (cellLife < lifeBorder) {
          // Cell is weak
          cellLife -= lifeBorder * 0.4;
          if (neighborLife[x][y] == 3) {
            cellLife += lifeBorder * 0.5;
          }
        } else {
          // Cell is fine
          cellLife += lifeBorder * 0.4;
          if (neighborLife[x][y] == 2) {
            cellLife += lifeBorder * 0.3;
          } else if (neighborLife[x][y] == 3) {
            cellLife += lifeBorder * 0.2;
          } else {
            cellLife -= lifeBorder * 0.4;
          }
        }
        cellLife -= (1.0 - _env) * lifeFull / 100.0;
        
        cells[x][y].setLife(constrain(cellLife, 0.0, lifeFull));
      }
    }
  }

  /**
   * drawField
   * draw the field based on the cells value.
   * @param  _cellSize : the base size of the cell
   */
  private void drawField(float _cellSize) {
    noStroke();
    for (int x = 0; x < fieldW; x++) {
      for (int y = 0; y < fieldH; y++) {
        int life = round(cells[x][y].getLife() / 10.0) * 10;
        
        float eSiz = _cellSize * life / lifeFull;
        float sSiz = _cellSize * sin(PI * life / lifeFull) * 0.25;
        pushMatrix();
        translate(x * _cellSize, y * _cellSize);
        fill(0.0, 0.0, 90.0, 100.0);
        if (life == lifeFull) {
          // corner round
          Map<String, Cell> nei = cells[x][y].getNeighbors();
          float tl = (nei.get("w").getLife() > 85.0 || nei.get("n").getLife() > 85.0) ? 0.0 : eSiz * 0.5;
          float tr = (nei.get("n").getLife() > 85.0 || nei.get("e").getLife() > 85.0) ? 0.0 : eSiz * 0.5;
          float br = (nei.get("e").getLife() > 85.0 || nei.get("s").getLife() > 85.0) ? 0.0 : eSiz * 0.5;
          float bl = (nei.get("s").getLife() > 85.0 || nei.get("w").getLife() > 85.0) ? 0.0 : eSiz * 0.5;
          rect(0.0, 0.0, eSiz, eSiz, tl, tr, br, bl);
        } else {
          circle(0.0, 0.0, eSiz);
        }
        if (sSiz > _cellSize * 0.125) {
          fill(0.0, 0.0, 20.0, 100.0);
          circle(0.0, 0.0, sSiz);
        }
        popMatrix();

      }
    }
  }

  /**
   * 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 Map neighbors;

  Cell() {
    myLife = 0.0;
  }

  /**
   * 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
   */
  public float getLife() {
    return myLife;
  }

  /**
   * setNeighbors
   * set the neighbor cells.
   * @param  _nei : neighbor cells
   */
  public void setNeighbors(Map<String, Cell> _nei) {
    neighbors = new HashMap();
    for (Map.Entry<String, Cell> entry : _nei.entrySet()) {
      neighbors.put(entry.getKey(), entry.getValue());
    }
  }

  /**
   * getNeighbors
   * get the neighbor cells.
   * @return : neighbor cells
   */
  public Map<String, Cell> getNeighbors() {
    return neighbors;
  }

}

/*
Copyright (C) 2026- 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/>
*/
  

Feel free to take this code, modify parameters, tweak initial conditions, or swap out the rules entirely to see what kind of patterns you can discover.

1. Tweak Key Parameters (Beginner)


float cellSize  = 18.0;
int   layoutMod = 13; // ex. common divisor of (width / cellSize - 1)
float lifeFull  = 90.0;

float envFactor = map(frmCnt, 0, frmMax - 1, 0.1, 0.8);

2. Change Initial Cell Layouts (Intermediate)


// cells construction
for (int x = 0; x < fieldW; x++) {
  for (int y = 0; y < fieldH; y++) {
    cells[x][y] = new Cell();
    cells[x][y].setLife(lifeFull * (((x * y) % layoutMod == 0) ? 0.6 : 0.9));
  }
}

3. Rewrite Environmental Rules (Advanced)


// calculate and set cells life
float lifeBorder = lifeFull * _env;
for (int x = 0; x < fieldW; x++) {
  for (int y = 0; y < fieldH; y++) {
    float cellLife = cells[x][y].getLife();
    if (cellLife < lifeBorder) {
      // Cell is weak
      cellLife -= lifeBorder * 0.4;
      if (neighborLife[x][y] == 3) {
        cellLife += lifeBorder * 0.5;
      }
    } else {
      // Cell is fine
      cellLife += lifeBorder * 0.4;
      if (neighborLife[x][y] == 2) {
        cellLife += lifeBorder * 0.3;
      } else if (neighborLife[x][y] == 3) {
        cellLife += lifeBorder * 0.2;
      } else {
        cellLife -= lifeBorder * 0.4;
      }
    }
    cellLife -= (1.0 - _env) * lifeFull / 100.0;
    
    cells[x][y].setLife(constrain(cellLife, 0.0, lifeFull));
  }
}

Final Thoughts

While the concept centers around "a Game of Life influenced by environmental conditions," this project is purely a creative interpretation rather than a rigorous academic model—I hope you enjoy this playful take on the "Game" of Life!

Finding a set of rules that yields continuous, aesthetically pleasing motion takes time. For me, creative coding is all about this endless cycle of testing tiny ideas, failing, and refining.

It’s an iterative, patient process, but the joy of seeing an abstract idea come alive on screen makes it addictive.

Previous Post
No Comment
Add Comment
comment url