5
\$\begingroup\$

JavaScript beginner here! I have written a Tic Tac Toe game is JS that seems to be unbeatable (Title says 'almost' because I'm not sure if it's really unbeatable or just I can't beat it :) ).
I really appreciate any help to improve my code and learn from my mistakes.
Project's CodePen = https://codepen.io/MiladM1715/full/ZEQOLmZ

    // Tic Tac Toe Win figures
var gameCtrl = (function () {
  var winningConditions, corners, randomCorner;
  winningConditions = [
    [3, 4, 5],
    [2, 4, 6],
    [0, 4, 8],
    [0, 1, 2],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
  ];

  corners = [0, 2, 6, 8];
  randomCorner = corners[Math.floor(Math.random() * 4)];

  //   If win possible? Win! if not? Block
  function winOrBlock(arr, marker, winCondition) {
    var status;
    // Count number of founded markers (first user & then opponent) if more the two, win or block
    var count = 0;
    if (arr[0] === marker) count++;
    if (arr[1] === marker) count++;
    if (arr[2] === marker) count++;
    if (count >= 2 && arr.includes("")) {
      // Return empty marker to use
      if (arr[0] === "") status = winCondition[0];
      if (arr[1] === "") status = winCondition[1];
      if (arr[2] === "") status = winCondition[2];
      return status;
    }
  }
  // Don't put marker somewhere that there's no chance to win
  function noStupidMove(arr, marker, winCondition) {
    var checkCorners;
    var count = 0;
    if (arr[0] === '') count++;
    if (arr[1] === '') count++;
    if (arr[2] === '') count++;
    if (arr.includes(marker) && count > 1) {
      return winCondition[arr.indexOf("")];
    }
  }
  // If none of others work
  function neturalMove(arr, marker, winCondition) {
    //   If win figures include marker, and there
    if(arr.includes(marker) && arr.includes('')) {
      return winCondition[arr.indexOf("")];
    }
  }

  //Function to add moves id to game board structure
  return {
    addToBoard: function (id, marker, board) {
      board[id] = marker;
    },

    // Works for first and 2nd move
    firstMoves: function (board, counter, moves) {
      var result;
      //   after opponent's first move, if Center is empty, place it in center, If not? random corner
      if (counter === 1) {
        return board[4] === "" ? 4 : randomCorner;
      } else {
        //   If it's opponent's second move, check moves array and decide. If none of conditions met, then return false and let winOrBlock or neturalMove do it's job
        if (moves[0] === 0 && moves[1] === 7) result = 6;
        if (moves[0] === 6 && moves[1] === 5) result = 1;
        if (moves[0] === 4 && moves[1] === 8) result = 2;
        if (moves[0] === 4 && moves[1] === 2) result = 8;
        return board[result] === "" ? result : false;
      }
    },

    // Check if there is a chance for win, block or netural move || check win too
    checkStatus: function (board, type, marker, counter) {
      var a, b, c, winCondition, callback, check, opMarker;

      // Set oponet marker based on currnt marker
      marker === "O" ? (opMarker = "X") : (opMarker = "O");

      if (type === "check" && counter !== 0) {
        // Call functions based on stategy 1.win 2.block 3.netural
        callback = [
          [winOrBlock, marker],
          [winOrBlock, opMarker],
          [noStupidMove, marker],
          [neturalMove, marker],
        ];
      } else if (type === "check" && counter === 0) {
        return randomCorner;
      } else if (type === "win") {
        callback = "1";
      }

      for (var x = 0; x < callback.length; x++) {
        for (var i = 0; i < winningConditions.length; i++) {
          winCondition = winningConditions[i];
          a = board[winCondition[0]];
          b = board[winCondition[1]];
          c = board[winCondition[2]];

          //   Check win or place number?
          if (type === "check") {
            check = callback[x][0]([a, b, c], callback[x][1], winCondition);
            if (check || check === 0) {
              return check;
            }
            // if check 'type' is "win" only check for win
          } else if (type === "win") {
            //   If a,b,c are same and not empty then it's a win
            if (a === b && b === c && c !== "") {
              return true;
            }
          }
        }
      }
    },
    // If there is no empty cell, it's a draw (called after win check)
    isDraw: function (board) {
      return !board.includes("");
    },
  };
})();

// Takes care of UI
var UICtrl = (function () {

  return {

    DOMstrings: {
      startBtn: '.start-btn',
      userScore: '.sc-',
      gameResult: '.result',
      finalMsg: '.msg',
      gameCells: '.cells',
      gameCell: '.cell',
    },

    clearUI: function () {
      var cells, cellArr;
      cells = document.querySelectorAll(this.DOMstrings.gameCell);
      cellArr = Array.prototype.slice.call(cells);
      cellArr.forEach(function (cur) {
        cur.textContent = "";
      });
    },
    // Add marker to UI
    addMarkerUI: function (id, marker) {
      var color;
      marker === "X" ? (color = "black") : (color = "white");
      document.getElementById(
        id
      ).innerHTML = `<span style="color: ${color}">${marker}</span>`;
    },

    // disable start btn afte start and Enable it after draw or win
    disableStartBtn: function (state) {
      document.querySelector(this.DOMstrings.startBtn).disabled = state;
    },

    // Display score on UI
    displayScore: function (player, score) {
      document.querySelector(this.DOMstrings.userScore + player).textContent = score[player];
    },

    // display Win or Draw result
    displayResult: function (win, draw, player) {
      var msg, resultDiv;
      player === 0 ? (player = "YOU WIN!") : (player = "YOU LOSE!");
      if (win) msg = player;
      if (draw) msg = "DRAW";
      resultDiv = document.querySelector(this.DOMstrings.gameResult);
      resultDiv.style.display = "flex";
      document.querySelector(this.DOMstrings.finalMsg).textContent = msg;
      setTimeout(function () {
        resultDiv.style.display = "none";
      }, 2000);
    },
  };
})();

// Control game behavior
var controll = (function () {
  var gameBoard,
    isActive,
    playerMarker,
    currentPlayer,
    score,
    counter,
    twoMoveArr,
    DOM;
  gameBoard = ["", "", "", "", "", "", "", "", ""];
  isActive = true;
  playerMarker = ["X", "O"];
  currentPlayer = 0;
  score = [0, 0];
  twoMoveArr = [];
  counter = 0;
  whoIsPlayingFirst = 0;
  DOM = UICtrl.DOMstrings;

  // Game Start
  document.querySelector(DOM.startBtn).addEventListener("click", function () {
    // 1.hide start btn
    UICtrl.disableStartBtn(true);

    // 2 Reset game UI
    UICtrl.clearUI();

    // 3 Active game
    isActive = true;

    // 4. Decide who's playing first
    changePlayer();
  });

  // changes player after hitting start btn and invokes functions
  function changePlayer() {
    whoIsPlayingFirst === 1 ? (whoIsPlayingFirst = 0) : (whoIsPlayingFirst = 1);
    whoIsPlayingFirst === 1 ? userPlay() : AIplay();
  }

  function userPlay() {
    document.querySelector(DOM.gameCells).addEventListener("click", function (e) {
      // Works only if clicked cell is empty and game is active
      if (isActive && e.target.textContent === "") {
        // 1. Get clicked cell and set marker
        var cellID = parseInt(e.target.id);
        var marker = playerMarker[0];

        //   Add Selected cell to board and UI
        handleDataUI(cellID, marker, gameBoard);

        // increase counter to findout play count
        counter++;
        // Push first two moves into an array to use it later fo blocking
        counter < 2 ? twoMoveArr.push(cellID) : (twoMoveArr = false);

        // Check for Win or Draw
        var win, draw;
        win = resultChecker(score, currentPlayer);
        draw = resultChecker(score, currentPlayer);
        if (!win && !draw) {
          AIplay();
        }
      }
    });
  }

  function AIplay() {
    //   Change player id to 1
    currentPlayer = 1;
    // Set Marker
    marker = playerMarker[1];
    // If User plays first
    if (whoIsPlayingFirst === 1) {
      // check for first and second moves
      firstTwo = gameCtrl.firstMoves(gameBoard, counter, twoMoveArr);
      if (counter < 3 && typeof firstTwo === "number") {
        cellID = firstTwo;
      } else {
        //   If itsn't two first moves or it returned False, Try to win, block or netural move
        cellID = gameCtrl.checkStatus(gameBoard, "check", marker, counter);
      }
      //   If AI plays first, if it's first move then, place marker on random corner. if not first move then try to win or block or do netural move
    } else {
      cellID = gameCtrl.checkStatus(gameBoard, "check", marker, counter);
    }
    // Add it to Data strucure and UI
    handleDataUI(cellID, marker, gameBoard);
    // Check result
    resultChecker(score, currentPlayer);
    currentPlayer = 0;
  }

//   adds moves to data and UI
  function handleDataUI(id, marker, board) {
    gameCtrl.addToBoard(id, marker, board);
    UICtrl.addMarkerUI(id, marker);
  }

//   Checks for win and draw
  function resultChecker(score, currentPlayer) {
    var win = gameCtrl.checkStatus(gameBoard, "win");
    var draw = gameCtrl.isDraw(gameBoard);
    if (win) {
      score[currentPlayer] += 1;
      UICtrl.displayScore(currentPlayer, score);
      UICtrl.displayResult(win, false, currentPlayer);
      resetGame();
      return true;
    }
    if (draw) {
      UICtrl.displayResult(false, draw, currentPlayer);
      resetGame();
      return true;
    }
    return false;
  }

//   Resets game after every game
  function resetGame() {
    document.querySelector(DOM.startBtn).textContent = "Play Again";
    gameBoard = ["", "", "", "", "", "", "", "", ""];
    currentPlayer = 0;
    isActive = false;
    counter = 0;
    twoMoveArr = [];
    // Enables start btn
    UICtrl.disableStartBtn(false);
  }

  return {
    init: function () {
      UICtrl.clearUI();
    },
  };
})(gameCtrl, UICtrl);

controll.init();
\$\endgroup\$
2
  • \$\begingroup\$ Eyup it is totally unbeatable I actually can't remember the name of the algorithm but what you did right there is a proper algorithm founded by someone I don't remember right now. \$\endgroup\$
    – XYBOX
    Commented Jun 13, 2020 at 21:43
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$
    – Mast
    Commented Jun 14, 2020 at 11:56

1 Answer 1

3
\$\begingroup\$

It's not unbeatable.

I beat it with the following series of moves (though I see you have some randomness so some tries may be required to replicate it)

I went first, then played middle bottom, ai responded middle middle, I played middle left, ai responded top right, I played bottom left, ai played bottom right, I played top left, and won.

This will inform my review of your code.

First of all I would say that if you suspect something like 'this is unbeatable', but you can't prove it. Then that's a brilliant impetus to rewrite your code so that it's easier to reason about and prove is unbeatable.

Some comments on your code, you shouldn't be using var in JavaScript. It has funny scoping, for example what do you believe the following example prints

function f() {
    for (var i = 0; i < 10; i++) {
        setTimeout(() => console.log(i));
    }
}

f();

You should prefer let and const.

In terms of structure, it would be good for you to separate the logic of playing the game, and the logic of representing the game. For example, the function for the AI deciding what it should play shouldn't concern itself with the representation of the board. Instead have a sensible structure which is an abstract representation of the board (say, an array of arrays), have a function which takes this representation and works out the next move, and another function which translates that move to the UI representation. This will make your code much easier to reason about.

\$\endgroup\$
1
  • \$\begingroup\$ I've blocked every possible way that i know, that's why i call it "Almost Unbeatable" because maybe there's some strategies that i don't know about. About using let : I'm an a beginner. So i started by learning ES5 first and as i know const and let interduced in ES6. That's why I didn't used them. \$\endgroup\$ Commented Jun 14, 2020 at 3:10

Not the answer you're looking for? Browse other questions tagged or ask your own question.