How to Color Conway's Game of Life Cells Based on Neighbor Count using NumPy and Matplotlib
Introduction
Conway's Game of Life is a classic cellular automaton that produces fascinating emergent patterns from simple rules. A common visual enhancement is coloring active cells dynamically based on their live neighbor counts. If you are using numpy for grid calculations and matplotlib.pyplot.imshow for rendering, achieving this requires mapping neighbor counts to distinct matrix values and configuring a custom ListedColormap.
The Core Challenge
In a standard Game of Life implementation, the grid contains binary values: 0 for dead cells and 1 for live cells. Passing this standard matrix to Matplotlib's imshow limits visual output to two colors. To render cells according to their neighbor counts, we need to transform the state matrix so that each alive cell holds a value representing its neighbor count (or neighbor count offset), while dead cells remain 0.
Step-by-Step Solution
- Calculate Neighbor Counts: Compute 2D neighbor counts using array slicing or convolution.
- Determine Alive Cells: Apply Game of Life rules (birth and survival) to determine which cells remain or become alive.
- Map Neighbor Values: Construct a visual matrix where dead cells are
0, and alive cells storeneighbor_count + 1. Offset by1so that an alive cell with0neighbors is distinct from a dead cell (value0). - Configure Colormap: Define a
ListedColormapwith 10 distinct colors (1 background color + 9 neighbor count state colors from 0 to 8).
Complete Python Code Implementation
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.colors import ListedColormap
N = 50 # Grid size
generations = 0
def create_universe(N=50, p=0.5):
return np.random.choice([0, 1], size=(N, N), p=[1-p, p])
universe = create_universe(N=N, p=0.5)
# Define 10 distinct colors:
# Color 0: Dead cell background (e.g., black)
# Colors 1-9: Alive cells with 0 to 8 neighbors
colors = [
'#111111', # 0: Dead (Dark Gray/Black)
'#440154', # 1: Alive, 0 neighbors
'#482677', # 2: Alive, 1 neighbor
'#414487', # 3: Alive, 2 neighbors
'#35608D', # 4: Alive, 3 neighbors
'#2A788E', # 5: Alive, 4 neighbors
'#21908C', # 6: Alive, 5 neighbors
'#22A884', # 7: Alive, 6 neighbors
'#7AD151', # 8: Alive, 7 neighbors
'#FDE725' # 9: Alive, 8 neighbors
]
cmap = ListedColormap(colors)
def animate(frame, universe, img):
global generations
# Enforce boundary conditions (zero out edges)
universe[0, :] = universe[-1, :] = universe[:, 0] = universe[:, -1] = 0
# Calculate neighbor counts
neighbor_count = np.zeros((N, N), dtype=int)
neighbor_count[1:-1, 1:-1] = (
universe[:-2, :-2] + universe[:-2, 1:-1] + universe[:-2, 2:] +
universe[1:-1, :-2] + universe[1:-1, 2:] +
universe[2:, :-2] + universe[2:, 1:-1] + universe[2:, 2:]
)
# Apply Game of Life rules
birth = (neighbor_count == 3) & (universe == 0)
survive = ((neighbor_count == 2) | (neighbor_count == 3)) & (universe == 1)
# Update universe state (binary 0 or 1)
universe[:] = 0
universe[birth | survive] = 1
# Recalculate neighbor counts on the updated universe for accurate post-step display
post_neighbor_count = np.zeros((N, N), dtype=int)
post_neighbor_count[1:-1, 1:-1] = (
universe[:-2, :-2] + universe[:-2, 1:-1] + universe[:-2, 2:] +
universe[1:-1, :-2] + universe[1:-1, 2:] +
universe[2:, :-2] + universe[2:, 1:-1] + universe[2:, 2:]
)
# Create display matrix: dead cells = 0, alive cells = neighbor_count + 1
display_grid = np.where(universe == 1, post_neighbor_count + 1, 0)
population = np.count_nonzero(universe)
img.set_data(display_grid)
ax.set_xlabel(f'Generation: {generations} | Population: {population}')
generations += 1
# Setup figure and axes
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xticks([])
ax.set_yticks([])
# Initialize plot matrix with vmin=0 and vmax=9
display_universe = np.zeros((N, N), dtype=int)
img = ax.imshow(display_universe, cmap=cmap, vmin=0, vmax=9, interpolation='nearest')
ani = FuncAnimation(fig, animate, fargs=(universe, img), frames=200, interval=100)
plt.show()Key Takeaways
By decoupling the logical simulation state (stored in universe) from the display state (display_grid), you gain total control over color customization. Set vmin=0 and vmax=9 in imshow to ensure fixed color index mappings across frames during animation.