-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcgol.py
More file actions
233 lines (194 loc) · 6.56 KB
/
cgol.py
File metadata and controls
233 lines (194 loc) · 6.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import argparse
import os
from PIL import Image
import random
import time
parser = argparse.ArgumentParser(
prog="cgol",
description="Conway's Game of Life in the terminal",
epilog="https://github.com/Koei32/cgol-in-terminal",
)
parser.add_argument(
"-i",
help="use an image file for game grid. (example: 'cgol.py -i ./sample_images/rpentomino.png')",
)
parser.add_argument(
"-r",
help="initialize a random grid. specify a grid size after -r. (example: 'cgol.py -r 16')",
)
parser.add_argument(
"-a", help="automatically steps every 0.5 seconds.", action="store_true"
)
args = parser.parse_args()
DEAD = " "
LIVE = "■"
def init_field():
"""
Returns a field of size `W*H` filled with dead (0) cells.
"""
field = []
for i in range(H):
row = []
for j in range(W):
row.append(0)
field.append(row)
return field
def init_random_field():
"""
Returns a field of size `W*H` with cells randomized (either `0` or `1`)
"""
if W == 0 or H == 0:
raise ValueError("Grid size cannot be zero.")
board = []
for i in range(H):
row = []
for j in range(W):
if random.randint(1, 2) == 1:
row.append(1)
else:
row.append(0)
board.append(row)
return board
def print_board(board):
"""
Prints the given game field to the console with a border around it.
"""
border_top = "┌" + "─" * ((W * 2) + 1) + "┐"
border_bottom = "\n└" + "─" * ((W * 2) + 1) + "┘"
rows = ""
for i in board:
rows += "\n│ "
for j in i:
rows += (DEAD if j == 0 else LIVE) + " "
rows += "│"
full = border_top + rows + border_bottom
print(full)
def img_to_field(image_path, threshold):
"""
Converts given image file into a scaled game field.
Takes in the image path and the threshold value to use for converting the color image to a bitonal image.
Returns a game field (2d list) scaled to be roughly 48 pixels in height.
"""
try:
img = Image.open(image_path)
# calculating the factor to reduce the image to a maximum of 48±1 pixels in height
if img.size[1] > 48:
reduce_factor = int(max(img.size) / 48)
img = img.reduce(reduce_factor)
# reduces image, sizeconverts it to grayscale, applies a threshold, and converts it to monochrome
img = img.convert("L").point(lambda p: 255 if p > threshold else 0).convert("1")
pixels = list(img.getdata())
w, h = img.size
except:
print("Image file not found or has an invalid format.")
return None
field = []
start, end = 0, w
# loop to construct a game field 2d list from a 1d list of pixels
for i in range(h):
field.append(pixels[start:end])
start += w
end += w
for j in range(w):
if field[i][j] == 255:
field[i][j] = 0
elif field[i][j] == 0:
field[i][j] = 1
else:
# this line is redundant, image will always be converted to monochrome at init
print(
"Image is not bitonal (image should contain only pure white (#ffffff) and black (#000000) pixels)"
)
return None
return field
def step(field):
"""
Takes the given game field, advances it by 1 simulation step, and returns the new field.
"""
temp = init_field()
# goes through the whole field and runs eval_cell on every cell.
for i in range(H):
for j in range(W):
temp[i][j] = eval_cell(i, j, field)
return temp
def eval_cell(m, n, field):
"""
Takes the coordinates of the cell to be evaluated and the game field as arguments.
Returns the resultant cell state after evaluating it according to the rules of CGOL.
The rules are:
1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
2. Any live cell with two or three live neighbours lives on to the next generation.
3. Any live cell with more than three live neighbours dies, as if by overpopulation.
4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
"""
# offsets from the given coordinates to get all neighbors
offsets = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
live_neighbors = 0
# goes through all cells offset from the given cell by `offsets` and
# increments live_neighbors if a live neighbor is found
for offset in offsets:
if m + offset[0] >= 0 and n + offset[1] >= 0:
neighbor = (m + offset[0], n + offset[1])
try:
if field[neighbor[0]][neighbor[1]] == 1:
live_neighbors += 1
except:
pass
# CGOL logic
if live_neighbors == 3:
return 1
if live_neighbors in [2, 3] and field[m][n] == 1:
return 1
else:
return 0
def clear_screen():
os.system("cls" if os.name == "nt" else "clear")
# if no args are given
if args.i is None and args.r is None:
print(
"""
_|_|_| _|_|_| _|_| _|
_| _| _| _| _|
_| _| _|_| _| _| _|
_| _| _| _| _| _|
_|_|_| _|_|_| _|_| _|_|_|_|
https://github.com/Koei32/cgol-in-terminal/
No arguments given. Try `cgol.py -r 16` for example. (use --help for help)"""
)
input("Press return to exit...")
quit()
# if image arg is given
if args.i is not None:
# default threshold is 0.5 (128)
game = img_to_field(args.i, 128)
W, H = len(game[0]), len(game)
if game is None:
print("Invalid game field.")
quit()
# if random field arg is given
elif args.r:
try:
W, H = int(args.r), int(args.r)
game = init_random_field()
except:
print("Invalid grid size. Must be non-zero integer.")
quit()
gen = 0
# main loop
while True:
try:
clear_screen()
print_board(game)
game = step(game)
gen += 1
print(f"\nGeneration: {gen}")
print(f"Grid Size: {W}x{H}")
print("Press Ctrl+C to stop...")
if args.a:
time.sleep(0.2)
pass
else:
input("Press enter to step...\r")
except KeyboardInterrupt:
print("Stopping...")
quit()