Using Python 3.7.0, I've programmed a 'Game of Life' simulator. The output is displayed using tkinter
. It's not meant to be limited to just Game of Life, though, as it's meant to also be used for a simple physics simulation project.
import tkinter as tk
#import pdb
class Cell:
#ref_dict allows the coords to act as a pointer to the cell.
ref_dict = {}
"""active_cells allows the grid to determine sim_cells.
Thus, it reduces the number of cells the grid needs to simulate.
"""
active_cells = set()
def __init__(
self, coords, size, grid_size, conditions = {},
active_color = "black", inactive_color = "white", state = False):
"""Create a new cell.
Positional arguments:
coords ---------- Coordinates of the cell within the grid.
Tuple (x, y)
size ------------ Side length of the cell in pixels.
Integer.
grid_size ------- Side length of the grid, in squares.
Integer.
Keyword arguments:
conditions ------ Dict of conditions the cell has.
Used for complex simulations.
Keys as names, values as floats.
E.g. 'temperature': 50.4
Dictionary.
active_color ---- Color of the cell when alive.
String or color reference.
inactive_color -- Color of the cell when dead.
String or color reference.
state ----------- Whether the cell is alive or dead.
Boolean.
This object simulates a cell, which evolves over time.
Here, the rules for evolution are from 'Conway's game of life'.
"""
self.coords = coords
self.size = size
self.grid_size = grid_size
self.conditions = conditions
self.active_color = active_color
self.inactive_color = inactive_color
self.state = state
"""The cell won't update it's state straight away.
Otherwise, it could cause a knock-on effect,
changing the number of neighbors for other cells.
To avoid this, the cell will calculate its new_state first.
Then, it can set self.state = self.new_state after all updates.
"""
self.new_state = False
self.neighbor_coords = ()
self.get_neighbor_coords()
self.alive_neighbors = 0
"""This allows instantiation without variable assignment,
by letting the coordinates act as a pointer.
"""
Cell.ref_dict[self.coords] = self
def valid_coord(self, coord):
"""Determine whether a given coordinate is within the grid.
x coordinates are measured from the origin.
Hence, they should not be >= to the grid_size.
This would indicate it was referring to a column right of the
rightmost column, due to everything being 0-indexed.
Similar rules apply for the y coordinate.
"""
(x, y) = coord
return (0 <= x < self.grid_size) and (0 <= y < self.grid_size)
def get_neighbor_coords(self):
"""Construct neighbor_coords."""
(x, y) = self.coords
self.neighbor_coords = tuple(filter(self.valid_coord,
[(x + a, y + b)
for a in range(-1, 2) for b in range(-1, 2) if a != 0 or b != 0]))
return self
def get_alive_neighbors(self):
"""Determine alive_neighbors."""
self.alive_neighbors = 0
for n in self.neighbor_coords:
if Cell.ref_dict[n].state:
self.alive_neighbors += 1
return self
def get_new_state(self):
"""Determine what the new state of the cell should be.
Here is where any different rules can be configured.
"""
if not self.neighbor_coords:
self.get_neighbor_coords().get_alive_neighbors()
else:
self.get_alive_neighbors()
if self.state:
if not 2 <= self.alive_neighbors <= 3:
self.new_state = False
else:
self.new_state = True
else:
if self.alive_neighbors == 3:
self.new_state = True
else:
self.new_state = False
return self
def update_state(self):
"""Set self.state to self.new_state; reset self.new_state.
Also will add/remove the cell from active_cells.
"""
self.state = self.new_state
if self.state and self.coords not in Cell.active_cells:
Cell.active_cells.add(self.coords)
elif not self.state and self.coords in Cell.active_cells:
Cell.active_cells.remove(self.coords)
self.new_state = False
return self
class Grid:
def __init__(
self, size, cell_size, active_color = "black",
inactive_color = "white", condition_dict = {},
initial_active_cells = []):
"""Create a new grid.
Positional arguments:
size ----------------- Size of the grid in cells.
Integer.
cell_size ------------ Size of a cell in pixels.
Integer.
Keyword arguments:
active_color --------- Color of a cell when alive.
String or color reference.
inactive_color ------- Color of a cell when dead.
String or color reference.
condition_dict ------- Nested dictionary.
Coordinates as keys, dicts of conditions as values.
Dictionary.
initial_active_cells - List of cells that start alive.
List.
"""
self.size = size
self.cell_size = cell_size
self.active_color = active_color
self.inactive_color = inactive_color
self.condition_dict = condition_dict
self.initial_active_cells = initial_active_cells
"""Only active cells and neighbors are simulated.
Coords in self.sim_cells. This attempts to reduce CPU usage.
"""
self.sim_cells = set()
self.create_cells().set_active_cells(initial_active_cells)
def create_cells(self):
"""Instantiate all new cells with required parameters."""
"""Conditions may not be specified.
The if statement thus avoids KeyErrors.
"""
if self.condition_dict:
for x in range(0, self.size):
for y in range(0, self.size):
Cell(
(x, y), self.cell_size, self.size,
conditions = self.condition_dict[(x, y)],
active_color = self.active_color,
inactive_color = self.inactive_color)
else:
for x in range(0, self.size):
for y in range(0, self.size):
Cell(
(x, y), self.cell_size, self.size,
active_color = self.active_color,
inactive_color = self.inactive_color)
return self
def set_active_cells(self, initial_active_cells):
"""Set states of cells specified by a list to True."""
for coord in initial_active_cells:
Cell.ref_dict[coord].new_state = True
Cell.ref_dict[coord].update_state()
return self
def get_sim_cells(self):
"""Get all cells that need to be simulated."""
self.sim_cells = set()
for coord in Cell.active_cells:
self.sim_cells.add(coord)
"""This finds all neighbors of active cells,
to determine which cells need to be simulated."""
self.sim_cells.update(
[(x + a, y + b)
for a in range(-1,2) for b in range(-1,2)
for (x,y) in self.sim_cells
if Cell.valid_coord(Cell.ref_dict[(x,y)], (x + a, y + b))])
return self
def update_grid(self):
"""Update simulated cells, and update the set of sim_cells."""
self.get_sim_cells()
"""Two loops are used to stop early death or birth of cells.
The grid is meant to evolve as a whole each tick,
and not cell-by-cell.
"""
for coord in self.sim_cells:
Cell.ref_dict[coord].get_new_state()
for coord in self.sim_cells:
Cell.ref_dict[coord].update_state()
return self
class App:
#Matches cell coordinates to the canvas squares.
canvas_dict = {}
def __init__(
self, grid_size, cell_size,
active_color = "black", inactive_color = "white",
condition_dict = {}, initial_active_cells = []):
"""Start a new game.
Positional arguments:
grid_size ------------ Size of grid, in cells.
Integer.
cell_size ------------ Size of a cell, in pixels.
Integer.
Keyword arguments:
active_color --------- Color of a cell when alive.
String or color reference.
inactive_color ------- Color of a cell when dead.
String or color reference.
condition_dict ------- Nested dictionary.
Coordinates as keys, dicts of conditions as values.
Dictionary.
initial_active_cells - List of cells that start alive.
List.
"""
self.grid_size = grid_size
self.cell_size = cell_size
self.active_color = active_color
self.inactive_color = inactive_color
self.condition_dict = condition_dict
self.initial_active_cells = initial_active_cells
self.size = grid_size * cell_size
self.grid = Grid(
self.grid_size, self.cell_size,
active_color = self.active_color,
inactive_color = self.inactive_color,
condition_dict = self.condition_dict,
initial_active_cells = self.initial_active_cells)
self.root = tk.Tk()
self.canvas = tk.Canvas(
self.root, bg = "white",
height = self.size, width = self.size)
self.canvas.pack()
self.render_canvas(canvas_created = False)
self.root.after(1000, self.refresh_display)
self.root.mainloop()
def refresh_display(self):
"""Update both grid and canvas."""
self.grid.update_grid()
self.render_canvas(canvas_created = True)
self.root.after(50, self.refresh_display)
def render_canvas(self, canvas_created = False):
if not canvas_created:
for x in range(0, self.grid_size):
for y in range(0, self.grid_size):
if (x, y) in self.initial_active_cells:
#This specifies the corners of the polygon.
App.canvas_dict[(x, y)] = self.canvas.create_polygon(
x * self.cell_size, y * self.cell_size,
(x+1) * self.cell_size, y * self.cell_size,
(x+1) * self.cell_size, (y+1) * self.cell_size,
x * self.cell_size, (y+1) * self.cell_size,
fill = self.active_color, outline = "black")
else:
App.canvas_dict[(x, y)] = self.canvas.create_polygon(
x * self.cell_size, y * self.cell_size,
(x+1) * self.cell_size, y * self.cell_size,
(x+1) * self.cell_size, (y+1) * self.cell_size,
x * self.cell_size, (y+1) * self.cell_size,
fill = self.inactive_color, outline = "black")
else:
for coord in self.grid.sim_cells:
if Cell.ref_dict[coord].state:
self.canvas.itemconfig(
App.canvas_dict[coord], fill = self.active_color)
else:
self.canvas.itemconfig(
App.canvas_dict[coord], fill = self.inactive_color)
if __name__ == "__main__":
#This gives the default pattern of a Gosper glider gun.
app = App(60, 5, initial_active_cells = [
(25,1), (23,2), (25,2), (13,3),
(14,3), (21,3), (22,3), (35,3),
(36,3), (12,4), (16,4), (21,4),
(22,4), (35,4), (36,4), (1,5),
(2,5), (11,5), (17,5), (21,5),
(22,5), (1,6), (2,6), (11,6),
(15,6), (17,6), (18,6), (23,6),
(25,6), (11,7), (17,7), (25,7),
(12,8), (16,8), (13,9), (14,9)])
My skill level is a beginner - this is my first non-trivial project that makes use of OOP concepts like classes. Most of my previous projects have been algorithm-based, for example writing an algorithm to crack the Caesar cipher.
I'm looking for feedback on how to make the code faster (the glider gun provided slows down after making about 3 gliders), as well as error handling. I haven't implemented any yet, but I'm not sure how much I need to account for - does every function need a try
... except
statement? Only every __init__
function? Any feedback in this area would be greatly appreciated.
Here is the code from a previous Q. I used this as a reference, but I tried my best not to directly copy it. However, some elements are quite similar to it (for example, both have 3 classes of Square/Cell, Grid and App).
I also have a problem with trying to import this module and changing the behaviour of Cell
. To do this, I have been told to make a subclass, and override when needed. However, Grid
creates new cells using the original Cell
constructor, so any changes I make won’t affect the cells Grid
creates.
Is there any way I can edit the behaviour of Cell
and get Grid
to make the new objects, without rewriting lots of functions?
2 Answers 2
In addition to what toolic has provided as feedback, even that DRYer code can be reduced, since the condition 2 <= self.alive_neighbors <= 3
already provides a boolean.
if 2 <= self.alive_neighbors <= 3:
self.new_state = True
else:
self.new_state = False
Becomes:
self.new_state = (2 <= self.alive_neighbors <= 3)
We can also see this in your code in another place:
if self.alive_neighbors == 3: self.new_state = True else: self.new_state = False
DRY
In the create_cells
function, the nested for
lines are repeated twice,
as are most of the lines in the Cell
call.
You could rearrange the code so that the if/else
statement is inside
the nested for
loops:
for x in range(self.size):
for y in range(self.size):
conditions = self.condition_dict[(x, y)] if self.condition_dict else {}
Cell(
(x, y), self.cell_size, self.size,
conditions = conditions,
active_color = self.active_color,
inactive_color = self.inactive_color)
The if/else
is replaced with a conditional expression.
I also simplified the range
calls by eliminating the default 0
start.
You may be able to do something similar to reduce repetition in the render_canvas
function.
Simpler
This if/else
statement in the get_new_state
function:
if not 2 <= self.alive_neighbors <= 3:
self.new_state = False
else:
self.new_state = True
is simpler with the not
removed (and the statement order swapped):
if 2 <= self.alive_neighbors <= 3:
self.new_state = True
else:
self.new_state = False
The same is true for:
if not self.neighbor_coords:
Comments
Deleted commented-out code:
#import pdb
Explore related questions
See similar questions with these tags.