Tic Tac Toe AI Feedback and Suggestions?

Tic Tac Toe AI Feedback and Suggestions?
0

#1

I’ve managed to create a tic tac toe game, but I’m not confident in my AI since it’s quite possible to beat the computer (who only plays defensively at this time). My thought process thus far is as follows: decide who goes first with a coin flip, when it’s computer’s turn computer maps/filters over the grids and figures out both which grids are currently empty and where on the grid player one is located. It then runs through a switch statement to either block player one from winning (quite lengthy with 24 possible combinations since there are only 8 possible win states with 3 permutations for each) or computer will default to choosing a random space on the grid if the computer is not in danger of losing. The result is that the computer plays defensively but still can beat since it only reacts defensively and won’t set itself up to win only block at the moment.

Am I on the right path here? I feel like the switch statement is excessively long (24 cases) and there must be a simpler way to go about this. And I’m aware that the initial ‘random’ grids the computer chooses when not in danger of losing leave room for the human player to strategize against it. I could add a second switch statement for the computer’s ‘free turns’ to check for an available path to victory, but the one I have now already seems like overkill. Thoughts?

Here’s an example of the logic I used (only including the first switch case for brevity)…

function computerResponse(){
  let availGrids = grids.filter(grid=>{
    if(!grid.classList.contains('p1')){
      return grid;
    }
  });
  availGrids = availGrids.filter(grid=>{
    if(!grid.classList.contains('p2')){
      return grid;
    }
  })
  const randomIndex = Math.floor(getRandom(0,availGrids.length));
  console.table(availGrids);
  // get the opponents positions
    const opponentGrids = grids.filter(grid=>{
      if (grid.classList.contains('p1')){
        return grid;
      }
    })

    switch(true){
      case (opponentGrids.includes(document.getElementById('seven')))
        && (opponentGrids.includes(document.getElementById('four'))) :
        if (availGrids.includes(document.getElementById('one'))){
          document.getElementById('one').textContent=player2;
          document.getElementById('one').classList.add('p2');
          break;
        }

tic tac toe project on codepen


#2

First off, you could simply the first part of your computerResponse function by replacing:

let availGrids = grids.filter(grid=>{
  if(!grid.classList.contains('p1')){
    return grid;
  }
});
availGrids = availGrids.filter(grid=>{
  if(!grid.classList.contains('p2')){
    return grid;
  }
});

with just one filter:

let availGrids = grids.filter(grid => !grid.classList.contains('p1') && !grid.classList.contains('p2'));

The same logic could be applied to creating the opponentGrids array by replacing:

const opponentGrids = grids.filter(grid => {
  if (grid.classList.contains("p1")) {
    return grid;
  }
});

with the following:

const opponentGrids = grids.filter(grid => grid.classList.contains("p1"));

To simply that large switch statement, you could create an array which would contain 24 sub arrays with the following element structure:

  • The first and second element of the oppenentGrids.include checks
  • The third element would be for the availGrids.includes check

Below is a sample of the first 3 sub arrays. I will let you create the other 21 sub arrays.

const computerLogic = [
  ["seven","four","one"],["seven","five","three"], ["seven","nine","eight"]
];

Also, to deal with the case default select statement case of using a random grid, we can use a variable called useRandom which is initialized to true and then using if one of the scenarios is found in computerLogic, then we use the third element of the applicable subarray and set useRandom to false. Lastly, you do a check to see if useRandom is true. If it is, then you use a random grid for player 2.

Putting all of this together, your computerResponse could go from 205 lines of code to about 40 lines of code (see below) :

function computerResponse() {
  let availGrids = grids.filter(
    grid => !grid.classList.contains("p1") && !grid.classList.contains("p2")
  );

  const randomIndex = Math.floor(getRandom(0, availGrids.length));
  // if there are no grids then the game MUST be over
  // do stuff here
  // get the opponents positions 
  const opponentGrids = grids.filter(grid => grid.classList.contains("p1"));

  const computerLogic = [
    ["seven","four","one"],["seven","five","three"], ["seven","eight","nine"],
   // add the remaining scenarios here
  ];
  
  let useRandom = true; // assumes we will use a random grid for player2
  for (let decision of computerLogic) {
    if ( opponentGrids.includes(document.getElementById(decision[0])) &&
    opponentGrids.includes(document.getElementById(decision[1])) &&
    availGrids.includes(document.getElementById(decision[2])) ) {
      userRandom = false; // will not use a random grid for player 2
      document.getElementById(decision[2]).textContent = player2;
      document.getElementById(decision[2]).classList.add("p2");  
      break; // stop searching because the correct grid was found
    }    
  }
  if (useRandom) {
    availGrids[randomIndex].textContent = player2;
    availGrids[randomIndex].classList.add("p2");
  }
  playerTurn = 1;
}

#3

Sweet! Thanks for the refactoring tips! :slight_smile: I think with everything simplified now it should be easier to make a ‘smarter’ AI that plays more offensively by leaning on the same computerLogic array to make decisions during free turns where it doesn’t need to block the opponent. Great stuff!


#4

I still did not like the two sections of repeated code that involved the following:
.textContent = player2; and .classList.add("p2"); plus I wanted to continue your functional approach using higher order functions, so I used the find function to get rid of the for loop syntax.

function computerResponse() {
  let availGrids = grids.filter(
    grid => !grid.classList.contains("p1") && !grid.classList.contains("p2")
  );
  const randomIndex = Math.floor(getRandom(0, availGrids.length));
  // if there are no grids then the game MUST be over
  // do stuff here
  // get the opponents positions 
  const opponentGrids = grids.filter(grid => grid.classList.contains("p1"));
  const computerLogic = [
    ["seven","four","one"],["seven","five","three"], ["seven","eight","nine"],
    // add the remaining scenarios here
  ];
  let foundGrid = computerLogic.find(decision => {
    return opponentGrids.includes(document.getElementById(decision[0])) &&
     opponentGrids.includes(document.getElementById(decision[1])) &&
     availGrids.includes(document.getElementById(decision[2]));        
  });  
  let gridToMark = foundGrid ? document.getElementById(gridToMark[2]) : availGrids[randomIndex];
  gridToMark.textContent = player2;
  gridToMark.classList.add("p2");
  playerTurn = 1;
}