Pygame is running game of life very slowly

Viewed 124

The couple other questions related to this were solved by moving a draw event out of a loop it shouldn't be in. I don't have this issue though. Any help would be greatly appreciated!

Python: 3.8

Pygame: 1.9.6

If you need to test play:

Run the game. Draw on the screen to place live cells. Click 'R' to start. You can also click 'S' after starting to stop and draw again, but you'll have to wait a few generations after clicking before it actually stops (due to the same lag I assume).

import pygame
import numpy

class Game():
    def __init__(self):
        self.Run()

    def GetAdj(self, x, y):
        nb = 0
        for c in range (-1, 2):
            for r in range (-1, 2):
                if r == 0 and c == 0:
                    pass
                else:
                    nposx = x + r
                    nposy = y + c
                    if nposx < len(self.pixels):
                        if nposy < len(self.pixels[nposx]):
                            if self.pixels[nposx][nposy] == 1:
                                nb += 1
        return nb

    def NextGeneration(self):
        newGeneration = numpy.zeros(self.ScreenWidth//2, self.ScreenHeight//2, dtype=int)
        for x, c in enumerate(self.pixels):
            for y, cell in enumerate(c):
                nbrs = self.GetAdj(x, y)
                if cell == 1:
                    if nbrs in [2, 3]:
                        newGeneration[x][y] = 1
                else:
                    if nbrs == 3:
                        newGeneration[x][y] = 1
        self.pixels = newGeneration

    def DrawBG(self):
        black = (0,0,0)
        white = (255,255,255)
        self.bg.fill(black)
        for c in range(self.ScreenWidth // self.cellsize):
            for r in range(self.ScreenHeight // self.cellsize):
                if self.pixels[c][r] == 1:
                    pygame.draw.rect(self.bg, white, (c*self.cellsize, r*self.cellsize, self.cellsize, self.cellsize))

    def Run(self):
        pygame.init()
        self.ScreenHeight = 720
        self.ScreenWidth = 1280
        self.ScreenSize = (self.ScreenWidth, self.ScreenHeight)
        screen = pygame.display.set_mode(self.ScreenSize)
        self.bg = pygame.Surface(self.ScreenSize)
        clock = pygame.time.Clock()
        mousedown = False
        self.pixels = numpy.zeros(self.ScreenWidth//2, self.ScreenHeight//2, dtype=int)
        self.cellsize = 10
        stage = 'Draw'
        running = True
        while running:
            clock.tick(60)
            events = pygame.event.get()
            for event in events:
                if event.type == pygame.QUIT:
                    running = False
            if stage == 'Draw':
                for event in events:
                    if event.type == pygame.MOUSEBUTTONUP and mousedown:
                        mousedown = False
                    elif event.type == pygame.MOUSEBUTTONDOWN:
                        mousedown = True
                    elif event.type == pygame.MOUSEMOTION and mousedown and stage == 'Draw':
                        mposx, mposy = pygame.mouse.get_pos()
                        self.pixels[mposx//self.cellsize][mposy//self.cellsize] = 1
                    elif event.type == pygame.KEYDOWN and event.key == pygame.K_r:
                        stage = 'Run'
                        self.NextGeneration()

            elif stage == 'Run':
                for event in events:
                    if event.type == pygame.KEYDOWN and event.key == pygame.K_s:
                        stage = 'Draw'
                self.NextGeneration()

            self.DrawBG()
            screen.blit(self.bg, (0,0))
            pygame.display.flip()

if __name__ == "__main__":
    Game()
2 Answers

The numpy array is way too big. You create an array for every pixel, not every cell. So don't calculate it for every cell, but for every pixel.

Change the size of the array in the NextGeneration and the Run method:

newGeneration = numpy.zeros(self.ScreenWidth//2, self.ScreenHeight//2, dtype=int)

newGeneration = numpy.zeros((self.ScreenWidth//self.cellsize, self.ScreenHeight//self.cellsize), dtype=int)

self.pixels = numpy.zeros(self.ScreenWidth//2, self.ScreenHeight//2, dtype=int)

self.cellsize = 10
self.pixels = numpy.zeros((self.ScreenWidth//self.cellsize, self.ScreenHeight//self.cellsize), dtype=int)

The GetAdj method can be greatly simplified with numpy.sum:

class Game():
    # [...]

    def GetAdj(self, x, y):
        x0, y0 = max(0, x-1), max(0, y-1)
        nb = numpy.sum(self.pixels[x0 : x+2, y0 : y+2]) - self.pixels[x, y]
        return nb

The performance can be improved further using scipy.ndimage.convolve and a lookup table (see Indexing Multi-dimensional arrays):

import numpy
from scipy.ndimage import convolve

class Game():
    def __init__(self):
        self.kernel = numpy.array([[1,1,1], [1,0,1], [1,1,1]])
        self.lookup = numpy.array([0,0,0,1,0,0,0,0,0, 0,0,1,1,0,0,0,0,0])
        self.Run()

    def NextGeneration(self):
        adjacent = convolve(self.pixels, self.kernel, mode='constant')
        newGeneration = self.lookup[self.pixels * 9 + adjacent]
        self.pixels = newGeneration

In addition to @Rabbid76's answer, you cal also implement the game with a sparse structure.

As we can see, having huge Numpy arrays is the bottleneck of your implementation. If you are just storing the information about cells that are alive, you can probably use a SciPy sparse boolean matrix, which essentially just initialises the information about cells that are alive. So even if you have a 1000x1000 grid with one cell alive, it will not actually store all those dead cells.

But even better, why not just use a Python set? For example, you could have a set of int tuples that represent the cells currently alive in each iteration. I found that this makes the implementation and logic a lot simpler, and the overheads of importing and creating huge numpy arrays kind of disappear.

I recently posted a Conway's Game Of Life implementation in fewer than 80 lines of Python, using Pygame. I use sets to store the live cells, so take a look and see how your implementation can be simplified with this approach.

Of course, if you are planning on having a full live cell board all the time, then this approach has its drawbacks. But I assume that, since you're using Python in the first place, you don't intend on having many computationally intensive setups (I'd recommend using native code for that, with heavy optimisations).

Related