<div id="container">

  <div class="game-narrative" id="game-narrative-one">
    <p class="game-narrative-text">In a world where two forces battle for domination of a war-torn landscape, only one will draw the line and reign supreme.</p>
    <button class="game-btn" id="narrative-one-btn">...</button>
  </div>

  <div class="game-narrative" id="game-narrative-two">
    <p class="game-narrative-text">The year is 2048.</p>
    <p class="game-narrative-text">In a post apocalyptic galaxy run by giant corporations, you are a cybernetically enhanced space marine with no memory of their past.</p>
    <button class="game-btn" id="narrative-two-btn">...</button>
  </div>

  <div class="game-narrative" id="game-narrative-three">
    <p class="game-narrative-text">Are you the chosen one foretold by prophecy?</p>
    <p class="game-narrative-text">Do you have the strength to survive...</p>
    <div id="narrative-three-btns">
      <button class="game-btn" id="narrative-three-btn">YES</button>
      <div></div>
      <a class="game-btn" id="puppies-btn" href="https://au.pinterest.com/explore/puppy-pictures/" target="_blank">NO</a>
    </div>
  </div>

  <div id = "header">
    <p class="dramatic-text">
      <span id = "tic-text">Tic </span>
      <span id = "tac-text">Tac </span>
      <span id = "doom-text">DOOM</span>
    </p>
  </div>


  <div id="game-configuration"> 
    <h2 id = "identity-label">Choose your mark</h2>
    <div id="identity-selection" class="row">
      <div class="cell identity-cell" value="X">X</div>
      <div class="cell identity-cell" value="O">O</div>
    </div>
  </div>

  <div id="game-grid">
    <div class="row">
      <div class="cell game-cell" id="c00"></div>
      <div class="cell game-cell" id="c01"></div>
      <div class="cell game-cell" id="c02"></div>
    </div>
    <div class="row">
      <div class="cell game-cell" id="c10"></div>
      <div class="cell game-cell" id="c11"></div>
      <div class="cell game-cell" id="c12"></div>
    </div>
    <div class="row">
      <div class="cell game-cell" id="c20"></div>
      <div class="cell game-cell" id="c21"></div>
      <div class="cell game-cell" id="c22"></div>
    </div>
    <div class="computer-threat">
      <p><span id ="computer-threat-text"></span></p>
    </div>
  </div>

  <div id="game-over">
    <h2 id="game-end-heading"></h2>
    <h3 id="game-end-subheading"></h2>
    <button class="game-btn" id="game-reset-btn">&#8634; Play again</button>
  </div>
  
</div>
* {
  margin: 0;
  padding: 0;
}

body {
  background-color: #FDE3A7;
  -webkit-transition-duration: 0.5s; /* Safari */
  transition-duration: 0.5s;
}

#container {

}

.game-narrative {
  margin: 0 auto; /* Center the item vertically & horizontally */
  position: fixed; /* Break it out of the regular flow */
  top: 0; left: 0; bottom: 0; right: 0; /* Set the bounds in which to center it, relative to its parent/container */
  
  padding: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
  resize: both;
  /* overflow: auto; */
  
  max-width: 700px;
  flex-direction: column;
  text-align: center;
}

.game-narrative-text {
  font-family: 'Share Tech', sans-serif;
  font-size: 2em;
  margin: 10px;
}

.game-btn {
  font-family: 'Share Tech', sans-serif;
  font-size: 2em;
  margin: 20px;
  
  -webkit-border-radius: 8;
  -moz-border-radius: 8;
  border-radius: 8px;
  color: #ffffff;
  font-size: 20px;
  background-color: #d35400;
  padding: 10px 20px 10px 20px;
  border: solid #F89406 2px;
  text-decoration: none;
  
}

.game-btn:hover {
  background-color: #e67e22;
  text-decoration: none;
}

.game-btn:focus {
  outline:0;
}

#narrative-three-btns {
  display: flex;
  flex-flow: row;
}

.dramatic-text {
  font-family: 'Trade Winds', cursive;
  font-size: 3em;
}

.computer-threat {
  font-family: 'Trade Winds', cursive;
  font-size: 1.5em;
  margin-top: 20px;
  color: #c0392b;
}

#header {
  text-align: center;
  margin: 10px;
  margin-top: 30px;
  color: #c0392b;
}

#game-configuration {
  
  margin: 0 auto; /* Center the item vertically & horizontally */
 
  padding: 20px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  resize: both;
  
}

#identity-selection {
  display: flex;
  justify-content: center;
  flex-direction: row;
}

.identity {
  margin: 0px 20px;
}

#identity-label {
  color: #FFF;
  font-family: 'Share Tech', sans-serif;
  font-size: 2em;
  text-align: center;
  vertical-align: middle;
  margin: 20px;
}

#game-grid {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: center;
}

.row {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
}

.cell {
  font-family: 'Gloria Hallelujah', cursive;
  color: #FFFFFF;
  background-color: #121a21;
  text-align: center;
  width: 100px;
  height: 100px;
  font-size: 3em;
  margin: 5px;
  
  border-radius: 10px;
  /* "pop-out" effect */
  box-shadow: 6px 6px 0px 0px #090d10;
  -webkit-transition-duration: 0.5s; /* Safari */
  transition-duration: 0.5s;
}

.cell:hover {
  color: #e74c3c;
  background-color: #34495e;
}

.cell-selected {
  color: #c0392b;
  transform: translate(3px,3px);
  box-shadow: 3px 3px 0px 0px #000000;
  background-color: #121a21;
  
}

.cell-selected:hover {
  /* Disabling hover on already selected cells */
  color: #c0392b;
  background-color: #121a21;
}

.cell-win {
  color: #e74c3c;
  background-color: #34495e;
  transition: all 1s ease-in-out;
  transform: scale(1.05);
}

.cell-win:hover {
  /* Disabling hover on win animation cells */
  background-color: #34495e;
}

#game-over {
  color: #FFF;
  font-family: 'Share Tech', sans-serif;
  
  margin: 0 auto; /* Center the item vertically & horizontally */
  position: fixed; /* Break it out of the regular flow */
  top: 0; left: 0; bottom: 0; right: 0; /* Set the bounds in which to center it, relative to its parent/container */
  
  padding: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
  resize: both;
  /* overflow: auto; */
  
  max-width: 700px;
  flex-direction: column;
  text-align: center;
}

#game-end-heading {
  font-size: 2em;
  margin: 10px;
}

#game-end-subheading {
  font-size: 1.5em;
  margin: 10px;
}

#game-reset-btn {
  font-family: 'Share Tech', sans-serif;
  font-size: 2em;
  margin: 20px;
  
  -webkit-border-radius: 8;
  -moz-border-radius: 8;
  border-radius: 8px;
  color: #ffffff;
  font-size: 20px;
  background-color: #d35400;
  padding: 10px 20px 10px 20px;
  border: solid #F89406 2px;
  text-decoration: none;
  
}

#game-reset-btn: hover {
  background: #3cb0fd;
  text-decoration: none;
}


// The game object, used to store current state of the game.
var game = {
  board: [[null, null, null],
          [null, null, null],
          [null, null, null]], // Rows of the game grid in 2D array form.
  playerMark: "", // The mark 'X' or 'O' the player uses to select a cell.
  aiMark: "", // The mark the computer uses to select a cell.
  turnsPlayed: 0, // If reaches 9 without a win, its a draw.
  playerTurn: true, // Flag tracking who's turn it is.
  nextMove: [null, null], // Used to store move calculated by minimax.
  winner: "", // Stores winning mark.
  gameOver: false // Flag indicating whether the game has ended.
}

var darkColor = "#2c3e50";

var $narrativeOne = $("#game-narrative-one");
var $narrativeTwo = $("#game-narrative-two");
var $narrativeThree = $("#game-narrative-three");
var $narrativeFour = $("#game-narrative-four");

var computerThreats = ["Prepare to suffer extreme humiliation!",
                      "I will destroy you!",
                      "I am invincible!",
                      "You cannot defeat me!",
                      "You will be annihilated!",
                      "You will fail!",
                      "Fear me!",
                      "Vengeance is mine!",
                      "I hunger!"]

var $identityBtn = $(".identity-cell");
var $gameBtn = $(".game-cell");
var $gameResetBtn = $("#game-reset-btn");

$(document).ready(function() {
  $narrativeOne.hide();
  $narrativeTwo.hide();
  $narrativeThree.hide();
  $narrativeFour.hide();
  $("#header").hide();
  $("#game-configuration").hide();
  $("#game-grid").hide();
  $("#game-over").hide();
  
  $narrativeOne.fadeIn(500);
});

$("#narrative-one-btn").on('click', function() {
  var transitionPeriod = 500;
  $narrativeOne.fadeOut(transitionPeriod);
  setTimeout(function() {
    $narrativeTwo.fadeIn(transitionPeriod);
  }, transitionPeriod);
});

$("#narrative-two-btn").on('click', function() {
  var transitionPeriod = 500;
  $narrativeTwo.fadeOut(transitionPeriod);
  setTimeout(function() {
    $narrativeThree.fadeIn(transitionPeriod);
  }, transitionPeriod);
});

$("#narrative-three-btn").on('click', function() {
  var transitionPeriod = 500;
  $narrativeThree.fadeOut(transitionPeriod);
  setTimeout(function() {
    $("#tic-text").hide();
    $("#tac-text").hide();
    $("#doom-text").hide();
    $("#header").show();
    
    $("#tic-text").show();
    setTimeout(function(){
      $("#tac-text").show();
      setTimeout(function() {
        $("body").css("background-color", darkColor);
        $("#doom-text").show();
        
        setTimeout(function(){
          $("#game-configuration").fadeIn(transitionPeriod);
          
        }, transitionPeriod * 2);
      }, transitionPeriod * 2);
    }, transitionPeriod * 2);
  }, transitionPeriod * 2);
});



// When a player initially chooses their mark before playing.
$identityBtn.on('click', function() {

  // Grabbing value from the HTML element
  game.playerMark = $(this).attr("value");
  if (game.playerMark === "X"){
    game.aiMark = "O";
  } else {
    game.aiMark = "X";
  }
  
  startGame();
});

function startGame(){
  // Transitioning between config menu to game grid.
  $("#game-configuration").hide();
  $("#game-grid").fadeIn(500);
  
  if (!game.playerTurn)
    aiPlay();
}

$gameBtn.on('click', function() {
  if (game.playerTurn) {
    // Parsing player's move
    var cell = $(this).attr("id");
    var row = parseInt(cell[1]);
    var col = parseInt(cell[2]);

    if (spaceFree(game.board, row, col)) {
      makePlay(game.playerMark, row, col); // Commit move to the game board.
      checkPlay(game.playerMark); // Check if the move resulted in a win.
    } else {
      // Do nothing (space already taken)
    }
  } else {
    // Do nothing (not player's turn)
  }

});

function aiPlay() {
  var aiThinkingDelay = 1000;
  setTimeout(function() {
    
    minimax(game, 0); // Use minimax to calculate the next optimal move.
    makePlay(game.aiMark, game.nextMove[0], game.nextMove[1]); // Commit move to the game board.
    checkPlay(game.aiMark); // Check if the move resulted in a win.
    
    
    var randThreat = computerThreats[Math.floor(Math.random() * computerThreats.length)];
    $("#computer-threat-text").text(randThreat);
    $("#computer-threat-text").fadeIn(250);
    setTimeout(function() {
      $("#computer-threat-text").fadeOut(250);
    }, 2000);
    
  }, aiThinkingDelay);
  
  
}

function checkPlay(mark) {
  const gameOverDelay = 2000;
  if (hasWon()) {
    const gameOverDelay = 2000; // Wait two second to allow win animation to play out.
    // Turn has resulted in a valid win
    setTimeout(function() {
      gameOver(mark); // After delay, transition to game-over menu.   
    }, gameOverDelay);
    
  } else if (game.turnsPlayed >= 9) {
    // There are no more turns that can be made, it is a draw.
    // Draw animation?
    setTimeout(function() {
      gameOver("draw");
    }, gameOverDelay);
    
  } else {
    game.playerTurn = !game.playerTurn; // Toggle turn between pc and player.
    if (!game.playerTurn) {
      aiPlay(); // If it's not the players turn, initiate computer turn.
    }
  }
}

function spaceFree(board, row, col) {
  // Checks if a player can mark a selected cell.
  return (board[row][col] === null)
}

function makePlay(mark, row, col) {
  // Saving move to game    
  game.board[row][col] = mark;
  game.turnsPlayed++;
  
  var cellId = "#c" + row + "" + col;
  // Stylising game cell to reflect an ai move.
  $(cellId).text(mark);
  $(cellId).addClass("cell-selected");
  
}

function minimax(state, depth){
  // Inspired by http://neverstopbuilding.com/minimax
  
  // Creating a replicated object of the game state to avoid
  // editing the existing game state (it has been passed 'byRef')
  // See http://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript/5344074#5344074
  var gameState = JSON.parse(JSON.stringify(state));
  
  if (gameState.gameOver){
    // If game is in an end state (win, lose, draw) return corresponding score (10, -10, 0)
    return getScore(gameState, depth);
    
  } else {
    depth++; // Iterate depth as algorithm gets recursively deeper. Used to choose moves that prolong defeat, hasten victory.
    var moves = []; // Used to store all possible moves in this current game state.
    var scores = []; // Used to store the corresponding scores resulting from each of those moves.
    
    moves = generateAllAvailableMoves(gameState); // Generate an array of all available coordinates on the game board.
    
    for (var i = 0; i < moves.length; i++) {
      // For each possible move, create a simulation game state where the move has been played.
      var possibleGameState = generatePossibleGame(gameState, moves[i]);
      // Then store the resultant score, recursively calling the minimax algorithm.
      scores.push(minimax(possibleGameState, depth));
    }
    
    if (gameState.playerTurn) {
      // MAX
      var maxScoreIndex = findIndexOfMax(scores); // In the case of it being the protagonist's turn, find the highest equating score.
      game.nextMove = moves[maxScoreIndex]; // Store move to be executed.
      return scores[maxScoreIndex]; 
    } else {
      // MIN
      var minScoreIndex = findIndexOfMin(scores); // In the case of it being the opponent's turn, find the lowest equating score.
      game.nextMove = moves[minScoreIndex]; 
      return scores[minScoreIndex];
    }
  }
}

// Equates game states to scores
// Wins equating to 10, loses equating to -10, draws or continued gameplay equating to 0.
function getScore(gameState, depth) {
  if (gameState.gameOver && gameState.winner === gameState.playerMark) {
    return 10 - depth;
  } else if (gameState.gameOver && gameState.winner === gameState.aiMark) {
    return depth - 10;
  } else {
    return 0;
  }
}

// Returns an array of the coordinates (row, column) of all available cells in a particular game state.
function generateAllAvailableMoves(gameState){
  const rowLength = 3;
  const colLength = 3;
  var availableMoves = [];
  
  for (var row = 0; row < rowLength; row++){
    for (var col = 0; col < colLength; col++){
      if (spaceFree(gameState.board, row, col)){
        // Scanning the game board for free spaces
        availableMoves.push([row, col]);
      }
    }
  }
  return availableMoves;
}

// Creates a simulated game state when a specified move is executed.
function generatePossibleGame(state, move){
  var gameState = JSON.parse(JSON.stringify(state));
  
  // Execute the move
  if (gameState.playerTurn){
    gameState.board[move[0]][move[1]] = gameState.playerMark;
  } else {
    gameState.board[move[0]][move[1]] = gameState.aiMark;
  }
  gameState.turnsPlayed++;
  
  // Check if the move has resulted in an end game state.
  if (checkWin(gameState)) {
    gameState.gameOver = true;
    if (gameState.playerTurn){
      gameState.winner = gameState.playerMark;
    } else {
      gameState.winner = gameState.aiMark;
    }
  } else if (gameState.turnsPlayed >= 9) {
    gameState.gameOver = true;
    gameState.winner = "draw";
  } else {
    gameState.playerTurn = !gameState.playerTurn;
  }
  
  return gameState;
}

// Finds the index of the highest value in an array.
function findIndexOfMax(arr) {
  var maxIndex = 0;
  if (arr.length > 1) {
    for (var i = 1; i < arr.length; i++){
      if (arr[i] > arr[maxIndex]){
        maxIndex = i;
      }
    }
  }
  return maxIndex;
}

// Finds the index of the lowest value in an array.
function findIndexOfMin(arr) {
  var minIndex = 0;
  if (arr.length > 1) {
    for (var i = 1; i < arr.length; i++){
      if (arr[i] < arr[minIndex]){
        minIndex = i;
      }
    }
  }
  return minIndex;
}

// Used to check if the last played move has resulted in a win.
function checkWin(gameState) {
  const numRows = 3;
  const numCols = 3;

  // Check for diagonal win right to left
  if (gameState.board[0][0] === gameState.board[1][1] &&
    gameState.board[1][1] === gameState.board[2][2] &&
    gameState.board[0][0] !== null) {
    // Right to left, top to bottom diagonal win
    return true;
  }

  // Check for diagonal win left to right
  if (gameState.board[0][2] === gameState.board[1][1] &&
    gameState.board[1][1] === gameState.board[2][0] &&
    gameState.board[0][2] !== null) {
    // Left to right, top to bottom diagonal win
    return true;
  }

  // Checking each row for a horizontal win
  for (var row = 0; row < numRows; row++) {
    if (gameState.board[row][0] === gameState.board[row][1] &&
      gameState.board[row][1] === gameState.board[row][2] &&
      gameState.board[row][0] !== null) {
      // Horizontal win
      return true;
    }
  }

  // Checking each column for a vertical win
  for (var col = 0; col < numCols; col++) {
    if (gameState.board[0][col] === gameState.board[1][col] &&
      gameState.board[1][col] === gameState.board[2][col] &&
      gameState.board[0][col] !== null) {
      // Vertical win
      return true;
    }
  }
  return false;
}

/** 

// Pre-Minimax AI, randomly selecting a space on the board.

function aiGenerateRandomPlay() {
  var randRow = Math.floor(Math.random() * 3);
  var randCol = Math.floor(Math.random() * 3);
  var validMove = spaceFree(randRow, randCol);
  while (!validMove) {
    randRow = Math.floor(Math.random() * 3);
    randCol = Math.floor(Math.random() * 3);
    validMove = spaceFree(randRow, randCol);
  }
  return [randRow, randCol];
}

*/


// Checking whether the last move made has triggered a win, and if so triggers a win animation.
function hasWon() {
  const numRows = 3;
  const numCols = 3;

  // Check for diagonal win right to left
  if (game.board[0][0] === game.board[1][1] &&
    game.board[1][1] === game.board[2][2] &&
    game.board[0][0] !== null) {
    // Right to left, top to bottom diagonal win
    console.log("Left to right, top to bottom diagonal win");
    // Win animation
    $("#c00").addClass("cell-win");
    $("#c11").addClass("cell-win");
    $("#c22").addClass("cell-win");
    
    return true;
  }

  // Check for diagonal win left to right
  if (game.board[0][2] === game.board[1][1] &&
    game.board[1][1] === game.board[2][0] &&
    game.board[0][2] !== null) {
    // Left to right, top to bottom diagonal win
    console.log("Right to left, top to bottom diagonal win");
    // Win animation
    $("#c02").addClass("cell-win");
    $("#c11").addClass("cell-win");
    $("#c20").addClass("cell-win");
    
    return true;
  }

  // Checking each row for a horizontal win
  for (var row = 0; row < numRows; row++) {
    if (game.board[row][0] === game.board[row][1] &&
      game.board[row][1] === game.board[row][2] &&
      game.board[row][0] !== null) {
      // Horizontal win
      console.log("Horizontal win");
      // Win animation
      $("#c" + row + "0").addClass("cell-win");
      $("#c" + row + "1").addClass("cell-win");
      $("#c" + row + "2").addClass("cell-win");
      return true;
    }
  }

  // Checking each column for a vertical win
  for (var col = 0; col < numCols; col++) {
    if (game.board[0][col] === game.board[1][col] &&
      game.board[1][col] === game.board[2][col] &&
      game.board[0][col] !== null) {
      // Vertical win
      console.log("Vertical win");
      // Win animation
      $("#c0" + col).addClass("cell-win");
      $("#c1" + col).addClass("cell-win");
      $("#c2" + col).addClass("cell-win");
      return true;
    }
  }

  return false;
}

// Transitions the screen from the game grid to a game over menu.
function gameOver(winCase) {
  $("#game-grid").hide();
  $("#game-over").fadeIn(500);

  if (winCase === game.playerMark) {
    // Player wins
    $("#game-end-heading").text("You have claimed victory.");
    $("#game-end-subheading").text("May you bathe in tic-tac-toe glory.");
    
  } else if (winCase === game.aiMark) {
    // PC wins
    $("#game-end-heading").text("Alas, the computer has claimed victory!");
    $("#game-end-subheading").text("May they bathe their circuits in tic-tac-toe glory.");
  } else {
    // Draw
    $("#game-end-heading").text("X and O, ancient enemies, have concluded their bout in a draw.");
    $("#game-end-subheading").text("Perhaps their feud will be settled in another life, another dimension...");
  }
}

$gameResetBtn.on('click', resetGame);

function resetGame() {
  $("#game-over").hide();
  $("#game-grid").hide();
  $("#game-configuration").fadeIn(500);

  $(".game-cell").empty();
  $(".cell").removeClass("cell-selected");
  $(".cell").removeClass("cell-win");
  
  game.board = [
    [null, null, null],
    [null, null, null],
    [null, null, null]
  ];
  game.turnsPlayed = 0;
  game.playerTurn = true;
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js