Created
November 17, 2017 13:52
-
-
Save htkcodes/4bc1fc03fd488e2167a3718baad266f1 to your computer and use it in GitHub Desktop.
TICTAC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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