-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlife.py
More file actions
executable file
·400 lines (345 loc) · 9.92 KB
/
life.py
File metadata and controls
executable file
·400 lines (345 loc) · 9.92 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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# John Conway's Game of Life
#
# Runs on CircuitPython
#
# Cellular automata displayed on a grid of cells
# Some cells begin populated
# Repeated generations are calculated using the following rules:
#
# 1. Live cells with 2 or 3 live neighbors survive
# 2. Dead cells with 3 live neighbors become live cells
# 3. Every other cell stays dead or dies
#
# Supports print and/or LED Grid output
#
# The implementation here begins with a World dictionary with:
#
# world['rows'] = Number of rows in the world grid
# world['columns'] = Number of columns in the world grid
# world['world_length'] = length of bytearray representing world
# world['cells'] = A bytearray() representing all of the world's cells
# world['past_cells'] = Bytearray() for previous generation(s)
# world['offsets'] = Offsets to neighbors from a cell
# In data, the world is represented as a linear array of bytes.
# Logically, the world is an nxm grid surrounded by a layer of zeros
# The zeros are a void barrier that limits the civilization
#
from random import randint
from board import TX, RX, A1, D0
import busio, digitalio, time, math
# MatrixN controls "n" matrices
from ledmatrix import MatrixN
# Pixel width and height of world (should be multiple of
# 8 if using an LED grid)
DISPLAY_WIDTH = const(16)
DISPLAY_HEIGHT = const(16)
MATRIX_ROTATE = True
# Display brightness (0-15)
DISPLAY_BRIGHTNESS = 0
# Output can be directed to the LED grid, print (serial out),
# or both: show_world(w, 'print', 'matrix')
OUTPUT_MODE = 'matrix'
# OUTPUT_MODE = 'print'
# How to seed the world (other options are available, see below)
LIFE_SEED = 'random'
#LIFE_SEED = 'bullseye'
# Options for generation delay, maximum number of generations,
# the pause in the timeline between simulations, and the number of
# generations to check for a repeat pattern
GENERATION_DELAY = .10
GENERATION_MAXIMUM = 300
TIMELINE_PAUSE = 1.0
HISTORY_DEPTH = 6
# Number of repetitions for 2-generation repeat patterns
MAX_PATTERN_REPEATS = 10
# LED Grid pins/ SPI setup
clk = RX
din = TX
cs = digitalio.DigitalInOut(A1)
spi = busio.SPI(clk, MOSI=din)
# Generation Reset Button
button_reset = digitalio.DigitalInOut(D0)
button_reset.direction = digitalio.Direction.INPUT
button_reset.pull = digitalio.Pull.UP
# Display is mapped to a number of 8x8 LED grids
display = MatrixN(spi, cs, DISPLAY_WIDTH, DISPLAY_HEIGHT)
# Initialize the display
display.init_display()
display.brightness(DISPLAY_BRIGHTNESS)
display.fill(0)
display.show()
# Returns a new world. Specify width and height.
def world(width, height):
row_length = width+2
first_cell = row_length+1
world_length = (height+2) * (width+2)
w = {
'rows' : height,
'columns' : width,
'world_length' : world_length,
'cells' : bytearray(world_length),
'past_cells' : [bytearray(math.ceil(world_length/8)) for l in range(HISTORY_DEPTH+1)],
'offsets' : [-first_cell, -first_cell+1, -first_cell+2, \
-1, +1, width+1, width+2, width+3]
}
return w
# Compress the a world's cells, 1 bit per cell
def compress_world (w):
# Shift old cells by one to make room
for c in range(HISTORY_DEPTH, 0, -1):
w['past_cells'][c][:] = w['past_cells'][c-1]
# Compress the current cells into the past_cells list
# Each cell will be 0 or 1. Shift the rightmost bit to the
# correct position (0-7). When each output byte is filled, go to
# to the next byte
output_byte = 0
w['past_cells'][0][output_byte] = 0
b = 0
for c in range (w['world_length']):
w['past_cells'][0][output_byte] = (w['past_cells'][0][output_byte] << 1) | w['cells'][c]
b = (b+1) % 8
if (b==0):
output_byte += 1
w['past_cells'][0][output_byte] = 0
# Returns True if current is unique in recent generations, False otherwise
def unique_world(w):
for g in range(HISTORY_DEPTH):
if (w['past_cells'][0] == w['past_cells'][g+1]):
return False
return True
# Seeds the world from a source ('random', 'frog', 'clapper', 'blinker', ...)
def seed_world(w, *argv):
rows = w["rows"]
columns = w["columns"]
# Clear the world
for c in range(w['world_length']): w['cells'][c]=0
# Clear past worlds
for c in range(HISTORY_DEPTH+1):
w['past_cells'][c][:] = bytearray(math.ceil(w['world_length']/8))
# Seed the world depending on type (default 'random')
# May combine more than one
seed_type = argv if len(argv)>0 else ['carousel']
if seed_type[0] == 'carousel':
seed_type = [[ \
'random', \
'frogger', \
'clapper', \
'nova', \
'blinkers', \
'bullseye', \
'glider' \
][randint(0,6)]]
# Add the seeds
for type in seed_type:
if (type=='random'):
cell = 1
for r in range(rows):
cell += (columns+2)
for c in range(columns):
w['cells'][cell+c] = randint(0,1)
elif (type=='frogger'):
pattern = bytes(
b'....OOO.' +\
'..O....O' +\
'..O....O' +\
'..O....O' +\
'........' +\
'....OOO.' +\
'........' +\
'........'
)
elif (type=='clapper'):
pattern = bytes(
b'........' + \
'........' + \
'...O....' + \
'...OO...' + \
'...OO...' + \
'....O...' + \
'........' + \
'........'
)
elif (type=='blinkers'):
pattern = bytes(
b'........' +\
'.....OOO' +\
'.O......' +\
'.O......' +\
'.O......' +\
'........' +\
'.....OOO' +\
'........'
)
elif (type=='nova'):
pattern = bytes(
b'........' +\
'........' +\
'........' +\
'....O...' +\
'...OOO..' +\
'..OO.OO.' +\
'........' +\
'........'
)
elif (type=='bullseye'):
pattern = bytes(
b'........' +\
'OOO.....' +\
'........' +\
'........' +\
'........' +\
'.....OO.' +\
'.....O.O' +\
'.....O..'
)
elif (type=='glider'):
pattern = bytes(
b'........' +\
'........' +\
'........' +\
'........' +\
'........' +\
'.OO.....' +\
'O.O.....' +\
'..O.....' \
)
elif (type=='void'):
pattern = bytes(
b'........' +\
'........' +\
'........' +\
'........' +\
'........' +\
'........' +\
'........' +\
'........'
)
elif (type=='untitled'):
pattern = bytes(
b'........' +\
'........' +\
'........' +\
'........' +\
'........' +\
'........' +\
'........' +\
'........'
)
# This catches the case of 'random' or unknown
# since 'pattern' won't exist
try:
orientation = randint(0,3)
cell = int(((DISPLAY_WIDTH - 8)/2) + (((DISPLAY_HEIGHT - 8)/2) * (DISPLAY_WIDTH + 2)) + 1)
for r in range(8):
cell += (columns+2)
for c in range(8):
if orientation == 0: w['cells'][cell+c] = pattern[(r*8) + c] == ord('O')
elif orientation == 1: w['cells'][cell+c] = pattern[(r*8) + (8-c-1)] == ord('O')
elif orientation == 2: w['cells'][cell+c] = pattern[((8-r-1)*8) + c] == ord('O')
elif orientation == 3: w['cells'][cell+c] = pattern[((8-r-1)*8) + (8-c-1)] == ord('O')
except:
pass
# Calculate the world's next generation
def next_generation(w):
rows = w["rows"]
columns = w["columns"]
past_cells = bytearray(w['world_length'])
past_cells[:] = w['cells']
row_start = 1
for r in range(rows):
row_start += (columns+2)
for c in range(columns):
# The index of this world cell
world_cell = row_start + c
# Take a census of this cells neighbors
census = 0
for o in w['offsets']:
census += past_cells[world_cell+o]
# Apply Conway's rules:
# Cells with 2 neighbors don't change
# Cells with 3 neighbors give birth
# Cells with <2 or >3 neighbors die
if (census!=2): w['cells'][world_cell] = 1 if (census == 3) else 0
# Show the world by printing, on an LED grid, or both
def show_world(w, *argv):
displays = argv if len(argv) > 0 else ['print']
for type in displays:
if type == 'print':
print_world(w)
elif type == 'matrix':
matrix_world(w)
def print_world(w):
rows = w["rows"] + 2
columns = w["columns"] + 2
for r in range(rows):
start_cell = columns * r
for c in w['cells'][start_cell:start_cell+columns]:
print(' .' if c == 0 else ' O', end="")
print()
print('\n')
def matrix_world(w):
rows = w["rows"]
columns = w["columns"]
display.fill(0)
for r in range(rows):
start_cell = columns+3 + r*(columns+2)
for c in range(columns):
if (w['cells'][start_cell+c] != 0):
if MATRIX_ROTATE:
rmod = r % 8
cmod = c % 8
x = (c - cmod) + 7 - rmod
y = (r - rmod) + cmod
else:
x = c
y = r
display.pixel(x,y,1)
display.show()
# Run a single simulation until the world is stable
# or until 'max' generations.
#
# 'w' is the world
# 't' is the time delay between generations
# 'max' is the maximum number of generations
# *argv can be empty, 'print' and/or 'matrix'
#
# For example:
#
# live_life(w, .5, 50)
# live_life(w, .25, 70, 'print')
# live_life(w, .25, 70, 'matrix')
# live_life(w, .25, 70, 'print', 'matrix')
#
def live_life(w, t, max, *argv):
repeats = 0
for g in range(GENERATION_MAXIMUM):
# Display the generation, generate the next one
# and compress it into history
show_world(w, *argv)
next_generation(w)
compress_world(w)
# Check if the world is stable. if so, break
if (repeats > MAX_PATTERN_REPEATS) or not button_reset.value:
break
# See if this world is unique in history (increment repeat number
# so we can let the pattern go for awhile to make it apparent)
elif not unique_world(w):
repeats += 1
time.sleep(t)
# Create a world
w = world(DISPLAY_WIDTH, DISPLAY_HEIGHT)
# Run continuous simulations seeding a new world each time
# Default world is 'random', but can be any number of shapes
# including 'carousel', which randomly chooses a seed pattern
#
# 'random', 'carousel', frogger', 'clapper', 'blinkers',
# 'glider', 'bullseye'
# examples:
# seed_world(x)
# seed_world(x, 'frogger')
# seed_world(x, 'blinkers', 'clapper')
#
while True:
seed_world(w, LIFE_SEED)
live_life(w, GENERATION_DELAY, GENERATION_MAXIMUM, OUTPUT_MODE)
time.sleep(TIMELINE_PAUSE)