Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<html>
  <head>
    <meta name="description" content="Connect 4 - UI" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <link href="https://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet">
    <script src="http://code.jquery.com/jquery.min.js"></script>
    <script src="http://documentcloud.github.io/underscore/underscore-min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
    <script type='text/javascript' src='https://cdn.firebase.com/v0/firebase.js'></script>
    <meta charset=utf-8 />
    <title>Connect Four AI (beat me if you can :-))</title>
  </head>
  <body ng-app="ConnectFourApp">
    <div ng-controller="AppCtrl" class="container">
      <div class="row">
        <div class="col-md-12 stats">
          <div ng-show="debugMode">
            Best Score: {{bestScore}}<br/>
            Depth: {{depth}}
          </div>
          <div class="connectfour" ng-show="!playing"><a href="http://en.wikipedia.org/wiki/Connect_Four" target="_blank">Connect Four</a></div>
          <div class="total">
            <span class="glyphicon glyphicon-chevron-right zippy-icon zippy-right-icon" ng-show="collapsed" ng-click="toggleStats()"></span>
            <span class="glyphicon glyphicon-chevron-down zippy-icon zippy-down-icon" ng-hide="collapsed" ng-click="toggleStats()"></span>
            <span class="zippy-title" ng-click="toggleStats()">{{total}} {{(total==1) && 'game' || 'games'}} played</stats>
          </div>
          <div class="game-stats" toggle-on="collapsed">
            <table class="table table-bordered">
              <tr>
                <td>COMPUTER</td>
                <td>Win</td>
                <td>Draw</td>
                <td>Loss</td>
              </tr>
              <tr ng-repeat="row in stats">
                <td>plays {{$index && 'second' || 'first'}}</td>
                <td ng-repeat="count in row">{{count}}</td>
              </tr>
            </table>
          </div>
        </div>
      </div>
      <div class="row">
        <div class="col-md-12 main-content">
          <table class="arrows table" ng-show="playing" ng-class="{placeHolder : turn==computerColor}">
            <tr>
              <td ng-repeat="dummy in colRange">
                <span ng-click="humanPlay($index)"
                      ng-show="isValid(1, $index) && $index != hoverAt"
                      ng-mouseover="hoverColumn($index)"                      
                      class="glyphicon glyphicon-arrow-down arrow-down red-turn"></span>
                <span ng-click="humanPlay($index)"
                      ng-show="isValid(2, $index) && $index != hoverAt"
                      ng-mouseover="hoverColumn($index)"                      
                      class="glyphicon glyphicon-arrow-down arrow-down yellow-turn"></span>
                <span ng-show="isFull($index)"
                      class="glyphicon glyphicon-arrow-down arrow-down placeHolder"></span>
                <div ng-show="$index == hoverAt && !isFull($index)" class="hovering">
                  <div ng-click="humanPlay($index)" class="red small-ball" ng-show="turn == 1"></div>
                  <div ng-click="humanPlay($index)" class="yellow small-ball" ng-show="turn == 2"></div>
                </div>
              </td>
            </tr>            
          </table>
          <table class="board table table-bordered" ng-mouseout="leaveBoard()">
            <tr ng-repeat="row in board">
              <td ng-repeat="square in row">
                <div class="square-container"
                     ng-class="{lastMove: $parent.$index == lastMove.row && $index == lastMove.col}"
                     ng-mouseover="hoverColumn($index)"
                     ng-click="humanPlay($index)">
                  <div class="empty player" ng-show="square==0"></div>
                  <div class="red player" ng-show="square==1" ></div>
                  <div class="yellow player" ng-show="square==2"></div>
                </div>
              </td>
            </tr>
          </table>
        </div>
      </div>
      <div class="row">        
        <div class="col-md-12 info">
          <div ng-hide="playing">
            <div ng-show="firstPlayer == 1">Computer will play first?</div>
            <div ng-hide="firstPlayer == 1">You will play first?</div>
          </div>
          <div ng-show="playing && !resultMsg">
            <div ng-show="isHumanTurn()">It's your turn to play.</div>
            <div ng-hide="isHumanTurn()">Computer is thinking...</div>
          </div>
          <div ng-show="resultMsg" class="game-result">{{resultMsg}}</div>
        </div>
      </div>
      <div class="row">        
        <div class="col-md-12 actions">
          <div ng-show="playing">
            <button ng-click="newGame()" class="btn btn-success" ng-disabled="computerColor == turn && !resultMsg">New game</button>
            <button ng-click="undo()" class="btn btn-primary" ng-disabled="!undoable()">Undo ({{undoLeft}})</button>
          </div>
          <div ng-hide="playing">
            <button ng-click="startGame()" class="btn btn-success">OK, go</button>
            <button ng-click="switchTurn()" class="btn btn-primary">Switch turn</button>
          </div>
        </div>
      </div>
      <div class="row">
        <div class="col-md-12 links">
          Source code:
          <a href="http://jsbin.com/exIjAQO/21/edit?html,javascript" target="_blank">UI</a>
          <a href="http://jsbin.com/acIhOwE/14/edit?javascript" target="_blank">AI</a>
        </div>
      </div>
    </div>
  </body>
</html>
 
var app = angular.module('ConnectFourApp', []);
// Common.
var ROWS = 6;
var COLS = 7;
var EMPTY = 0;
var RED = 1;
var YELLOW = 2;
var DRAW = 3;
// UI.
var HUMAN = 0;
var COMPUTER = 1;
var UNDO_LIMIT = 3;
// Engine.
var DEFAULT_TIME_LIMIT = 3000; // 3s.
var TIME_DELTA = 2000; // +-2s.
// Firebase.
//FB_DB = 'dev';
FB_DB = 'prod';
function getOpp(player) {
  return player ? (player === RED ? YELLOW : RED) : EMPTY;
}
function inside(r, c) {
  return r >= 0 && c >= 0 && r < ROWS && c < COLS;
}
app.factory('engineService', function($q, $timeout) {
  var worker = new Worker('http://jsbin.com/acIhOwE/14');
  worker.postMessage('_anything_');
  
  var thinkRequestQueue = [];
  
  worker.addEventListener('message', function(e) {
    if (e.data.msgType && e.data.msgType === 'think') {
      if (thinkRequestQueue.length) {
        var firstCallback = thinkRequestQueue.shift();
        firstCallback(e);
      } else {
        alert('The thinkRequestQueue is empty on receiving new message from worker, something went wrong.');
      }
    }
  });
  
  function makePlayRequest(timeLimit, callback) {
    thinkRequestQueue.push(callback);
    worker.postMessage({
      msgType: 'think',
      timeLimit: timeLimit
    });
  }
  
  var engineService = {};
  
  engineService.init = function(board, turn) {
    worker.postMessage({
      msgType: 'init',
      board: board,
      turn: turn
    });
  };
  
  engineService.makeMove = function(move) {
    worker.postMessage({
      msgType: 'makeMove',
      move: move
    });
  };
  
  engineService.unMakeMove = function() {
    worker.postMessage({
      msgType: 'unMakeMove'
    });
  };
  
  engineService.think = function(timeLimit) {
    var d = $q.defer();    
    makePlayRequest(timeLimit, function(e) {
      $timeout(function() {
        d.resolve(e.data.result);
      });
    });
    return d.promise;
  };
  
  return engineService;
});
app.factory('gameService', function() {
  var gameService = {};
  
  function createEmptyBoard(rows, cols) {
    var board = new Array(rows);
    for (var i = 0; i < rows; i++) {
      var row = new Array(cols);
      for (var j = 0; j < cols; j++) {
        row[j] = EMPTY;
      }
      board[i] = row;
    }
    return board;
  }
  
  var board, turn, moves, moveCount, result;
  
  gameService.newGame = function() {
    board = createEmptyBoard(ROWS, COLS);
    turn = RED;
    result = 0;
    moveCount = 0;
    moves = [];
  };
  
  function isWinningAt(i, j) {
    function toward(i, j, di, dj) {
      var count = 0;
      while (true) {
        i += di;
        j += dj;
        if (!inside(i, j) || board[i][j] !== turn) break;
        count++;
      }
      return count;
    }
    
    return (toward(i, j, +1, 0) >= 3) ||
      (toward(i, j, 0, +1) + toward(i, j, 0, -1) >= 3) ||
      (toward(i, j, +1, +1) + toward(i, j, -1, -1) >= 3) ||
      (toward(i, j, +1, -1) + toward(i, j, -1, +1) >= 3);
  }
  
  function findFirstNonEmptyRow(col) {
    var row = 0;
    while (row < ROWS && board[row][col] === EMPTY) row++;
    return row;
  }
  
  gameService.makeMove = function(col) {
    if (col < 0 || col >= COLS || board[0][col] !== EMPTY) {
      return false;
    }
    moves[moveCount++] = col;
    var row = findFirstNonEmptyRow(col);
    board[row - 1][col] = turn;
    if (isWinningAt(row - 1, col)) {
      result = turn;
    } else if (moveCount + 1 === ROWS * COLS) {
      result = DRAW;
    }    
    turn = getOpp(turn);
    return true;
  };
  
  gameService.unMakeMove = function() {
    var col = moves[moveCount - 1];
    moveCount--;
    var row = findFirstNonEmptyRow(col);
    board[row][col] = EMPTY;
    result = 0;
    turn = getOpp(turn);
  };
  
  gameService.getBoard = function() {
    return board;
  };
  
  gameService.getTurn = function() {
    return turn;
  };
  
  gameService.getResult = function() {
    return result;
  };
  
  gameService.getLastMove = function() {
    var row, col;
    if (moveCount) {
      col = moves[moveCount - 1];
      row = findFirstNonEmptyRow(col);
    } else {
      row = ROWS;
      col = COLS;
    }
    return {row: row, col: col};
  };
  
  gameService.getMoveCount = function() {
    return moveCount;
  };
  
  return gameService;
});
app.factory('statsService', function() {
  var statsRef = new Firebase('https://connectfour.firebaseio.com/' + FB_DB + '/stats');
  
  var statsService = {};
  var currentResult;
  
  statsService.setValueCallback = function(callback) {
    var stats;
    statsRef.on('value', function(snapshot) {
      var stats = snapshot.val();
      if (!stats) {
        stats = {
          0 : {0 : 0, 1 : 0, 2 : 0},
          1 : {0 : 0, 1 : 0, 2 : 0}
        };
        statsRef.set(stats);
      }
      callback(stats);
    });
  };
  
  statsService.newGame = function() {
    currentResult = undefined;
  };
  
  function updateStats(turn, result, delta) {
    if (!delta) {
      delta = 1;
    }
    statsRef.child(turn - 1).child(result - 1).transaction(function(currentValue) {
      return currentValue + delta;
    });
  }
  
  statsService.setResult = function(turn, result) {
    if (currentResult === result) return;
    if (currentResult && currentResult !== result) {
      updateStats(turn, currentResult, -1);
    }
    updateStats(turn, result);
    currentResult = result;
  };
  
  return statsService;
});
app.directive('toggleOn', function() {
  return {
    link: function(scope, element, attrs) {
      scope.$watch(attrs.toggleOn, function(value) {
        if (value) {
          $(element).slideUp();
        } else {
          $(element).slideDown();
        }
      });
    }
  };
});
app.controller('AppCtrl', function($scope, $timeout, $location, gameService, engineService, statsService) {
  
  $scope.total = 0;
  $scope.collapsed = true;
  $scope.toggleStats = function() {
    $scope.collapsed = !$scope.collapsed;
  };
  
  statsService.setValueCallback(function(stats) {
    var total = 0;
    for (var i = 0; i < stats.length; i++) {
      for (var j = 0; j < stats[i].length; j++) {
        total += stats[i][j];
      }
    }
    $scope.safeApply(function() {
      $scope.total = total;
      $scope.stats = stats;
    });
  });
  
  $scope.safeApply = function(fn) {
    var phase = this.$root.$$phase;
    if(phase == '$apply' || phase == '$digest') {
      if(fn && (typeof(fn) === 'function')) {
        fn();
      }
    } else {
      this.$apply(fn);
    }
  };
  
  $scope.debugMode = $location.path() === '/d';
  
  function getPlayerType(turn) {
    return turn === RED ? $scope.firstPlayer : $scope.secondPlayer;
  }
  
  $scope.isHumanTurn = function() {
    return getPlayerType($scope.turn) === HUMAN;
  };
  
  $scope.switchTurn = function() {
    if ($scope.firstPlayer === HUMAN) {
      $scope.firstPlayer = COMPUTER;
      $scope.secondPlayer = HUMAN;
      $scope.computerColor = RED;
      $scope.humanColor = YELLOW;
    } else {
      $scope.firstPlayer = HUMAN;
      $scope.secondPlayer = COMPUTER;
      $scope.humanColor = RED;
      $scope.computerColor = YELLOW;
    }
  };
  
  function handleGameStateChanged() {
    $scope.turn = gameService.getTurn();
    $scope.result = gameService.getResult();
    $scope.lastMove = gameService.getLastMove();
  }
  
  $scope.newGame = function() {
    gameService.newGame();
    statsService.newGame();
    $scope.board = gameService.getBoard();
    handleGameStateChanged();
    
    $scope.playing = false;
    $scope.resultMsg = '';
    $scope.bestScore = 0;
    $scope.depth = 0;
    $scope.undoLeft = UNDO_LIMIT;
    
    engineService.init($scope.board, $scope.turn);
  };
  
  $scope.firstPlayer = COMPUTER;
  $scope.switchTurn();
  
  $scope.newGame();
  $scope.colRange = _.range(COLS);
  
  $scope.startGame = function() {
    $scope.playing = true;
    if (getPlayerType($scope.turn) === COMPUTER) {
      computerPlay();
    }    
  };  
  
  $scope.isFull = function(col) {
    return $scope.board[0][col] !== EMPTY;
  };
  
  $scope.isValid = function(player, col) {
    return !$scope.result && $scope.turn === player && !$scope.isFull(col);
  };
  
  
  var announceResult = function(turn) {
    if (turn === DRAW) {
      $scope.resultMsg = 'The game is draw.';
    } else {
      $scope.resultMsg = (turn === $scope.computerColor) ? 'Computer has won. If you enjoy playing, please upvote || +1 || tweet.' : 'Congratulation, you have won.';
    }    
    var resultForComputer;
    if (turn === DRAW) {
      resultForComputer = 2; // DRAW
    } else {
      resultForComputer = turn === $scope.computerColor ? 1 : 3; // WIN or LOSS
    }
    statsService.setResult($scope.computerColor, resultForComputer);
    $scope.hoverAt = undefined;
  };
  
  $scope.humanPlay = function(col) {
    if (!$scope.result && $scope.humanColor === $scope.turn) {
      play(col);
    }
  };
  
  $scope.undoable = function() {
    return gameService.getMoveCount() >= 2 &&
        $scope.undoLeft > 0 &&
        ($scope.resultMsg || $scope.isHumanTurn());
  };
  
  $scope.undo = function() {
    gameService.unMakeMove();
    engineService.unMakeMove();
    // Undo until it is human's turn.
    if (gameService.getTurn() === $scope.computerColor) {
      gameService.unMakeMove();
      engineService.unMakeMove();
    }
    handleGameStateChanged();
    $scope.resultMsg = '';
    $scope.bestScore = 0;
    $scope.depth = 0;
    $scope.undoLeft--;
  };
  
  function play(col) {    
    if (gameService.makeMove(col)) {
      engineService.makeMove(col);
      handleGameStateChanged();
      if ($scope.result) {
        announceResult($scope.result);
        return;
      }
      if (getPlayerType($scope.turn) === COMPUTER) {
        computerPlay();
      }
    } else {
      alert('Invalid move.');
    }
  }
  
  function computerPlay() {
    var randomTimeLimit = DEFAULT_TIME_LIMIT - TIME_DELTA + Math.floor(Math.random() * 2 * TIME_DELTA);
    engineService.think(randomTimeLimit).then(function(thinkResult) {
      play(thinkResult.bestMove);
      $scope.bestScore = thinkResult.bestScore;
      $scope.depth = thinkResult.depth;
    });
  }
  
  $scope.hoverColumn = function(col) {
    if ($scope.playing && !$scope.result) {
      $scope.hoverAt = col;
    }
  };
  
  $scope.leaveBoard = function() {
    if ($scope.playing && !$scope.result) {
      $scope.hoverAt = undefined;
    }
  };
});
Output

You can jump to the latest bin by adding /latest to your URL

Dismiss x
public
Bin info
js_ninjapro
0viewers