Last active
October 16, 2024 16:54
-
-
Save ramalho/da5590bc38c973408839 to your computer and use it in GitHub Desktop.
John Conway's Game of Life implemented with coroutines, by Brett Slatkin
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 | |
# Copyright 2014 Brett Slatkin, Pearson Education Inc. | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
""" | |
====================================================== | |
John Conway's Game of Life implemented with coroutines | |
====================================================== | |
by Brett Slatkin | |
This script was refactored by Luciano Ramalho from code listed in Item | |
40 of Brett Slatkin's excellent book "Effective Python: 59 Specific Ways | |
to Write Better Python". | |
The original code is published on Github under the Apache 2.0 license: | |
https://github.com/bslatkin/effectivepython/blob/master/example_code/item_40.py | |
The refactoring included: | |
- replacing the top-level testing code with doctests; | |
- changing the ``Grid.query`` method to ``Grid.__getitem__``, taking a | |
tuple of coordinates as argument; | |
- similarly, changing the ``Grid.assign`` method to ``Grid.__setitem__``; | |
Running this script produces no output. To run the doctests, use:: | |
$ python3 -m doctest coro_life.py | |
No output will be generated if all doctests pass. To see doctest output, | |
use:: | |
$ python3 -m doctest coro_life.py -v | |
-------------------------------------------- | |
Doctests for specific components of the code | |
-------------------------------------------- | |
Drive ``count_neighbors`` | |
========================= | |
Drive the ``count_neighbors`` coroutine with fake data:: | |
>>> it = count_neighbors(10, 5) | |
>>> next(it) # Get the first query, for q1 | |
Query(y=11, x=5) | |
>>> it.send(ALIVE) # Send q1 state, get q2 | |
Query(y=11, x=6) | |
>>> it.send(ALIVE) # Send q2 state, get q3 | |
Query(y=10, x=6) | |
>>> # Send q3 ... q7 states, get q4 ... q8 | |
>>> [it.send(state) for state in (EMPTY)*5] # doctest: +ELLIPSIS | |
[Query(y=9, x=6), Query(y=9, x=5), ..., Query(y=11, x=4)] | |
>>> try: | |
... it.send(EMPTY) # Send q8 state, drive coroutine to end | |
... except StopIteration as e: | |
... count = e.value # Value from return statement | |
... | |
>>> count | |
2 | |
Drive ``step_cell`` | |
=================== | |
Drive the ``step_cell`` coroutine with fake data:: | |
>>> it = step_cell(10, 5) | |
>>> next(it) # Initial location query | |
Query(y=10, x=5) | |
>>> [it.send(st) for st in (ALIVE)*5 + (EMPTY)*3] # doctest: +ELLIPSIS | |
[Query(y=11, x=5), Query(y=11, x=6), ... Query(y=11, x=4)] | |
>>> it.send(EMPTY) # Send q8 state, get game decision | |
Transition(y=10, x=5, state='-') | |
Test ``Grid`` | |
============= | |
Put a glider in a 5x9 grid: | |
>>> grid = Grid(5, 9) | |
>>> grid[0, 3] = ALIVE | |
>>> grid[1, 4] = ALIVE | |
>>> grid[2, 2] = ALIVE | |
>>> grid[2, 3] = ALIVE | |
>>> grid[2, 4] = ALIVE | |
>>> print(grid) | |
---*----- | |
----*---- | |
--***---- | |
--------- | |
--------- | |
<BLANKLINE> | |
Run the game for 5 generations | |
============================== | |
Test ``ColumnPrinter``, ``simulate`` and ``live_a_generation``:: | |
>>> columns = ColumnPrinter() | |
>>> sim = simulate(grid.height, grid.width) | |
>>> for i in range(5): | |
... columns.append(str(grid)) | |
... grid = live_a_generation(grid, sim) | |
... | |
>>> print(columns) # doctest: +NORMALIZE_WHITESPACE | |
0 | 1 | 2 | 3 | 4 | |
---*----- | --------- | --------- | --------- | --------- | |
----*---- | --*-*---- | ----*---- | ---*----- | ----*---- | |
--***---- | ---**---- | --*-*---- | ----**--- | -----*--- | |
--------- | ---*----- | ---**---- | ---**---- | ---***--- | |
--------- | --------- | --------- | --------- | --------- | |
Introductory diagram | |
==================== | |
The first Game of Life example in Item 40 of "Effective Python" | |
(with an added generation showing zero surviving cells):: | |
>>> grid = Grid(5, 5) | |
>>> grid[1, 1] = ALIVE | |
>>> grid[2, 2] = ALIVE | |
>>> grid[2, 3] = ALIVE | |
>>> grid[3, 3] = ALIVE | |
>>> columns = ColumnPrinter() | |
>>> sim = simulate(grid.height, grid.width) | |
>>> for i in range(6): | |
... columns.append(str(grid)) | |
... grid = live_a_generation(grid, sim) | |
... | |
>>> print(columns) # doctest: +NORMALIZE_WHITESPACE | |
0 | 1 | 2 | 3 | 4 | 5 | |
----- | ----- | ----- | ----- | ----- | ----- | |
-*--- | --*-- | --**- | --*-- | ----- | ----- | |
--**- | --**- | -*--- | -*--- | -**-- | ----- | |
---*- | --**- | --**- | --*-- | ----- | ----- | |
----- | ----- | ----- | ----- | ----- | ----- | |
Blinker demo | |
============ | |
The blinker is the simplest oscillator:: | |
>>> grid = Grid(5, 5) | |
>>> for i in range(1, 4): | |
... grid[2, i] = ALIVE | |
... | |
>>> columns = ColumnPrinter() | |
>>> sim = simulate(grid.height, grid.width) | |
>>> for i in range(8): | |
... columns.append(str(grid)) | |
... grid = live_a_generation(grid, sim) | |
... | |
>>> print(columns) # doctest: +NORMALIZE_WHITESPACE | |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | |
----- | --*-- | ----- | --*-- | ----- | --*-- | ----- | --*-- | |
-***- | --*-- | -***- | --*-- | -***- | --*-- | -***- | --*-- | |
----- | --*-- | ----- | --*-- | ----- | --*-- | ----- | --*-- | |
----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | |
""" | |
from collections import namedtuple | |
ALIVE = '*' | |
EMPTY = '-' | |
TICK = object() | |
Query = namedtuple('Query', 'y x') | |
Transition = namedtuple('Transition', 'y x state') | |
def count_neighbors(y, x): | |
n_ = yield Query(y + 1, x + 0) # North | |
ne = yield Query(y + 1, x + 1) # Northeast | |
e_ = yield Query(y + 0, x + 1) # East | |
se = yield Query(y - 1, x + 1) # Southeast | |
s_ = yield Query(y - 1, x + 0) # South | |
sw = yield Query(y - 1, x - 1) # Southwest | |
w_ = yield Query(y + 0, x - 1) # West | |
nw = yield Query(y + 1, x - 1) # Northwest | |
neighbor_states = [n_, ne, e_, se, s_, sw, w_, nw] | |
return sum(1 for state in neighbor_states if state == ALIVE) | |
def game_logic(state, neighbors): | |
if state == ALIVE: | |
if neighbors < 2: | |
return EMPTY # Die: Too few | |
elif neighbors > 3: | |
return EMPTY # Die: Too many | |
else: | |
if neighbors == 3: | |
return ALIVE # Regenerate | |
return state | |
def step_cell(y, x): | |
state = yield Query(y, x) | |
neighbors = yield from count_neighbors(y, x) | |
next_state = game_logic(state, neighbors) | |
yield Transition(y, x, next_state) | |
def simulate(height, width): | |
while True: | |
for y in range(height): | |
for x in range(width): | |
yield from step_cell(y, x) | |
yield TICK | |
class Grid(object): | |
def __init__(self, height, width): | |
self.height = height | |
self.width = width | |
self.rows = [[EMPTY] * self.width for _ in range(self.height)] | |
def __str__(self): | |
output = '' | |
for row in self.rows: | |
for cell in row: | |
output += cell | |
output += '\n' | |
return output | |
def __getitem__(self, position): | |
y, x = position | |
return self.rows[y % self.height][x % self.width] | |
def __setitem__(self, position, state): | |
y, x = position | |
self.rows[y % self.height][x % self.width] = state | |
def live_a_generation(grid, sim): | |
progeny = Grid(grid.height, grid.width) | |
item = next(sim) | |
while item is not TICK: | |
if isinstance(item, Query): | |
state = grid[item.y, item.x] | |
item = sim.send(state) | |
else: # Must be a Transition | |
progeny[item.y, item.x] = item.state | |
item = next(sim) | |
return progeny | |
class ColumnPrinter(object): | |
def __init__(self): | |
self.columns = [] | |
def append(self, data): | |
self.columns.append(data) | |
def __str__(self): | |
row_count = 1 | |
for data in self.columns: | |
row_count = max(row_count, len(data.splitlines()) + 1) | |
rows = [''] * row_count | |
for j in range(row_count): | |
for i, data in enumerate(self.columns): | |
line = data.splitlines()[max(0, j - 1)] | |
if j == 0: | |
rows[j] += str(i).center(len(line)) | |
else: | |
rows[j] += line | |
if (i + 1) < len(self.columns): | |
rows[j] += ' | ' | |
return '\n'.join(rows) |
Great idea, @j14x! I've implemented your idea, just returning the sum()
; no need for count
. Thanks!
If you are like me and have some trouble understanding this code, here is the link of Brett Slatkins sample Chapter, thankfully referenced by @ramalho
https://effectivepython.com/2015/03/10/consider-coroutines-to-run-many-functions-concurrently
@ramalho Thank you very much for your great book!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Coming here from your book Fluent Python, great refactor from Brett's snippet.
In line 210,
count
could be refactor to use a genexp andsum
:count = sum(1 for state in neighbor_states if state == ALIVE)
What do you think?