<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
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |