6
\$\begingroup\$

This is one of my first steps with GUI. I've tried an implementation of Conway's Game of Life with TkInter. I would appreciate any opinions about my code, especially about the separation of GUI and logic.

import Tkinter as tk
from copy import deepcopy
from random import randrange

def random_fields(width):
    """
    Returns a set of tuples of random integers, eg: set((2,4), (6,1)).

    These tuples are meant as coordiantes.
    The parameter width is the maximum value of x and y.
    """
    fields_alive = width * width // 6
    fields = set()
    for _ in range(fields_alive):
        x = randrange(width)
        y = randrange(width)
        fields.add((x,y))
    return fields

class Field(object):
    """ Represents a field in Game of Life. """
    def __init__(self, x, y):
        """ Constructs a "dead" field at coords x, y """
        self.x = x
        self.y = y
        self.is_alive = False

    def __str__(self):
        return "Field: \n   x: {0}; y: {1}; is_alive: {2}\n".format(self.x, self.y, self.is_alive)

    def change(self):
        """ Changes from dead to alive and vice-versa. """
        if self.is_alive:
            self.is_alive = False
        else:
            self.is_alive = True


class GameOfLifeMatrix(object):
    """
    Represents a matrix in Game of Life and contains the logic.

    The global STARTFIELDS is a dict, with some well known repeating patterns like:
    "blinker" and "toad"
    """
    STARTFIELDS = { "blinker": ((2,1), (2,2), (2,3)),
                    "toad": ((2,2), (3,2), (4,2), (1,3), (2,3), (3,3))
                   }
    def __init__(self, width = 25, start_fields = "random"):
        """ 
        Constructs a new square matrix for Game Of Life. 

        parameters:
        width: integer, the width and height of the matrix
        start_fields, you can provide: 
         - "blinker" or "toad" for the well known patterns
         - "random" to create a random matrix
         - a set of tuples containing specified "alive" coordinates
        """
        if not self.STARTFIELDS.get(start_fields) is None:
            self.start_fields = self.STARTFIELDS[start_fields]
        elif start_fields == "random":
            self.start_fields = random_fields(width)
        else:
            self.start_fields = start_fields

        self.width = width
        self.generation = 0
        self.matrix = list()
        for x in range(width):
            row = list()
            self.matrix.append(row)
            for y in range(width):
                field = Field(x, y)
                if (x, y) in self.start_fields:
                    field.change()
                row.append(field)

    def _should_change(self, field):
        """ Determines if a given field should be changed according to the rules of the game"""
        if (   field.x == 0
            or field.y == 0
            or field.x == self.width - 1
            or field.y == self.width - 1):
            # i.e.: field is at the border -> should not change?
            return False

        alive_neighbours = 0
        for x in range(field.x -1, field.x +2):
            for y in range(field.y -1, field.y +2):
                if x == field.x and y == field.y:
                    # field is not neighbour of itself
                    continue
                if self.matrix[x][y].is_alive:
                    alive_neighbours += 1

        if (not field.is_alive) and alive_neighbours == 3:
            return True
        elif field.is_alive and (alive_neighbours < 2 or alive_neighbours > 3):
            return True
        else:
            return False 

    def next_generation(self):
        """ Evolves the matrix to the next generation. """
        self.generation += 1
        new_matrix = deepcopy(self.matrix)
        for field in self.fields():
            if self._should_change(field):
                new_matrix[field.x][field.y].change()
        self.matrix = new_matrix


    def fields(self):
        """ Generator, yields all fields of the matrix. """
        for row in self.matrix:
            for field in row:
                yield field




class GameOfLifeApp(tk.Frame):
    """ Represents the TkInter-Version of the game """
    def __init__(self, game, generation_interval_ms = 600, color_alive = "#000", color_dead = "#fff"):
        """
        Constructs the TkInter app.

        parameters:
        game: an object of type GameOfLifeMatrix
        generation_interval_ms: integer time in milliseconds to display the next genereation
        color_alive and color_dead: TkInter-color-strings for alive and dead fields
            default dead: white "#fff"; default alive: black "#000"
        """
        self.game = game
        self.generation_interval_ms = generation_interval_ms
        self.color_alive = color_alive
        self.color_dead = color_dead
        self.widgets = dict()
        self.root = tk.Tk()
        tk.Frame.__init__(self, self.root)
        self.grid()

        for field in self.game.fields():
            widget = tk.Label(height = 1, width = 2, bg = self.color_dead, relief = "ridge")
            widget.grid(column = field.x, row = field.y)
            if field.is_alive:
                widget.configure(bg = self.color_alive)
            self.widgets[(field.x, field.y)] = widget

        self.root.after(self.generation_interval_ms, self.draw)


    def draw(self):
        """ Draws the new generation. """
        for field in self.game.fields():
            if field.is_alive:
                self.widgets[(field.x, field.y)].configure(bg = self.color_alive)
            else:
                self.widgets[(field.x, field.y)].configure(bg = self.color_dead)
        self.game.next_generation()
        self.root.after(self.generation_interval_ms, self.draw)


if __name__ == "__main__":

    game = GameOfLifeMatrix()
    app = GameOfLifeApp(game)
    app.mainloop()
\$\endgroup\$
  • \$\begingroup\$ I'd advice you to have a look at youtube.com/watch?v=o9pEzgHorH0 from the 17th minute. \$\endgroup\$ – SylvainD Jun 21 '13 at 15:49
  • \$\begingroup\$ Yes, i thought about converting "Field" to a dictionary, but i asked myself if that verbosity has any major drawbacks? In fact in this module i tried to be as "readable" as possible \$\endgroup\$ – user1436660 Jun 21 '13 at 17:12
  • \$\begingroup\$ You might be interested in this answer which uses Pygame instead Tkinter. \$\endgroup\$ – Gareth Rees Dec 15 '13 at 14:40
2
\$\begingroup\$

In terms of your specific question, I think you have done a good job of separating out the simulation and the presentation. However, you have a few slightly awkward bits of code:

def change(self):
    """ Changes from dead to alive and vice-versa. """
    if self.is_alive:
        self.is_alive = False
    else:
        self.is_alive = True

could be simplified to

def change(self):
    """ Changes from dead to alive and vice-versa. """
    self.is_alive = not self.is_alive

and

if not self.STARTFIELDS.get(start_fields) is None:

to

if start_fields in self.STARTFIELDS:

and

    self.matrix = list()
    for x in range(width):
        row = list()
        self.matrix.append(row)
        for y in range(width):
            field = Field(x, y)
            if (x, y) in self.start_fields:
                field.change()
            row.append(field)

to

matrix = [[Field(x, y) for y in range(width)] for x in range(width)]
for x, y in self.start_fields:
    matrix[x][y].change()

Your approach to next_generation could be improved; if you made a list of all cells that _should_change, then changed them, you could do it in-place and wouldn't need the deepcopy. Separating out _should_change was a good idea, but you could make more of it.

\$\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.