Conways Game of Life: Part 1

A (simple) implementation of Conway's Game of Life in Javascript

June 21, 2015 - 15 minute read -
code kata

One of the things I like to do in my spare time are Code Katas. For the uninitiated, code katas are simple exercises so you can practise coding. The exercise I have assigned myself is to create an implementation of Conway’s Game of Life using Javascript.

Conway’s Game of Life is described as a zero player game because, once the initial layout has been set, the game plays itself. It is a 2D grid based game where each cell has (up to) 8 neighbours and can be described as “dead” or “alive”. For each round of the game the following rules are applied to each cell;

  • If the cell is alive and has fewer than 2 living neighbours it dies due to underpopulation.
  • If the cell is alive and has more than 3 living neighbours it dies due to overpopulation.
  • If the cell is dead and has exactly 3 living neighbours it becomes live due to reproduction.

My implementation splits the code into 2 parts - the underlying game engine and the game/view component that interacts with the user interface. The game engine will accept a seed argument, which will be a 2D array of numbers, where 1 will represent a living node and 0 a dead node. It will also expose a property to change the seed and a function named mutate, which will run the rules against the board. I have used the revealing prototype pattern to expose only the members I want to expose and keep internal logic safely internal.

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
(function() {

    GameEngine = function(seed) {
        this.seed = seed;
    };

    GameEngine.prototype = (function() {

        var clone2DArray = function(array) {
            return array.slice().map(function (row) { return row.slice(); });
        };

        var calculateLiveNeighbours = function(array, x, y) {
            var previousRow = array[y-1] || [];
            var nextRow = array[y+1] || [];

            return [
                previousRow[x-1], previousRow[x], previousRow[x+1],
                array[y][x-1], array[y][x+1],
                nextRow[x-1], nextRow[x], nextRow[x+1]
            ].reduce(function(previous, current) {
                return previous + current;
            }, 0);
        };

        return {
            set seed(array) {
                this.height = array.length;
                this.width = array[0].length;
                this.board = clone2DArray(array);
            },

            mutate: function() {
                var board = clone2DArray(this.board);

                for (var y = 0; y < this.height; y++) {
                    for (var x = 0; x < this.width; x++) {
                        var liveNeighbours = calculateLiveNeighbours(board, x, y);
                        var isAlive = !!this.board[y][x];

                        if (isAlive) {
                            if (liveNeighbours < 2 || liveNeighbours > 3) {
                                this.board[y][x] = 0;
                            }
                        } else {
                            if (liveNeighbours === 3) {
                                this.board[y][x] = 1;
                            }
                        }
                    }
                }
            }
        };
    })();
})();

The game itself requires the element in which it will run in the constructor plus the (optional) initial size and maximum size of the grid. The game creates all of the controls required, including a start/stop button, a reset button and a grid size select. The nodes are represented using (heavily styled) checkboxes and a table is used to organise them into a grid. The game exposes the size property and start, stop and reset methods.

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
(function() {

    function hasClass(element, className) {
        return element.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
    }
     
    function addClass(element, className) {
        if (!hasClass(element, className)) {
            element.className += " " + className;
        }
    }
     
    function removeClass(element, className) {
        if (hasClass(element, className)) {
            var regex = new RegExp('(\\s|^)' + className + '(\\s|$)');
            element.className = element.className.replace(regex, ' ');
        }
    }

    Game = (function() {

        var createTrigger = function(game) {
            var trigger = document.createElement('button');
            addClass(trigger, 'trigger');
            trigger.addEventListener('click', function() {
                if (game.started) {
                    game.stop();
                } else {
                    game.start();
                }
            });
            return trigger;
        };

        var createResetButton = function(game) {
            var resetButton = document.createElement('button');
            resetButton.innerText = 'Reset';
            resetButton.addEventListener('click', function() {
                game.reset();
            });
            return resetButton;
        };

        var createGrid = function(game) {
            var grid = document.createElement('table');
            grid.addEventListener('change', function(event) {
                if (event.target.nodeName.toLowerCase() === 'input') {
                    game.stop();
                }
            });
            grid.addEventListener('keyup', function(event) {
                var node = event.target;
                if (node.nodeName.toLowerCase() === 'input') {
                    var y = node.position[0];
                    var x = node.position[1];
    
                    switch (event.keyCode) {
                        case 37:
                            if (x > 0) {
                                game.nodes[y][x-1].focus();
                            }
                            break;
                        case 38:
                            if (y > 0) {
                                game.nodes[y-1][x].focus();
                            }
                            break;
                        case 39:
                            if (x < game.size - 1) {
                                game.nodes[y][x+1].focus();
                            }
                            break;
                        case 40:
                            if (y < game.size - 1) {
                                game.nodes[y+1][x].focus();
                            }
                            break;
                    }
                }
            });
            return grid;
        };

        var createSizeSelector = function(game, initialSize, maxSize) {
            var selector = document.createElement('select');
            selector.setAttribute('name', 'size-selector');
            addClass(selector, 'size-selector');
            for (var i = initialSize; i <= maxSize; i++) {
                var option = document.createElement('option');
                if (i === initialSize) {
                    option.setAttribute('selected', 'selected');
                }
                option.value = i;
                option.text = i;
                selector.appendChild(option);
            }
            selector.addEventListener('change', function() {
                game.size = selector.options[selector.selectedIndex].value;
            });
            var container = document.createElement('div');
            addClass(container, 'size-selector-container');
            var label = document.createElement('label');
            label.innerText = 'Grid size';
            label.setAttribute('for', 'size-selector');
            container.appendChild(label);
            container.appendChild(selector);
            return container;
        };

        return function(element, initialSize, maxSize) {
            initialSize = initialSize || 10;
            maxSize = maxSize || 25;

            this.started = false;
            this.element = element;
            addClass(this.element, 'conway-game');

            var fragment = document.createDocumentFragment();
            fragment.appendChild(createTrigger(this));
            fragment.appendChild(createResetButton(this));
            this.grid = createGrid(this);
            fragment.appendChild(this.grid);
            fragment.appendChild(createSizeSelector(this, initialSize, maxSize))
            this.element.appendChild(fragment);

            this.size = initialSize;
        };
    })();

    Game.prototype = (function() {
        var gridSize = 0,
            runner = null,
            createBoard = function(nodes) {
                return nodes.map(function (row) {
                    return row.map(function (node) {
                        return +node.checked;
                    });
                });
            },
            initialise = function () {
                var fragment = document.createDocumentFragment();
                this.grid.innerHTML = '';
                this.nodes = [];

                for (var y = 0; y < this.size; y++) {
                    var row = document.createElement('tr');
                    this.nodes[y] = [];

                    for (var x = 0; x < this.size; x++) {
                        var node = document.createElement('input');
                        node.type = 'checkbox';
                        node.position = [y, x];
                        this.nodes[y][x] = node;

                        var cell = document.createElement('td');
                        cell.appendChild(node);
                        row.appendChild(cell);
                    }
                    fragment.appendChild(row);
                }

                this.grid.appendChild(fragment);
            };

        return {

            get size() {
                return gridSize;
            },

            set size(value) {
                if (this.size !== value) {
                    gridSize = value;
                    initialise.call(this);
                }
            },

            start: function() {
                var game = this;
                game.started = true;
                addClass(game.element, 'running');
                var engine = new GameEngine(createBoard(game.nodes));
                runner = setInterval(function() {
                    engine.mutate();
                    for (var y = 0; y < game.size; y++) {
                        for (var x = 0; x < game.size; x++) {
                            game.nodes[y][x].checked = !!engine.board[y][x];
                        }
                    }
                }, 1000);
            },

            stop: function() {
                if (runner) {
                    clearInterval(runner);
                    runner = null;
                    this.started = false;
                    removeClass(this.element, 'running');
                }
            },

            reset: function() {
                this.stop();
                for (var y = 0; y < this.size; y++) {
                    for (var x = 0; x < this.size; x++) {
                        this.nodes[y][x].checked = false;
                    }
                }
            }
        }
    })();
})();

Finally, here it is in all it’s glory…