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()