7
\$\begingroup\$

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 of setTimeout? 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:&nbsp;0<input type="checkbox" value=0 />&nbsp;1<input type="checkbox" value=1 />&nbsp;2<input type="checkbox" value=2 />&nbsp;3<input type="checkbox" value=3 checked="checked" />&nbsp;4<input type="checkbox" value=4 />&nbsp;5<input type="checkbox" value=5 />&nbsp;6<input type="checkbox" value=6 />&nbsp;7<input type="checkbox" value=7 />&nbsp;8<input type="checkbox" value=8 />&nbsp;
      </div>
      <div id="SurvivalThresholds">
        S:&nbsp;0<input type="checkbox" value=0 />&nbsp;1<input type="checkbox" value=1 />&nbsp;2<input type="checkbox" value=2 checked="checked" />&nbsp;3<input type="checkbox" value=3 checked="checked" />&nbsp;4<input type="checkbox" value=4 />&nbsp;5<input type="checkbox" value=5 />&nbsp;6<input type="checkbox" value=6 />&nbsp;7<input type="checkbox" value=7 />&nbsp;8<input type="checkbox" value=8 />&nbsp;
      </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>&nbsp; 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>

\$\endgroup\$
  • \$\begingroup\$ I think you still have massive performance optimization potential, given the current answers. Interested? \$\endgroup\$ – le_m Apr 26 '17 at 22:12
  • \$\begingroup\$ @le_m Absolutely! \$\endgroup\$ – Thriggle Apr 26 '17 at 22:26
1
+100
\$\begingroup\$

In answer to your questions, optimizing key pieces of code and a small algorithm change can produce significant improvement.

All testing was done on a Win32 notebook with Intel Atom CPU N2600 @ 1.6 GHz and 1GB of RAM, running Mozilla Firefox and a text editor.

Other answers and comments have adequately addressed questions of coding style which I don't need to repeat.

JavaScript, Optimization and Life

Optimization for JavaScript principally looks at code structure (e.g. taking code out of a loop that doesn't change loop values), modifications to an algorithm, and in this case avoiding a DOM update that would trigger page layout refresh.

JavaScript arrays use associative lookup which tends to be expensive. Replacing associative arrays with ArrayBuffers might, and maybe, lead to significant speed improvement but would call for almost a total rewrite and has not been looked into further.

Premature Optimization

Updating the screen with successive generations in automatic mode was the focus of investigation. Code used for setup, user interaction and creation of the initial population was considered a case of premature optimization and not given attention.

This left functions loopNextGeneration, nextGeneration, getSumNeighbors, checkSurvives and checkBorn to look into.

Measurement Data

Additional code was used to display measured performance. It does update the DOM and trigger a page layout refresh, but only once a second. If you wish to make use of it, here it is:

HTML placed before the end </body> tag:

<div>Steps:  <span id="speed"></span></div>
<div id="period"></div>

Script to insert before start of existing code:

/**************** speedo for testing ****************/
 let speedo = {
  start: 0,   n: 0, total: 0, // in steps
  t0: 0, m: 0, totalMs: 0, fastest: 0, slowest: 0, // in ms
  mark0: function () { this.t0 = Date.now();},
  mark1: function () { 
    let period = Date.now()-this.t0;
    if( period < this.fastest || !this.fastest) {
      this.fastest = period;
    }
    else if( period > this.slowest) {
      this.slowest = period;
    }
    this.totalMs += period;
    this.m++;
  },
  init: function () {
    this.n=this.total=this.m=this.totalMs=this.fastest=
    this.slowest=this.start=this.startMs = 0;
  },
  speed: document.getElementById("speed"),
  period: document.getElementById("period"),
  monitor: function () {
    let o = speedo;
    if(!running) {
        o.init();
    }
    else if(!o.start) {
        o.start = stepNum;
    }
    else {
      let steps = stepNum - o.start;
      o.start = stepNum;
      o.total += steps;
      o.n++;
      o.speed.textContent =  steps + " /sec, average: " + (o.total/o.n).toFixed(2);
      o.period.textContent = "fastest: " + o.fastest + "ms, slowest: " + o.slowest
       + ", average: " + (o.totalMs/o.m).toFixed(1);
    }
  }
 };
 setInterval( speedo.monitor, 1000);
/******************************************************/

and to generate generation period data:

speedo.mark0();  // at start of nextGeneration
speedo.mark1();  // at end of nextGeneration

Optional DOM Update?

//  CurrentStepSpan.innerHTML = stepNum;

Commenting out display of the step number marginally improved speed from about 29 to about 32 steps per second for a small, zero density display running as fast as possible.

loopNextGeneration

A bare bones

function loopNextGeneration() {
    running = setInterval( nextGeneration, delay);
    return;
}

increased the small, zero density display rate from about 32 to about 50 steps per second, at the cost of losing corrections to maintain a fixed average processing period.

getSumNeighbors

This function is called for each cell and uses multiple array look ups for the same element. For a medium sized board, zero density, the fastest time went from about 65ms to 63ms. Small perhaps but cumulative. Code used:

  function getSumNeighbors(cell){
  let
    x = cell.x,  // was i
    y = cell.y,  // was j
    left = x ? x-1 : width - 1,
    right = x + 1 < width ? x + 1 : 0,
    below = y  ? y-1 : height -1,
    above = y + 1 < height ? y + 1 : 0,
    lifeLeft = life[left],
    lifeMid = life[x],
    lifeRight = life[right];

    return +lifeLeft [above][curr]
           +lifeLeft [y][curr]
           +lifeLeft [below][curr]
           +lifeMid  [above][curr]
           +lifeMid  [below][curr]
           +lifeRight[above][curr]
           +lifeRight[y][curr]
           +lifeRight[below][curr]
  }

nextGeneration

Aggressively optimizing the code structure of next Generation, mostly removing multiple look-ups of life[i] and life[i][j] and in-lining code for checkSurvives and checkBorn, was able to improved the time for a step by around 4% again - cumulatively progressive but not sufficient to radically alter the user's experience.

The Algorithm Change

Updating the canvas with a white rectangle when it is already white is unnecessary!

In nextGeneration, changing

if(life[i][j] !== lastHighlightedCell){

to

let lifeXY = life[i][j];  
if(lifeXY!== lastHighlightedCell && (lifeXY.age + 185 < 259)) { //i.e. (lifeXY.age<74)

produced frame rates in excess of 120/sec for a medium size zero density display in combination with previous changes. A medium size non zero density display can over-speed when run as quickly as possible, working better at 60ms/step in the test machine.

requestAnimationFrame

requestAnimationFrame is useful for synchronizing canvas updates with screen refresh. If canvas drawing cannot run with a frequency above about half the screen refresh rate, requestAnimationFrame serves no particular purpose. It could be coded as an option, but probably not of great use on a less powerful machine.


Updated Notes:

  • Black cells that are already black don't need to be repainted either.
  • Replacing count up for loops in nextGeneration with count down loops produces a small but measurable gain.
  • Swapping next and curr using a temporary variable had negligible impact - code below uses the temporary variable (less lines).
  • Putting life.length = width; in resetworld releases unused cells for garbage collection if a user tries colossal by mistake.
  • height and width usage appears interchanged in some places of code, working because they have the same value.
  • Calling thresholdPicker.onchange(); after definition helps maintain previous settings if the page is reloaded.
  • To support retina displays (I don't have one) you could try increasing cell size rather than the number of cells, or providing it as an option.
  • After other optimizations were performed, triggering page layout was not a bottle neck. I don't think I made a mistake, but would not put the step count on the canvas without measurable evidence that a useful speed gain was produced.

For the record, this is the last version of nextGeneration tested - without claim it should be final. Magic number 74 was calculated as (255 plus twice the cell's age increment minus 185) :

  function nextGeneration() {
  speedo.mark0(); // measure
    stepNum += 1;
    let temp = curr; // swap next & curr
    curr = next;
    next = temp;
    let size = cellSize - cellBorderWidth;
    let i = width;
    while( i--){
      let column = life[i];
      let j=height;
      while( j--) {
        let cell = column[j];  
        let sumAll = getSumNeighbors( cell);
        if( cell[curr]) {
          cell.age = 0;
          if( !cell[next]) { // next is currently previous
            ctx.fillStyle = foreground;       
            ctx.fillRect(i*cellSize+cellBorderWidth, j*cellSize+cellBorderWidth, size, size);
          }
          cell[ next] = checkSurvives( sumAll);
        }
        else {          
          cell.age += 2;
          let lum = 185 + cell.age; // a measure of lightness or luminosity in HSL
          if (lum > 255) {
            lum = 255;
          }
          if( (cell.age < 74) && cell!== lastHighlightedCell)) {
            ctx.fillStyle = "rgb(255," + lum + "," + lum + ")";
            ctx.fillRect(i*cellSize+cellBorderWidth, j*cellSize+cellBorderWidth, size, size);
          }
          cell[next] = checkBorn( sumAll);
        }        
      }
    }
    CurrentStepSpan.textContent = stepNum;
  speedo.mark1();  // measure.
  }

After optimization the test machine was able to run many of the game variations in huge mode at an acceptable speed. Before optimization not even close. Thank you for the question, very entertaining computer art!

\$\endgroup\$
  • \$\begingroup\$ This is amazing feedback, and highlights some aspects of the code (like repeatedly accessing the first array element in a two dimensional array) that I never even thought about from a performance perspective. If I keep the step number, I'll probably move it to the canvas now that you pointed out the performance concern. Thank you for going into so much detail!. \$\endgroup\$ – Thriggle Dec 23 '16 at 3:35
3
\$\begingroup\$

As I'm currently on break on my phone, this will be superficial. I glanced it over, and 2 main things stood out to me:

  • You have a fair number of "magic numbers"; numbers loose in your code without an obvious meaning. The most notable example I saw was let wrinkles = 185 +.... Why 185? Simply giving this number a name would help with maintainability. Are you going to remember what this number means when you come back in a year?

  • You seem to be using +s as a means of casting strings to ints. This is a new one for me, and it took a second to realize what you were doing. If this is idiomatic JS, disregard this point. I think a more explicit cast using parseInt would be better though.

\$\endgroup\$
2
\$\begingroup\$

My initial thought is that this code is too heavily coupled between game logic and display logic. I would consider splitting this apart such that the logic (the rules, configuration of the rules, management of matrix state, etc) would be better separated from the display logic (input controls, rendering the view of model state, etc.).

I would strive to define your different objects in the system along lines such as:

  • game configuration - config for rules, board size, refresh rate, etc.
  • game board - defines size, holds matrix of individual cells, no display logic here but perhaps can have callbacks attached to it to allow for rendering board state (i.e. in canvas element)
  • individual cells - contain alive/dead state and perhaps relationships, age, etc. (basically your critter element)
  • game manager - provides methods for interacting with game board/cells, accepts/stores game configuration, has logic to randomly seed board.
  • controls - provide tangible DOM elements to which you can bind game manager methods

With regards to using requestAnimationFrame(), I would get in the habit of always using this for animations, assuming that your are primary targeting modern borwser that support it. You can use setInterval or setTimeout as fallback for those browsers that do not support it.

This arcticle does a good job explaning high-level use of requestAnimationFrame as well as give sample polyfill (fallback code) for borwsers that do not support it.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the advice! This gives me a lot to chew on. I'll see what I can do about decoupling the game with the interface, although I'm concerned that I might inadvertently introduce performance penalties if I'm not careful with my implementation. I do have my doubts about that source for requestAnimationFrame since it seems to suggest using setTimeout as a wrapper to limit executions to the desired frequency. \$\endgroup\$ – Thriggle Dec 20 '16 at 21:36
  • \$\begingroup\$ @Thriggle setTimeout wrapper is not a problem there as the request to repainted is still handled by animation frame request. Think of setTimeout in that case as a throttle. \$\endgroup\$ – Mike Brant Dec 22 '16 at 4:55
  • \$\begingroup\$ Thanks, Mike. I played with requestAnimationFrame as a replacement for setTimeout but was disappointed that it ended up causing the logic to execute more slowly at max speed. I think it's because I wasn't being surgical enough; I'm guessing I should use setTimeout to throttle the operations, like you said, but maybe invoke requestAnimationFrame to draw on the canvas, not to perform the related calculations across all the cells. \$\endgroup\$ – Thriggle Dec 22 '16 at 17:11
  • 1
    \$\begingroup\$ @Thriggle yes. You should ONLY put actual drawing/animation logic in requestAnimationFrame(). So decoupling game board logic from rendering logic is crucial. \$\endgroup\$ – Mike Brant Dec 22 '16 at 22:09
  • \$\begingroup\$ But is there a performant way to do that in a case like this where I'm looping through an array of objects and performing some kind of operation? It seems like an unnecessary performance sink to do the same loop twice, first to perform calculations on each cell and then again to render the cells on the canvas. Especially given that I want to render the results after every calculation-- this isn't a video game where I might want the video to skip frames in order to keep up with the underlying logic. I'd really appreciate any suggestions and examples. \$\endgroup\$ – Thriggle Dec 22 '16 at 22:17
0
\$\begingroup\$

Inside of your nextGeneration function you have this little piece of code

        let wrinkles = 185 + (life[i][j].age);
        if (wrinkles > 255) {
            wrinkles = 255;
        }

I think I would prefer to make this a little more compact by making it into a ternary like this

wrinkles = wrinkles < 255 ? wrinkles : 255;

This could be made more clear by defining what 185 and 255 are, right now they are just Magic Numbers.

\$\endgroup\$
  • 1
    \$\begingroup\$ Great advice. Getting those numbers clearly defined made me rethink some of my terminology (such as age) and exposed some opportunities for me to better optimize the code. \$\endgroup\$ – Thriggle Dec 20 '16 at 21:38
  • 1
    \$\begingroup\$ what is the downvote for? please explain, I am open to learning as much as anyone else. \$\endgroup\$ – Malachi Dec 23 '16 at 14:17
  • 1
    \$\begingroup\$ Not sure who gave you the downvote, hopefully they voice their criticism aloud. \$\endgroup\$ – Thriggle Dec 23 '16 at 19:34
0
\$\begingroup\$

reducing repetition

The getNeighbors() and getSumNeighbors() functions perform a lot of identical work.

I explored collapsing getSumNeighbors() into a simple reduction of getNeighbors(), as seen below...

function getSumNeighbors(cell){
    return getNeighbors(cell).reduce(function(a,b){
        return a+ +(b[curr]);
    },0);
}

...but ultimately decided against it due to performance concerns.

I did, however, adopt recommendations from Traktor53's answer to use temporary variables to cache and minimize the number of times arrays are accessed.

The new getSumNeighbors function looks like this:

function getSumNeighbors(cell){ 
    let i = cell.x, j = cell.y;
    let onLeftEdge = j === 0,
        onRightEdge = j == maxX,
        onTopEdge = i === 0,
        onBottomEdge = i == maxY;
    let leftX = onLeftEdge ? maxX : j - 1,
        rightX = onRightEdge ? 0 : j + 1,
        lifeBottom = life[onBottomEdge ? 0 : i + 1],
        lifeMiddle = life[i],
        lifeTop = life[onTopEdge ? maxY : i - 1];
    return lifeBottom[leftX][curr]+ //bl
        lifeBottom[j][curr]+ // bm
        lifeBottom[rightX][curr]+ // br
        lifeMiddle[leftX][curr]+ //l
        lifeMiddle[rightX][curr]+ // r
        lifeTop[leftX][curr]+ // tl
        lifeTop[j][curr]+ // tm
        lifeTop[rightX][curr]; //tr
}

Similarly, the code for calculating the color value of a dead cell is duplicated across three different functions: nextGeneration(), highlightCell(), and clearHighlightedCell().

I pulled that logic out into its own function called getTrailColor() (which I'll talk about more in the "smoothing out the wrinkles" section below).

reducing mathematical operations per iteration

The getNeighbors() function performs a lot of very simple math; it adds and subtracts one from the x and y coordinates of the current cell to determine the x and y coordinates of neighboring cells, and determines the maximum values for both x and y based on the height and width of the map.

It also has a lot of redundant ternary operations repeatedly checking the same conditions to determine whether the neighboring cell is beyond the edge of the map, thus inducing the wrap-around effect.

In my updated code, instead of recalculating those values, I perform the necessary mathematical operations once each and cache the values in variables.

I also moved the calculation of the maximum x and y values out to the resetWorld() function (captured in two new variables maxX and maxY), since those only need to be recalculated if the map dimensions change.

 function getNeighbors(cell){
     let i = cell.x, j = cell.y;
     let onLeftEdge = j === 0,
       onRightEdge = j == maxX,
       onTopEdge = i === 0,
       onBottomEdge = i == maxY;
     let leftX = onLeftEdge ? maxX : j - 1,
       rightX = onRightEdge ? 0 : j + 1,
       lifeBottom = life[onBottomEdge ? 0 : i + 1],
       lifeMiddle = life[i],
       lifeTop = life[onTopEdge ? maxY : i - 1];
     return [lifeBottom[leftX], //bl
       lifeBottom[j], // bm
       lifeBottom[rightX], // br
       lifeMiddle[leftX], //l
       lifeMiddle[rightX], // r
       lifeTop[leftX], // tl
       lifeTop[j], // tm
       lifeTop[rightX]]; //tr
}

smoothing out the wrinkles

(aka: eliminating magic numbers/strings)

A few reviewers rightly jumped on the mysterious code featuring the wrinkles variable and the magical transformations that take place to translate that to the color trail left behind by cells when they die.

defining constants

I ended up putting those magic numbers and any other magic numbers/strings I could find into descriptive constants defined near the top of the code.

const CELLSIZE = 8,
 CELLBORDERWIDTH = 1,
 FOREGROUNDCOLOR = "black",
 BACKGROUNDCOLOR = "white",
 HIGHLIGHTCOLOR = "#90ff20",
 BORDERCOLOR = "rgb(250,250,250)",
 MAXTRAILHUE = 255,
 BASETRAILHUE = 185,
 TRAILDECAYRATE = 2;

using those constants to calculate color

I then used those hue and decay rate constants in my new getTrailColor() function.

Also, for the sake of clarity, I renamed cell.age to cell.decay, since it really indicates how recently the cell died (with higher values meaning that the trail is no longer visible).

function getTrailColor(cell){
    let trailColor = BASETRAILHUE + (cell.decay);
    if (trailColor > MAXTRAILHUE) {
        return BACKGROUNDCOLOR;
    }
    return "rgb(255,"+trailColor+","+trailColor+")";
}

eliminating unnecessary calculations

Collapsing that logic into a single function also revealed an opportunity for a performance enhancement: I only need to calculate the trail color when the cell is dead!

In my original code, I'm calculating the color for each cell even if it's already alive. The updated code to get the trail color (snipped from the nextGeneration() function) looks like this:

let cell = life[i][j],
  sumAll = getSumNeighbors(cell);
if (cell[curr]) {
    cell.decay = 0;
    ctx.fillStyle = FOREGROUNDCOLOR;       
    ctx.fillRect(i*CELLSIZE+CELLBORDERWIDTH, j*CELLSIZE+CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH);
    cell[next] = checkSurvives(sumAll);
} else {          
    cell.decay += TRAILDECAYRATE;
    if(cell !== lastHighlightedCell && cell.decay < HUEDECAYCAP){
        ctx.fillStyle = getTrailColor(cell); 
        ctx.fillRect(i*CELLSIZE+CELLBORDERWIDTH, j*CELLSIZE+CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH);
    }
    cell[next] = checkBorn(sumAll);
}

A similar restructuring took place in the highlightCell() and clearHighlightedCell() functions.

minor things

acknowledging weirdness

I'm using a few JavaScript techniques that aren't necessarily common, but which I'm okay with keeping.

These include using the addition operator to implicitly cast strings to numbers like so

+(someVariable) 

and using bitwise shifting to round a number down to the nearest integer value like so

x = x / CELLSIZE >>> 0;

There are cases (not likely to be encountered with this code) where those techniques would be inadequate, so it's good to be aware of them so that I only use them intentionally, not instinctively.

euthanizing the useless critter

Mike Brant's comment about my critter object made me realize I had an unnecessary intermediate variable there to store the critter before assigning it to a cell in the life array.

for (let i = 0; i < height; i++) {
    life[i] = [];
    for (let j = 0; j < width; j++) {
        life[i][j] = {
          a: alive,
          b: false,
          decay: alive ? 0 : MAXTRAILHUE,
          x: i,
          y: j
        };       
    }
}

RIP, critter.

Updated code with the above changes:

'use strict';
(function() {
  const CELLSIZE = 8,
    CELLBORDERWIDTH = 1,
    FOREGROUNDCOLOR = "black",
    BACKGROUNDCOLOR = "white", 
    HIGHLIGHTCOLOR = "#90ff20",
    BORDERCOLOR = "rgb(250,250,250)",
    MAXTRAILHUE = 255,
    BASETRAILHUE = 185,
    TRAILDECAYRATE = 2,
    canvas = document.getElementById("output").appendChild(document.createElement("canvas")),
    chkBirth = document.getElementById("BirthThresholds").querySelectorAll("input"),
    chkSurvival = document.getElementById("SurvivalThresholds").querySelectorAll("input"),
    thresholdPicker = document.getElementById("thresholdPicker"),
    currentStepSpan = document.getElementById("CurrentStep");
  const HUEDECAYCAP = MAXTRAILHUE - BASETRAILHUE;
  let height = getSize(),
    width = getSize(),
    life = [],
    delay = getDelay(),    
    birthThresh = [3],
    survThresh = [2, 3],
    running = null,
    stepNum = 0,
    lastHighlightedCell,
    curr, 
    next,
    maxX,
    maxY;
  const 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").addEventListener("click",nextGeneration);
  document.getElementById("size").addEventListener("change",restart);
  document.getElementById("density").addEventListener("change",restart);
  document.getElementById("reset").addEventListener("click",restart);
  document.getElementById("delay").addEventListener("change",function() {
    delay = getDelay();
  });
  document.getElementById("clear").addEventListener("click",function() {
    resetWorld(0);
  });
  document.getElementById("auto").addEventListener("click",function() {
    if (running) {
      clearTimeout(running);
      document.getElementById("next").disabled = false;
      running = null;
      this.value = "Auto";
      currentStepSpan.innerHTML = stepNum;
    } else {
      document.getElementById("next").disabled = true;
      this.value = "Pause";
      running = setTimeout(loopNextGeneration, delay);
    }
  });
   for (let i = 0; i < chkBirth.length; i++) {
    (function(box) {
      box.addEventListener("click",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.addEventListener("click",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.addEventListener("change",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();
    maxX = width-1;
    maxY = height-1;
    delay = getDelay();
    curr = "b";
    next = "a";
    canvas.height = height*CELLSIZE;
    canvas.width = width*CELLSIZE;
    ctx.fillStyle = BORDERCOLOR;
    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 alive = Math.random() < dens;
        let cell = {
          a: alive,
          b: false,
          decay: alive ? 0 : MAXTRAILHUE,
          x: i,
          y: j
        }; 
        life[i][j] = cell;
        ctx.fillStyle = BACKGROUNDCOLOR; 
        ctx.fillRect(i*CELLSIZE+CELLBORDERWIDTH, j*CELLSIZE+CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH);
      }
    }
    nextGeneration();
  }
  function loopNextGeneration() {
    let startTime = +new Date, delta;
    nextGeneration();
    if(delay === 0 || (delta = +new Date - startTime) >= delay){
      running = setTimeout(loopNextGeneration,0);
    }else{
      running = setTimeout(loopNextGeneration, delay - delta);
    }
  }
  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 cell = life[i][j],
         sumAll = getSumNeighbors(cell);
        if (cell[curr]) {
          cell.decay = 0;
          ctx.fillStyle = FOREGROUNDCOLOR;       
          ctx.fillRect(i*CELLSIZE+CELLBORDERWIDTH, j*CELLSIZE+CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH);
          cell[next] = checkSurvives(sumAll);
        } else {          
          cell.decay += TRAILDECAYRATE;
          if(cell !== lastHighlightedCell && cell.decay < HUEDECAYCAP){
            ctx.fillStyle = getTrailColor(cell); 
            ctx.fillRect(i*CELLSIZE+CELLBORDERWIDTH, j*CELLSIZE+CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH);
           }
           cell[next] = checkBorn(sumAll);
        }
      }
    }
    currentStepSpan.innerHTML = stepNum;
  }  
  function toggleCell(cell){
    if(!cell[curr]){
      cell[curr] = true;
      cell.decay = 0;
      cell[next] = checkSurvives(getSumNeighbors(cell));
      ctx.fillStyle = FOREGROUNDCOLOR;
    }else{
      cell[curr] = false;
      cell[next] = checkBorn(getSumNeighbors(cell));
      ctx.fillStyle = BORDERCOLOR;
    }
    adjustNeighbors(cell);
    ctx.fillRect(cell.x*CELLSIZE+CELLBORDERWIDTH, cell.y*CELLSIZE+CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH);
  }
  function getSumNeighbors(cell){ 
    let i = cell.x, j = cell.y;
    let onLeftEdge = j === 0,
      onRightEdge = j == maxX,
      onTopEdge = i === 0,
      onBottomEdge = i == maxY;
    let leftX = onLeftEdge ? maxX : j - 1,
        rightX = onRightEdge ? 0 : j + 1,
        lifeBottom = life[onBottomEdge ? 0 : i + 1],
        lifeMiddle = life[i],
        lifeTop = life[onTopEdge ? maxY : i - 1];
    return lifeBottom[leftX][curr]+ //bl
      lifeBottom[j][curr]+ // bm
      lifeBottom[rightX][curr]+ // br
      lifeMiddle[leftX][curr]+ //l
      lifeMiddle[rightX][curr]+ // r
      lifeTop[leftX][curr]+ // tl
      lifeTop[j][curr]+ // tm
      lifeTop[rightX][curr]; //tr
  }
  function getNeighbors(cell){
    let i = cell.x, j = cell.y;
    let onLeftEdge = j === 0,
      onRightEdge = j == maxX,
      onTopEdge = i === 0,
      onBottomEdge = i == maxY;
    let leftX = onLeftEdge ? maxX : j - 1,
        rightX = onRightEdge ? 0 : j + 1,
        lifeBottom = life[onBottomEdge ? 0 : i + 1],
        lifeMiddle = life[i],
        lifeTop = life[onTopEdge ? maxY : i - 1];
    return [lifeBottom[leftX], //bl
      lifeBottom[j], // bm
      lifeBottom[rightX], // br
      lifeMiddle[leftX], //l
      lifeMiddle[rightX], // r
      lifeTop[leftX], // tl
      lifeTop[j], // tm
      lifeTop[rightX]]; //tr
  }
  function adjustNeighbors(cell){
    let neighbors = getNeighbors(cell);
    for(let i = 0, len = neighbors.length; i < len; i++){
      let c = neighbors[i];
      if(c[curr]){
        c[next] = checkSurvives(getSumNeighbors(c));
      }else{
        c[next] = checkBorn(getSumNeighbors(c));
      }
    }
  }
  function getTrailColor(cell){
    let trailColor = BASETRAILHUE + (cell.decay);
    if (trailColor > MAXTRAILHUE) {
      return BACKGROUNDCOLOR;
    }
    return "rgb(255,"+trailColor+","+trailColor+")";
  }
  function highlightCell(cell){
    if(lastHighlightedCell){
      ctx.fillStyle = lastHighlightedCell[curr] ? FOREGROUNDCOLOR : getTrailColor(lastHighlightedCell); 
      ctx.fillRect(lastHighlightedCell.x*CELLSIZE+CELLBORDERWIDTH, lastHighlightedCell.y*CELLSIZE+CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH);
    }
    ctx.fillStyle = HIGHLIGHTCOLOR;
    ctx.fillRect(cell.x*CELLSIZE+CELLBORDERWIDTH, cell.y*CELLSIZE+CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH, CELLSIZE-CELLBORDERWIDTH);
    lastHighlightedCell = cell;
  }
  function clearHighlightedCell(){
  if(lastHighlightedCell){
      ctx.fillStyle = lastHighlightedCell[curr] ? FOREGROUNDCOLOR : getTrailColor(lastHighlightedCell); 
      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:&nbsp;0<input type="checkbox" value=0 />&nbsp;1<input type="checkbox" value=1 />&nbsp;2<input type="checkbox" value=2 />&nbsp;3<input type="checkbox" value=3 checked="checked" />&nbsp;4<input type="checkbox" value=4 />&nbsp;5<input type="checkbox" value=5 />&nbsp;6<input type="checkbox" value=6 />&nbsp;7<input type="checkbox" value=7 />&nbsp;8<input type="checkbox" value=8 />&nbsp;
      </div>
      <div id="SurvivalThresholds">
        S:&nbsp;0<input type="checkbox" value=0 />&nbsp;1<input type="checkbox" value=1 />&nbsp;2<input type="checkbox" value=2 checked="checked" />&nbsp;3<input type="checkbox" value=3 checked="checked" />&nbsp;4<input type="checkbox" value=4 />&nbsp;5<input type="checkbox" value=5 />&nbsp;6<input type="checkbox" value=6 />&nbsp;7<input type="checkbox" value=7 />&nbsp;8<input type="checkbox" value=8 />&nbsp;
      </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>&nbsp; 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>

\$\endgroup\$
  • 1
    \$\begingroup\$ Looks like some good refactoring. You are basically one step away from being able to split your configuration out. So you can simply generate configuration from your controls and pass that configuration when you are instantiating a new board. \$\endgroup\$ – Mike Brant Dec 22 '16 at 22:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.