Last active
November 10, 2017 13:21
-
-
Save bbjubjub2494/95799b35e2d3aac54cdd0e4a7c8d2037 to your computer and use it in GitHub Desktop.
functional conway game of life using sets
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
#!/usr/bin/env python3 | |
"""Python 3.6 KISS implementation of Conway's game of life | |
Inspired by Stop Writing Classes by Jack Diederich, | |
https://www.youtube.com/watch?v=o9pEzgHorH0 | |
""" | |
from collections import defaultdict | |
from itertools import * | |
from typing import * | |
Point = Tuple[int, int] | |
def neighbours(c: Point) -> Sequence[Point]: | |
x, y = c | |
return [ | |
(x-1,y-1),(x ,y-1),(x+1,y-1), | |
(x-1,y ), (x+1,y ), | |
(x-1,y+1),(x ,y+1),(x+1,y+1), | |
] | |
Grid = FrozenSet[Point] | |
def tick(grid: Grid) -> Grid: | |
ncount: Dict[Point, int] = defaultdict(int) | |
for c in grid: | |
for n in neighbours(c): | |
ncount[n] += 1 | |
return frozenset(c for c, x in ncount.items() | |
if x == 2 and c in grid | |
or x == 3) | |
"""some cheap basic non-exhaustive correctness tests""" | |
block = frozenset({(0,0), (0,1), (1,0), (1,1)}) | |
assert tick(block) == block | |
blinker1 = frozenset({(1,0), (1,1), (1,2)}) | |
blinker2 = frozenset({(0,1), (1,1), (2,1)}) | |
assert tick(blinker1) == blinker2 | |
assert tick(blinker2) == blinker1 | |
glider1 = frozenset({(0,0), (1,1), (1,2), (2,1), (2,0)}) | |
glider3 = frozenset({(1,0), (2,1), (2,2), (3,1), (1,2)}) | |
assert tick(tick(glider1)) == glider3 | |
"""simple RLE parser | |
Not accounted for: the usual ! terminator for RLE-encoded patterns on the internet. | |
""" | |
import re | |
_RLE_ELT = re.compile(r'(\d*)(b|o)(\$?)') | |
def rle_parse(data: str) -> Grid: | |
return frozenset(_rle_parse(data)) | |
def _rle_parse(data: str) -> Iterator[Point]: | |
pos = 0 | |
x, y = 0, 0 | |
for elt in iter(lambda: _RLE_ELT.match(data, pos), None): | |
rep = int(elt[1]) if elt[1] else 1 | |
if elt[2] == 'o': | |
yield from zip(range(x, x+rep), repeat(y)) | |
if elt[3]: | |
x, y = 0, y+1 | |
else: | |
x += rep | |
pos = elt.end() | |
acorn = frozenset({(0,0), (1,0), (1,2), (3,1), (4,0), (5,0), (6,0)}) | |
diehard = frozenset({(0,1), (1,0), (1,1), (5,0), (6,0), (6,2), (7,0)}) | |
glider = rle_parse('bo$2bo$3o') | |
gosper_glider_gun = rle_parse('24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4bobo$10bo5bo7bo$11bo3bo$12b2o') | |
herschel = rle_parse('o$3o$obo$2bo') | |
lay = frozenset({(0,0), (2,0), (2,1), (4,2), (4,3), (4,4), (6,3), (6,4), (6,5), (7,4)}) | |
lay = frozenset({(0,0), (0,1), (0,2), (0,4), (1,0), (2,3), (2,4), (3,1), (3,2), (3,4), (4,0), (4,2), (4,4)}) | |
line = rle_parse('8ob5o3b3o6b7ob5o') | |
lwss = frozenset({(0,0), (0,3), (1,4), (2,0), (2,4), (3,1), (3,2), (3,3), (3,4)}) | |
mess = frozenset({(0,3), (0,5), (1,4), (1,5), (2,4), (3,0), (3,1), (3,2), (4,2), (5,1)}) | |
r_pentomino = frozenset({(0,1), (0,2), (1,0), (1,1), (2,1)}) | |
toad = frozenset({(0,1), (0,2), (0,3), (1,0), (1,1), (1,2)}) | |
import contextlib, curses, time, math | |
# A character represents two cells on top of each other. | |
# This maps (top_cell_alive, bottom_cell_alive) to how it can be displayed. | |
_draw_charmap = { | |
(False, False): b' ', | |
(True, False): '\u2580'.encode(), | |
(False, True): '\u2584'.encode(), | |
(True, True): '\u2588'.encode(), | |
} | |
def _draw(win, grid: Grid) -> None: | |
"""draw the grid on the curses window win""" | |
win.clear() | |
if not grid: | |
win.noutrefresh() | |
return | |
xs, ys = zip(*grid) | |
# maximum more like supremum | |
xmin, ymin, xmax, ymax = min(xs), min(ys)//2, max(xs)+1, max(ys)//2+1 | |
Y, X = win.getmaxyx() | |
xhalf1, yhalf1 = X // 2, Y // 2 | |
xhalf2, yhalf2 = X - xhalf1, Y - yhalf1 | |
xmin, ymin = max(xmin, -xhalf1), max(ymin, -yhalf1) | |
xmax, ymax = min(xmax, xhalf2), min(ymax, yhalf2) | |
for winy, y in enumerate(range(ymin, ymax), yhalf1 + ymin): | |
y *= 2 | |
for winx, x in enumerate(range(xmin, xmax), xhalf1 + xmin): | |
win.insstr(winy, winx, _draw_charmap[(x, y) in grid, (x, y+1) in grid]) | |
win.noutrefresh() | |
class suppress(contextlib.suppress, contextlib.ContextDecorator): | |
pass | |
@curses.wrapper | |
@suppress(KeyboardInterrupt) | |
def main(stdscr): | |
FREQ = .2 | |
curses.curs_set(0) | |
win = stdscr | |
Y, X = win.getmaxyx() | |
grid_win = win.derwin(Y-1, X, 0, 0) | |
stat_win = win.derwin(Y-1, 0) | |
grid = gosper_glider_gun | |
_draw(grid_win, grid) | |
stat_win.insstr('-- INIT --', curses.A_BOLD) | |
stdscr.refresh() | |
time.sleep(FREQ*4 - math.fmod(time.time(), FREQ)) | |
for n in count(1): | |
grid = tick(grid) | |
time.sleep(FREQ - math.fmod(time.time(), FREQ)) | |
_draw(grid_win, grid) | |
stat_win.clear() | |
stat_win.insstr(str(n).rjust(X)) | |
stat_win.noutrefresh() | |
curses.doupdate() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment