Last active
March 21, 2022 22:52
-
-
Save mikelane/89c580b7764f04cf73b32bf4e94fd3a3 to your computer and use it in GitHub Desktop.
Conway's Game of Life implemented using a 2d convolution.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import click | |
import difflib | |
import numpy as np | |
import random | |
import sys | |
import time | |
from os import name, system | |
from scipy.ndimage import convolve | |
np.set_printoptions(threshold=20000, linewidth=350, formatter={"str_kind": lambda x: x}) | |
def _clear_screen(): | |
if name == "nt": | |
system("cls") | |
else: | |
system("clear") | |
def _print_board(board, mark="0"): | |
_clear_screen() | |
to_print = np.where(board == 1, mark, " ") | |
print(to_print) | |
def _is_similar(board, next_board, cutoff): | |
return np.sum(board == next_board) / board.size > cutoff | |
@click.command() | |
@click.option( | |
"--seed", | |
"board_seed", | |
type=click.Choice(["random", "gliders"]), | |
default="random", | |
help="How to initialize the board, default random", | |
) | |
@click.option( | |
"-w", | |
"--width", | |
"board_width", | |
type=int, | |
default=75, | |
help="board width, default 101 columns", | |
) | |
@click.option( | |
"-h", | |
"--height", | |
"board_height", | |
type=int, | |
default=37, | |
help="board height, default 51 rows", | |
) | |
@click.option( | |
"--stop_after", | |
"number_of_iterations", | |
default=float("inf"), | |
help="Stop after this many iterations, default is no limit", | |
) | |
@click.option( | |
"--sleep", | |
"sleep_duration", | |
type=float, | |
default=0.25, | |
help="How long to sleep before updating state", | |
) | |
@click.option( | |
"--alive_mark", | |
"mark", | |
type=str, | |
default="•", | |
help="What mark to use for a living cell", | |
) | |
def run_game( | |
board_width, board_height, board_seed, number_of_iterations, sleep_duration, mark | |
): | |
global boards | |
# Convolving on this kernel does a count of cells around the center | |
kernel = np.array([[1, 1, 1], | |
[1, 0, 1], | |
[1, 1, 1]], dtype=np.uint8) | |
# A well-known game of life stable, moving character | |
glider = np.array([[0, 1, 0], | |
[0, 0, 1], | |
[1, 1, 1]], dtype=np.uint8) | |
if board_seed == "gliders": | |
board = np.zeros(shape=(board_height, board_width), dtype=np.uint8) | |
h, w = glider.shape | |
num_gliders = board.size // (9 * 25) | |
for _ in range(num_gliders): | |
i, j = ( | |
random.randint(0, board_height - h), | |
random.randint(0, board_width - w), | |
) | |
board[i : i + h, j : j + w] = glider | |
else: | |
board = np.random.randint( | |
0, 2, size=(board_height, board_width), dtype=np.uint8 | |
) | |
_print_board(board, mark=mark) | |
count = 0 | |
cutoff = 1 | |
while count < number_of_iterations: | |
time.sleep(sleep_duration) | |
count += 1 | |
# Run a single 2D convolutional filter over the board with constant 0 padding | |
convolved_board = convolve(board, kernel, mode="wrap") | |
# The kernel we used finds the sum of the 8 cells around a given cell | |
# So we can do a bit of fancy numpy work to get the next board | |
next_board = ( | |
((board == 1) & (convolved_board > 1) & (convolved_board < 4)) | |
| ((board == 0) & (convolved_board == 3)) | |
).astype(np.uint8) | |
if count % 10 == 0: | |
cutoff -= 0.001 | |
_print_board(next_board, mark=mark) | |
print( | |
f"count: {count}, cutoff: {cutoff:0.3f}, diff: {np.sum(board == next_board)}/{board.size} ({np.sum(board == next_board)/board.size:0.4f})" | |
) | |
if _is_similar(board, next_board, cutoff): | |
sys.exit(0) | |
board = next_board | |
if __name__ == "__main__": | |
run_game() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment