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 store neighbor_count + 1. Offset by 1 so that an alive cell with 0 neighbors is distinct from a dead cell (value 0).
  • Configure Colormap: Define a ListedColormap with 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.