Skip to content

Instantly share code, notes, and snippets.

@hdurdle
Created September 2, 2025 06:57
Show Gist options
  • Save hdurdle/cfbf13d54cfbe8a9c14aba4e5fca2080 to your computer and use it in GitHub Desktop.
Save hdurdle/cfbf13d54cfbe8a9c14aba4e5fca2080 to your computer and use it in GitHub Desktop.
WOPR sim / Game of Life
from machine import Pin, SPI
from time import sleep
import max7219
import random
import gc
# --- hardware ---
spi = SPI(0, sck=Pin(2), mosi=Pin(3))
cs = Pin(5, Pin.OUT)
button = Pin(0, Pin.IN, Pin.PULL_UP)
prev_button_state = 1
display = max7219.Matrix8x8(spi, cs, 12)
display.brightness(0)
# --- grid ---
WIDTH, HEIGHT = 96, 8
SIZE = WIDTH * HEIGHT
# --- options ###
FRAME_RATE = 24
REPEAT_LIMIT = FRAME_RATE
TICK_TIME = 1/FRAME_RATE
RANDOM_MODE = 0
# reusable grid buffers
grid = bytearray(SIZE)
next_grid = bytearray(SIZE)
def idx(x, y):
return y * WIDTH + x
def draw_grid(buf):
for y in range(HEIGHT):
base = y * WIDTH
for x in range(WIDTH):
display.pixel(x, y, 1 if buf[base + x] else 0)
display.show()
def randomise_in_place(buf):
for i in range(SIZE):
# MicroPython's random has getrandbits; alternativelly use randint(0,1)
buf[i] = random.getrandbits(1)
def update_in_place(src, dst):
# Compute next generation into 'dst' using toroidal wrap
for y in range(HEIGHT):
y_up = (y - 1) % HEIGHT
y_dn = (y + 1) % HEIGHT
yb = y * WIDTH
yub = y_up * WIDTH
ydb = y_dn * WIDTH
for x in range(WIDTH):
xm = (x - 1) % WIDTH
xp = (x + 1) % WIDTH
# Sum 8 neighbors
n = (
src[yub + xm] + src[yub + x] + src[yub + xp] +
src[yb + xm] + src[yb + xp] +
src[ydb + xm] + src[ydb + x] + src[ydb + xp]
)
c = src[yb + x]
dst[yb + x] = 1 if (c and (n == 2 or n == 3)) or (not c and n == 3) else 0
# --- lightweight cycle detection (fixed-size, hash-based) ---
class LifeCycleDetector:
def __init__(self, max_history=16):
self.max_history = max_history
self.seen = {} # hash -> generation index
self.order = [] # FIFO of hashes
self.generation = 0
@staticmethod
def _hash32(buf):
# FNV-1a 32-bit
h = 2166136261
for b in buf:
h ^= b
h = (h * 16777619) & 0xFFFFFFFF
return h
def update(self, buf):
h = self._hash32(buf)
gen = self.generation
self.generation += 1
if h in self.seen:
period = gen - self.seen[h]
if period > 0:
return True, period
self.seen[h] = gen
self.order.append(h)
if len(self.order) > self.max_history:
old = self.order.pop(0)
# Safe to remove; keeps dict bounded
if old in self.seen and self.seen[old] <= gen - self.max_history:
del self.seen[old]
return False, None
det = LifeCycleDetector(max_history=16)
# --- main ---
randomise_in_place(grid)
while True:
sleep(TICK_TIME)
button_state = button.value()
if prev_button_state == 0 and button_state == 1:
print("Toggle Animation Type")
RANDOM_MODE ^= 1
prev_button_state = button_state
draw_grid(grid)
if RANDOM_MODE == 1:
randomise_in_place(next_grid)
timeflip = random.randint(0,3)
if (timeflip == 0):
sleep(1-TICK_TIME)
elif (timeflip == 1):
sleep(1.5-TICK_TIME)
elif (timeflip == 2):
sleep(2-TICK_TIME)
else:
sleep(0.5-TICK_TIME)
else:
update_in_place(grid, next_grid)
steady, period = det.update(next_grid)
if steady and period and period > REPEAT_LIMIT:
randomise_in_place(next_grid)
# Swap buffers; no allocations
grid, next_grid = next_grid, grid
# Keep fragmentation under control
if (det.generation & 31) == 0:
gc.collect()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment