Conway's Game of Life is a classic demonstration of emergence, where higher level patterns form from a few simple rules. Fantastic patterns emerge when the game is let to run long enough.
The rules here, to borrow from Wikipedia, are as follows:
- Any live cell with fewer than two live neighbours dies, as if by underpopulation.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by overpopulation.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
Below is a simple Conway's Game of Life implementation:
In [ ]:
import random
def new_board(x, y, num_live_cells=2, num_dead_cells=3):
"""Initializes a board for Conway's Game of Life"""
board = []
for i in range(0, y):
# Defaults to a 3:2 dead cell:live cell ratio
board.append([random.choice([0] * num_dead_cells + [1] * num_live_cells) for _ in range(0, x)])
return board
def get(board, x, y):
"""Return the value at location (x, y) on a board, wrapping around if out-of-bounds"""
return board[y % len(board)][x % len(board[0])]
def assign(board, x, y, value):
"""Assigns a value at location (x, y) on a board, wrapping around if out-of-bounds"""
board[y % len(board)][x % len(board[0])] = value
def count_neighbors(board, x, y):
"""Counts the number of living neighbors a cell at (x, y) on a board has"""
return sum([
get(board, x - 1, y),
get(board, x + 1, y),
get(board, x, y - 1),
get(board, x, y + 1),
get(board, x + 1, y + 1),
get(board, x + 1, y - 1),
get(board, x - 1, y + 1),
get(board, x - 1, y - 1)])
def process_life(board):
"""Creates the next iteration from a passed state of Conway's Game of Life"""
next_board = new_board(len(board[0]), len(board))
for y in range(0, len(board)):
for x in range(0, len(board[y])):
num_neighbors = count_neighbors(board, x, y)
is_alive = get(board, x, y) == 1
if num_neighbors < 2 and is_alive:
assign(next_board, x, y, 0)
elif 2 <= num_neighbors <= 3 and is_alive:
assign(next_board, x, y, 1)
elif num_neighbors > 3 and is_alive:
assign(next_board, x, y, 0)
elif num_neighbors == 3 and not is_alive:
assign(next_board, x, y, 1)
else:
assign(next_board, x, y, 0)
return next_board
In [ ]:
from IPython.display import clear_output
import time
def draw_board(board):
res = ''
for row in board:
for col in row:
if col == 1:
res += '* '
else:
res += ' '
res += '\n'
return res
board = new_board(20, 20)
NUM_ITERATIONS = 100
for i in range(0, NUM_ITERATIONS):
print('Iteration ' + str(i + 1))
board = process_life(board)
res = draw_board(board)
print(res)
time.sleep(0.1)
clear_output(wait=True)
We can use either the PointCloudLayer or ScatterplotLayer from deck.gl to visualize the game.
In [ ]:
import numpy as np
import pandas as pd
import pydeck as deck
PINK = [155, 155, 255, 245]
PURPLE = [255, 155, 255, 245]
SCALING_FACTOR = 1000.0
def convert_board_to_df(board):
"""Makes the board matrix into a list for easier processing"""
rows = []
for x in range(0, len(board[0])):
for y in range(0, len(board)):
rows.append([[x / SCALING_FACTOR, y / SCALING_FACTOR], PURPLE if board[y][x] else PINK])
return pd.DataFrame(rows, columns=['position', 'color'])
board = new_board(30, 30)
records = convert_board_to_df(board)
layer = deck.Layer(
'PointCloudLayer',
records,
get_position='position',
get_color='color',
get_radius=40)
view_state = deck.ViewState(latitude=0.00, longitude=0.00, zoom=13, bearing=44, pitch=45)
r = deck.Deck(layers=[layer], initial_view_state=view_state, map_style='')
r.show()
To play the game over time, we call update
in a loop.
In [ ]:
NUM_ITERATIONS = 100
display(r.show())
for i in range(0, NUM_ITERATIONS):
board = process_life(board)
records = convert_board_to_df(board)
layer.data = records
r.update()
time.sleep(0.1)