1
2
3
4
5
6
7
8(function (global) {
9 'use strict';
10
11 const Life = (typeof module !== 'undefined' && module.exports)
12 ? require('./life.js')
13 : global.Life;
14
15 const results = [];
16
17 function test(name, fn) {
18 try {
19 fn();
20 results.push({ name: name, pass: true });
21 } catch (err) {
22 results.push({ name: name, pass: false, error: err.message });
23 }
24 }
25
26 function assert(condition, message) {
27 if (!condition) {
28 throw new Error(message || 'assertion failed');
29 }
30 }
31
32 function assertEqual(actual, expected, message) {
33 const a = JSON.stringify(actual);
34 const e = JSON.stringify(expected);
35 if (a !== e) {
36 throw new Error((message ? message + ' — ' : '') + 'expected ' + e + ' but got ' + a);
37 }
38 }
39
40 function gridFromRows(rows) {
41 return rows.map(function (row) { return row.slice(); });
42 }
43
44 function liveCoords(grid) {
45 const coords = [];
46 for (let r = 0; r < grid.length; r++) {
47 for (let c = 0; c < grid[r].length; c++) {
48 if (grid[r][c]) coords.push([r, c]);
49 }
50 }
51 return coords;
52 }
53
54
55
56
57
58 test('createGrid builds the requested dimensions, all dead', function () {
59 const grid = Life.createGrid(3, 5);
60 assertEqual(Life.getRows(grid), 3);
61 assertEqual(Life.getCols(grid), 5);
62 assertEqual(Life.countLiveCells(grid), 0);
63 });
64
65 test('setCell/getCell round-trip', function () {
66 const grid = Life.createGrid(4, 4);
67 Life.setCell(grid, 1, 2, 1);
68 assertEqual(Life.getCell(grid, 1, 2, false), 1);
69 assertEqual(Life.getCell(grid, 2, 1, false), 0);
70 });
71
72 test('getCell returns 0 out of bounds when not wrapping', function () {
73 const grid = Life.createGrid(3, 3);
74 assertEqual(Life.getCell(grid, -1, 0, false), 0);
75 assertEqual(Life.getCell(grid, 0, 3, false), 0);
76 assertEqual(Life.getCell(grid, 3, 3, false), 0);
77 });
78
79 test('getCell wraps around edges when wrapping is enabled', function () {
80 const grid = Life.createGrid(3, 3);
81 Life.setCell(grid, 0, 0, 1);
− assertEqual(Life.getCell(grid, 3, 0, true), 1, 'row wraps to 0');
− assertEqual(Life.getCell(grid, -1, 0, true), 1, 'negative row wraps to last row');
− assertEqual(Life.getCell(grid, 0, -1, true), 0);
82 assertEqual(Life.getCell(grid, 3, 0, true), 1, 'row 3 wraps to row 0');
83 Life.setCell(grid, 2, 0, 1);
84 assertEqual(Life.getCell(grid, -1, 0, true), 1, 'row -1 wraps to last row (2)');
85 assertEqual(Life.getCell(grid, 0, -1, true), 0, 'col -1 wraps to last col (2), which is dead');
86 Life.setCell(grid, 2, 2, 1);
− assertEqual(Life.getCell(grid, -1, -1, true), 1, 'both axes wrap simultaneously');
87 assertEqual(Life.getCell(grid, -1, -1, true), 1, 'both axes wrap simultaneously (-1 -> 2 on each)');
88 });
89
90 test('countLiveCells sums all live cells', function () {
91 const grid = gridFromRows([
92 [1, 0, 1],
93 [0, 1, 0],
94 [0, 0, 0]
95 ]);
96 assertEqual(Life.countLiveCells(grid), 3);
97 });
98
99
100
101
102
103 test('countLiveNeighbors counts all 8 surrounding cells', function () {
104 const grid = gridFromRows([
105 [1, 1, 1],
106 [1, 0, 1],
107 [1, 1, 1]
108 ]);
109 assertEqual(Life.countLiveNeighbors(grid, 1, 1, false), 8);
110 });
111
112 test('countLiveNeighbors ignores off-grid neighbors without wrap', function () {
113 const grid = gridFromRows([
114 [1, 1, 0],
115 [1, 0, 0],
116 [0, 0, 0]
117 ]);
118
119 assertEqual(Life.countLiveNeighbors(grid, 0, 0, false), 3);
120 });
121
122 test('countLiveNeighbors counts wrapped neighbors across edges', function () {
123 const grid = Life.createGrid(3, 3);
124 Life.setCell(grid, 2, 2, 1);
125 Life.setCell(grid, 2, 0, 1);
126 Life.setCell(grid, 0, 2, 1);
127 assertEqual(Life.countLiveNeighbors(grid, 0, 0, true), 3);
128 assertEqual(Life.countLiveNeighbors(grid, 0, 0, false), 0);
129 });
130
131
132
133
134
135 test('underpopulation: a live cell with fewer than 2 neighbors dies', function () {
136 const grid = gridFromRows([
137 [0, 0, 0],
138 [0, 1, 0],
139 [0, 0, 1]
140 ]);
141 const next = Life.nextGeneration(grid, false);
142 assertEqual(next[1][1], 0, 'lone live cell should die from isolation');
143 });
144
145 test('overpopulation: a live cell with more than 3 neighbors dies', function () {
146 const grid = gridFromRows([
147 [1, 1, 1],
148 [1, 1, 1],
149 [0, 0, 0]
150 ]);
151 const next = Life.nextGeneration(grid, false);
152 assertEqual(next[0][1], 0, 'center-top cell has 5 live neighbors, dies');
153 assertEqual(next[1][1], 0, 'center cell has 5 live neighbors, dies');
154 });
155
156 test('survival: a live cell with 2 or 3 neighbors survives', function () {
157 const grid = gridFromRows([
158 [1, 1, 0],
159 [1, 0, 0],
160 [0, 0, 0]
161 ]);
162 const next = Life.nextGeneration(grid, false);
163 assertEqual(next[0][0], 1, 'top-left has exactly 2 live neighbors, survives');
164 });
165
166 test('reproduction: a dead cell with exactly 3 neighbors becomes alive', function () {
167 const grid = gridFromRows([
168 [1, 1, 0],
169 [1, 0, 0],
170 [0, 0, 0]
171 ]);
172 const next = Life.nextGeneration(grid, false);
173 assertEqual(next[1][1], 1, 'center cell has exactly 3 live neighbors, is born');
174 });
175
176 test('dead cell with other than 3 neighbors stays dead', function () {
177 const grid = gridFromRows([
178 [1, 1, 0],
179 [0, 0, 0],
180 [0, 0, 0]
181 ]);
182 const next = Life.nextGeneration(grid, false);
183 assertEqual(next[1][1], 0, 'only 2 live neighbors, stays dead');
184 });
185
186
187
188
189
190 test('block still life is stable across generations', function () {
191 const grid = Life.placePattern(Life.createGrid(6, 6), Life.PATTERNS.block, 2, 2);
192 const next = Life.nextGeneration(grid, false);
193 assertEqual(next, grid, 'block should be unchanged');
194 });
195
196 test('blinker oscillates with period 2', function () {
197 let grid = Life.placePattern(Life.createGrid(5, 5), Life.PATTERNS.blinker, 2, 1);
198 const gen1 = Life.nextGeneration(grid, false);
199 const gen2 = Life.nextGeneration(gen1, false);
200 assert(JSON.stringify(gen1) !== JSON.stringify(grid), 'blinker should change shape after 1 generation');
201 assertEqual(gen2, grid, 'blinker should return to its original shape after 2 generations');
202 assertEqual(Life.countLiveCells(gen1), 3);
203 });
204
205 test('toad oscillates with period 2', function () {
206 let grid = Life.placePattern(Life.createGrid(6, 6), Life.PATTERNS.toad, 2, 1);
207 const gen1 = Life.nextGeneration(grid, false);
208 const gen2 = Life.nextGeneration(gen1, false);
209 assert(JSON.stringify(gen1) !== JSON.stringify(grid), 'toad should change shape after 1 generation');
210 assertEqual(gen2, grid, 'toad should return to its original shape after 2 generations');
211 });
212
213 test('glider translates diagonally by (1,1) every 4 generations', function () {
214 let grid = Life.placePattern(Life.createGrid(12, 12), Life.PATTERNS.glider, 1, 1);
215 let next = grid;
216 for (let i = 0; i < 4; i++) {
217 next = Life.nextGeneration(next, false);
218 }
219 const originalCoords = liveCoords(grid).map(function (c) { return [c[0] + 1, c[1] + 1]; }).sort();
220 const shiftedCoords = liveCoords(next).sort();
221 assertEqual(shiftedCoords, originalCoords, 'glider shape should reappear shifted by (1,1)');
222 });
223
224
225
226
227
228 test('pattern library has the expected classic patterns', function () {
229 assert(Life.PATTERNS.glider, 'glider pattern missing');
230 assert(Life.PATTERNS.gliderGun, 'glider gun pattern missing');
231 assert(Life.PATTERNS.pulsar, 'pulsar pattern missing');
232 });
233
234 test('glider pattern has 5 live cells', function () {
235 assertEqual(Life.PATTERNS.glider.cells.length, 5);
236 });
237
238 test('pulsar pattern has 48 live cells and is stable-shaped after 3 generations', function () {
239 assertEqual(Life.PATTERNS.pulsar.cells.length, 48);
240 let grid = Life.placePattern(Life.createGrid(20, 20), Life.PATTERNS.pulsar, 3, 3);
241 let next = grid;
242 for (let i = 0; i < 3; i++) {
243 next = Life.nextGeneration(next, false);
244 }
245 assertEqual(next, grid, 'pulsar has period 3 and should match its original shape');
246 });
247
248 test('Gosper glider gun pattern has 36 live cells', function () {
249 assertEqual(Life.PATTERNS.gliderGun.cells.length, 36);
250 });
251
252 test('glider gun produces a new live glider-sized cluster after 30 generations', function () {
253 let grid = Life.placePattern(Life.createGrid(60, 90), Life.PATTERNS.gliderGun, 2, 2);
254 const before = Life.countLiveCells(grid);
255 let next = grid;
256 for (let i = 0; i < 30; i++) {
257 next = Life.nextGeneration(next, false);
258 }
259 const after = Life.countLiveCells(next);
260 assert(after > before, 'glider gun should have spawned extra live cells (gliders) by generation 30');
261 });
262
263
264
265
266
267 test('placePattern stamps cells without clearing unrelated live cells', function () {
268 let grid = Life.createGrid(6, 6);
269 Life.setCell(grid, 5, 5, 1);
270 grid = Life.placePattern(grid, Life.PATTERNS.block, 0, 0);
271 assertEqual(Life.getCell(grid, 5, 5, false), 1, 'pre-existing live cell should survive placement');
272 assertEqual(Life.countLiveCells(grid), Life.PATTERNS.block.cells.length + 1);
273 });
274
275 test('placePattern does not mutate the input grid', function () {
276 const grid = Life.createGrid(6, 6);
277 const before = JSON.stringify(grid);
278 Life.placePattern(grid, Life.PATTERNS.glider, 0, 0);
279 assertEqual(JSON.stringify(grid), before, 'original grid should be untouched');
280 });
281
282
283
284
285
286 function report() {
287 const failed = results.filter(function (r) { return !r.pass; });
288
289 if (typeof document !== 'undefined') {
290 const container = document.getElementById('results');
291 if (container) {
292 const summary = document.createElement('p');
293 summary.className = 'summary ' + (failed.length ? 'summary-fail' : 'summary-pass');
294 summary.textContent = (results.length - failed.length) + ' / ' + results.length + ' tests passed';
295 container.appendChild(summary);
296
297 const list = document.createElement('ul');
298 results.forEach(function (r) {
299 const item = document.createElement('li');
300 item.className = r.pass ? 'pass' : 'fail';
301 item.textContent = (r.pass ? 'PASS' : 'FAIL') + ' — ' + r.name + (r.error ? ' (' + r.error + ')' : '');
302 list.appendChild(item);
303 });
304 container.appendChild(list);
305 }
306 }
307
308 if (typeof console !== 'undefined') {
309 results.forEach(function (r) {
310 console.log((r.pass ? 'PASS' : 'FAIL') + ' - ' + r.name + (r.error ? ': ' + r.error : ''));
311 });
312 console.log(results.length - failed.length + '/' + results.length + ' tests passed');
313 }
314
315 if (typeof process !== 'undefined' && failed.length > 0) {
316 process.exitCode = 1;
317 }
318 }
319
320 report();
321})(typeof window !== 'undefined' ? window : globalThis);
322
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.