Skip to content

Instantly share code, notes, and snippets.

@htkcodes
Created November 17, 2017 13:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save htkcodes/4bc1fc03fd488e2167a3718baad266f1 to your computer and use it in GitHub Desktop.
Save htkcodes/4bc1fc03fd488e2167a3718baad266f1 to your computer and use it in GitHub Desktop.
TICTAC
<html>
<head>
<title>
Tic-Tac Toe</title>
<link rel="stylesheet" href="main.css" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Kavoon" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Raleway" rel="stylesheet">
</head>
<body>
<div class="newGameDialog">
<button id="reset-button">Play again?</button>
</div>
<div class="container" id="site-wrapper">
<div class="page-header">
<h1>Tic Tac Toe</h1>
</div>
<div class="container board-container">
</div>
<div class="container display-container">
<h2 id="message"></h2>
<div class="container player-container" id="p1">
<ul>
<li class="player-name">
<input type="text" value="Player 1" class="player-input-name" data-player="p1">
</li>
<li class="player-mark">
<select name="p1-select-marker" class="player-select-marker" data-player="p1">
<option value="X" selected>X</option>
<option value="O">O</option>
</select>
</li>
<li class="player-controller">
<select name="p1-select-controller" class="player-select-controller" data-player="p1">
<option value="person" selected>Person</option>
<option value="easy">Ez</option>
<option value="regular">Regular</option>
<option value="difficult">Deluxe</option>
</select>
</li>
</ul>
</div>
<div class="container player-container" id="p2">
<ul>
<li class="player-name">
<input type="text" value="Player 2" class="player-input-name" data-player="p2">
</li>
<li class="player-controller">
<select name="p2-select-controller" class="player-select-controller" data-player="p2">
<option value="person">Person</option>
<option value="easy">Ez</option>
<option value="regular">Regular</option>
<option value="difficult" selected>Dexlue</option>
</select>
</li>
<li class="player-mark">
<select name="p2-select-marker" class="player-select-marker" data-player="p2">
<option value="X">X</option>
<option value="O" selected>O</option>
</select>
</li>
</ul>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.0.0.js" integrity="sha256-jrPLZ+8vDxt2FnE1zvZXCkCcebI/C8Dt5xyaQBjxQIo=" crossorigin="anonymous"></script>
<script>
//Instantiates Grids
function Cell(row, col) {
this.row = row;
this.col = col;
this.$element = null;
this.value = null;
}
Cell.prototype = {
constructor: Cell,
hasValue: function() {
return this.value !== null;
},
setValue: function(mark) {
this.value = mark;
if (this.$element) {
this.$element.text(this.value);
}
},
setDisplay: function($element) {
this.$element = $element;
this.$element.text(this.value);
}
};
//A grid is a collection of cells arranged in a square of rows and columns.
function Grid(size) {
this.size = size;
this.cells = [];
//Initialize the cells array and render the grid
for (var i = 0; i < this.size; i++) {
//Create row
this.cells[i] = [];
//Fill columns with cells
for (var j = 0; j < this.size; j++) {
this.cells[i][j] = new Cell(i, j);
}
}
}
Grid.prototype = {
constructor: Grid,
getCell: function(row, col) {
return this.cells[row][col];
},
getRow: function(i) {
return this.cells[i];
},
getCol: function(i) {
return this.cells.map(function(row, idx) {
return row[i];
});
},
getDiag: function(i) {
//Valid diagonal indices are 0 (down-right) & 1 (up-right)
return this.cells.map(function(row, idx) {
return row[i === 0 ? idx : row.length - idx - 1];
});
},
getEmptyCells: function() {
var emptyCells = [];
for (var i = 0; i < this.cells.length; i++) {
for (var j = 0; j < this.cells[i].length; j++) {
if (!this.cells[i][j].hasValue()) {
emptyCells.push(this.cells[i][j]);
}
}
}
return emptyCells;
},
getMatchingSet: function() {
//Return a row, column, or main diagonal for which all cells match
function allMatch(arr) {
var first = arr[0];
return arr.every(function(el) {
return first.hasValue() && el.value === first.value;
});
}
//Iterate over all rows, columns, and diagonals, returning the first
//matching set encountered
for (var i = 0; i < this.size; i++) {
if (allMatch(this.getRow(i))) {
return this.getRow(i);
}
if (allMatch(this.getCol(i))) {
return this.getCol(i);
}
if ((i === 0 || i === 1) && allMatch(this.getDiag(i))) {
return this.getDiag(i);
}
}
return null;
},
clone: function() {
//Create a deep clone of a grid, including clones of its cells
var clone = new Grid(this.size);
for (var i = 0; i < clone.cells.length; i++) {
for (var j = 0; j < clone.cells[0].length; j++) {
clone.cells[i][j].setValue(this.cells[i][j].value);
}
}
return clone;
},
renderDisplay: function($displayContainer) {
//Create DOM elements to display the grid
this.$displayContainer = $displayContainer;
this.$displayContainer.empty();
for (var i = 0; i < this.cells.length; i++) {
//Create row element
var $row = $("<div />", {
class: "row",
"data-row": i
});
for (var j = 0; j < this.cells[0].length; j++) {
//Fill columns with cells
var $cell = $("<div />", {
class: "cell",
"data-row": i,
"data-col": j
});
$row.append($cell);
//Store a reference to the display in the grid cell
this.cells[i][j].setDisplay($cell);
}
//Add the row to the DOM
this.$displayContainer.append($row);
}
}
};
//A controller determines the behavior of a player and selects moves
function Controller(game, type, player) {
this.game = game;
this.type = type;
this.player = player;
}
Controller.prototype = {
constructor: Controller,
takeTurn: function() {
}
};
//The player controller waits for player input.
function playerController(game, player) {
Controller.call(this, game, "person", player);
}
playerController.prototype = Object.create(Controller.prototype);
playerController.prototype.constructor = playerController;
playerController.prototype.takeTurn = function() {
this.game.display.setMessage(this.player.name + "'s turn!");
return true;
};
//The easy controller plays randomly
function EasyController(game, player) {
Controller.call(this, game, "easy", player);
}
EasyController.prototype = Object.create(Controller.prototype);
EasyController.prototype.constructor = EasyController;
EasyController.prototype.takeTurn = function() {
var moves = this.game.board.getEmptyCells();
if (moves.length > 0) {
this.game.move(moves[Math.floor(Math.random()*moves.length)]);
return true;
}
};
function regularController(game,player)
{
Controller.call(this,game,"regular",player);
regularController.prototype=Object.create(Controller.prototype)
regularController.prototype.constructor=regularControllerController;
regularController.prototype.takeTurn = function() {
var players = this.game.getPlayersById(this.player.id);
var moves = this.game.board.getEmptyCells();
//If it is possible to win this turn, do so:
for (var i = 0; i < moves.length; i++) {
var testBoard = this.game.board.clone();
testBoard.getCell(moves[i].row, moves[i].col).setValue(players.player.marker);
if (this.game.isWinner(testBoard, players.player)) {
this.game.move(moves[i]);
return true;
}
}
//If the opponent will be able to win next turn, block them:
for (var i = 0; i < moves.length; i++) {
var testBoard = this.game.board.clone();
testBoard.getCell(moves[i].row, moves[i].col).setValue(players.opponent.marker);
if (this.game.isWinner(testBoard, players.opponent)) {
this.game.move(moves[i]);
return true;
}
}
//Otherwise, move randomly:
if (moves.length > 0) {
this.game.move(moves[Math.floor(Math.random()*moves.length)]);
return true;
}
};
}
//The "difficult" controller implements the Minimax algorithm to play perfectly
function difficultController(game, player) {
Controller.call(this, game, "difficult", player);
}
difficultController.prototype = Object.create(Controller.prototype);
difficultController.prototype.constructor = difficultController;
difficultController.prototype.takeTurn = function() {
var players = this.game.getPlayersById(this.player.id);
this.move = null;
//Call minimax, which will run recursively and set this.move to a value
this.minimax(this.game.board.clone(), players, 0);
//If successful, actually make the selected move
if (this.move) {
this.game.move(this.move);
}
};
difficultController.prototype.gradeBoard = function(board) {
//Assign a "grade" to the board, returning 1 for a win, -1 for loss, and 0
//for a draw. If the board is not in a terminal state, return null.
var moves = board.getEmptyCells();
if (this.game.isWinner(board, this.player)) {
return 1;
} else if (this.game.isWinner(board, this.player.getOpponent())) {
return -1;
} else if (moves.length === 0){
return 0;
} else {
return null;
}
};
difficultController.prototype.minimax = function(board, players, depth) {
//Minimax is a recursive algorithm that "grades" the boards resulting from all
//possible moves. The current player seeks to maximize the grade, while the
//opponent seeks to minimize it.
//First, check if the game is complete and just return the grade if so:
var grade = this.gradeBoard(board);
if (grade !== null) {
return {grade: grade, depth: depth};
}
//Otherwise, prepare to iterate over all available moves
var moves = board.getEmptyCells();
var grades = [];
//Swap player and opponent for the next turn
var nextPlayers = {player: players.opponent, opponent: players.player};
for (var i = 0; i < moves.length; i++) {
//Create a new board and take the current move
var nextBoard = board.clone();
var nextMove = nextBoard.getCell(moves[i].row, moves[i].col);
nextMove.setValue(players.player.marker);
//Recursively call minimax on the new state of the board
grades.push(this.minimax(nextBoard, nextPlayers, depth + 1));
}
//Choose the "best" of the available moves based on their grades
var bestMove = this.chooseGradedMove(moves, grades, players);
//If we are currently grading the original (depth 0) board, store the move
if (depth === 0) {
this.move = this.game.board.getCell(bestMove.row, bestMove.col);
}
//Return the best grade and depth
return {grade: bestMove.grade, depth: bestMove.depth};
};
difficultController.prototype.chooseGradedMove = function(moves, grades, players){
//The current player seeks to maximize the grade, whereas the opponent seeks
//to minimize it. Both players seek to maximize depth (prolong the game).
if (players.player === this.player) {
var best = grades.reduce(function(prev, cur) {
if (cur.grade > prev.grade || (cur.grade === prev.grade && cur.depth > prev.depth)) {
return cur;
} else {
return prev;
}
});
} else {
var best = grades.reduce(function(prev, cur) {
if (cur.grade < prev.grade || (cur.grade === prev.grade && cur.depth > prev.depth)) {
return cur;
} else {
return prev;
}
});
}
//Randomly select one of the possible moves with the best grade and depth
var possibleMoves = moves.filter(function(move, idx) {
return grades[idx].grade === best.grade && grades[idx].depth === best.depth;
});
var move = possibleMoves[Math.floor(Math.random()*possibleMoves.length)];
return {row: move.row, col: move.col, grade: best.grade, depth: best.depth};
};
function Player(game, id, name, marker, controllerType) {
this.game = game;
this.id = id;
this.name = name;
this.marker = marker;
this.controller = null;
this.setController(controllerType);
this.score = 0;
}
Player.prototype = {
constructor: Player,
setMarker: function(marker) {
if (this.marker !== marker) {
this.marker = marker;
return true;
}
return false;
},
setController: function(controllerType) {
if (controllerType === "person") {
this.controller = new playerController(this.game, this);
} else if (controllerType === "easy") {
this.controller = new EasyController(this.game, this);
} else if (controllerType === "regular") {
this.controller = new regularController(this.game, this);
} else if (controllerType === "difficult") {
this.controller = new difficultController(this.game, this);
} else {
this.controller = null;
}
},
getOpponent: function() {
return this.game.getPlayersById(this.id).opponent;
}
}
function Game($boardContainer, $displayContainer) {
this.$boardContainer = $boardContainer;
this.$displayContainer = $displayContainer;
this.display = new Display(this, $displayContainer);
this.p1 = new Player(this, "p1", "Player 1", "X", "person");
this.p2 = new Player(this, "p2", "Player 2", "O", "difficult");
this.startMatch();
}
Game.prototype = {
constructor: Game,
startMatch: function() {
//Close the modal restart display if it is open
this.display.newGameModalClose();
//Replace the current board with a new one
this.board = new Grid(3);
this.board.renderDisplay(this.$boardContainer);
this.display.update();
//Start a new match
this.curPlayer = this.getPlayersByMarker("X").player;
this.curPlayer.controller.takeTurn();
},
isValidMove: function(cell) {
//Check whether it is possible to move on a particular cell
return !cell.hasValue();
},
activateCell: function(row, col) {
//Attempt to activate or play in a cell
if (this.curPlayer && this.curPlayer.controller.type === "person") {
this.move(this.board.getCell(row, col));
}
},
isWinner: function(board, player) {
//Check for a winner
var match = board.getMatchingSet();
return (Array.isArray(match) && match[0].value === player.marker);
},
getPlayersById: function(playerId) {
//Return player and opponent by id
if (playerId === "p1") {
return {player: this.p1, opponent: this.p2};
} else if (playerId === "p2") {
return {player: this.p2, opponent: this.p1};
} else {
return null;
}
},
getPlayersByMarker: function(marker) {
//Return player and opponent by marker
if (this.p1.marker === marker) {
return {player: this.p1, opponent: this.p2};
} else if (this.p2.marker === marker) {
return {player: this.p2, opponent: this.p1};
} else {
return null;
}
},
setPlayerMarker: function(playerId, marker) {
//Attempt to set a player marker to X or O. If this is a change, also
//toggle the other player's marker.
var players = this.getPlayersById(playerId);
if(players.player.setMarker(marker)) {
players.opponent.setMarker(marker === "X" ? "O" : "X");
this.display.update();
}
},
setPlayerName: function(playerId, name) {
//Set player's name
this.getPlayersById(playerId).player.name = name;
},
setPlayerController: function(playerId, controllerType) {
//Set player's controller
var players = this.getPlayersById(playerId);
players.player.setController(controllerType);
//If the current player's controller has changed, take its turn.
if(players.player === this.curPlayer) {
this.curPlayer.controller.takeTurn();
}
},
nextTurn: function() {
//Check game state
if (this.isWinner(this.board, this.curPlayer)) {
//Victory!
this.curPlayer.score++;
this.endGame(this.curPlayer.name + " wins!");
} else if (this.board.getEmptyCells().length === 0) {
//Draw
this.endGame("It's a draw.");
} else {
//Still playing, so toggle the current player and let it take its turn
this.curPlayer = (this.curPlayer === this.p1) ? this.p2 : this.p1;
this.curPlayer.controller.takeTurn();
}
},
move: function(cell) {
//Attempt to move on a specified cell
if (!this.isValidMove(cell)) {
return false;
}
cell.setValue(this.curPlayer.marker);
this.nextTurn();
},
endGame: function(message) {
this.curPlayer = null;
this.display.setMessage(message);
this.display.newGameModalOpen(this.$boardContainer);
}
};
function Display(game, $displayContainer) {
this.game = game;
this.$message = $displayContainer.find("#message");
this.p1Display = this.registerPlayerDisplay($displayContainer.find("#p1"));
this.p2Display = this.registerPlayerDisplay($displayContainer.find("#p2"));
}
Display.prototype = {
constructor: Display,
registerPlayerDisplay: function($playerContainer) {
return {
$name: $playerContainer.find(".player-input-name"),
$marker: $playerContainer.find(".player-select-marker"),
$controller: $playerContainer.find(".player-select-controller")
};
},
setMessage: function(msg) {
this.$message.html(msg);
},
updatePlayerDisplay: function(player, playerDisplay) {
playerDisplay.$name.val(player.name);
playerDisplay.$marker.val(player.marker);
},
update: function() {
this.updatePlayerDisplay(this.game.p1, this.p1Display);
this.updatePlayerDisplay(this.game.p2, this.p2Display);
},
newGameModalOpen: function($boardContainer) {
boardWidth = $boardContainer.width();
boardHeight = $boardContainer.height();
boardPos = $boardContainer.position();
$('.newGameDialog').css({
left: boardPos.left + boardWidth / 2 - 150,
top: boardPos.top + boardHeight / 2 - 100
});
$('.newGameDialog').fadeIn();
},
newGameModalClose: function() {
$('.newGameDialog').fadeOut();
}
};
$(document).ready(function() {
var game = new Game($(".board-container"), $(".display-container"), $("#newGameModal"));
$("#reset-button").on("click", function(event) {
event.preventDefault();
game.startMatch();
});
$(".board-container").on("click", ".cell", function() {
game.activateCell($(this).data("row"), $(this).data("col"));
});
$(".player-container").on("change", ".player-input-name", function() {
game.setPlayerName($(this).data("player"), $(this).val());
});
$(".player-container").on("change", ".player-select-marker", function() {
game.setPlayerMarker($(this).data("player"), $(this).val());
});
$(".player-container").on("change", ".player-select-controller", function() {
game.setPlayerController($(this).data("player"), $(this).val());
});
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment