-
-
Save theY4Kman/ef5796cf1087f79c2312963d0151d668 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 random | |
import sys | |
import time | |
from io import StringIO | |
import click | |
import numpy as np | |
from asciimatics.event import MouseEvent | |
from asciimatics.screen import Screen | |
from scipy.ndimage import convolve | |
np.set_printoptions(threshold=20000, linewidth=350, formatter={"str_kind": lambda x: x}) | |
@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, | |
) | |
screen = Screen.open(height=len(board) + 10) # extra space for summaries, etc | |
try: | |
next_board = board | |
_print_board(screen, next_board, mark=mark) | |
# Timestamp to begin processing the board after | |
# (allows us to stop the world for user input) | |
resume_board_at = 0 | |
count = 0 | |
cutoff = 1 | |
while count < number_of_iterations: | |
count += 1 | |
while True: | |
# Handle user input | |
event = screen.get_event() | |
# Allow user to add new cells | |
if event and isinstance(event, MouseEvent): | |
x = event.x // 2 | |
y = event.y | |
if x < board_width and y < board_height: | |
board[y, x] = not board[y, x] | |
if board[y, x]: | |
screen.paint(mark, x=x * 2, y=y, colour=Screen.COLOUR_GREEN) | |
screen.refresh() | |
resume_board_at = time.monotonic() + 1.0 | |
if time.monotonic() < resume_board_at: | |
screen.print_at( | |
f'Paused for interaction. Resuming in {resume_board_at - time.monotonic():.2f}s', | |
x=0, | |
y=board_height, | |
) | |
screen.refresh() | |
time.sleep(0.1) | |
else: | |
break | |
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(screen, next_board, mark=mark) | |
screen.print_at( | |
f"count: {count}, " | |
f"cutoff: {cutoff:0.3f}, " | |
f"diff: {np.sum(board == next_board)}/{board.size} ({np.sum(board == next_board)/board.size:0.4f})", | |
x=0, | |
y=len(board), | |
) | |
screen.refresh() | |
if _is_similar(board, next_board, cutoff): | |
sys.exit(0) | |
board = next_board | |
finally: | |
screen.close() | |
def _print_board(screen: Screen, board, mark="0"): | |
to_print = np.where(board == 1, mark, " ") | |
for y, row in enumerate(to_print): | |
buffer = StringIO() | |
np.savetxt(buffer, row, '%s', encoding='utf-8') | |
screen.print_at(buffer.getvalue(), 0, y) | |
def _is_similar(board, next_board, cutoff): | |
return np.sum(board == next_board) / board.size > cutoff | |
if __name__ == "__main__": | |
run_game() |
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
scipy | |
numpy | |
click | |
asciimatics |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment