Wordle-styled game of life

Created for https://twitter.com/piannaf/status/1483070891961499651

by piannaf

HTML

<div ng-app="myApp" ng-controller="Ctrl">
  <h1>AngularJS Game of Life</h1>
  Height: <input type="text" size="3" ng-model="height"/>
  Width: <input type="text" size="3" ng-model="width"/>
  <button ng-click="newGame()">New</button>
  <button ng-click="next()">Next</button>
  <button ng-click="play()">Play</button>

  <table id="board">
      <tr ng-repeat="row in board">
          <td ng-repeat="cell in row" ng-click="toggle($parent.$index, $index)"
              class="{{cellClass($parent.$index, $index)}}">
             <span ng-show="cell"></span>
          </td>
      </tr>
  </table>

  <p class="history" ng-show="history.length > 0">Previous steps (click to rewind)</p>
  <table class="history">
      <tr>
          <td ng-repeat="step in history" ng-click="$parent.step($index)">{{$index}}</td>
      </tr>
  </table>

</div>

CSS

body {
  background-color: #121213;
  color: #d7dadc;
}
h1 {
    font-size: 1.3em;
    font-style: bold;    
}
table {
  border-spacing: 5px;
  border-collapse: separate;
}
table#board td {
    width: 40px;
    height: 40px;
    text-align: center;
    vertical-align: middle;
    border: solid 1px #3a3a3c;
}
table#board td.new {
    background-color: #b59f3b;
}
table#board td.die {
    background-color: #3a3a3c;
}
table#board td.alive {
    background-color: #538d4e;
}
table#board td.empty {
    background-color: #121213;
}

.history {
    color: #777;
}
table.history td {
    width: 1.5em;
    height: 1.5em;
    text-align: center;
    vertical-align: middle;
    border: solid 1px #eee;
}

JavaScript

angular.module('myApp', []);

function Ctrl($scope, $timeout) {
  
    var play = function() {
        $scope.next();
        $timeout(play, 500);
    }
    
    $scope.newGame = function () {
        $scope.history = [];
        $scope.board = init($scope.height, $scope.width);
    };
    
    $scope.next = function () {
        $scope.history.push($scope.board);
        $scope.board = computeNext($scope.board);
    };
    
    $scope.play = function () {
        $timeout(play, 500);
    }

    $scope.step = function (index) {
        $scope.board = $scope.history[index];
        $scope.history = $scope.history.slice(0, index);
    };
    
    $scope.toggle = function (row, cell) {
        $scope.history = []; // Reset history as it is no longer accurate       
        $scope.board[row][cell] = !$scope.board[row][cell];
    };
    
    $scope.cellClass = function (row, cell) {
        if (willDie($scope.board, row, cell)) {
            return "die";
        }
        if (newCell($scope.board, row, cell)) {
            return "new";
        }
        if (cellAt($scope.board, row, cell)) {
        		return "alive"
        }
        return "empty";
    };

    $scope.height = 6;
    $scope.width = 5;
    $scope.newGame();
    
    function init(height, width) {
        var board = [];
        for (var h = 0 ; h < height ; h++) {
            var row = [];
            for (var w = 0 ; w < width ; w++) {
                row.push(false);
            }
            board.push(row); 
        }
        return board;
    }
    
    function computeNext(board) {
        var newBoard = [];
        for (var r = 0 ; r < board.length ; r++) {
            var newRow = [];
            for (var c = 0 ; c < board[r].length ; c++) {
                newRow.push(willLive(board, r, c) || newCell(board, r, c));
            }
            newBoard.push(newRow); 
        }
        return newBoard;
    }
    
    function willLive(board, row, cell) {
        return cellAt(board, row, cell) 
...