Skip to content

Instantly share code, notes, and snippets.

@tuna-f1sh
Last active January 23, 2024 06:11
Show Gist options
  • Save tuna-f1sh/9e6ff4552f75de3705cae6d3c044b1cc to your computer and use it in GitHub Desktop.
Save tuna-f1sh/9e6ff4552f75de3705cae6d3c044b1cc to your computer and use it in GitHub Desktop.
Async task that updates PIL Image with Game of Life
"""
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