Welcome, guest | Sign In | My Account | Store | Cart
__author__ = 'Peter'

'''
This is a simulator for Conway's game of life (google to find the rules:
v4 with additional functionality over v3:
    - load or save boards to/from files: OK
    - random fill (with variable density): OK
    - improved layout and added menu bar: OK
    - warp yes/no: OK
    - show / hide grid: OK
    - increase / decrease speed: OK
    - improved code (PEP 8, and logical improvements, especially in modifying mutable objects in functions): OK
'''

import tkinter as tk
import tkinter.filedialog as tkf
import tkinter.font as tkfont
import tkinter.simpledialog as tkd
import sys
import random


def load_board_from_file(filename=None):
    if filename is None:
        filename = tkf.askopenfilename(defaultextension='.gol',
                                       filetypes=(('game of life files', '*.gol'), ('All files', '*.*')))
    board_f = open(filename, 'r')
    row = board_f.readline().strip('\n')
    y = 1
    bd = []
    while row != "":
        bd.append(list(row))
        y += 1
        row = board_f.readline().strip('\n')
    board_f.close()

    # TODO insert some tests here to check if the selected file meets the required file

    return bd, filename


def create_random_board(density):

    for row in range(len(board)):
        for col in range(len(board[0])):
            r = random.randrange(100)
            if r > density:
                board[row][col] = "."
            else:
                board[row][col] = "#"


def clear():
    global step
    rows = len(board)
    cols = len(board[0])
    for row in range(rows):
        for col in range(cols):
            board[row][col] = "."
    step = 0
    display_board(board)


def count_surrounding(row, col, bd):
    count = 0
    rows = len(bd)
    cols = len(bd[0])
    w = warp.get()
    for rr in range(row-1, row+2):
        if rr < 0:
            r = rows-1
        elif rr > rows-1:
            r = 0
        else:
            r = rr
        for cc in range(col-1, col+2):
            if cc < 0:
                c = cols-1
            elif cc > cols-1:
                c = 0
            else:
                c = cc
            if not (r == row and c == col):  # de cel in kwestie overslaan!
                if bd[r][c] == "#":
                    if not ((rr != r or cc != c) and w == 0):
                        count += 1
                    # if warping is off, then cells at the other side shouldn't be counted
    return count


def lifecycle(before):
    rows = len(before)
    cols = len(before[0])
    after = []
    for row in range(rows):         # initialize after
        after.append(list("."*cols))

    for row in range(rows):
        for col in range(cols):
            cs = count_surrounding(row, col, before)
            if before[row][col] == "#" and cs < 2:
                after[row][col] = "."
            elif before[row][col] == "#" and cs > 3:
                after[row][col] = "."
            elif before[row][col] == "." and cs == 3:
                after[row][col] = "#"
            else:
                after[row][col] = before[row][col]
    return after


def countliving():
    return str(alive)


def switch_cell(event):      # turn cell on or off with mouse click
    global alive
    cx = event.x
    cy = event.y
    bx = cx//sz
    by = cy//sz
    if bx < len(board[0]) and by < len(board):
        if board[by][bx] == ".":
            board[by][bx] = "#"
        else:
            board[by][bx] = "."
        display_board(board)


def display_board(bd):
    MyCanvas.delete(tk.ALL)
    cols = len(bd[0])
    rows = len(bd)
    counter = 0
    colors = ['grey', 'orange']
    ol_color = colors[showgrid.get()]
    for x in range(cols):
        for y in range(rows):
            rect = (x*sz, y*sz, (x+1)*sz, (y+1)*sz)
            if bd[y][x] == "#":
                MyCanvas.create_rectangle(rect, outline="black", fill="orange")
                counter += 1
            else:
                MyCanvas.create_rectangle(rect, outline=ol_color)
    stats = "living cells: " + str(counter) + "\n\ngeneration: " + str(step)
    MyCanvas.create_text((10, 10), text=stats, fill='white', anchor='nw', )  # show stats on canvas
    statreport.configure(text=stats)                                         # update stats in widget


def life(from_startbutton=False, from_stepbutton=False):
    global step
    global board
    global pauze
    global alive
    step += 1
    board = lifecycle(board)
    display_board(board)         # draw board and count living cells
    if from_startbutton is True:     # function has been called from start button and not from recursion (root.after...)
        pauze = False            # necessary for restart the startbutton is pressed after the pauzebutton
    checkpauze(pauze)
    if step <= steps and not pauze and not from_stepbutton:
        delay = speedcontrol.get()
        root.after(delay, life)  # root.after(delay, life()) is WRONG:  The function life needs to be passed as argument
                                 # life() will pass the result of life, i.e. execute it, before root.mainloop()


def checkpauze(p=False):
    global pauze
    pauze = p


def save_board_to_file(bd):
    filename = tkf.asksaveasfilename(defaultextension='.gol',
                                     filetypes=(('game of life files', '*.gol'), ('All files', '*.*')))
    if filename:
        f = open(filename, 'w')
        for row in range(len(bd)):
            f.write(''.join(board[row]) + '\n')
        f.close()


def load_board(file=None):      # filename passed when reopening (resetting) same file
    global board
    global step
    global openfile
    board = []
    board, openfile = load_board_from_file(file)
    step = 1
    display_board(board)


def rand_board():
    global board
    global step
    global openfile
    density = tkd.askinteger('Density', 'enter a cell density between 0 and 100')
    create_random_board(density)
    step = 1
    openfile = 'empty_board.gol'
    display_board(board)


def makemenu(win):
    top = tk.Menu(win)  # win=top-level window
    win.config(menu=top)  # set its menu option
    filemenu = tk.Menu(top)
    filemenu.add_command(label='Open...', command=load_board, underline=0)
    filemenu.add_command(label='Save...', command=lambda: save_board_to_file(board), underline=0)
    filemenu.add_command(label='Quit', command=sys.exit, underline=0)
    top.add_cascade(label='File', menu=filemenu, underline=0)
    edit = tk.Menu(top, tearoff=False)
    edit.add_command(label='Clear', command=clear, underline=0)
    edit.add_command(label='Randomize', command=rand_board, underline=0)
    edit.add_separator()
    top.add_cascade(label='Edit', menu=edit, underline=0)


#########  main program ###########

alive = 0
sz = 12                                 # cell size for visualization
steps = 1000                              # run a number of steps
                            # milliseconds
step = 1
board, openfile = load_board_from_file('empty_board.gol')
#warp = 0
#warp = tk.IntVar()                                # warp around the edges ?
pauze = False                               # p = pauze status false at program start

root = tk.Tk()
root.title("Conway's Game of Life")
makemenu(root)
ui = tk.Frame(root, bg='white')                       # user interface
ui2 = tk.Frame(root, bg='white')
#      Define the user interaction widgets
MyCanvas = tk.Canvas(root, width=len(board[0])*sz + 1, height=len(board)*sz+1, highlightthickness=0, bd=0, bg='grey')
warp = tk.IntVar()                                # warp around the edges ?
showgrid = tk.IntVar()
# quitbutton = tk.Button(ui, text='QUIT', width=10, command=sys.exit)
startbutton = tk.Button(ui, text='START', width=10, command=lambda: life(from_startbutton=True))
pauzebutton = tk.Button(ui, text='PAUSE', width=10, command=lambda: checkpauze(p=True))
stepbutton = tk.Button(ui, text='STEP', width=10, command=lambda: life(from_stepbutton=True))
clearbutton = tk.Button(ui, text='CLEAR', width=10, command=clear)
# savebutton = tk.Button(ui, text='SAVE', width=10, command=lambda: save_board_to_file(board))
# loadbutton = tk.Button(ui, text='LOAD', width=10, command=load_board)
restartbutton = tk.Button(ui, text='RESET', width=10, command=lambda: load_board(openfile))
randombutton = tk.Button(ui, text='RANDOMIZE', width=10, command=rand_board)
statreport = tk.Label(root, text="      ", bg='white', justify=tk.LEFT, relief=tk.GROOVE,
                      font=tkfont.Font(weight='bold'))
speedcontrol = tk.Spinbox(ui2, width=5, values=(5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 1000))
speedcontrol_label = tk.Label(ui2, text='Delay between steps:', bg='white')
warpcontrol = tk.Checkbutton(ui2, text='Warp around edges? ', variable=warp, bg='white')
showgridcontrol = tk.Checkbutton(ui2, text='show grid? ', variable=showgrid, bg='white',
                                 command=lambda: display_board(board))
                                 # board must be redrawn, because toggle grid will not be shown before start is pressed
                                 # because display_board(board) sits inside life()
showgridcontrol.select()
# quitbutton.grid(row=0, column=0, padx=10, pady=10)
startbutton.grid(row=1, column=0, padx=10, pady=10)
pauzebutton.grid(row=2, column=0, padx=10, pady=10)
stepbutton.grid(row=3, column=0, padx=10, pady=10)
clearbutton.grid(row=4, column=0, padx=10, pady=10)
# savebutton.grid(row=5, column=0, padx=10, pady=10)
# loadbutton.grid(row=6, column=0, padx=10, pady=10)
restartbutton.grid(row=7, column=0, padx=10, pady=10)
randombutton.grid(row=8, column=0, padx=10, pady=10)
speedcontrol_label.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky=tk.NW)
speedcontrol.grid(row=0, column=2, padx=10, pady=5, sticky=tk.NW)
warpcontrol.grid(row=1, column=3, columnspan=3, padx=10, ipadx=5, sticky=tk.NW)
showgridcontrol.grid(row=0, column=3, columnspan=2, padx=10, pady=5, ipadx=5, sticky=tk.NW)


#       Put everything on the screen
display_board(board)
MyCanvas.bind("<Button-1>", switch_cell)
ui.pack(side=tk.RIGHT, expand=tk.YES, fill=tk.BOTH)
MyCanvas.pack(side=tk.TOP, expand=tk.YES, fill=tk.BOTH)
ui2.pack(side=tk.LEFT, expand=tk.YES, fill=tk.BOTH, anchor=tk.W)
statreport.pack(side=tk.RIGHT, expand=tk.NO, fill=tk.NONE)
root.mainloop()

History