Created
July 22, 2016 06:23
-
-
Save chemok78/edaa81d32b9288387983c24352ea88cc to your computer and use it in GitHub Desktop.
Unbeatable Tic Tac Toe
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 lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<!--to make sure latest rendering mode for IE--> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<!--to ensure proper rendering and touch zooming--> | |
<title>Tic Tac Toe</title> | |
</head> | |
<body> | |
<div class="wrapper"> | |
<div id="a1" class="cell"></div> | |
<div id="a2" class="cell"></div> | |
<div id="a3" class="cell"></div> | |
<div id="b1" class="cell"></div> | |
<div id="b2" class="cell"></div> | |
<div id="b3" class="cell"></div> | |
<div id="c1" class="cell"></div> | |
<div id="c2" class="cell"></div> | |
<div id="c3" class="cell"></div> | |
</div> | |
<!-- Modal --> | |
<div class="modal fade" id="myModal" role="dialog"> | |
<div class="modal-dialog"> | |
<!-- Modal content--> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<button type="button" class="close" data-dismiss="modal">×</button> | |
<h4 class="modal-title">Tic Tac Toe: you can try but you will never beat me ;)</h4> | |
</div> | |
<div class="modal-body"> | |
<p>Pick X or O</p> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" id="chooseX" class="btn btn-default" data-dismiss="modal" value="X">X</button> | |
<button type="button" id="chooseO" class="btn btn-default" data-dismiss="modal" value="O">O</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> |
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
//AI player starts | |
//human chooses X or O to play with | |
//AI player never looses: win or draw | |
//AI uses miniMax algorithm | |
//Game restarts automatically after end state | |
$(document).ready(function() { | |
var resize = function() { | |
$('.wrapper').css({ | |
position: 'absolute', | |
left: ($(window).width() - $('.wrapper').outerWidth()) / 2, | |
//outerWidth is a jQuery method for width including padding and border | |
//difference between window with and wrapper width divided by 2 | |
top: ($(window).height() - $('.wrapper').outerHeight()) / 2, | |
}); | |
} | |
resize(); | |
//call the resize function once when the document is loaded | |
$(window).resize(function() { | |
//call the resize function every time the window resizes | |
resize(); | |
}); | |
//global variables | |
var currentState = null; | |
//global variable to hold the current state, will be an instance of State class. | |
var currentGame = null; | |
//global variable to hold the current game, will be an instance of the Game class | |
var computer = null; | |
//global variable to hold the computer player, will be an instance of AIplayer class | |
var firstMove = [0, 2, 4, 6, 8]; | |
//Unbeatable Tic Tac Toe and AI player starting, means always placing token in center or corner for first move. Array with indexes for these positions on board. Will be used to generate a random index | |
var State = function(old) { | |
//Represents a state the game is in | |
//Used for actual state of the game + virtual (next) states look ahaed miniMax algorithm | |
//input: old state, returns: a new state | |
this.turn = " "; | |
//public: who's turn it is: human or AI. Letter X or O | |
this.result = ""; | |
//public:result of the game: still running, X wins, O wins, draw | |
if (typeof old !== "undefined") { | |
//public: board configuration in the current state | |
//is initialized as a board with AI player token in center or corner in the modal when choosing X or 0 | |
//construct board, turn and result from old state when it is not undefined | |
this.board = old.board.slice(); | |
this.turn = old.turn; | |
this.result = old.result; | |
} | |
this.empty = function() { | |
//public: returns array with empty cells board in the current state | |
var indices = []; | |
for (var i = 0; i < this.board.length; i++) { | |
if (this.board[i] === null) { | |
indices.push(i); | |
} | |
} | |
return indices; | |
} //this.empty | |
this.end = function() { | |
//public: check end state function | |
//returns true or false. If true returns also this.result = "X wins" or "O wins" or "draw | |
var board = this.board; | |
//copy current board in local variable to work with | |
for (var i = 0; i <= 6; i = i + 3) { | |
//check rows | |
if (board[i] !== null && board[i] === board[i + 1] && board[i + 1] === board[i + 2]) { | |
this.result = board[i] + " " + "wins"; | |
return true; | |
} | |
} //check rows | |
for (var i = 0; i <= 2; i++) { | |
//check columns | |
if (board[i] !== null && board[i] === board[i + 3] && board[i + 3] === board[i + 6]) { | |
this.result = board[i] + " " + "wins"; | |
return true; | |
} | |
} //check columns | |
for (var i = 0, j = 4; i <= 2; i = i + 2, j = j - 2) { | |
if (board[i] !== null && board[i] == board[i + j] && board[i + j] === board[i + 2 * j]) { | |
this.result = board[i] + " " + "wins"; | |
return true; | |
} | |
} | |
if (this.result !== "X wins" || this.result !== "O wins") { | |
//check draw | |
var draw = this.empty(); | |
if (draw.length === 0) { | |
this.result = "draw"; | |
return true; | |
} else { | |
return false; | |
} | |
} //check draw | |
} //this.end | |
this.changeTurn = function() { | |
//function to change the turn as in this.turn | |
if (this.turn === "X") { | |
this.turn = "O"; | |
} else { | |
this.turn = "X"; | |
} | |
} //this.changeTurn | |
}; //var State | |
var game = function(computerPlayer) { | |
//class to hold the current game. | |
//Intialized once in the modal, start of the game | |
//methods score, advance and start game | |
this.AI = ""; | |
//token for the AI player, set in the modal. X or O. | |
this.human = ""; | |
//token for the human player, set in the modal. X or O. | |
this.status = "beginning"; | |
//track game status. Is beginning, set to running in this.gamestart and set to "ended" when someone wins or draw | |
this.score = function(_state) { | |
//score the game when state is an end state | |
//used in end of game and look ahead in virtual states of miniMax | |
//input is a state in current game or virtual (next) state in miniMax | |
//Calculated from AI player perspective. He tries to maximize and human to minimize | |
//AI player wins: 10 points, lose: -10 points, draw: 0 points | |
if (_state.result !== "still running") { | |
var AIWin = currentGame.AI + " " + "wins"; | |
var humanWin = currentGame.human + " " + "wins"; | |
if (AIWin == _state.result) { | |
return 10; | |
} else if (humanWin == _state.result) { | |
return -10; | |
} else { | |
return 0; | |
} | |
} //if statement | |
} //this.score | |
this.advance = function() { | |
//advances current game from one state to another or ends + restarts game | |
//check here if a game is end state. If end state restart the game | |
//if not an end state and AI player's turn, notify AI to make move by calling AIplayer.makeMove. | |
if (currentState.end()) { | |
//call currentState.end() and check if it's true or false. If true reset game | |
this.status = "ended"; | |
//change currentGame.status from running to ended. | |
//get X or O wins | |
var winLetter = currentState.result; | |
if(winLetter.charAt(0) === currentGame.AI){ | |
alert("Computer Wins!"); | |
} else if(currentState.result === "draw") { | |
alert("It's a draw!") | |
} else { | |
alert("You win!"); | |
} | |
currentState = new State(); | |
//Reset the currentState | |
//place AI token randomly in center or one of the corners | |
var startIndex = Math.floor(Math.random() * 5); | |
var startPosition = firstMove[startIndex]; | |
currentState.board = [null, null, null, null, null, null, null, null, null]; | |
currentState.board[startPosition] = currentGame.AI; | |
currentState.result = "still running"; | |
//set currentState.result from the result to still running | |
currentState.turn = currentGame.human; | |
//after AI places token in centre or corner, it's the human player's turn. | |
currentGame.status = "beginning"; | |
//start game status with beginning again | |
window.setTimeout(function() { | |
//reset the board HTML view after 2 seconds. | |
$('.cell').html(""); | |
switch (startPosition) { | |
case 0: | |
$('#a1').html(currentGame.AI); | |
break; | |
case 1: | |
$('#a2').html(currentGame.AI); | |
break; | |
case 2: | |
$('#a3').html(currentGame.AI); | |
break; | |
case 3: | |
$('#b1').html(currentGame.AI); | |
break; | |
case 4: | |
$('#b2').html(currentGame.AI); | |
break; | |
case 5: | |
$('#b3').html(currentGame.AI); | |
break; | |
case 6: | |
$('#c1').html(currentGame.AI); | |
break; | |
case 7: | |
$('#c2').html(currentGame.AI); | |
break; | |
case 8: | |
$('#c3').html(currentGame.AI); | |
} | |
}, 2000); | |
currentGame.startGame(); | |
//reset the game by calling startGame() again | |
} else { | |
//not an end state | |
//if the turn is AI player, call AIplayer makeMove() with algorithm | |
if (currentGame.AI === currentState.turn) { | |
computer.makeMove(); | |
} | |
} | |
} //advance | |
this.startGame = function() { | |
//starts a Game | |
this.status = "running"; | |
//changes currentGame.status from beginning to running | |
this.advance(); | |
//calls advance for the first time. In the end checks who's turn it is and advances to the game | |
} // start game | |
} // game | |
var AImoves = function(position) { | |
//available moves for the AI player | |
//input: the new position on the board the AI player could possibly go to | |
//with transition function | |
//public: position on board to make move to | |
this.newPosition = position; | |
//public: minimax value of that new position | |
this.miniMax = 0; | |
this.transition = function(state) { | |
//public: transition function: take current state, make move, generate next state, change turn, returns next state | |
//this will be used to update the state or virtually update the state in miniMax function. In miniMax and AIplayer.makeMove() | |
//input is a state, returns a new state with the move/new position | |
var next = new State(state); | |
//local variable to hold the next state (virtually) based on the currentState as parameter | |
next.board[this.newPosition] = state.turn; | |
//turn in the state, letter X or O. Place letter X or O the new position for the new board | |
next.changeTurn(); | |
//change turn | |
return next; | |
//returns the next state | |
} | |
} //AImoves | |
var AIplayer = function() { | |
//class for AI player. Used with instance computer | |
//minimax function and makeMove function. makeMove calls minimax private function | |
function minimaxVal(state) { | |
//recursive function that returns a minimax score for a game state | |
//private and can only be called within this class | |
//input is a state and returns an miniMax score | |
//AIplayer tries to maximize and we use this function from his perspective. Human player tries to minimize | |
if (state.end()) { | |
//base case: when reach an end state, return the score as miniMax and stop if/else | |
//use game.score method | |
return currentGame.score(state); | |
} else { | |
//if not end state, start recursion to get the miniMax value | |
var stateScore; | |
//to store the miniMax value of the state | |
//this value is available through the whole recursion below | |
if (state.turn === currentGame.AI) { | |
//if the turn is AI initialize stateScore with a very high, if human player is in turn, initialize with a very low number | |
stateScore = -1000; | |
} else { | |
stateScore = 1000; | |
} | |
var availablePositions = state.empty(); | |
//save the available positions for next level state in an array using state.empty method | |
var availableStates = availablePositions.map(function(position) { | |
//loop through all the available positions(indexes of the board array) and virtually make that move and create a new state | |
var move = new AImoves(position); | |
//create an AImove object with the new position first | |
//move.newPosition = position | |
//move.miniMax = 0; | |
//move.transition: creates a virtual state in variable next, places the letter belonging to AI, changes the state turn, returns the state | |
var nextState = move.transition(state); | |
//for each position do an AI move virtually | |
//save it in nextState variable | |
return nextState; | |
//return nextState to save it in availableStates array | |
}); | |
//now we have an array availableStates with all the next possible states based on possible moves | |
availableStates.forEach(function(nextState) { | |
//loop through every available next state and call miniMax on them | |
var nextScore = minimaxVal(nextState); | |
//save the miniMax in nextScore (we havent done anything with stateScore yet) | |
//recursive call here because minimaxVal is here called within itself. | |
if (state.turn === currentGame.AI) { | |
//AI player will maximixe. Only update the stateScore if the score of the next State is larger. AIplayer will always choose the next possible state with the highest score and that will be the score for the current state. | |
if (nextScore > stateScore) | |
stateScore = nextScore; | |
} else { | |
// It's the human players turn, will minimize. Only update the stateScore if the score of the next state is smaller. Human will always choose the next possible state with the lowest score and that will be the score for the current state. | |
if (nextScore < stateScore) | |
stateScore = nextScore; | |
} | |
}); | |
return stateScore; | |
//return the stateScore after miniMax calculation finishes | |
} //if state.end(); | |
} //miniMax | |
this.makeMove = function() { | |
//select best move based on the minimax score and actually make that move | |
//a way to call the AIplayer to play. Called from game.advance(); | |
var availableMoves = currentState.empty(); | |
//generate an array of the empty cells for the given state = available moves, using currentState empty method, and save in array of availableMoves | |
//availablemoves is an array of all the indexes of empty cells on the board | |
var possibleStates = availableMoves.map(function(position) { | |
//for each of the empty cells place the AI token and generate the beloging State. | |
//save in new array possibleStates:array with possible next States including position (this.newPosition) and minimax (this.miniMax) | |
var action = new AImoves(position); | |
//create the object for possible moves to the next state using the position | |
var next = action.transition(currentState); | |
//use the AImoves transition method to virtually make a move to the empty cell and create a new state. | |
//Save that virtual new state as next | |
action.miniMax = minimaxVal(next); | |
//call miniMax with the next state and save it in action object | |
return action; | |
//now we have an array with miniMax values | |
}); | |
possibleStates.sort(function(first, second) { | |
//because AIplayer will maximize its own score. In order to select best move, we sort the possibleStates array Descending and we select the first state for the best move | |
if (first.miniMax > second.miniMax) { | |
return -1; | |
} else if (first.miniMax < second.miniMax) { | |
return 1; | |
} else { | |
return 0; | |
} | |
}); | |
//make the move by AI | |
var chosenMove = possibleStates[0]; | |
//Choose the first element in descending sorted array of state by minimax | |
var placeMove = chosenMove.newPosition; | |
//take the newPosition attribute and save that as a move to place | |
switch (placeMove) { | |
//update the html board of the state | |
case 0: | |
$('#a1').html(currentGame.AI); | |
break; | |
case 1: | |
$('#a2').html(currentGame.AI); | |
break; | |
case 2: | |
$('#a3').html(currentGame.AI); | |
break; | |
case 3: | |
$('#b1').html(currentGame.AI); | |
break; | |
case 4: | |
$('#b2').html(currentGame.AI); | |
break; | |
case 5: | |
$('#b3').html(currentGame.AI); | |
break; | |
case 6: | |
$('#c1').html(currentGame.AI); | |
break; | |
case 7: | |
$('#c2').html(currentGame.AI); | |
break; | |
case 8: | |
$('#c3').html(currentGame.AI); | |
} | |
currentState = chosenMove.transition(currentState); | |
//actually move the currentState to new state using AIplayer.transition method | |
currentGame.advance(); | |
} //this.move | |
} // AIPlayer | |
$('.cell').on("click", function() { | |
//humanPlayer here | |
//if AIplayer makes move, then inside that it changes the turn to humanplayer, making the board available for click event | |
//If humanPlayer makes a move, then it changes the turn back to AIplayer and calls currentGame.advance that calls AIplayer.makeMove() | |
//check if a cell is clickable, meaning there is no value there. | |
if (this.innerHTML == "" && currentGame.status == "running" && currentGame.human == currentState.turn) { | |
//if the html of the cell is empty && game is running && human players turn | |
switch (this.id) { | |
//update the html board of the state | |
case "a1": | |
currentState.board[0] = currentGame.human; | |
break; | |
case "a2": | |
currentState.board[1] = currentGame.human; | |
break; | |
case "a3": | |
currentState.board[2] = currentGame.human; | |
break; | |
case "b1": | |
currentState.board[3] = currentGame.human; | |
break; | |
case "b2": | |
currentState.board[4] = currentGame.human; | |
break; | |
case "b3": | |
currentState.board[5] = currentGame.human; | |
break; | |
case "c1": | |
currentState.board[6] = currentGame.human; | |
break; | |
case "c2": | |
currentState.board[7] = currentGame.human; | |
break; | |
case "c3": | |
currentState.board[08] = currentGame.human; | |
} | |
this.innerHTML = currentGame.human; | |
//show X or O, the token human plays with stored in currentGame | |
currentState.changeTurn(); | |
currentGame.advance(); | |
} else { | |
alert("Click an empty cell or it's not your turn!"); | |
} | |
}); | |
currentGame = new game(); | |
//start instance of Game class in global variable currentGame | |
currentState = new State(); | |
//start instance of State class in global variable currentState. Used throughout game to represent the actual state (not the virtual next ones in miniMax algorithm) | |
computer = new AIplayer(); | |
//start instance of AIplayer in global variable computer | |
var startIndex = Math.floor(Math.random() * 5); | |
//Random number between 0-4 to use as index for firstMove array of board positions | |
var startPosition = firstMove[startIndex]; | |
//Get start index from firstMove array | |
currentState.board = [null, null, null, null, null, null, null, null, null]; | |
//start the state with an empty board | |
$('#chooseX').on("click", function() { | |
//player chooses X, AI is O | |
currentState.turn = "X"; | |
//human player starts, board is initialized with a token for AI plyer in center or corner, so that's a real move | |
currentState.result = "still running"; | |
//will be set to "X wins", "O wins" or "draw" when the game reaches end state | |
currentGame.AI = "O"; | |
//AI player plays with O in global game | |
currentGame.human = "X"; | |
//human player plays with X in global game | |
currentState.board[startPosition] = currentGame.AI; | |
///place the token for AI player on randomy generated position center or corner | |
switch (startPosition) { | |
//update the board of the state with the first move for AI player | |
case 0: | |
$('#a1').html(currentGame.AI); | |
break; | |
case 1: | |
$('#a2').html(currentGame.AI); | |
break; | |
case 2: | |
$('#a3').html(currentGame.AI); | |
break; | |
case 3: | |
$('#b1').html(currentGame.AI); | |
break; | |
case 4: | |
$('#b2').html(currentGame.AI); | |
break; | |
case 5: | |
$('#b3').html(currentGame.AI); | |
break; | |
case 6: | |
$('#c1').html(currentGame.AI); | |
break; | |
case 7: | |
$('#c2').html(currentGame.AI); | |
break; | |
case 8: | |
$('#c3').html(currentGame.AI); | |
} | |
currentGame.startGame(); | |
//call start of the game in global variable global game | |
}); | |
$('#chooseO').on("click", function() { | |
//player chooses O, AI ix X | |
currentState.turn = "O"; | |
//human player starts, board is initialized with a token for AI player in center or corner, so that's a real move | |
currentState.result = "still running"; | |
//will be set to "X wins", "O wins" or "draw" when the game reaches end state | |
currentGame.AI = "X"; | |
//AI player plays with X in global game | |
currentGame.human = "O"; | |
//human player plays with "O" in global game state | |
currentState.board[startPosition] = currentGame.AI; | |
///place the token for AI player on randomy generated position center or corner | |
switch (startPosition) { | |
//update the board of the state with the first move for AI player | |
case 0: | |
$('#a1').html(currentGame.AI); | |
break; | |
case 1: | |
$('#a2').html(currentGame.AI); | |
break; | |
case 2: | |
$('#a3').html(currentGame.AI); | |
break; | |
case 3: | |
$('#b1').html(currentGame.AI); | |
break; | |
case 4: | |
$('#b2').html(currentGame.AI); | |
break; | |
case 5: | |
$('#b3').html(currentGame.AI); | |
break; | |
case 6: | |
$('#c1').html(currentGame.AI); | |
break; | |
case 7: | |
$('#c2').html(currentGame.AI); | |
break; | |
case 8: | |
$('#c3').html(currentGame.AI); | |
} | |
currentGame.startGame(); | |
//start the game with global game | |
}); | |
$('#myModal').modal('show'); | |
}); //document.ready |
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
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script> | |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script> |
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
body { | |
min-height: 100%; | |
background-repeat: no-repeat; | |
background-size: cover; | |
} | |
.wrapper { | |
height: 600px; | |
width: 600px; | |
background-color: #8e44ad; | |
oveflow-y: auto; | |
color: white | |
} | |
.wrapper div { | |
width: 200px; | |
height: 200px; | |
float: left; | |
} | |
#a1 { | |
border-bottom: 1px solid white; | |
border-right: 1px solid white; | |
} | |
#a3 { | |
border-left: 1px solid white; | |
border-bottom: 1px solid white; | |
} | |
#b2 { | |
border: 1px solid white; | |
} | |
#c1 { | |
border-top: 1px solid white; | |
border-right: 1px solid white; | |
} | |
#c3 { | |
border-top: 1px solid white; | |
border-left: 1px solid white; | |
} | |
.cell { | |
color: white; | |
text-align: center; | |
line-height: 200px; | |
font-size: 36px; | |
} | |
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
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment