Search code examples
javascripttic-tac-toe2d-games

Tic Tac Toe JS game


I don't know exactly what is wrong in cod, can you help me find it? What I know is that 'available' array is not correctly emptying and computer sometimes plays two moves in row.

link to code on github: https://github.com/skrrrt-and-boom/tic-tac-toe

link to website: https://skrrrt-and-boom.github.io/tic-tac-toe/

const board = document.querySelector('#board')
const p = document.querySelectorAll('p')
const results = document.querySelector('#results')
const square = document.querySelectorAll('.kafelek')

let content = [
  ['','',''],
  ['','',''],
  ['','',''],
];

let players = ['X', 'O'];
let computer = players[0];
let human = players[1];
let currentPlayer;
let available = [];

function random(available) {
  let x = Math.random() * 10;
  x = Math.floor(x)
  return (Math.floor(available / (x+1)))
}

function setup() {
  currentPlayer = 0;
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      available.push([i, j]);
    }
  }
}

setup()

function equals(a, b, c) {
  return (a===b && b===c && a !== '')
}

function checkWin() {
  let winner = null;

  //    horizontal
  for (let i = 0; i < 3; i++) {
    if (equals(content[i][0], content[i][1], content[i][2])) {
      winner = content[i][0];
      return (results.textContent = winner + ' Wins')
    }
  }

  //    vertical
  for (let j = 0; j < 3; j++) {
    if (equals(content[0][j], content[1][j], content[2][j])) {
      winner = content[0][j];
      return (results.textContent = winner + ' Wins')
    }
  }

  //    Diagonal
  if (equals(content[0][0], content[1][1], content[2][2])) {
    winner = content[0][0];
    return (results.textContent = winner + ' Wins')
  }

  if (equals(content[2][0], content[1][1], content[0][2])) {
    winner = content[2][0];
    return (results.textContent = winner + ' Wins')
  }

  if (winner === null && available.length === 0) {
    return (results.textContent = 'Tie')
  }
}

function nextTurn() {
  let index = Math.floor(random(available.length));
  console.log(index);
  let spot = available.splice(index, 1)[0];
  //if (spot === undefined) {
  //  spot = available.splice(0, 1)[0];
  //}
  content[spot[0]][spot[1]] = computer;
  draw();
  checkWin()
}

function draw() {
  let k = 0;
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      if (p[k].textContent === '') {
        square[k].addEventListener('click', () => game(i, j))
      }
      let spot = content[i][j];
      p[k].textContent = spot;
      k++;
    }
  }
}
function game(i, j) {
  content[i][j] = human;
  let sub = [i, j];
  let matchEvery = (available, ind, sub) => available[ind].every((el, i) => el == sub[i]);
  let searchForArray = (available = [], sub = []) => {
    let ind = -1;
    let {
      length: len } = available;
    while (len--) {
      if (available[len].length === sub.length && matchEvery(available, len, sub)){
        ind = len;
        break;
      };
    };
    return ind;
  };
  console.log(searchForArray(available, sub), available);
  available.splice(searchForArray(available, sub), 1);
  nextTurn();
}

nextTurn();

Solution

  • The computer plays 2 moves or more in a row because you attach event handlers to empty cells every time you call draw(). Event handlers for advancing the game should only be attached once during game setup, namely in the setup() method. Otherwise, if an empty cell has multiple "click' handlers, they will each be executed when the user clicks, and each gives the computer a chance to make a move.

    Another issue I found was in your random() function. I'm not entirely sure how you were thinking about it, but to return a random nonnegative integer up to some upper bound, just do Math.floor(Math.random() * upperBound).

    In regards to the available array, if I were you, I would reconsider if it's necessary in the first place. The available array can somewhat be thought of as the "inverse" to the board or content since content shows which moves have been made and available tracks which moves are left. However, consequently, you can determine what is left solely using the current state of content by extracting the indices of still empty cells. Otherwise, you effectively manage the same state in 2 different places, and that's difficult to ensure that they remain consistent with each other. So I suggest you remove it and rethink how you determine which move the computer makes.

    These changes will fix some of your issues, however there seem to be a few more before you'll have a completely working version. For example, I noticed that a tie can be declared prematurely, and that the user is able to entirely overwrite the computer's moves by clicking on a taken cell. I recommend following an online tutorial to make sure you are covering all the user interaction edge cases. The important thing is to not give up and to keep learning!

    Here is your code with the first 2 changes I suggested for reference.

    const board = document.querySelector('#board')
    const p = document.querySelectorAll('p')
    const results = document.querySelector('#results')
    const square = document.querySelectorAll('.kafelek')
    
    let content = [
      ['', '', ''],
      ['', '', ''],
      ['', '', ''],
    ];
    
    let players = ['X', 'O'];
    let computer = players[0];
    let human = players[1];
    let currentPlayer;
    let available = [];
    
    function random(upperBound) {
      return Math.floor(Math.random() * upperBound);
    }
    
    function setup() {
      currentPlayer = 0;
      for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
          available.push([i, j]);
          square[i * 3 + j].addEventListener('click', () => game(i, j))
        }
      }
    }
    
    setup()
    
    function equals(a, b, c) {
      return (a === b && b === c && a !== '')
    }
    
    function checkWin() {
      let winner = null;
    
      //    horizontal
      for (let i = 0; i < 3; i++) {
        if (equals(content[i][0], content[i][1], content[i][2])) {
          winner = content[i][0];
          return (results.textContent = winner + ' Wins')
        }
      }
    
      //    vertical
      for (let j = 0; j < 3; j++) {
        if (equals(content[0][j], content[1][j], content[2][j])) {
          winner = content[0][j];
          return (results.textContent = winner + ' Wins')
        }
      }
    
      //    Diagonal
      if (equals(content[0][0], content[1][1], content[2][2])) {
        winner = content[0][0];
        return (results.textContent = winner + ' Wins')
      }
    
      if (equals(content[2][0], content[1][1], content[0][2])) {
        winner = content[2][0];
        return (results.textContent = winner + ' Wins')
      }
    
      if (winner === null && available.length === 0) {
        return (results.textContent = 'Tie')
      }
    }
    
    function nextTurn() {
      let index = Math.floor(random(available.length));
      // console.log(index);
      let spot = available.splice(index, 1)[0];
      //if (spot === undefined) {
      //  spot = available.splice(0, 1)[0];
      //}
      content[spot[0]][spot[1]] = computer;
      draw();
      checkWin()
    }
    
    function draw() {
      for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
          let spot = content[i][j];
          p[i * 3 + j].textContent = spot;
        }
      }
    }
    
    function game(i, j) {
      content[i][j] = human;
      let sub = [i, j];
      let matchEvery = (available, ind, sub) => available[ind].every((el, i) => el == sub[i]);
      let searchForArray = (available = [], sub = []) => {
        let ind = -1;
        let {
          length: len
        } = available;
        while (len--) {
          if (available[len].length === sub.length && matchEvery(available, len, sub)) {
            ind = len;
            break;
          };
        };
        return ind;
      };
      // console.log(searchForArray(available, sub), available);
      available.splice(searchForArray(available, sub), 1);
      nextTurn();
    }
    
    nextTurn();
    html {
      height: 100%;
    }
    
    body {
      height: 100%;
      margin: 0;
      display: flex;
      justify-content: center;
      font-size: 120px;
      font-family: sans;
      flex-direction: column;
    }
    
    #board {
      align-self: center;
      width: 500px;
      height: 500px;
      border: 3px solid;
      display: grid;
      grid-template-columns: 1fr 1fr 1fr;
    }
    
    .kafelek {
      display: flex;
      justify-content: center;
      border: 3px solid;
      cursor: pointer;
    }
    
    p {
      margin: 0;
      height: 144px;
      align-self: center;
      cursor: pointer;
    }
    
    #results {
      font-size: 70px;
      margin-top: 70px;
      text-align: center;
    }
    <div id="board">
        <div class="kafelek">
            <p></p>
        </div>
        <div class="kafelek">
            <p></p>
        </div>
        <div class="kafelek">
            <p></p>
        </div>
        <div class="kafelek">
            <p></p>
        </div>
        <div class="kafelek">
            <p></p>
        </div>
        <div class="kafelek">
            <p></p>
        </div>
        <div class="kafelek">
            <p></p>
        </div>
        <div class="kafelek">
            <p></p>
        </div>
        <div class="kafelek">
            <p></p>
        </div>
    </div>
    <div id="results">Click to play</div>