Build a Tic-Tac-Toe Game - Build a Tic-Tac-Toe Game

Tell us what’s happening:

It’s failing tests 9, 10, and 11. But it does what it’s supposed to do when I run it, as far as I can tell.

Your code so far

<!-- file: index.html -->
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>Tic-Tac-Toe</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
    <script
      data-plugins="transform-modules-umd"
      type="text/babel"
      src="index.jsx"
    ></script>
    <link rel="stylesheet" href="styles.css" />
</head>

<body>
    <div id="root"></div>
    <script
      data-plugins="transform-modules-umd"
      type="text/babel"
      data-presets="react"
      data-type="module"
    >
      import { Board } from './index.jsx';
      ReactDOM.createRoot(document.getElementById('root')).render(<Board />);
    </script>
</body>

</html>
/* file: styles.css */
.container {
  display: grid;
  grid-template-columns: auto auto auto;
  width: 200px;
}

button.square {
  width: 50px;
  height: 50px;
  margin: 10px;
}
/* file: index.jsx */
const { useState, useEffect } = React;

export function Board() {

  const wins = [
    [0,1,2],
    [3,4,5],
    [6,7,8],
    [0,3,6],
    [1,4,7],
    [2,5,8],
    [0,4,8],
    [2,4,6]
  ]

  const [buttons,setButtons] = useState(Array(9).fill(false));
  const [player,setPlayer] = useState("X");
  const [count, setCount] = useState(0);
  const [display, setDisplay]=useState('Next Player: X');
  const [gameOver, setGameOver]=useState(false);
  const [xMoves, setXMoves]=useState([]);
  const [oMoves, setOMoves]=useState([]);

  useEffect(()=> {
        for (let i = 0; i < wins.length; i++) {
          if (wins[i].every(z => xMoves.includes(z))) {
            setGameOver(true);
            setDisplay("Winner: X");
            
          }
          else if (wins[i].every(z => oMoves.includes(z))) {
            setGameOver(true);
            setDisplay("Winner: O")
            
          }
          else if (count === 9) {
          setGameOver(true);
          setDisplay("Draw!")
    }
          else if (count < 9 && !gameOver)  {
            setDisplay(`Next Player: ${player}`);
          } 
  }})

  function handleClick(index) {
    const btnStatus = buttons.map((value,i) => 
      {return i === index ? player : value} 
    )
    player === 'X' ? setXMoves([...xMoves, index]) : setOMoves([...oMoves, index]);
    player === 'X' ? setPlayer('O') : setPlayer('X');
    
    setButtons(prev => btnStatus)
    setCount(count + 1);
    
    }

  
  function reset() {
    setButtons(Array(9).fill(false));
    setPlayer("X");
    setDisplay('Next Player: X');
    setCount(0);
    setGameOver(false);
    setXMoves([]);
    setOMoves([]);
  }
 


  return (
    <div>
    <h1>Tic-Tac-Toe</h1>
    <p>{display}</p>
    <div className='container'>
      
      {
        buttons.map( (_,index) => 
        <button className='square' 
        key={index}
        disabled = {buttons[index] || gameOver}
        onClick={() => handleClick(index)}>
        {buttons[index]}
        </button>
      )}
    
    </div>
    <button id="reset" onClick={reset}>Reset Game</button>
    </div>
  );

}

Your browser information:

User Agent is: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36

Challenge Information:

Build a Tic-Tac-Toe Game - Build a Tic-Tac-Toe Game

GitHub Link: freeCodeCamp/curriculum/challenges/english/blocks/lab-tic-tac-toe/67e3a6b7f60b4085588189e6.md at main · freeCodeCamp/freeCodeCamp · GitHub

Since I last looked at it, you guys added a “Socrates” thing that supposedly will give me a hint. The hint it gives is “Your button.square elements are still clickable after a win because the disabled attribute does not reliably reflect the gameOver state. Ensure that the disabled property is set based on gameOver for every square and try again.”

But that hint is incorrect. My code for the buttons includes “disabled = {buttons[index] || gameOver}” which makes all the buttons disabled either when they have been clicked, or when gameOver is true. And it works properly when I run the code. How can I keep it from failing this test?

The tests fail because the win/draw logic is inside useEffect, which runs after React renders the UI. Thus, when the test checks the DOM immediately after a click, gameOver remains false, so the button isn’t disabled.

Besides, useEffect might be unnecessary for this project. The win/draw logic can be handled directly inside handleClickwithout any delay.

When handling it directing inside handleClick, it activates the boxes upon clicking them, but the first click fails to update the next-player message. Subsequent clicks update both, but that leaves the next-player message permanently out of sync with who is actually making the next move, and it also fails to recognize the game is over until a square is clicked after the game is won. Fixing that was why I put it in the useEffect in the first place. I just changed the useEffect block into a separate function called checkWins, and calling that at the end of each handleClick call resulted in the same behavior. Any ideas what I am doing wrong?

The issue is that checkWins() is reading the state from before the current click.

Calling setXMoves, setOMoves, setPlayer, or setCount does not immediately change those variables inside the current handleClick call. So when you call checkWins() at the end of it, the function still sees the previous moves, player, and count. That is why the status and winner detection are always one move behind.

With your current structure, calculate the updated moves, count, and next player in local variables first, then pass those values into checkWins() and use the same values in the state setters.