I've just finished implementing Conway's Game of Life in python-3.6:
import curses
import numpy as np
import time
from collections import defaultdict
from enum import Enum
from typing import List
class State(Enum):
''' enum class to represent possible cell states '''
dead = 0
alive = 1
class Board:
''' Board class to represent the game board '''
def __init__(self, m : int, n : int, init : List[List[int]]):
self.m = m # the number of rows
self.n = n # the number of columns
self.board_ = [
[State(init[i][j]) for j in range(self.n)] for i in range(self.m)
]
def __str__(self) -> str:
''' return the __str__ representation of a Board object
* represents a live cell, and a space represents a dead one
'''
return '\n'.join([
''.join(
['*' if cell.value else ' ' for cell in row]
) for row in self.board_]
)
@property
def population(self):
''' population — the number of live cells on the board '''
return sum(cell.value for row in self.board_ for cell in row)
def count_live_neighbours(self, x : int, y : int) -> int:
''' count the live neighbours of a cell '''
count = 0
for i in range(x - 1, x + 2):
for j in range(y - 1, y + 2):
if (i == x and j == y) or i < 0 or j < 0:
continue
# handle IndexErrors raised during invalid indexing operations
try:
count += self.board_[i][j].value
except IndexError:
continue
return count
def next_cell_state(self, x : int, y : int) -> State:
count = self.count_live_neighbours(x, y)
cur_state = self.board_[x][y]
# determine the next state based on the current state and
# number of live neighbours
if count in {2, 3} and cur_state == State.alive:
return cur_state
elif count == 3 and cur_state == State.dead:
return State.alive
return State.dead
def next_board_state(self) -> List[List[State]]:
''' return board configuration for the next state '''
return [
[self.next_cell_state(i, j) for j in range(self.n)] for i in range(self.m)
]
def advance_state(self):
''' update the board configuration with the config for the next state '''
self.board_ = self.next_board_state()
def has_live_cells(self) -> bool:
''' return whether there are any live cells or not '''
return any(cell.value for row in self.board_ for cell in row)
if __name__ == '__main__':
arr = np.random.choice([0, 1], (20, 100), p=[0.90, 0.1])
board = Board(arr.shape[0], arr.shape[1], init=arr.tolist())
step = 0
while board.has_live_cells():
step += 1
print(board)
print('f{step} {board.population}')
board.advance_state()
time.sleep(.1)
To run the code, just invoke python3.6 conway.py
. The board shape is fixed; but the configuration is randomly generated with NumPy.
My aim with this project was:
- Implement a concise and pythonic version of the algorithm
- Get better with Python's OOP constructs
- Kill time :-)
There are a few redundant functions and properties which felt would be useful at some point of time.
My next step with this program would likely be to host it on a server and let people run it on their browser with a Step and Run button. This might influence how I'd need to structure my code.
1 Answer 1
Your code is pretty good, and there's not much I'd change. However I would change it in the following ways:
- It's standard to use
UPPER_SNAKE_CASE
for Enum values. - It's preferred to use
typing.Sequence
overtyping.List
to annotate arguments. - When you have to comment what a variable is, it indicates you've chosen a bad name. Instead use
rows
overm
. Either put your comprehensions on one line or split them out.
# Current way [ [State(init[i][j]) for j in range(self.n)] for i in range(self.m) ] # One line [[State(init[i][j]) for j in range(self.n)] for i in range(self.m)] # Multiple lines [ [ State(init[i][j]) for j in range(self.n) ] for i in range(self.m) ]
It's easier to read a comprehension in a function if you keep the brackets together.
# Current return '\n'.join([ ''.join( ['*' if cell.value else ' ' for cell in row] ) for row in self.board_] ) # Together return '\n'.join([ ''.join(['*' if cell.value else ' ' for cell in row]) for row in self.board_]) # Together and multiple lines return '\n'.join([ ''.join([ '*' if cell.value else ' ' for cell in row ]) for row in self.board_ ])
You don't need to use the surrounding brackets if your comprehension is the only argument to a function. However, this may be a couple of u seconds slower, which shouldn't matter to most people.
return '\n'.join( ''.join( '*' if cell.value else ' ' for cell in row ) for row in self.board_ )
I'd make another function
neighbours
. I find this nicer, as finding the neighbors can be a little annoying, and take away from the actual algorithm.I'd also make the algorithm use a premade list, that filters out
x==i
already.NEIGHBOURS = [(i, j) for i in range(-1, 2) for j in range(-1, 2) if i or j] def neighbours(self, x: int, y: int) -> Iterator[Tuple[State, int, int]]: for i, j in ((x+i, y+j) for i, j in NEIGHBOURS): try: yield self.board_[i][j], i, j except IndexError: pass def count_live_neighbours(self, x : int, y : int) -> int: ''' count the live neighbours of a cell ''' return sum(cell.value for cell, _, _ in self.neighbours(x, y))
Your
next_cell_state
function can be simplified.- Your two
if
s returnState.ALIVE
. - They can be merged together.
- You can return early if
count
is three. - You can simplify the rest of the checks.
def next_cell_state(self, x : int, y : int) -> State: count = self.count_live_neighbours(x, y) if count == 3 or (count == 2 and self.board_[x][y] == State.ALIVE): return State.ALIVE return State.DEAD
- Your two
import numpy as np
import time
from enum import Enum
from typing import List, Iterator, Sequence, Tuple
NEIGHBOURS = [(i, j) for i in range(-1, 2) for j in range(-1, 2) if i or j]
class State(Enum):
''' enum class to represent possible cell states '''
DEAD = 0
ALIVE = 1
class Board:
''' Board class to represent the game board '''
def __init__(self, rows : int, columns : int, init : Sequence[Sequence[int]]):
self.rows = rows # the number of rows
self.columns = columns # the number of columns
self.board_ = [
[
State(init[i][j])
for j in range(self.columns)
]
for i in range(self.rows)
]
def __str__(self) -> str:
''' return the __str__ representation of a Board object
* represents a live cell, and a space represents a dead one
'''
return '\n'.join([
''.join([
'*' if cell.value else ' '
for cell in row
])
for row in self.board_
])
@property
def population(self) -> int:
''' population — the number of live cells on the board '''
return sum(cell.value for row in self.board_ for cell in row)
def has_live_cells(self) -> bool:
''' return whether there are any live cells or not '''
return any(cell.value for row in self.board_ for cell in row)
def neighbours(self, x: int, y: int) -> Iterator[Tuple[State, int, int]]:
for i, j in ((x+i, y+j) for i, j in NEIGHBOURS):
try:
yield self.board_[i, j], i, j
except IndexError:
pass
def count_live_neighbours(self, x : int, y : int) -> int:
''' count the live neighbours of a cell '''
return sum(cell.value for cell, _, _ in self.neighbours(x, y))
def next_cell_state(self, x : int, y : int) -> State:
count = self.count_live_neighbours(x, y)
if count == 3 or (count == 2 and self.board_[x][y] == State.ALIVE):
return State.ALIVE
return State.DEAD
def next_board_state(self) -> List[List[State]]:
''' return board configuration for the next state '''
return [
[
self.next_cell_state(i, j)
for j in range(self.columns)
]
for i in range(self.rows)
]
def advance_state(self):
''' update the board configuration with the config for the next state '''
self.board_ = self.next_board_state()
if __name__ == '__main__':
arr = np.random.choice([0, 1], (20, 100), p=[0.90, 0.1])
board = Board(arr.shape[0], arr.shape[1], init=arr.tolist())
step = 0
while board.has_live_cells():
step += 1
print(board)
print('f{step} {board.population}')
board.advance_state()
time.sleep(.1)
-
\$\begingroup\$ Okay, thanks. A couple of other things. In the same function, you're yielding
self.board_[i, j]
, while I believe you meant...[i][j]
, but that's a small thing. For consistency, would it also be better fornext_board_state
to hint a return type ofList[List[State]]
orSequence[Sequence[State]]
? \$\endgroup\$coldspeed– coldspeed2018年05月03日 11:46:52 +00:00Commented May 3, 2018 at 11:46 -
1\$\begingroup\$ @cᴏʟᴅsᴘᴇᴇᴅ Wat, IDK why I did that... Thanks :) You can change your returns to use
Sequence
, however I wouldn't. First the docs say not to. "Generic version oflist
. Useful for annotating return types." This is as abstract collection types are, likely, intended to be like interfaces in languages like C#. WhereList
is just a class. And so you want to work on interfaces, and return classes / interfaces. \$\endgroup\$2018年05月03日 11:52:41 +00:00Commented May 3, 2018 at 11:52 -
\$\begingroup\$ "6. You don't need to use the surrounding brackets if your comprehension is the only argument to a function.": yes you do, performance is better with the brackets, else it forces
join
to call the gencomp to build a list anyway (so no memory benefits) and it's slower!!: stackoverflow.com/questions/37782066/… \$\endgroup\$Jean-François Fabre– Jean-François Fabre2018年05月03日 11:54:27 +00:00Commented May 3, 2018 at 11:54 -
\$\begingroup\$ @Jean-FrançoisFabre The above code works without the brackets. As far as I see it, if you're using Python you don't really care about a couple of ms, as otherwise you'd be using any other language. \$\endgroup\$2018年05月03日 11:57:01 +00:00Commented May 3, 2018 at 11:57
-
1\$\begingroup\$ it works without the brackets, but it works faster with the brackets, so I can't let you suggest to remove something that works faster for the sake of readability, no. \$\endgroup\$Jean-François Fabre– Jean-François Fabre2018年05月03日 11:57:47 +00:00Commented May 3, 2018 at 11:57
Explore related questions
See similar questions with these tags.
curses
module (so as to overwrite the previous state's output). \$\endgroup\$import curses
at all at the moment? \$\endgroup\$