6
\$\begingroup\$

To challenge myself, I set out to create a tic-tac-toe game, complete with a Minimax-driven AI. I had already attempted this challenge once when I was following the FreeCodeCamp curriculum.

For this review I would kindly ask everyone who is nice enough to give it a moment of their time to please advise me if my approach is the best way of interacting with the DOM? I am currently using objects with DOM elements as properties. This avoids having to use IDs, classes or data attributes. I am still very much a beginner so I am trying to use Vanilla JS as much as possible, figuring it would give me a good basis for any frameworks I might decide to learn.

I also have a question about performance. I know it is not the best, but my understanding of JS/CS in general is not deep enough to know how to better it. As it stands, the initial call to Minimax() takes a whopping 1080 ms on average.

For a working example, see this code Pen.

Here is the JS for easy reference:

const BOARD = document.querySelector('.board');
const BUTTONS = document.querySelector('.buttons')
let GRID = [];
function Player () {

  this.state = 0;
  this.symbol;
  this.moveCount = 0;
}
function PlayerAI () {
  this.state = 1;
  this.symbol;
  this.moveCount;
  this.moveCountClone;
}

function Game (state) {
  this.running = state;
}
let game = new Game(true);
let player = new Player;
let playerAI = new PlayerAI;

function init(){
  game.running = true;
  BUTTONS.addEventListener('click', function(e){

    player.symbol = e.target.textContent;

    player.symbol == 'O' ? playerAI.symbol = 'X' : playerAI.symbol = 'O';
    BUTTONS.classList.add('hidden');
    GRID.forEach(function(el){
      el.activate()
    })
    startGame(player, playerAI);

  })

}

// CELL FILE
let count = 1;
let fakeBoard = [2,2,2,0,0,0,2,2,2];

function Cell (index) {
  this.index = index;
  this.state = 2;// default state;
  this.parent = BOARD;
  this.DOMElement = document.createElement("div");
}

Cell.prototype.render = function () {

  this.parent.appendChild(this.DOMElement);
  this.DOMElement.classList.add("cell");
}

Cell.prototype.activate = function() {
  this.DOMElement.classList.add("active");
}

// draw grid
for(var i = 0; i < 9; i++){
  GRID.push(new Cell(i));
  GRID[i].render();
}

// END CELL FILE


function startGame (){

    GRID.forEach(function(cell){
    cell.DOMElement.addEventListener('click', function(){
      if(cell.DOMElement.textContent == ''){
        cell.DOMElement.classList.remove('active');
        cell.DOMElement.textContent = player.symbol;
        cell.state = player.state;
        handleWin(checkForTheWin(GRID, winStateMatrix, player))
        if(game.running === true){
            AIMove();
        }
      }
    });
  })
}


function AIMove () {
    let myMoves = possibleMoves(GRID);

    if(myMoves.length > 0){
    let myMove = myMoves[0].index;


    let next = miniMax(GRID, 0);

    makeMove(GRID, next.index, playerAI)
    displayMoveOnBoard(next.index, playerAI)
    handleWin(checkForTheWin(GRID, winStateMatrix, playerAI));

  }
}
// ai 1, human 0


function score(int, depth){
  if(int  == 1){
    return 100 - depth;
  }
  if(int == 0){
    return -100 + depth;
  }
  if(int == 2){
    return 0; // prefer draw, should be 0;
  }
}


function miniMax(gameState, depth){

    let moves = possibleMoves(gameState);
    bestMove = moves[0];
    bestScore = -1000;
    moves.forEach(function(move){
      let clone = JSON.parse(JSON.stringify(gameState));
      makeMove(clone, move.index, playerAI);
      let score = min(clone, depth+1);
      if(score > bestScore){
        bestMove = move;
        bestScore = score;
      }
    })
    return bestMove;
}
function max(gameState, depth){
  let check = checkForTheWin(gameState, winStateMatrix, player);
  if(check >= 0){
    return score(check, depth);
  }
  let moves = possibleMoves(gameState);
  let bestScore = -1000;
  let bestMove;
  moves.forEach(function(move){
      let clone = JSON.parse(JSON.stringify(gameState));
      makeMove(clone, move.index, playerAI);
      let score_value = min(clone, depth+1);
      if(score_value > bestScore){
        bestScore = score_value;
        bestMove = move;
      }
  })
  return bestScore;
}


function min(gameState, depth){
  let check = checkForTheWin(gameState, winStateMatrix, playerAI);
  if(check){
    return score(check, depth);
  }
  let moves = possibleMoves(gameState);
  let bestScore = 1000;
  let bestMove;
  moves.forEach(function(move){
      let clone = JSON.parse(JSON.stringify(gameState));
      makeMove(clone, move.index, player);
      let score_value = max(clone, depth+1);
      if(score_value < bestScore){
        bestScore = score_value;
        bestMove = move;
      }
  })
  return bestScore;
}

let winStateMatrix = [[0,1,2],
                      [3,4,5],
                      [6,7,8],
                      [0,3,6],
                      [1,4,7],
                      [2,5,8],
                      [0,4,8],
                      [2,4,6]]


function checkForTheWin(gamestate, winMatrix, whichPlayer){

  let stateOne = 1;
  let stateZero = 0;
  for(let i = 0; i < winMatrix.length; i++){

   if(gamestate[winMatrix[i][0]].state == whichPlayer.state && gamestate[winMatrix[i][1]].state == whichPlayer.state && gamestate[winMatrix[i][2]].state == whichPlayer.state){
     if(whichPlayer.state == stateOne){

       return 1;
     }
     if(whichPlayer.state == stateZero){
       //
       return 0;
     }
   }
  }

  let moves = possibleMoves(gamestate);

  if(!moves){
    return 2;
  }
}

function handleWin(potentialWin){
  //takes in checkforthewin function
  if(potentialWin == 2){
    game.running = false;
    alert('DRAW')
    window.location.reload(true);
  }
  if(potentialWin == 1){
    game.running = false;
    alert('AI WIN, RELOAD THE PAGE TO PLAY AGAIN')
    window.location.reload(true);
  }
  if(potentialWin == 0){
    game.running = false;
    alert('PLAYER WIN, RELOAD THE PAGE TO PLAY AGAIN')
    window.location.reload(true);
  }

}

function possibleMoves (gamestate){

    let arr = gamestate.filter(function(el){
      return el.state == 2;
    })
    if(arr.length < 1){
      return false;
    } else {
      return arr;
    }
}

function displayMoveOnBoard(moveAsIndex, whichPlayer){
  GRID[moveAsIndex].DOMElement.classList.remove('active');
  GRID[moveAsIndex].DOMElement.textContent = whichPlayer.symbol;
}

function makeMove(board, moveAsIndex, whichPlayer){
  board[moveAsIndex].state = whichPlayer.state;
}

init();
\$\endgroup\$
2
\$\begingroup\$

Feedback

checkmark The code makes good use of prototypes to assign the functions.

checkmark The code uses class names to manipulate the style of elements instead of directly altering the style attribute.

One reason Minimax() takes so long might be that the moves get cloned, and those contain references to the DOM elements. You could move those DOM element references out to an array and store an index from that array. I will try to see if that helps when I get a chance.

Suggestions:

Assignment within Ternary operator

player.symbol == 'O' ? playerAI.symbol = 'X' : playerAI.symbol = 'O';

This could be simplified by pulling the assignment out of the ternary operator:

playerAI.symbol = player.symbol == 'O' ? 'X' : 'O';

let vs const

In function checkForTheWin(), there are two variables stateOne and stateOne declared with the keyword let. Those variables don't appear to get re-assigned. If the value never changes, why not use const?

Actually, there is this block when whichPlayer.state is equal to the three gamestate values:

if(whichPlayer.state == stateOne){
   return 1;
 }
 if(whichPlayer.state == stateZero){
   //
   return 0;
 }

Why not just return stateOne or stateZero, or better yet, just return whichPlayer.state and get rid of those state values?

Duplicated code

The code contained in the miniMax(), max() and min() callback functions to moves.forEach() looks very identical (except for the comparison operators). It would be wise to abstract out common code into function(s) where possible.

Unused variables?

These variable don't appear to be used after being assigned. Am I missing something? Perhaps those were for testing...

// CELL FILE
let count = 1;
let fakeBoard = [2,2,2,0,0,0,2,2,2];

Adding elements to the DOM

In this article, there is a section Use a DOM DocumentFragment, which suggests calling document.createDocumentFragment() and adding elements to that, then after all elements are added to the fragment, append that to the DOM. Your code would need a little re-working as far as the cells go but shouldn't be a major job...

Possible use of Event Delegate

If there were a lot more elements on the page that needed to have click events handled, I would suggest using an event delegate instead of adding an event listener to each one (e.g. the cells) - especially if the elements would happen to be removed from the DOM in some cases.

Actually, the codepen example appears to already have an onclick function defined - that could be turned into an event delegate to handle all click events.

Update

I tried removing the DOM references from each cell and replacing it with an index into an array of the elements. That didn't appear to help speed up the miniMax() function much at all. It appears to stem from the fact that minMax() calls min(), which calls max(), which might also call min() depending on the depth... I would seek a simpler approach to finding the optimal place for the AI player to make a move.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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