Game of Life [Python + Tkinter]

Here's ~2 hours of mucking around making the game of life in python with tkinter. Not much to do.

Press enter to pause/continue.
Square brackets to change delay between updates (limited between 0.0125s and 1.6s).
Backspace to clear.
You can only toggle populations and change speed when paused.

Probably not the best written thing in the world, but I don't care. This was just for the fun of it. Looking at cellular automata. In fact I'm pretty sure it's got a couple bugs. Just got to the point at which I can click buttons and it works for a bit and started playing with it, lol.

My main gripe would be the fact that labels are pretty slow to create and update. Would probably be much better to use a widget with direct pixel access instead and just use the coordinates of clicks on the window to figure out what to populate/depopulate.

import tkinter
import time
import threading

class gameOfLife:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.makeGrid()        
        self.paused = True
        self.delay = 0.2

        self.gui = GoLGUI(self)

    def clock(self):
        while True:
            if not self.paused:
                self.tick()
            time.sleep(self.delay)

    def toggletick(self):
        self.paused = not self.paused
        if self.paused:
            self.gui.window.wm_title("PAUSED x"+str(self.delay))
        else:
            self.gui.window.wm_title("x"+str(self.delay))
            
    def changeSpeed(self, multiplicand):
        if self.paused:
            if (self.delay * multiplicand) > 0.01 and (self.delay * multiplicand) < 2:
                self.delay *= multiplicand
                if self.paused:
                    self.gui.window.wm_title("PAUSED x"+str(self.delay))
                else:
                    self.gui.window.wm_title("x"+str(self.delay))
    
    def reset(self):
        if self.paused:
            self.makeGrid()
            for x in range(self.width):
                for y in range(self.height):
                    self.gui.buttonsDict["{0}.{1}".format(x, y)].configure(bg="#FF00FF")
            self.gui.window.update()
            for x in range(self.width):
                for y in range(self.height):
                    self.gui.buttonsDict["{0}.{1}".format(x, y)].configure(bg="#000000")
            self.gui.window.update()

    def makeGrid(self):
        self.grid = {x:{y:False for y in range(self.height)} for x in range(self.width)}
        
    def changeState(self, x, y):
        if self.paused:
            self.grid[x][y] = not self.grid[x][y]
            if self.grid[x][y]:
                self.gui.buttonsDict["{0}.{1}".format(x, y)].configure(bg="#FFFFFF")
            else:
                self.gui.buttonsDict["{0}.{1}".format(x, y)].configure(bg="#000000")

    def tick(self):        
        nextGrid = {x:{y:False for y in range(self.height)} for x in range(self.width)}

        for x in range(self.width):
            for y in range(self.height):
                if self.grid.get(x).get(y):
                    nextGrid[x][y] = sum([sum([self.grid.get(xi, {}).get(yi, False) for yi in range(y-1, y+2)]) for xi in range(x-1,x+2)])-1 in [2, 3]
                    if nextGrid[x][y] != self.grid[x][y]:
                        self.gui.buttonsDict["{0}.{1}".format(x, y)].configure(bg="#000000")
                else:
                    nextGrid[x][y] = sum([sum([self.grid.get(xi, {}).get(yi, False) for yi in range(y-1, y+2)]) for xi in range(x-1,x+2)]) == 3
                    if nextGrid[x][y] != self.grid[x][y]:
                        self.gui.buttonsDict["{0}.{1}".format(x, y)].configure(bg="#FFFFFF")

        self.gui.window.update()
        self.grid = nextGrid

class GoLGUI:
    def __init__(self, game):
        self.game = game
        
        self.window = tkinter.Tk()
        self.window.wm_title("PAUSED x"+str(self.game.delay))
        self.window.resizable(False, False)
        self.window.geometry("800x800")

        self.buttonsDict = {}
        
        for x in range(self.game.width):
            self.window.grid_columnconfigure(x, weight=1)
            for y in range(self.game.height):
                self.window.grid_rowconfigure(y, weight=1)
                buttonCmd = self.buttonCmdGen(x, y)
                self.buttonsDict["{0}.{1}".format(x, y)] = tkinter.Label(self.window, bg="#000000")
                self.buttonsDict["{0}.{1}".format(x, y)].grid(row=y, column=x, sticky="nesw")
                self.buttonsDict["{0}.{1}".format(x, y)].bind("<Button-1>", buttonCmd)
        
        self.window.bind("[", lambda a:self.game.changeSpeed(2))
        self.window.bind("]", lambda a:self.game.changeSpeed(0.5))
        
        self.window.bind("<Return>", lambda a:self.game.toggletick())
        self.window.bind("<BackSpace>", lambda a:self.game.reset())
        
    def buttonCmdGen(self, x, y):
        return lambda a: self.game.changeState(x, y)

game = gameOfLife(50, 50)
loop = threading.Thread(target=game.clock)
loop.start()
game.gui.window.mainloop()

If anything, it'd be fun to see the people from the #noobsofpython and #knightsofpython improve or rewrite this! @Miguel_Sensacion

2 Likes

@_Cat Very nice ! as soon as I get a moment I will give it a go ! Im currently working on the update to The Noobs of Python: Ep.2.1 - List : Slice, Loops and more

1 Like

I redid it with the tkinter canvas instead, had another hour free.

import tkinter
import threading
import time

highlightCol = "#FF00FF"
bgCol = "#000000"
popCol = "#FFFFFF"

class GoL:
    def __init__(self, width, height, scale):
        self.width = width
        self.height = height
        self.scale = scale

        self.halt = False
        self.paused = True
        self.delay = 1

        self.gui = GoLGUI(self, width, height, scale)
        self.gui.updateTitle(self.paused, self.delay)

        self.makeTable()

    def start(self):
        t = threading.Thread(target=self.clock)
        t.start()

    #Construction funcs       
    def makeTable(self):
        self.table = {}
        for x in range(self.width):
            for y in range(self.height):
                self.table[(x, y)] = False

    #Keybinded functions
    def reset(self):
        if not self.halt:
            self.halt = True
            self.makeTable()
            self.gui.reset()
            self.halt = False
        
    def togglePause(self):
        if not self.halt:
            self.paused = not self.paused
            self.gui.updateTitle(self.paused, self.delay)

    def changeSpeed(self, increment):
        if round(self.delay+increment, 2) >= 0 and round(self.delay+increment, 2) <= 2:
            self.delay = round(self.delay+increment, 2)
            self.gui.updateTitle(self.paused, self.delay)

    def togglePop(self, event):
        if (not self.halt) and self.paused:
            x = int(event.x/self.scale)
            y = int(event.y/self.scale)

            if x >= 0 and x < self.width and y >= 0 and y < self.height:
                self.table[(x, y)]= not self.table[(x, y)]
                
                if self.table[(x,y)]:
                    self.gui.changeColour(x, y, popCol)
                else:
                    self.gui.changeColour(x, y, bgCol)

    #Update funcs
    def clock(self):
        while True:
            if (not self.paused) and (not self.halt):
                self.tick()
            time.sleep(self.delay)

    def tick(self):        
        nextTable = {}
        
        for x in range(self.width):
            for y in range(self.height):
                if self.table[(x, y)]:
                    nextTable[(x, y)] = sum([sum([self.table.get((xi, yi), False) for xi in range(x-1, x+2)]) for yi in range(y-1, y+2)])-1 in [2, 3]
                    if nextTable[(x, y)] != self.table[(x, y)]:
                        self.gui.changeColour(x, y, bgCol)
                else:
                    nextTable[(x, y)] = sum([sum([self.table.get((xi, yi), False) for xi in range(x-1, x+2)]) for yi in range(y-1, y+2)])-1 == 2
                    if nextTable[(x, y)] != self.table[(x, y)]:
                        self.gui.changeColour(x, y, popCol)
 
        self.gui.window.update()
        self.table = nextTable

class GoLGUI:
    def __init__(self, game, width, height, scale):
        self.game = game
        self.width = width
        self.height = height
        self.scale = scale

        self.window = tkinter.Tk()
        self.window.geometry("{0}x{1}".format(self.width*self.scale, self.height*self.scale))
        self.window.resizable(False, False)

        self.canvas = tkinter.Canvas(self.window, width=self.width*self.scale, height=self.height*self.scale)
        self.canvas.grid(row=0, column=0, sticky="nesw")

        self.makeGrid()

        self.window.bind("<Button-1>", lambda a: self.game.togglePop(a))
        self.window.bind("<B1-Motion>", lambda a: self.game.togglePop(a))
        self.window.bind("<Return>", lambda a: self.game.togglePause())
        self.window.bind("[", lambda a: self.game.changeSpeed(-0.05))
        self.window.bind("]", lambda a: self.game.changeSpeed(0.05))
        self.window.bind("<BackSpace>", lambda a: self.game.reset())

    #Construction funcs
    def makeGrid(self):
        self.boxes = {}
        
        for x in range(0, self.width):
            for y in range(0, self.height):
                self.boxes[(x, y)] = self.canvas.create_rectangle(x*self.scale, y*self.scale,
                                                                  (x*self.scale)+self.scale, (y*self.scale)+self.scale,
                                                                  fill=bgCol, activefill=highlightCol)

    #Update funcs
    def updateTitle(self, paused, delay):
        if paused:
            self.window.wm_title("{0}s | PAUSED".format(delay))
        else:
            self.window.wm_title("{0}s".format(delay))
            
    def changeColour(self, x, y, col):
        self.canvas.itemconfig(self.boxes[(x, y)], fill=col)

    def reset(self):
        for x in range(self.width):
            for y in range(self.height):
                self.canvas.itemconfig(self.boxes[(x, y)], fill=highlightCol)
            self.canvas.update()
            
        time.sleep(0.1)
        for x in range(self.width):
            for y in range(self.height):
                self.canvas.itemconfig(self.boxes[(x, y)], fill=bgCol)
            self.canvas.update()


a = GoL(80, 80, 10)
a.start()
a.gui.window.mainloop()

I think this is much nicer.

1 Like

Here's rule 90 with it. Each cell is the xor of the cells above its horizontal neighbours. Think of each horizontal line as an iteration of the 1D array.

import tkinter
import threading
import time

highlightCol = "#FF00FF"
bgCol = "#000000"
popCol = "#FFFFFF"

class R90:
    def __init__(self, width, height, scale):
        self.width = width
        self.height = height
        self.scale = scale

        self.halt = False
        self.paused = True
        self.delay = 1

        self.constants = []
    
        self.gui = R90GUI(self, width, height, scale)
        self.gui.updateTitle(self.paused, self.delay)

        self.makeTable()

    def start(self):
        t = threading.Thread(target=self.clock)
        t.start()

    #Construction funcs       
    def makeTable(self):
        self.table = {}
        for x in range(self.width):
            for y in range(self.height):
                self.table[(x, y)] = False

    #Keybinded functions
    def reset(self):
        if not self.halt:
            self.halt = True
            self.constants = []
            self.makeTable()
            self.gui.reset()
            self.halt = False
        
    def togglePause(self):
        if not self.halt:
            self.paused = not self.paused
            self.gui.updateTitle(self.paused, self.delay)

    def changeSpeed(self, increment):
        if round(self.delay+increment, 2) >= 0 and round(self.delay+increment, 2) <= 2:
            self.delay = round(self.delay+increment, 2)
            self.gui.updateTitle(self.paused, self.delay)

    def togglePop(self, event):
        if (not self.halt) and self.paused:
            x = int(event.x/self.scale)
            y = int(event.y/self.scale)

            self.table[(x, y)]= not self.table[(x, y)]
            
            if self.table[(x,y)]:
                self.constants.append((x, y))
                self.gui.changeColour(x, y, highlightCol)
            else:
                self.constants.remove((x, y))
                self.gui.changeColour(x, y, bgCol)

    #Update funcs
    def clock(self):
        while True:
            if (not self.paused) and (not self.halt):
                self.tick()
            time.sleep(self.delay)

    def tick(self):        
        nextTable = {}
        
        for x in range(self.width):
            for y in range(self.height):
                if not (x, y) in self.constants:
                    if self.table.get((x-1, y-1), False) != self.table.get((x+1, y-1), False):
                        nextTable[(x, y)] = True
                        if nextTable[(x, y)] != self.table[(x, y)]:
                            self.gui.changeColour(x, y, popCol)
                    else:
                        nextTable[(x, y)] = False
                        if nextTable[(x, y)] != self.table[(x, y)]:
                            self.gui.changeColour(x, y, bgCol)
                else:
                    nextTable[(x, y)] = True
                    
        self.gui.window.update()
        self.table = nextTable

class R90GUI:
    def __init__(self, game, width, height, scale):
        self.game = game
        self.width = width
        self.height = height
        self.scale = scale

        self.window = tkinter.Tk()
        self.window.geometry("{0}x{1}".format(self.width*self.scale, self.height*self.scale))
        self.window.resizable(False, False)

        self.canvas = tkinter.Canvas(self.window, width=self.width*self.scale, height=self.height*self.scale)
        self.canvas.grid(row=0, column=0, sticky="nesw")

        self.makeGrid()

        self.window.bind("<Button-1>", lambda a: self.game.togglePop(a))
        self.window.bind("<B1-Motion>", lambda a: self.game.togglePop(a))
        self.window.bind("<Return>", lambda a: self.game.togglePause())
        self.window.bind("[", lambda a: self.game.changeSpeed(-0.05))
        self.window.bind("]", lambda a: self.game.changeSpeed(0.05))
        self.window.bind("<BackSpace>", lambda a: self.game.reset())

    #Construction funcs
    def makeGrid(self):
        self.boxes = {}
        
        for x in range(0, self.width):
            for y in range(0, self.height):
                self.boxes[(x, y)] = self.canvas.create_rectangle(x*self.scale, y*self.scale,
                                                                  (x*self.scale)+self.scale, (y*self.scale)+self.scale,
                                                                  fill=bgCol, activefill=highlightCol)

    #Update funcs
    def updateTitle(self, paused, delay):
        if paused:
            self.window.wm_title("{0}s | PAUSED".format(delay))
        else:
            self.window.wm_title("{0}s".format(delay))
            
    def changeColour(self, x, y, col):
        self.canvas.itemconfig(self.boxes[(x, y)], fill=col)

    def reset(self):
        for x in range(self.width):
            for y in range(self.height):
                self.canvas.itemconfig(self.boxes[(x, y)], fill=highlightCol)
            self.canvas.update()
            
        time.sleep(0.1)
        for x in range(self.width):
            for y in range(self.height):
                self.canvas.itemconfig(self.boxes[(x, y)], fill=bgCol)
            self.canvas.update()


a = R90(80, 80, 10)
a.start()
a.gui.window.mainloop()

Weeeeeeeeeeeee

Cells populated by the user are highlightCol and constant.

1 Like