Skip to content

Instantly share code, notes, and snippets.

@chemok78
Created July 22, 2016 06:23
Show Gist options
  • Save chemok78/edaa81d32b9288387983c24352ea88cc to your computer and use it in GitHub Desktop.
Save chemok78/edaa81d32b9288387983c24352ea88cc to your computer and use it in GitHub Desktop.
Unbeatable Tic Tac Toe
<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">&times;</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>
//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
<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>
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;
}
<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