6
\$\begingroup\$

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?

toolic
15.1k5 gold badges29 silver badges211 bronze badges
asked Jul 8, 2018 at 14:14
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

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
answered Aug 21 at 15:15
\$\endgroup\$
3
\$\begingroup\$

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
answered Aug 21 at 12:54
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.