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