Game of Life, separation of logic / GUI

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)
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:
"""
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.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()
• I'd advice you to have a look at youtube.com/watch?v=o9pEzgHorH0 from the 17th minute. – SylvainD Jun 21 '13 at 15:49
• 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 – user1436660 Jun 21 '13 at 17:12
• You might be interested in this answer which uses Pygame instead Tkinter. – Gareth Rees Dec 15 '13 at 14:40

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.