I created a simple game of life and I used console prints to debug and visualize it at first. However, when I tried to introduce GUI (using Python tkinter
), the game slowed down to a crawl. Everything is working fine, as far as I'm concerned, but I would like to know how to improve performance.
Notes:
- I asked something similar on Stackoverflow, but that was a specific question about a bug I think I had. Hope that's OK
- I have a design bug, where when a shape gets to the outskirts of the grid, it starts to behave badly. Known, and not the primary issue currnetly
Thanks in advance, really appriciate any input (include styling, abuse-of-methods / Python built-ins, alogrithmic and anything that comes up to mind in order to improve).
# Game of life
from random import randint
import numpy as np
from copy import deepcopy
from enum import Enum
import tkinter as tk
class State(Enum):
Dead = 0
Alive = 1
def __str__(self):
return str(self.value)
class Cell:
def __init__(self, m, n, state):
self.m = np.uint(m)
self.n = np.uint(n)
self.state = state
def kill(self):
self.state = State.Dead
def birth(self):
self.state = State.Alive
def __str__(self):
return '({},{}) {}'.format(self.m, self.n, self.state)
def __repr__(self):
return '({},{}) {}'.format(self.m, self.n, self.state)
class Game:
def __init__(self, m, n, alive_cells = None):
self.m = m
self.n = n
self.grid = np.ndarray((m,n), dtype = np.uint8)
if alive_cells:
self.cells = [Cell(i // n,i % n, State.Alive if (i // n,i % n) in alive_cells else State.Dead) for i in range(m*n)]
else:
self.cells = [Cell(i / n,i % n,randint(0,1)) for i in range(m*n)]
# GUI #
self.top = tk.Tk()
self.cell_size = 10000 // 400 #(self.m * self.n)
self.canvas = tk.Canvas(self.top, bg="gray", height=self.m *self. cell_size, width=self.n * self.cell_size)
self.rectangulars = []
def populate_grid(self):
for cell in self.cells:
self.grid[cell.m,cell.n] = cell.state.value
def show(self, show_GUI = True, print_2_console = False):
self.populate_grid()
if print_2_console:
print('#'*self.m*3)
print(self.grid)
if show_GUI:
self.draw_canvas()
def iterate(self):
'''
Rules:
(1) If cell has less than 2 neighbours, it dies
(2) If cell has more than 3 neighbours, it dies
(3) If cell has 2-3 neighbours, it survives
(4) If cell has 3 neighbours, it rebirths
'''
new_cells = []
for cell in self.cells:
alive_neighbours = 0
for i in range(cell.m - 1, cell.m + 2):
for j in range(cell.n - 1, cell.n + 2):
if i == cell.m and j == cell.n:
continue
else:
try:
alive_neighbours += self.grid[i,j]
except IndexError:
pass
tmp = deepcopy(cell)
if alive_neighbours < 2 or alive_neighbours > 3:
tmp.kill()
elif alive_neighbours == 3:
tmp.birth()
else: # == 2
pass
new_cells.append(tmp)
self.cells = new_cells
self.show()
def draw_canvas(self):
# delete old rectangulars
for rect in self.rectangulars:
self.canvas.delete(rect)
for cell in self.cells:
if cell.state == State.Alive:
color = 'blue'
else:
color = 'red'
self.rectangulars.append(self.canvas.create_rectangle(cell.n*self.cell_size, cell.m*self.cell_size, (1+cell.n)*self.cell_size, (1+cell.m)*self.cell_size, fill=color))
self.canvas.pack()
self.update_canvas()
self.top.mainloop()
def update_canvas(self): # iterate -> show -> draw_canvas
self.top.after(100, self.iterate)
if __name__ == "__main__":
glider = (40, 40, ((1,3), (2,3), (2,1), (3,2), (3,3)))
small_exploder = (30, 30, ((10,10), (11,9), (11,10), (11,11), (12,9), (12,11), (13,10)))
M, N, STARTING_LIVE_CELLS, ITERATIONS = *small_exploder, 0
g = Game(M, N, STARTING_LIVE_CELLS)
g.show()
1 Answer 1
The main problem is that you're attempting to create about 9000 canvas objects a second. While the canvas is fairly flexible and powerful, it starts to have performance issues once you create a few tens of thousands of objects, even when you delete them later. Since you are creating 9000 objects per second, you're going to hit that limit very quickly.
You also have the problem that you continually append to self.rectangulars
but you never remove anything from that array. So, every second that array grows by 9000 elements. Since you attempt to iterate over that entire list every 100ms, the program will continue to get slower and slower.
The solution is to re-use the canvas items rather than recreate them. The create_rectangle
method returns an identifier that you can use forever. For example, to
reset the entire grid you could do something like this:
for item in self.rectangulars:
self.canvas.itemconfigure(item, background=gray)
Explore related questions
See similar questions with these tags.