2
\$\begingroup\$

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:

  1. I asked something similar on Stackoverflow, but that was a specific question about a bug I think I had. Hope that's OK
  2. 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()
\$\endgroup\$
2
\$\begingroup\$

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)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.