Last active
January 23, 2024 06:11
-
-
Save tuna-f1sh/9e6ff4552f75de3705cae6d3c044b1cc to your computer and use it in GitHub Desktop.
Async task that updates PIL Image with Game of Life
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
""" | |
Game of Life async task that updates passed PIL Image with generations | |
loosely based on https://codereview.stackexchange.com/questions/125292/conways-game-of-life-in-python-saving-to-an-image | |
but fixes the critical bug which runs the frame update loop on a transient grid resulting in wrong behaviour - I don't have enough points to post fix! | |
Copied from part of a bigger project I'm working on, this can still be run with the python flipdot module imported using a script. | |
""" | |
import random, asyncio | |
from PIL import Image | |
NEIGHBOURS = range(-1, 2) | |
DEAD_OR_ALIVE = ((0,0,0), (255,255,255)) | |
def initial(im: Image, dead_or_alive:tuple=DEAD_OR_ALIVE): | |
""" | |
Fill passed image with random dead/alive cells | |
:param im Image: image to intialise | |
:param dead_or_alive tuple: (dead RGB, alive RGB) | |
""" | |
grid = im.load() | |
gx, gy = im.size | |
for y in range(gy): | |
for x in range(gx): | |
random_state = random.randint(0, 5) | |
grid[x, y] = dead_or_alive[1] if random_state == 4 else dead_or_alive[0] | |
def compute_next_generation(im: Image, dead_or_alive:tuple=DEAD_OR_ALIVE): | |
""" | |
Calculate the next generation in the game of life based on current image, im | |
:param im Image: current generation image | |
:param dead_or_alive tuple: (dead RGB, alive RGB) | |
""" | |
width, height = im.size | |
# keep current state during scan | |
grid = im.copy().load() | |
# image reference to update | |
update = im.load() | |
for y in range(height): | |
for x in range(width): | |
# N N N | |
# N C N | |
# N N N | |
neighbours = sum( | |
sum(grid[(x+xx) % width, (y+yy) % height]) | |
for yy in NEIGHBOURS | |
for xx in NEIGHBOURS | |
) - sum(grid[x, y]) | |
# exactly three is alive | |
if neighbours == sum(dead_or_alive[1] * 3): | |
update[x, y] = dead_or_alive[1] | |
# two remains alive/dead | |
elif neighbours == sum(dead_or_alive[1] * 2): | |
pass | |
# dead if greater than 3 or less than 2 | |
else: | |
update[x, y] = dead_or_alive[0] | |
del grid | |
async def game_of_life(d, refresh=0.2, white=True, duration=60, reset=False): | |
""" | |
Main task that updates image with next generation at supplied refresh rate for number of rounds based on passed duration | |
:param d Display: flip-dot display class | |
:param refresh float: refresh rate (s) default 0.2 | |
:param white bool: white alive if True else black | |
:param duration float: desired run time | |
:param reset bool: clear display at start or use curret image as first generation | |
""" | |
on = (255,255,255) | |
off = (0,0,0) | |
dead_or_alive = (off, on) | |
if not white: dead_or_alive = dead_or_alive[::-1] | |
# auto duration is 60 seconds | |
if duration == 0: duration = 60 | |
rounds = int(duration / refresh) | |
gsize = d.im.size | |
if reset: | |
d.reset(white=not white) | |
initial(d.im, dead_or_alive=dead_or_alive) | |
for i in range(rounds): | |
compute_next_generation(d.im, dead_or_alive=dead_or_alive) | |
d.send() | |
await asyncio.sleep(refresh) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment