|
| 1 | +# Conway's Game of Life |
| 2 | +# --------------------- |
| 3 | +# |
| 4 | +# from Wikipedia: |
| 5 | +# ************************************ |
| 6 | +# Rules |
| 7 | +# |
| 8 | +# The universe of the Game of Life is an infinite, two-dimensional orthogonal grid of square cells, |
| 9 | +# each of which is in one of two possible states, live or dead (or populated and unpopulated, respectively). |
| 10 | +# Every cell interacts with its eight neighbours, which are the cells that are horizontally, |
| 11 | +# vertically, or diagonally adjacent. At each step in time, the following transitions occur: |
| 12 | +# |
| 13 | +# Any live cell with fewer than two live neighbours dies, as if by underpopulation. |
| 14 | +# Any live cell with two or three live neighbours lives on to the next generation. |
| 15 | +# Any live cell with more than three live neighbours dies, as if by overpopulation. |
| 16 | +# Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. |
| 17 | +# |
| 18 | +# The initial pattern constitutes the seed of the system. |
| 19 | +# The first generation is created by applying the above rules simultaneously to every cell in the seed, |
| 20 | +# live or dead; births and deaths occur simultaneously, |
| 21 | +# and the discrete moment at which this happens is sometimes called a tick. |
| 22 | +# [nb 1] Each generation is a pure function of the preceding one. |
| 23 | +# The rules continue to be applied repeatedly to create further generations. |
| 24 | +# ************************************* |
| 25 | +# |
| 26 | +# Rules for Game of life per number of neighbours |
| 27 | +# alive = True; dead = False |
| 28 | +# neigbours action if True action if False |
| 29 | +# 0 False False |
| 30 | +# 1 False False |
| 31 | +# 2 True False |
| 32 | +# 3 True True |
| 33 | +# 4 False False |
| 34 | +# 5 False False |
| 35 | +# 6 False False |
| 36 | +# 7 False False |
| 37 | +# 8 False False |
| 38 | + |
| 39 | + |
| 40 | +import tkinter as tk |
| 41 | +import numpy as np |
| 42 | + |
| 43 | +# make new tkinter and canvas objects |
| 44 | +def generate_tk_canvas(height, width): |
| 45 | + print("make tkinter and canvas objects") |
| 46 | + new_tk_root = tk.Tk() |
| 47 | + new_tk_root.title(txt) |
| 48 | + new_tk_canvas = tk.Canvas(new_tk_root, background = "black", |
| 49 | + height = height, width = width) |
| 50 | + new_tk_canvas.pack() |
| 51 | + return new_tk_canvas, new_tk_root |
| 52 | + |
| 53 | +# counts the number of neighbours at location row,col in field |
| 54 | +# handles wrap around when at a border of the field |
| 55 | +def neighbours2(field, row, col): |
| 56 | + # all the row and col indexes to be checked |
| 57 | + rows = [(row-1)%Nrow, row, (row+1)%Nrow] |
| 58 | + cols = [(col-1)%Ncol, col, (col+1)%Ncol] |
| 59 | + # make 3x3 arrays containing row and col indexes |
| 60 | + R,C = np.meshgrid(rows,cols) |
| 61 | + # sum the elements equal to True, do not count the row,col element itself |
| 62 | + n = np.sum(field[R,C]) - field[row, col] |
| 63 | + return n |
| 64 | + |
| 65 | +# fill field "target" with new values according to the rules |
| 66 | +# and based on values in field "source" |
| 67 | +def update_field(source, target, gen_count): |
| 68 | + for row in range(Nrow): |
| 69 | + for col in range(Ncol): |
| 70 | + n = neighbours2(source, row, col) |
| 71 | + if n == 2: |
| 72 | + target[row, col] = source[row, col] |
| 73 | + elif n == 3: |
| 74 | + target[row, col] = True |
| 75 | + else: |
| 76 | + target[row, col] = False |
| 77 | + return gen_count+1 |
| 78 | + |
| 79 | +# draw graphical representations of "field" on tkinter canvas "cvs" |
| 80 | +def drawfield(field, cvs): |
| 81 | + # delete previous |
| 82 | + cvs.delete('all') |
| 83 | + # draw vertical grid lines |
| 84 | + x = 0 |
| 85 | + for col in range(Ncol): |
| 86 | + cvs.create_line(x, 0, x, screen_height, fill = grid_color) |
| 87 | + x += dx |
| 88 | + # draw horizontal grid lines |
| 89 | + y = 0 |
| 90 | + for row in range(Nrow): |
| 91 | + cvs.create_line(0, y, screen_width, y, fill = grid_color) |
| 92 | + y += dy |
| 93 | + # get row and column indexes of elements in array field equal to True |
| 94 | + rows, cols = np.where(field == True) |
| 95 | + # draw rectangles at locations in grid given by indexes in rows, cols |
| 96 | + for row, col in zip(rows, cols): |
| 97 | + x = col * dx; y = row * dy |
| 98 | + cvs.create_rectangle(x, y, x+dx, y+dy, fill = cell_color, outline = grid_color) |
| 99 | + cvs.create_text(100, 40, text = f"{txt}\nGeneration: {gen_count}", |
| 100 | + anchor = "nw", font = ("TkFixedFont",18,"normal"), fill = text_color) |
| 101 | + cvs.update() |
| 102 | + |
| 103 | +# this functions performs update on one of the field arrays |
| 104 | +# and shows new data on the tkinter canvas |
| 105 | +# this funtion calls itself after delay "dt" |
| 106 | +def update_generation(): |
| 107 | + global gen_count, field1, field2 # these global vars have to be modified in this function |
| 108 | + drawfield(field1, canvas1) |
| 109 | + gen_count = update_field(field1, field2, gen_count) |
| 110 | + field1, field2 = field2, field1 # swap arrays, latest one becomes source to calc next one |
| 111 | + root1.after(dt, update_generation) |
| 112 | + |
| 113 | +# stops program after mouse click |
| 114 | +def click_handler(event): |
| 115 | + # event also has x & y attributes |
| 116 | + if event.num == 1: |
| 117 | + root1.destroy() |
| 118 | + |
| 119 | + |
| 120 | +# parameters |
| 121 | +Nrow = 50; Ncol = 75 # number of cells in 2 dimensions |
| 122 | +screen_width = 1500; screen_height = 1000 # image size in pixels |
| 123 | +dx = int(screen_width / Ncol); dy = int(screen_height / Nrow) |
| 124 | +dt = 75 # time delay between steps in milliseconds |
| 125 | +txt = "Conway's Game of Life using Python, tkinter and numpy" |
| 126 | +grid_color = "#505050" |
| 127 | +text_color = "#808080" |
| 128 | +cell_color = "red" |
| 129 | + |
| 130 | +# initialise fields |
| 131 | +# the fields are numpy arrays of booleans |
| 132 | +field1 = np.full((Nrow, Ncol), False, dtype = np.bool_) |
| 133 | +field2 = np.full((Nrow, Ncol), False, dtype = np.bool_) |
| 134 | + |
| 135 | +# patterns |
| 136 | +# some known patters are defined here as 2 numpy arrays containing |
| 137 | +# their row and column index values |
| 138 | +# a "blinker", stationary |
| 139 | +blinker_rows = np.array((0,1,2)) |
| 140 | +blinker_cols = np.array((0,0,0)) |
| 141 | +# a "toad", stationary |
| 142 | +toad_rows = np.array((1,1,1,0,0,0)) |
| 143 | +toad_cols = np.array((0,1,2,1,2,3)) |
| 144 | +# a "glider", moves diagonally |
| 145 | +glider_rows = np.array((1,2,2,1,0)) |
| 146 | +glider_cols = np.array((0,1,2,2,2)) |
| 147 | +# a "gosper's gun", complicated machine which generates moving "gliders" |
| 148 | +gosper_gun_rows = np.array((5,5,6,6, 5, 6, 7, 4, 8, 3, 9, 3, 9, 6, 4, 8, 5, 6, 7, 6, 3, 4, 5, 3, 4, 5, 2, 6, 1, 2, 6, 7, 3, 4, 3, 4)) |
| 149 | +gosper_gun_cols = np.array((1,2,1,2,11,11,11,12,12,13,13,14,14,15,16,16,17,17,17,18,21,21,21,22,22,22,23,23,25,25,25,25,35,35,36,36)) |
| 150 | + |
| 151 | +# insert patters in field |
| 152 | +# adding the patterns to field1 by setting correct elements to True |
| 153 | +# adding values to the row or column vectors defines the location in the field |
| 154 | +# subtracting a number with the vector flips the orietation of the pattern |
| 155 | +field1[gosper_gun_rows+15, gosper_gun_cols+5] = True |
| 156 | + |
| 157 | +# make tkinter root and canvas objects |
| 158 | +canvas1, root1 = generate_tk_canvas(screen_height, screen_width) |
| 159 | + |
| 160 | +# define event handler for mouse click |
| 161 | +root1.bind("<Button>", click_handler) |
| 162 | + |
| 163 | +# global variable keeps track of number of generations calculated |
| 164 | +gen_count = 0 |
| 165 | + |
| 166 | +# call this function for the first time, will call itself from then on with time delay |
| 167 | +update_generation() |
| 168 | + |
| 169 | +# tkinter main loop |
| 170 | +tk.mainloop() |
| 171 | + |
| 172 | + |
0 commit comments