harunpehlivan
3/6/2018 - 7:09 PM

Tic-Tac-Toe Game w/ Minimax Algorithm

Tic-Tac-Toe Game w/ Minimax Algorithm

Tic-Tac-Toe Game w/ Minimax Algorithm

Build a Tic Tac Toe Game - freecodecamp A Tic-Tac-Toe application with artificial intelligence based on a modified version of the minimax algorithm.

A Pen by HARUN PEHLİVAN on CodePen.

License.

<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://codepen.io/U-ways/pen/qNLZrg" rel="stylesheet" />
/* Green Lab template overrides/additions
==============================================================================*/
#header {
  height: 75px;
  margin: 0 0 0 0;
} #proj-subheading {
  height: 25px;
  font-size: 1em;
  font-style: italic;
  margin: 0 0 0 1vh;
} #details-body {
  margin-top: 15px;
} #proj-details summary {
  margin-top: 15px;
}

/* Color Scheme
================================================================================
Main Colors:
#c5dec2 - #95a893
#3e5050 - #3a4949
#263331 - #333
Matching R&G:
#549042 - #8e0000
*//* App
==============================================================================*/
#app {
  margin-top: 15px;
  justify-content: flex-start;
}

#blocks-con {
  margin: auto;
  flex-wrap: wrap;
  width: 430px;
  padding: 20px 15px;
  background-color: #739595;
  border-radius: 4%;
  border: 11px solid #c5dec2;
  box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.5)
} #blocks-con > div {
  height: 120px;
  width: 120px;
  padding: 5px;
  color: #3e5050;
  font-size: 100px;
  font-weight: normal;
  text-align: center;
  border: 2.5px solid #c5dec2;
  border-radius: 2%;
  cursor: pointer;
  user-select: none;
}

/*Uncomment the colors so you can understand what I did*/
/*(-n+3) Selects Only The First 3*/
#blocks-con > div:nth-child(-n+3) {
  border-top: none;
/*   color: red; */
} #blocks-con > div:nth-child(n+7) {
  border-bottom: none;
/*   color: yellow; */
} #blocks-con > div:nth-child(3n) {
  border-right: none;
/*   color: blue; */
} #blocks-con > div:nth-child(3n+1) {
  border-left: none;
/*   color: green; */
}

/* Symbol selection & Misc
============================================*/
#selection {
  width: inherit;
  margin-top: 0;
  margin-bottom: 3vh;
  padding: 0 0 2px 0;
  text-align: center;
  background-color: #c5dec2;
  color: #3b4c4c;
}

.xo {
  font-weight: 300;
  cursor: pointer;
  user-select: none;
  font-size: 2em;
  vertical-align: middle;
} .xo:hover {
  color: #739595;
  text-decoration: underline;
} .xo_active {
  color: #739595;
  text-decoration: underline;
  font-weight: 300;
}

#draw, #won, #lost {
  font-weight: 600;
}

.win {
  color: #c5dec2 !important;
  animation: 1.2s blinker .6s linear;
  animation-iteration-count: 2;
  transition: 1.2s;
} @keyframes blinker {
  50% { color: #374848;}
}

/* Reset button
============================================*/
#reset {
  width: 200px;
  margin: auto;
  margin-top: 10px;
  color: #3b4c4c;
  font-size: 1.3em;
  font-weight: 600;
  background-color: #c5dec2;
  border-radius: 4%;
  border: 11px solid #c5dec2;
  cursor: pointer;
} #reset:active {
  position: relative;
  top: 4px;
}
<script src="https://codepen.io/U-ways/pen/qNLZrg"></script>
/*jshint esversion: 6*/
/* Initial selector/variable assignments
==============================================================================*/
const blocks     = document.querySelectorAll('#blocks-con > div');
let board        = new Array(9).fill(0);
let computer     = [1,"O"]; // [Player id, Symbol]
let user         = [2,"X"];
let firstChoice  = true;

const winCombos = [
  [0,4,8], [2,4,6],           // Diagonal
  [0,1,2], [3,4,5], [6,7,8],  // Horizontal
  [0,3,6], [1,4,7], [2,5,8]   // Vertical
];

const x = document.querySelector("#x");
const o = document.querySelector("#o");
x.addEventListener("click", x_or_o);
o.addEventListener("click", x_or_o);

/* O & X Selection function
==============================================================================*/
function x_or_o() {
  if (this.id === "x") {
    user[1]     = "X";
    computer[1] = "O";
    o.classList.remove("xo_active");
    this.classList.add("xo_active");
    reset();
  } else {
    user[1]     = "O";
    computer[1] = "X";
    x.classList.remove("xo_active");
    this.classList.add("xo_active");
    reset();
    aiIntialChoice();
  }
}

/* Blocks listeners assignment function (Used for initial assignment & board reset)
==============================================================================*/
function addBlocksEventsHandler() {
  blocks.forEach(
    block => block.addEventListener("click", blockEvents)
  );
}

function blockEvents() {
  let blockNumber = this.getAttribute("data-blockNumber");
  update(this, blockNumber);
}

addBlocksEventsHandler();

/* Blocks listeners removal function (Used for board reset)
==============================================================================*/
function removeBlocksEventsHandler() {
  blocks.forEach(
    block => block.removeEventListener("click", blockEvents)
  );
}

/* Update board function
==============================================================================*/
function update(block, number) {
  block.removeEventListener("click", blockEvents);
  board[number]   = user[0];
  block.innerHTML = user[1];
  boardResult(user[0]);
  return (firstChoice) ? aiIntialChoice() : ai(board);
}

/* Board Result Checker
==============================================================================*/
/** @function boardResult(player)
 * Take a @param player as an argument and loop through @const winCombos
 * to determine if a winning combination occurred.
 *
 * If a @const winCombos element had a full house (counter === 3), remove blocks events
 * to prevent further gameplay and trigger the winning  'ceremony'.
 */
function boardResult(player){
  for (let i = 0; i < winCombos.length; i++) {
    let counter = 0;
    for (let j = 0; j < winCombos[i].length; j++) {
      if (board[winCombos[i][j]] !== player) {
        break; // Early break as a mismatch found
      } else {
        counter++;
      }
    }
    if (counter === 3) {
      removeBlocksEventsHandler();
      return ceremony(winCombos[i]);
    }
  }
}
const ceremony = combo => {
  combo.forEach((winningBlock) => blocks[winningBlock].classList.add("win"));
};

/* Board Reset (Returns everything to its original state)
==============================================================================*/
function reset() {
  firstChoice  = true;
  board        = new Array(9).fill(0);
  blocks.forEach(
    block => {
      block.classList.remove("win");
      block.innerHTML = "";
  });
  removeBlocksEventsHandler();
  addBlocksEventsHandler();
  if (computer[1] === "X") {aiIntialChoice();}
}
const resetBtn = document.querySelector("#reset");
resetBtn.addEventListener("click", reset);

/*  Initial AI choice function: (performance boost compared to using minimax first)
==============================================================================*/
function aiIntialChoice() {
  if (computer[1] === "O") {
    const choice             = (board[4] === 0) ? 4 : 2;
    board[choice]            = computer[0];
    blocks[choice].innerHTML = computer[1];
    blocks[choice].removeEventListener("click", blockEvents);
  } else {
    board[6]            = computer[0];
    blocks[6].innerHTML = computer[1];
    blocks[6].removeEventListener("click", blockEvents);
  }
  firstChoice = false;
}

/* Artificial Intelligence functionality
==============================================================================*/
function ai(currentBoard) {
  let choicesLeft = choiceFinder(board).length;
  if (choicesLeft === 0) {return false;}
  function choiceFinder(board) {
    let choicesLeft = [];
    board.forEach((block, index) => {if (block === 0){choicesLeft.push(index);}});
    return choicesLeft;
  }
  /** @function boardState(board)
   *  Returns an integer:
   *    +10 = user lost
   *    -10 = user won
   *      0 = tie or neutral board state
   */
  function boardState(board) {
    for (let i = 0; i < winCombos.length; i++) {
      let score = [0,0]; // [computer,user]
      for (let j = 0; j < winCombos[i].length; j++) {
        if (board[winCombos[i][j]] === 0) {
          break; // Out of loop as a mismatch found
        } else if (board[winCombos[i][j]] === computer[0]) {
          score[0]++;
        } else {
          score[1]++;
        }
      } // End of [j]
      if (score[0] === 3) {
        return 10;
      } else if (score[1] === 3) {
        return -10;
      }
    } // End of [i]
    return 0; // No winning combinations found.
  }

  /* U-minimax (see comment below) algorithm to find the best choice for computer
  ==============================================================================*/
  /** @function [u-]minimax(currentBoard,turn)
   *  My minimax explanation took about 60 lines (~100 columns each).
   *  Try reading this as a starter: https://en.wikipedia.org/wiki/Minimax 
   *
   *  NOTE:
   *   The minimax algorithm I implemented is not the same as the standard one. It is an algorithm
   *   I created to make a 'realistic' use of the stack. It climbs down from the stack like a
   *   "real" human  instead of creating external values to find the best move.
   */
  function minimax(currentBoard, turn) {
    let depth = choiceFinder(currentBoard).length; // Track stack depth
    function recursion(board, turn, choiceMade) {
      let choicesLeft = choiceFinder(board);
      let nextTurn    = (turn === computer[0]) ? user[0] : computer[0];
      let state       = boardState(board);
      if (choicesLeft.length === 0 || state !== 0) {
        return state;
      } else {
        let childValues = choicesLeft.map(
          (choice) => {
            let nextBoard      = [...board];
            nextBoard[choice]  = turn;
            let nextBoardState = recursion(nextBoard, nextTurn);
            return [choice, nextBoardState];
          }
        ).sort((a,b) => b[1] - a[1]);
        if (depth !== choicesLeft.length) {
          return (turn === computer[0]) ? childValues[0][1] : childValues[childValues.length-1][1];
        } else {
          return childValues[0][0];
        }
      } // else
    } // recursion
    return recursion(currentBoard, turn);
  } // minimax
  let aiChoice               = minimax(currentBoard, computer[0]);
  board[aiChoice]            = computer[0];
  blocks[aiChoice].innerHTML = computer[1];
  blocks[aiChoice].removeEventListener("click", blockEvents);
  return boardResult(computer[0]);
}

/* Adding project Details
==============================================================================*/
addProjDetails(0, 'Project Overview', 'This is a Tic-Tac-Toe application with an integrated artificial intelligence based on a modified version of the popular game theory algorithm for decision making; minimax.');
addProjDetails(0, 'Techniques/Technologies', 'Nothing special, vanilla HTML, CSS and JavaScript. Flexbox layout module and a bunch of event listeners to track user progress and interactivity. <br><br>I implemented a customized version of the minimax algorithm. My objective was to make a \'realistic\' use of the stack. It climbs down from the stack like a \'real\' human, instead of creating external values to find the best move. I will not go into detail about that as this section was not made for documentation purposes.');
addProjDetails(0, 'Future implementations', 'I am planning to Implement the Alpha-Beta helping search algorithm, add extra rows and columns to the application, improve the board result algothrim, and then build on top of the AI to create a chess engine. A long-term vision, but will get there eventually.');

addProjDetails(1, 'Hurdles encountered', 'The minimax algothrim was definitely a big challenge as I did not have any prerequisite knowledge about game theory, how the stack works in JavaScript, recursion, and the like...');
addProjDetails(1, 'Reflection', 'The Tic-Tac-Toe application took me less than 10 hours to build. However, It took me about 80-120 working hours in a span of 2 months to obtain the prerequisite knowledge and understand the minimax algothrim to get the AI to work. On a positive note, lots of stuff started to make sense as I filled lots of gaps within my understanding. <br><br>If I decided to give up at any time but the 120th hour, I would\'ve <b>never</b> made this application to work as expected. Just a small reminder for everyone out there.');
#container
  header#header.grid.cols-6
    section#proj-heading
      h1 Tic-Tac-Toe Application
      h3#proj-subheading w/ Minimax Algorithm
    details#proj-details
      summary Project Info
      section#details-body.grid.cols-2.rows-2
        .details-column
        .details-column
  main#app.flex-c
    #app-con.flex-c
      section#selection
        h2
          | - Choose 
          span#x.xo.xo_active X
          | or 
          span#o.xo O
          span#draw  Win
          | , 
          span#won Draw 
          | or 
          span#lost Lose
          | . -
      section#blocks-con.flex-r
        .block data-blocknumber="0" 
        .block data-blocknumber="1" 
        .block data-blocknumber="2" 
        .block data-blocknumber="3" 
        .block data-blocknumber="4" 
        .block data-blocknumber="5" 
        .block data-blocknumber="6" 
        .block data-blocknumber="7" 
        .block data-blocknumber="8" 
      button#reset.flex-c Reset
  footer#footer
    p
      | Last Updated:
      time datetime="2017-05-03"  03/05/2017
      | &nbsp | &nbsp
      a href="https://codepen.io/harunpehlivan" target="_blank" 
        i.fa.fa-codepen aria-hidden="true" 
        | HARUN PEHLİVAN