Traditional 2D-grid systems are probably the most famous forms of cellular automata. Pioneered by John von Neumann in the late 40's, cellular automata became famous in the 70's with the implementation of "Conway's Game of Life". However, there a multiple forms of cellular automata, some of which are not restricted to grids anymore.
The world of automata is vast and diverse. If you're interested in this family of algorithms I recommend to you:



Langton's ant

This algorithm is a very simple form of cellular automata. A 2D grid system is made of tiles that can be white or black. An agent is placed at a particular row and column in the grid. If the agent lands in a white tile, the agent turns 90 deg. in a clockwise direction, changes the tile's color to black, then moving forward. The same goes for black tiles, except the rotation is counter-clockwise.
Let's take a look at how the algorithm works

The parameters of the Algorithm

  • RES: int - tile size
  • DIMS: (int,int) - Number of columns and rows of tiles that make the grid
  • SCREEN: (int,int) - Dimensions of the screen in pixels
  • FPS: int - Frames per second of animation
  • TILE: Tile() - Class Tile that represent the basic unit of information in the system
  • GRID: [[TILE, ..., n=DIMS[0]], ..., n = DIMS[1]] - a 2D matrix of TILE with dimensions given by the columns and rows of DIMS
  • CURRENT_ANT: TILE - A position in the grid, considered the ant
  • DIRECTION: [int, int, int, int] - current direction of the ant as one hot vector with the order of directions: Right, Down, Left, Up
# MODULES
import pygame
import random
import math

# DATA DEFS
RES = 4
DIMS = [160,160]
SCREEN = (RES*DIMS[0],RES*DIMS[1])
display = pygame.display.set_mode(SCREEN)
FPS = 120

# DD. TILE
# tile = Tile(int, int,int)
# interp. a tile to be stepped on by an ant
class Tile():
    def __init__(self,x,y):
        self.x = x * RES
        self.y = y * RES
        self.c = x
        self.r = y
        self.rect = pygame.Rect(self.x, self.y, RES, RES)
        self.rect.topleft = (self.x, self.y)
        self.state = 0

    def drawSquare(self):
        pygame.draw.rect(display,self.getColor(),self.rect)

    def getColor(self):
        if self.state == 0: return "white"
        return "black"

# tile = Tile(0,0)   #stub

# DD. TILEROW
# tileRow = [TILE, ..., n=DIMS[0]]
# interp. a row of tiles in the system
tileRow = []
for c in range(DIMS[0]):
    tile = Tile(c,0)
    tileRow.append(tile)

# --------TEMPLATE FOR TILEROW--------
# for tile in tileRow:
#   ... tile



# DD. GRID
# grid = [TILEROW, ..., n=DIMS[1]]
# interp. a grid of tiles in a 2D array
grid = []
for r in range(DIMS[1]):
    tileRow = []
    for c in range(DIMS[0]):
        tile = Tile(c,r)
        tileRow.append(tile)
    grid.append(tileRow)

# --------TEMPLATE FOR GRID -------
# for tileRow in grid:
#     for tile in tileRow:
#         ...tile


# DD. CURRENTANT
# ca = TILE
# interp. a tile from the grid to represent a current ant
ca = grid[DIMS[0]//2][DIMS[1]//2]

# DD. DIRECTION
# dir = [int,int,int,int]
# interp. the direction of the ant as RIGHT, DOWN, LEFT, UP
dir_R = [1,0,0,0] #RIGHT
dir_D = [0,1,0,0] #DOWN
dir_L = [0,0,1,0] #LEFT
dir_U = [0,0,0,1] #UP

The Algorithm

  • Is the CURRENT_ANT state 0 (white)?
    • T: correct the DIRECTION to index + 1. Change the color to Black
    • F: correct the DIRECTION to index - 1. Change the color to white
  • Update the position of the CURRENT_ANT, following the new DIRECTION

Here's a step by step tutorial on how to implement Langton's algorithm!
# Langton's ant is a two-dimensional universal Turing machine with a very simple set of rules but complex emergent
# behavior. It was invented by Chris Langton in 1986 and runs on a square lattice of black and white cells.
# The universality of Langton's ant was proven in 2000.[2] The idea has been generalized in several different ways,
# such as turmites which add more colors and more states.

# MODULES
import pygame
import random
import math

# DATA DEFS
RES = 4
DIMS = [160,160]
SCREEN = (RES*DIMS[0],RES*DIMS[1])
display = pygame.display.set_mode(SCREEN)
FPS = 120

# DD. TILE
# tile = Tile(int, int,int)
# interp. a tile to be stepped on by an ant
class Tile():
    def __init__(self,x,y):
        self.x = x * RES
        self.y = y * RES
        self.c = x
        self.r = y
        self.rect = pygame.Rect(self.x, self.y, RES, RES)
        self.rect.topleft = (self.x, self.y)
        self.state = 0

    def drawSquare(self):
        pygame.draw.rect(display,self.getColor(),self.rect)

    def getColor(self):
        if self.state == 0: return "white"
        return "black"

# tile = Tile(0,0)   #stub

# DD. TILEROW
# tileRow = [TILE, ..., n=DIMS[0]]
# interp. a row of tiles in the system
tileRow = []
for c in range(DIMS[0]):
    tile = Tile(c,0)
    tileRow.append(tile)

# --------TEMPLATE FOR TILEROW--------
# for tile in tileRow:
#   ... tile



# DD. GRID
# grid = [TILEROW, ..., n=DIMS[1]]
# interp. a grid of tiles in a 2D array
grid = []
for r in range(DIMS[1]):
    tileRow = []
    for c in range(DIMS[0]):
        tile = Tile(c,r)
        tileRow.append(tile)
    grid.append(tileRow)

# --------TEMPLATE FOR GRID -------
# for tileRow in grid:
#     for tile in tileRow:
#         ...tile


# DD. CURRENTANT
# ca = TILE
# interp. a tile from the grid to represent a current ant
ca = grid[DIMS[0]//2][DIMS[1]//2]

# DD. DIRECTION
# dir = [int,int,int,int]
# interp. the direction of the ant as RIGHT, DOWN, LEFT, UP
dir_R = [1,0,0,0] #RIGHT
dir_D = [0,1,0,0] #DOWN
dir_L = [0,0,1,0] #LEFT
dir_U = [0,0,0,1] #UP

################# CODING #################
dir = list(dir_R)


def draw():
    display.fill("#1e1e1e")
    for tileRow in grid:
        for tile in tileRow:
            tile.drawSquare()
    pygame.display.update()
    pygame.time.Clock().tick(FPS)

# FD. updateTile()
# purp. update the states of the tile where the ant is at this frame
# If state is 0, change it to 1, else 0
def updateTile():
    if grid[ca.r][ca.c].state == 0:
        grid[ca.r][ca.c].state = 1
    else:
        grid[ca.r][ca.c].state = 0

# FD. updateDir()
# purp. determine the direction of the variable DIRECTION based on the state of CURRENTANT
# if state is 0 ("white"), go to the next index in the list dir
def updateDir():
    global ca
    global dir
    if ca.state == 0:
        newOnePos = (dir.index(1)+1) % len(dir)
        dir = [0 for i in range(4)]
        dir[newOnePos] = 1
    else:
        newOnePos = (dir.index(1)-1) % len(dir)
        dir = [0 for i in range(4)]
        dir[newOnePos] = 1


    # move ant
    if dir == dir_R:
        ca = grid[(ca.r+1)%len(grid)][ca.c]
    if dir == dir_D:
        ca = grid[ca.r][(ca.c+1)%len(grid[0])]
    if dir == dir_L:
        ca = grid[ca.r-1][ca.c]
    if dir == dir_U:
        ca = grid[ca.r][ca.c-1]


def update():
    global dir
    updateTile()
    updateDir()
    # if the color is white the state is 0. Change it to 1, change direction 90 degrees to the right, then move in that dir
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
        



while True:
    draw()
    update()