0

I decided to try out Python and it's been fun so far. However while messing around with tkinter I encountered a problem which I haven't been able to solve for hours. I've read some things and tried different stuff but nothing works. I've got the code so far that I think the program should run fine. Except for the fact that I can't make it loop and thus update automatically.

So my question is: how can I call a function with tkinters loop options in an infite loop fashion?

Simple game of life: I wrote basicly 2 classes. A Matrix which stores and handles the single cells and the game itself which utilizes the matrix class through some game logic and basic user input.

First the game class as there is my loop problem:

from tkinter import *
from ButtonMatrix import *

class Conway:

 def __init__(self, master, size = 20, cell_size = 2):

    self.is_running = True
    self.matrix = ButtonMatrix(master,size,cell_size)
    self.matrix.randomize()
    self.matrix.count_neighbours()
    self.master = master

    # playbutton sets boolean for running the program in  a loop
    self.playbutton = Button(master, text = str(self.is_running), command = self.stop)
    self.playbutton.grid(row = 0 , column = size +1 )

    #Test button to trigger the next generation manually. Works as itended. 
    self.next = Button(master, text="next", command = self.play)
    self.next.grid(row = 1, column = size +1)


 def play(self): # Calculates and sets the next generation. Intended to be used in a loop

   if self.is_running:
        self.apply_ruleset()
        self.matrix.count_neighbours()
        self.apply_colors()


 def apply_ruleset(self):

    #The ruleset of conways game of life. I wish i knew how to adress each element
    #without using these two ugly loops all the time

    size = len(self.matrix.cells)

    for x in range (size):
        for y in range (size):

            if self.cell(x,y).is_alive():
                if self.cell(x,y).neighbours < 2 or self.cell(x,y).neighbours > 3:
                    self.cell(x,y).toggle()
            if not self.cell(x,y).is_alive() and self.cell(x,y).neighbours == 3:
                self.cell(x,y).toggle()



 def apply_colors(self): #Some flashy colors just for fun  

    size = len(self.matrix.cells)

    for x in range (size):
        for y in range (size):

            if self.cell(x,y).is_alive():
                if self.cell(x,y).neighbours < 2 or self.cell(x,y).neighbours > 3:
                    self.cell(x,y).button.configure(bg = "chartreuse3")
            if not self.cell(x,y).is_alive() and self.cell(x,y).neighbours == 3:
                self.cell(x,y).button.configure(bg = "lightgreen")


 def cell(self,x,y):        
    return self.matrix.cell(x,y)

 def start (self): #start and stop set the boolean for the loop. They work and switch the state properly
    self.is_running = True
    self.playbutton.configure(text=str(self.is_running), command =self.stop)

 def stop (self): 
    self.is_running = False
    self.playbutton.configure(text=str(self.is_running), command =self.start)


#Test program. I can't make the loop work. Manual update via next button                                     works however
root = Tk()
conway = Conway(root)
root.after(1000, conway.play())
root.mainloop()

The Matrix (only for interested readers):

from tkinter import *
from random import randint

class Cell: 

 def __init__(self,master, cell_size = 1):

    self.alive = False
    self.neighbours = 0

    # initializes a squares shaped button that fills the grid cell
    self.frame = Frame(master, width= cell_size*16, height = cell_size*16) 
    self.button = Button(self.frame, text = self.neighbours, command = self.toggle, bg ="lightgray")
    self.frame.grid_propagate(False) 
    self.frame.columnconfigure(0, weight=1) 
    self.frame.rowconfigure(0,weight=1) 
    self.button.grid(sticky="wens") 


 def is_alive(self):

    return self.alive


 def add_neighbour(self):

    self.neighbours += 1


 def toggle (self):

    if self.is_alive()  :
        self.alive = False
        self.button.configure( bg = "lightgray")
    else:
        self.alive = True
        self.button.configure( bg = "green2")


class ButtonMatrix: 

 def __init__(self, master, size = 3, cell_size = 3):

    self.master = master
    self.size = size
    self.cell_size = cell_size
    self.cells = []
    for x in range (self.size):
        row = []
        self.cells.append(row)
    self.set_cells()


 def cell(self, x, y):

    return self.cells[x][y]


 def set_cells(self):

    for x in range (self.size):
        for y in range (self.size):
            self.cells[x] += [Cell(self.master, self.cell_size)]
            self.cell(x,y).frame.grid(row=x,column=y)


 def count_neighbours(self): # Checks 8 sourounding neighbours for their stats and sets a neighbour counter

    for x in range(self.size):
        for y in range(self.size):

            self.cell(x,y).neighbours = 0

            if y  < self.size-1:
                if self.cell(x,y+1).is_alive(): self.cell(x,y).add_neighbour() # Right
                if x > 0 and self.cell(x-1,y+1).is_alive(): self.cell(x,y).add_neighbour() #Top Right
                if x < self.size-1 and self.cell(x+1,y+1).is_alive(): self.cell(x,y).add_neighbour() #Bottom Right

            if x > 0 and self.cell(x-1,y).is_alive(): self.cell(x,y).add_neighbour()# Top
            if x < self.size-1 and self.cell(x+1,y).is_alive():self.cell(x,y).add_neighbour() #Bottom

            if y >  0:
                if self.cell(x,y-1).is_alive(): self.cell(x,y).add_neighbour() # Left
                if x > 0 and self.cell(x-1,y-1).is_alive():  self.cell(x,y).add_neighbour() #Top Left
                if x < self.size-1 and self.cell(x+1,y-1).is_alive(): self.cell(x,y).add_neighbour() #Bottom Left

            self.cell(x,y).button.configure(text = self.cell(x,y).neighbours)


 def randomize (self):
    for x in range(self.size):
       for y in range(self.size):
            if self.cell(x,y).is_alive(): self.cell(x,y).toggle()
            rando = randint(0,2)
            if rando == 1: self.cell(x,y).toggle()
swimfar
  • 147
  • 6
The Fool
  • 16,715
  • 5
  • 52
  • 86
  • I'm not sure I understand your question. Is it just that you want `conway.play` to run every second until the program quits, instead of just once after one second and then never again? – abarnert May 17 '18 at 21:14
  • @abarnert yes exaclty thats what I want. I tried many things by now but nothings works :( – The Fool May 17 '18 at 21:18

1 Answers1

3

There are two problems with your code:

root.after(1000, conway.play())

First, you're not telling Tkinter to call conway.play after 1 second, you're calling conway.play() right now, which returns None, and then telling Tkinter to call None after 1 second. You want to pass the function, not call it:

root.after(1000, conway.play)

Meanwhile, after does not mean "call this function every 1000ms", it means "call this function once, after 1000ms, and then never again". The easy way around this is to just have the function ask to be called again in another 1000ms:

def play(self): # Calculates and sets the next generation. Itended to use in a loop

    if self.is_running:
        self.apply_ruleset()
        self.matrix.count_neighbours()
        self.apply_colors()
    self.master.after(1000, self.play)

This is explained in the docs for after:

This method registers a callback function that will be called after a given number of milliseconds. Tkinter only guarantees that the callback will not be called earlier than that; if the system is busy, the actual delay may be much longer.

The callback is only called once for each call to this method. To keep calling the callback, you need to reregister the callback inside itself:

class App:
    def __init__(self, master):
        self.master = master
        self.poll() # start polling

    def poll(self):
        ... do something ...
        self.master.after(100, self.poll)

(I'm assuming nobody's going to care if your timing drifts a little bit, so after an hour you might have 3549 steps or 3627 instead of 3600+/-1. If that's a problem. you have to get a bit more complicated.)

Community
  • 1
  • 1
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • This solution looks promising and very close to what im trying to achive hoever i get an snytax error unindet does not match any outer intendation level.. normaly stuff like this is fixed by formating the code with tabs and so on. but in this case it doesnt work its this line self.master.after(1000, self.play) – The Fool May 17 '18 at 21:33
  • 1
    @TheFool Normally stuff like this is fixed by _not_ formatting the code with tabs, but the exact opposite. The indentation in the answer is correct. But if your source file is using tabs—or, worse, mixing tabs and spaces—and you paste code copied off SO into it, it often doesn't work. In that case, if you don't know how to make your editor fix things, just retype it manually. Consistently using 4 spaces for indentation, never tabs, and using an editor that does most of that automatically for you, will make most of those problems go away. – abarnert May 17 '18 at 21:38
  • allright I worked with backspaces and spaces and now it runs as i wish :) thank you so much. I will allways remeber to let the function call for itself and to not put () behind the desired function. – The Fool May 17 '18 at 21:42