-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgame-of-life.py
More file actions
267 lines (224 loc) · 8.79 KB
/
Copy pathgame-of-life.py
File metadata and controls
267 lines (224 loc) · 8.79 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
from base64 import urlsafe_b64decode
from random import random
from flask import Flask
try:
from fastly_compute.wsgi import WsgiHttpIncoming
except ImportError:
# We're running this using the Flask debug server.
running_under_compute = False
else:
running_under_compute = True
app = Flask(__name__)
# Board width and height. We assume a square board for now. If you change this,
# change the f"{i:010000b}" format string below to be the new value squared.
WIDTH = HEIGHT = 50
def decompressed_board(compressed: str) -> str:
"""Decompress the board representation sent from JS, returning a B&W board
string ("10011011"...).
Viceroy will pass us no more than 1936 bytes of the board. (Or maybe the
entire URL gets truncated at 1965b.) This overcomes that (for the board
sizes we're interested in).
:arg compressed: A urlsafe_b64encode()d representation of the bit-packed
black-and-white board. (We don't need color info in order to compute the
next board.)
This saves 83% space. RLE would save about 90% but would depend on the board
and would be slower to compute.
"""
if compressed == "none":
return "none"
i = int.from_bytes(urlsafe_b64decode(compressed))
return f"{i:02500b}"
@app.route("/board/<compressed_board>")
def board(compressed_board: str):
"""Return the next frame of the Game Of Life, given the current one. If "none"
is given instead, return a new random board."""
cells = decompressed_board(compressed_board)
# Random board on start:
if cells == "none":
return "".join("1" if random() < 0.1 else "0" for _ in range(HEIGHT * WIDTH))
# Otherwise, evolve 1 step:
new_cells = ""
for i in range(len(cells)):
new_cells += new_cell_color(cells, i)
return new_cells
def new_cell_color(cells, cell_index):
"""Compute the new color of a single cell at the given offset."""
count = sum(
(cells[neighbor_index] != "0") for neighbor_index in neighbors(cell_index)
)
if cells[cell_index] != "0":
if 2 <= count <= 3:
return str(count)
else:
return "0"
elif count == 3:
return "1"
return "0"
def xy_neighbors(x, y):
"""Return an iterable of the x-y coordinates of all the adjacent cells,
omitting any that are outside the board bounds."""
x_can_grow = x + 1 < WIDTH
y_can_grow = y + 1 < HEIGHT
x_can_shrink = x >= 1
y_can_shrink = y >= 1
if x_can_grow:
yield x + 1, y
if x_can_shrink:
yield x - 1, y
if y_can_grow:
yield x, y + 1
if y_can_shrink:
yield x, y - 1
if x_can_grow and y_can_grow:
yield x + 1, y + 1
if x_can_grow and y_can_shrink:
yield x + 1, y - 1
if x_can_shrink and y_can_grow:
yield x - 1, y + 1
if x_can_shrink and y_can_shrink:
yield x - 1, y - 1
def neighbors(cell_index):
"""Return an iterable of the indices of all the adjacent cells, omitting any
that are outside the board bounds."""
y, x = divmod(cell_index, WIDTH)
for neighbor_x, neighbor_y in xy_neighbors(x, y):
yield neighbor_y * WIDTH + neighbor_x
@app.route("/")
def root():
return """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Conway's Game of Life</title>
</head>
<body>
<svg style="width: 100%%" viewBox="0 0 %(viewbox_width)s %(viewbox_width)s" id="grid" xmlns="http://www.w3.org/2000/svg">
</svg>
<p style="color: gray; font-weight: bold; font-family: sans-serif; text-align: right;"><span id="fps">…</span> <span style="font-size: 75%%">FPS</span></p>
<script defer>
const colors = {"0": "white",
"1": "#E44DA2",
"2": "#86E9C9",
"3": "#A577DF"};
let board = "none";
const max = %(width)s;
let recent_boards = ["", "", "", "", "", "", ""];
let boredom_counter = 0;
let fps = 0;
// Draw a square grid of circles, and return a 2D array of them.
function initGrid() {
let svg = document.getElementById("grid");
let grid = [];
for (let y = 0; y < max; y++) {
row = [];
grid.push(row);
for (let x = 0; x < max; x++) {
let circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", 5 + x * 10);
circle.setAttribute("cy", 5 + y * 10);
circle.setAttribute("r", 4.5);
circle.setAttribute("fill", "white");
svg.appendChild(circle);
row.push(circle);
}
}
return grid;
}
// Compress the board, shucking off color info and interpreting the
// remaining 1s and 0s as binary digits. Interpret as an int, and
// encode using URL-safe base64.
function compressedBoard(board) {
if (board == "none") {
return "none";
}
// Collapse to black-and-white:
let binary = board.replace(/2|3/g, "1");
// Pad to multiple of 8 bits if necessary:
binary = binary.padStart(Math.ceil(binary.length / 8) * 8, "0");
// Convert binary string to bytes:
const bytes = [];
for (let i = 0; i < binary.length; i += 8) {
const byte = binary.slice(i, i + 8);
bytes.push(parseInt(byte, 2));
}
// Convert bytes to URL-safe base64:
let ret = "";
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
for (let i = 0; i < bytes.length; i += 3) {
// Get next 3 bytes:
const byte1 = bytes[i] || 0;
const byte2 = bytes[i + 1] || 0;
const byte3 = bytes[i + 2] || 0;
// Pack them into a 24-bit number:
const combined = (byte1 << 16) | (byte2 << 8) | byte3;
// Pull out four 6-bit values:
const char1 = base64Chars[(combined >> 18) & 0x3F];
const char2 = base64Chars[(combined >> 12) & 0x3F];
const char3 = i + 1 < bytes.length ? base64Chars[(combined >> 6) & 0x3F] : "=";
const char4 = i + 2 < bytes.length ? base64Chars[combined & 0x3F] : "=";
ret += char1 + char2 + char3 + char4;
}
return ret;
}
async function updateGrid(grid) {
// Fetch new grid:
// These days, max querystring URL length is 65K in Firefox. Only Edge is shorter, at 2083.
board = await (await fetch("./board/" + compressedBoard(board))).text();
// Draw it:
let i = 0;
for (let y = 0; y < max; y++) {
for (let x = 0; x < max; x++) {
grid[y][x].setAttribute("fill", colors[board[i++]]);
}
}
fps += 1;
}
// If we've seen this board configuration a bunch of times recently,
// clear the board and start with a fresh one.
function assuageBoredom() {
for (recent_board of recent_boards) {
if (board == recent_board) {
boredom_counter += 1;
break;
}
}
recent_boards.shift();
recent_boards.push(board);
if (boredom_counter >= 10) {
board = "none";
// Has to be at least 7 long to detect the union of 3-tick and
// 2-tick oscillators going at it:
recent_boards = ["", "", "", "", "", "", ""];
boredom_counter = 0;
}
}
// Main loop
async function startAnimation(grid) {
await updateGrid(grid);
assuageBoredom();
animationId = requestAnimationFrame(function() { startAnimation(grid) });
}
function startFpsCounter() {
el = document.getElementById("fps");
const secs = 3;
function updateFpsReadout () {
el.textContent = Math.round(fps / secs);
fps = 0;
setTimeout(updateFpsReadout, secs * 1000);
}
setTimeout(updateFpsReadout, secs * 1000);
}
function main() {
let grid = initGrid();
startAnimation(grid);
startFpsCounter();
}
main();
</script>
</body>
</html>
""" % {"width": WIDTH, "viewbox_width": 10 + WIDTH * 10}
if running_under_compute:
HttpIncoming = WsgiHttpIncoming(app, reuse_sandboxes_for_ms=300)