forked from johnhw/glgol
-
Notifications
You must be signed in to change notification settings - Fork 0
/
callahan.py
191 lines (138 loc) · 5.03 KB
/
callahan.py
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
"""
Implements the lookup tables and manual packing/unpacking routines
to operate in the 16 state CA.
The encoding is as follows:
* The original binary pattern is packed into a 16 state format (2x2 binary cells -> one 16 state "block").
+---+
|a b|
|c d|
+---+
# packed is a 4 bit integer code for one block
packed = a + (b << 1) + (c << 2) + (d << 3)
* A lookup table mapping every 4x4 "superblock" of binary cells to a 2x2 successor is created and stored as a texture. This encodes the Life rule
(or any other outer-totalistic rule). F' means the successor of cell F
in the next generation after applying the Life rule.
4x4 -> 2x2 centre in next generation
+-------+
|a b c d| +------+
|e F G h| -> | F' G'|
|i J K l| | J' K'|
|m n o p| +------+
+-------+
This superblock is split into 4 2x2 parts, with the NX' being the centre block in the next generation (note this introduces an offset, but this is easily compensated for)
+---+ +---+
NW = |a b| NE = |c d|
|e F| |G h|
+---+ +---+
+---+ +---+
SE = |i J| SW = |K l|
|m n| |o p|
+---+ +---+
+----+
NX'= |F'G'|
|J'K'|
+----+
So that the final table maps each 4x4 superblock to a new 2x2 block NX, offset by one cell to the northwest.
+-----+ +------+
|NW NE| -> |NX' * |
|SW SE| |* * |
+-----+ +------+
lookup_table[NW, NE, SW, SE] = NX'
"""
import numpy as np
import lifeparsers
import re
def mkeven_integer(arr):
# force even size
return np.pad(
arr, ((arr.shape[0] % 2, 0), (arr.shape[1] % 2, 0)), "constant"
).astype(np.uint8)
def pack_callahan(arr):
# pack a NumPy array into 16 state/4 bit 2x2 cell format
return (
arr[::2, ::2]
+ (arr[1::2, ::2] << 1)
+ (arr[::2, 1::2] << 2)
+ (arr[1::2, 1::2] << 3)
)
def unpack_callahan(cal_arr):
# unpack from 4 bit 2x2 cell format into standard array
unpacked = np.zeros((cal_arr.shape[0] * 2, cal_arr.shape[1] * 2), dtype=np.uint8)
unpacked[::2, ::2] = cal_arr & 1
unpacked[1::2, ::2] = (cal_arr >> 1) & 1
unpacked[::2, 1::2] = (cal_arr >> 2) & 1
unpacked[1::2, 1::2] = (cal_arr >> 3) & 1
return unpacked
# parse a b3s23 style rule
def parse_rule(rule):
birth_survive = re.findall(r"[bB]([0-9]+)\s*\/?\s*[sS]([0-9]+)", rule)[0]
def digits(seq):
return [int(d) for d in seq]
return digits(birth_survive[0]), digits(birth_survive[1])
# table maps 16 state cell, encoded as:
# ab
# cd
# packed = a + (b<<1) + (c<<2) + (d<<3)
# to RGBA
def callahan_colour_table():
from colorsys import yiq_to_rgb
colour_table = np.ones((16, 4), dtype=np.uint8)
for iv in range(16):
a = iv & 1
b = (iv >> 1) & 1
c = (iv >> 2) & 1
d = (iv >> 3) & 1
y = (a + b + c + d) / 4.0
i = ((a - b) + (c - d)) / 3.0
q = ((a - c) + (b - d)) / 3.0
colour_table[iv, :3] = [int(x * 255) for x in yiq_to_rgb(y, i, q)]
return colour_table
def create_callahan_table(rule="b3s23"):
"""Generate the lookup table for the cells."""
# map predecessors to successor
s_table = np.zeros((16, 16, 16, 16), dtype=np.uint8)
# map 16 "colours" to 2x2 cell patterns
birth, survive = parse_rule(rule)
# apply the rule to the 3x3 block of cells
def apply_rule(*args):
n = sum(args[1:])
ctr = args[0]
if ctr and n in survive:
return 1
if not ctr and n in birth:
return 1
return 0
# abcd
# eFGh
# iJKl
# mnop
# pack format
# ab
# cd
# packed = a + (b<<1) + (c<<2) + (d<<3)
# generate all 16 bit strings
for iv in range(65536):
bv = [(iv >> z) & 1 for z in range(16)]
a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p = bv
# compute next state of the inner 2x2
nw = apply_rule(f, a, b, c, e, g, i, j, k)
ne = apply_rule(g, b, c, d, f, h, j, k, l)
sw = apply_rule(j, e, f, g, i, k, m, n, o)
se = apply_rule(k, f, g, h, j, l, n, o, p)
# compute the index of this 4x4
nw_code = a | (b << 1) | (e << 2) | (f << 3)
ne_code = c | (d << 1) | (g << 2) | (h << 3)
sw_code = i | (j << 1) | (m << 2) | (n << 3)
se_code = k | (l << 1) | (o << 2) | (p << 3)
# compute the state for the 2x2
next_code = nw | (ne << 1) | (sw << 2) | (se << 3)
# get the 4x4 index, and write into the table
s_table[nw_code, ne_code, sw_code, se_code] = next_code
return s_table
def pack_life(lif):
# pack into 4 bit format
lif_int = mkeven_integer(lif)
packed = pack_callahan(lif_int)
return packed
def load_life(fname):
return lifeparsers.to_numpy(lifeparsers.autoguess_life_file(fname)[0])