This simulator visualizes Conway's Game of Life and other lifelike cellular automata rule-sets.
Features:
- You can choose from any of the preselected birth/survival thresholds or specify custom thresholds ad hoc.
- You can click on cells to toggle them from alive to dead and vice versa.
- You can manually step through the generations with the "Next Step" button or click "Auto Play" to watch your life advance on its own.
- You can change the speed of the autoplay.
- You can randomly seed the world with life of a specified density.
- Recently deceased cells leave a visible trail that fades with every passing generation
Implementation Details:
- I'm using a two-dimensional array to track the map and live/dead status of the cells
- I'm drawing the cells on the screen using a 2D canvas
- At the advancement of each generation, I loop through all cells and for each cell check the sum of its live neighbor cells to determine whether it is born (if dead) or survives (if alive)
Where I'd like help:
- Performance optimization: anything I can do to improve performance (especially on larger maps and low-memory machines) without sacrificing the above features (potentially giving the user the option to toggle features that impact performance)
- Would it be beneficial to use
requestAnimationFrame
instead ofsetTimeout
? What would be the benefits? - Conforming to best practices and eliminating code smells
'use strict';
(function() {
let cellSize = 8,
cellBorderWidth = 1,
height = getSize(),
width = getSize(),
life = [],
delay = getDelay(),
canvas = document.getElementById("output").appendChild(document.createElement("canvas")),
birthThresh = [3],
survThresh = [2, 3],
running = null,
chkBirth = document.getElementById("BirthThresholds").querySelectorAll("input"),
chkSurvival = document.getElementById("SurvivalThresholds").querySelectorAll("input"),
thresholdPicker = document.getElementById("thresholdPicker"),
CurrentStepSpan = document.getElementById("CurrentStep"),
stepNum = 0,
foreground = "black",
lastHighlightedCell,
curr,
next;
let ctx = canvas.getContext("2d");
resetWorld(getDensity());
// attach all necessary event handlers
canvas.addEventListener("mouseup", function(event){
let x = event.pageX - canvas.offsetLeft,
y = event.pageY - canvas.offsetTop;
x = x / cellSize >>> 0;
y = y / cellSize >>> 0;
toggleCell(life[x][y]);
});
canvas.addEventListener("mousemove", function(event){
let x = event.pageX - canvas.offsetLeft,
y = event.pageY - canvas.offsetTop;
x = x / cellSize >>> 0;
y = y / cellSize >>> 0;
highlightCell(life[x][y]);
});
canvas.addEventListener("mouseout",clearHighlightedCell);
document.getElementById("next").onclick = nextGeneration;
document.getElementById("size").onchange = restart;
document.getElementById("density").onchange = restart;
document.getElementById("reset").onclick = restart;
document.getElementById("delay").onchange = function() {
delay = getDelay();
};
document.getElementById("clear").onclick = function() {
resetWorld(0);
};
document.getElementById("auto").onclick = function() {
if (running) {
clearTimeout(running);
document.getElementById("next").disabled = false;
running = null;
this.value = "Auto";
} else {
document.getElementById("next").disabled = true;
this.value = "Pause";
running = setTimeout(loopNextGeneration, delay);
}
}
for (let i = 0; i < chkBirth.length; i++) {
(function(box) {
box.onclick = function() {
let index = birthThresh.indexOf(+(box.value));
if (!box.checked && index >= 0) {
birthThresh.splice(index, 1);
} else if (box.checked && index === -1) {
birthThresh.push(+(box.value));
}
};
})(chkBirth[i]);
}
for (let i = 0; i < chkSurvival.length; i++) {
(function(box) {
box.onclick = function() {
let index = survThresh.indexOf(+(box.value));
if (!box.checked && index >= 0) {
survThresh.splice(index, 1);
} else if (box.checked && index === -1) {
survThresh.push(+(box.value));
}
};
})(chkSurvival[i]);
}
thresholdPicker.onchange = function(){
if(thresholdPicker.value === "Custom"){
document.querySelector(".thresholds").style.display = "block";
birthThresh = [];
for(let i = 0; i < chkBirth.length; i++){
if(chkBirth[i].checked){
birthThresh.push(+(chkBirth[i].value));
}
}
survThresh = [];
for(let i = 0; i < chkSurvival.length; i++){
if(chkSurvival[i].checked){
survThresh.push(+(chkSurvival[i].value));
}
}
}else{
document.querySelector(".thresholds").style.display = "none";
var temp = JSON.parse(thresholdPicker.value);
birthThresh = temp.b;
survThresh = temp.s;
}
}
function getSize() {
return +(document.getElementById("size").value);
}
function getDelay() {
return +(document.getElementById("delay").value);
}
function getDensity() {
return +(document.getElementById("density").value);
}
function restart() {
resetWorld(getDensity());
}
function resetWorld(dens) {
stepNum = 0;
height = getSize();
width = getSize();
delay = getDelay();
curr = "b";
next = "a";
canvas.height = height*cellSize;
canvas.width = width*cellSize;
ctx.fillStyle = "rgb(250,250,250)";
ctx.fillRect(0, 0, width*cellSize, height*cellSize);
for (let i = 0; i < height; i++) {
life[i] = [];
for (let j = 0; j < width; j++) {
let critter = {
a: Math.random() < dens,
b: false,
age: 255,
x: i,
y: j
};
life[i][j] = critter;
}
}
nextGeneration();
}
function loopNextGeneration() {
let startTime = new Date();
nextGeneration();
let endTime = new Date();
running = setTimeout(loopNextGeneration, endTime - startTime < delay ? delay - (endTime - startTime) : 0);
}
function nextGeneration() {
stepNum += 1;
if (curr == "a") {
curr = "b";
next = "a";
} else {
curr = "a";
next = "b";
}
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
let sumAll = getSumNeighbors(life[i][j]);
if (life[i][j][curr]) {
life[i][j].age = 0;
ctx.fillStyle = foreground;
ctx.fillRect(i*cellSize+cellBorderWidth, j*cellSize+cellBorderWidth, cellSize-cellBorderWidth, cellSize-cellBorderWidth);
} else {
life[i][j].age += 2;
let wrinkles = 185 + (life[i][j].age);
if (wrinkles > 255) {
wrinkles = 255;
}
if(life[i][j] !== lastHighlightedCell){
ctx.fillStyle = "rgb(255," + wrinkles + "," + wrinkles + ")";
ctx.fillRect(i*cellSize+cellBorderWidth, j*cellSize+cellBorderWidth, cellSize-cellBorderWidth, cellSize-cellBorderWidth);
}
}
if (life[i][j][curr]) {
life[i][j][next] = checkSurvives(sumAll);
} else {
life[i][j][next] = checkBorn(sumAll);
}
}
}
CurrentStepSpan.innerHTML = stepNum;
}
function getSumNeighbors(cell){
let i = cell.x, j = cell.y;
let onLeftEdge = j === 0,
onRightEdge = j == width - 1,
onTopEdge = i === 0,
onBottomEdge = i == height - 1;
return +(life[(onBottomEdge ? 0 : i + 1)][(onLeftEdge ? width - 1 : j - 1)][curr]) + // bottom left
+(life[(onBottomEdge ? 0 : i + 1)][j][curr]) + // bottom middle
+(life[(onBottomEdge ? 0 : i + 1)][(onRightEdge ? 0 : j + 1)][curr]) + // bottom right
+(life[i][(onLeftEdge ? width - 1 : j - 1)][curr]) + // left
+(life[i][(onRightEdge ? 0 : j + 1)][curr]) + // right
+(life[(onTopEdge ? height - 1 : i - 1)][(onLeftEdge ? width - 1 : j - 1)][curr]) + // upper left
+(life[(onTopEdge ? height - 1 : i - 1)][j][curr]) + // upper middle
+(life[(onTopEdge ? height - 1 : i - 1)][(onRightEdge ? 0 : j + 1)][curr]); // upper right
}
function toggleCell(cell){
if(!cell[curr]){
cell[curr] = true;
cell[next] = checkSurvives(getSumNeighbors(cell));
ctx.fillStyle = foreground;
}else{
cell[curr] = false;
cell[next] = checkBorn(getSumNeighbors(cell));
ctx.fillStyle = "#eaeaea";
}
adjustNeighbors(cell);
ctx.fillRect(cell.x*cellSize+cellBorderWidth, cell.y*cellSize+cellBorderWidth, cellSize-cellBorderWidth, cellSize-cellBorderWidth);
}
function getNeighbors(cell){
let i = cell.x, j = cell.y;
let onLeftEdge = j === 0,
onRightEdge = j == width - 1,
onTopEdge = i === 0,
onBottomEdge = i == height - 1;
return [(life[(onBottomEdge ? 0 : i + 1)][(onLeftEdge ? width - 1 : j - 1)]), //bl
(life[(onBottomEdge ? 0 : i + 1)][j]), // bm
(life[(onBottomEdge ? 0 : i + 1)][(onRightEdge ? 0 : j + 1)]), // br
(life[i][(onLeftEdge ? width - 1 : j - 1)]), //l
life[i][(onRightEdge ? 0 : j + 1)], // r
life[(onTopEdge ? height - 1 : i - 1)][(onLeftEdge ? width - 1 : j - 1)], // tl
life[(onTopEdge ? height - 1 : i - 1)][j], // tm
life[(onTopEdge ? height - 1 : i - 1)][(onRightEdge ? 0 : j + 1)]]; //tr
}
function adjustNeighbors(cell){
let neighbors = getNeighbors(cell);
for(let i = 0, len = neighbors.length; i < len; i++){
if(neighbors[i][curr]){
neighbors[i][next] = checkSurvives(getSumNeighbors(neighbors[i]));
}else{
neighbors[i][next] = checkBorn(getSumNeighbors(neighbors[i]));
}
}
}
function highlightCell(cell){
if(lastHighlightedCell){
let wrinkles = 185 + (lastHighlightedCell.age);
if (wrinkles > 255) {
wrinkles = 255;
}
ctx.fillStyle = lastHighlightedCell[curr] ? foreground : "rgb(255,"+wrinkles+","+wrinkles+")";
ctx.fillRect(lastHighlightedCell.x*cellSize+cellBorderWidth, lastHighlightedCell.y*cellSize+cellBorderWidth, cellSize-cellBorderWidth, cellSize-cellBorderWidth);
}
ctx.fillStyle = "#90ff20";
ctx.fillRect(cell.x*cellSize+cellBorderWidth, cell.y*cellSize+cellBorderWidth, cellSize-cellBorderWidth, cellSize-cellBorderWidth);
lastHighlightedCell = cell;
}
function clearHighlightedCell(){
if(lastHighlightedCell){
let wrinkles = 185 + (lastHighlightedCell.age);
if (wrinkles > 255) {
wrinkles = 255;
}
ctx.fillStyle = lastHighlightedCell[curr] ? foreground : "rgb(255,"+wrinkles+","+wrinkles+")";
ctx.fillRect(lastHighlightedCell.x*cellSize+cellBorderWidth, lastHighlightedCell.y*cellSize+cellBorderWidth, cellSize-cellBorderWidth, cellSize-cellBorderWidth);
}
lastHighlightedCell = false;
}
function checkSurvives(sum) {
for (let i = 0, len = survThresh.length; i < len; i++) {
if (sum === survThresh[i]) {
return true;
}
}
return false;
}
function checkBorn(sum) {
for (let i = 0, len = birthThresh.length; i < len; i++) {
if (sum === birthThresh[i]) {
return true;
}
}
return false;
}
})();
#output {
cursor: crosshair;
display: block;
}
.thresholds {
font-family: consolas;
font-size: 10pt;
display:none;
}
#next {
float: left;
}
#controls {
max-width: 400px;
text-align: right;
background-color: #efefef;
padding: 4px;
}
#settings {
background-color: #dfdfdf;
border: 1px solid black;
padding: 3px;
max-width: 475px;
}
<div>
<div id="settings">
<div>
Birth/Survival Thresholds: <select id="thresholdPicker">
<options>
<option value='{"b":[1,3,5,7],"s":[1,3,5,7]}' >Replicator (B1357/S1357)</option>
<option value='{"b":[2],"s":[]}' >Seeds (B2/S)</option>
<option value='{"b":[3],"s":[2,3]}' selected="selected">Conway's Game of Life (B3/S23)</option>
<option value='{"b":[3],"s":[0,1,2,3,4,5,6,7,8]}' >Life Without Death (B3/S012345678)</option>
<option value='{"b":[3,6,7,8],"s":[3,4,6,7,8]}' >Day and Night (B3678/S34678)</option>
<option value='{"b":[3,5,6,7,8],"s":[5,6,7,8]}' >Diamoeba (B35678/S5678)</option>
<option value='{"b":[3,6,8],"s":[2,4,5]}' >Morley (B368/S245)</option>
<option value='{"b":[4,6,7,8],"s":[3,5,6,7,8]}' >Twisted Majority (B4678/S35678)</option>
<option value='{"b":[3],"s":[2,3,4]}' >Maze Maker (B3/S234)</option>
<option value='{"b":[4,5,6],"s":[2,3,4,5]}' >Echo Chambers (B456/S2345)</option>
<option value="Custom">Custom</option>
</options>
</select>
</div>
<div class="thresholds">
<div id="BirthThresholds">
B: 0<input type="checkbox" value=0 /> 1<input type="checkbox" value=1 /> 2<input type="checkbox" value=2 /> 3<input type="checkbox" value=3 checked="checked" /> 4<input type="checkbox" value=4 /> 5<input type="checkbox" value=5 /> 6<input type="checkbox" value=6 /> 7<input type="checkbox" value=7 /> 8<input type="checkbox" value=8 />
</div>
<div id="SurvivalThresholds">
S: 0<input type="checkbox" value=0 /> 1<input type="checkbox" value=1 /> 2<input type="checkbox" value=2 checked="checked" /> 3<input type="checkbox" value=3 checked="checked" /> 4<input type="checkbox" value=4 /> 5<input type="checkbox" value=5 /> 6<input type="checkbox" value=6 /> 7<input type="checkbox" value=7 /> 8<input type="checkbox" value=8 />
</div>
</div>
Size:<select id="size">
<options>
<option value=11>Tiny</option>
<option value=31 selected="selected">Small</option>
<option value=61>Medium</option>
<option value=91>Large</option>
<option value=111>Huge</option>
<option value=211>Enormous</option>
<option value=451>Collossal</option>
</options>
</select> Density:<select id="density">
<options>
<option value=1>100%</option>
<option value=.95>95%</option>
<option value=.9>90%</option>
<option value=.85>85%</option>
<option value=.8>80%</option>
<option value=.7>70%</option>
<option value=.6>60%</option>
<option value=.55>55%</option>
<option value=.5>50%</option>
<option value=.45>45%</option>
<option value=.4>40%</option>
<option value=.3>30%</option>
<option value=.2>20%</option>
<option value=.1 selected="selected">10%</option>
<option value=.05>5%</option>
<option value=.03>3%</option>
<option value=.0>0%</option>
</options>
</select>
<input id="reset" type="button" value="Randomize" />
<input id="clear" type="button" value="Clear" />
</div>
<div id="controls">
<input id="next" type="button" value="Next Step" />
<span id="CurrentStep"></span>
<select id="delay">
<options>
<option value=1000>Slow (1 sec delay)</option>
<option value=400>Normal (400ms delay)</option>
<option value=200>Quick (200ms delay)</option>
<option value=60 selected="selected">Fast (60ms delay)</option>
<option value=0>As Fast As Possible</option>
</options>
</select>
<input id="auto" type="button" value="Auto Play" />
</div>
</div>
<div id="output"></div>