3D Tic Tac Toe React state

I’m making a 3d tic tac toe game in React (extending the fcc lab) that adds a vertical dimension to the game. EDIT: You can test my app here: https://3-dads-tactical-tuba-time.vercel.app/

Everything was working ok until I tried to add a computer player. I know it has something to do with it state update vs. DOM. I still fundamentally do not understand how to work with React state.

For example, if player X wins, computer O will still take a turn (and can win, overwriting X’s win!). If I win as X though, the board is disabled. The logic for both is the same, just a line:

  if (winner) return;

and winner is using setState.

Not sure how to get computermove to see the new state. I tried using useEffect() to check for a winner when the board changes and I tried adding a setTimeout delay on the computermove to wait for state update.

Additionally there’s some other problems with computermove like this happens:

But it might also be related to detecting state although that is odd.

Any help would be greatly appreciated, thanks!

import { useState, useRef, useEffect } from 'react';
import './App.css'

const winningPatterns = [
  // Horizontal rows in each layer (XY plane)
  ["0,0,0","0,1,0","0,2,0"],
  ["0,0,1","0,1,1","0,2,1"],
  ["0,0,2","0,1,2","0,2,2"],
  ["1,0,0","1,1,0","1,2,0"],
  ["1,0,1","1,1,1","1,2,1"],
  ["1,0,2","1,1,2","1,2,2"],
  ["2,0,0","2,1,0","2,2,0"],
  ["2,0,1","2,1,1","2,2,1"],
  ["2,0,2","2,1,2","2,2,2"],

  // Vertical columns in each layer (XY plane)
  ["0,0,0","0,0,1","0,0,2"],
  ["0,1,0","0,1,1","0,1,2"],
  ["0,2,0","0,2,1","0,2,2"],
  ["1,0,0","1,0,1","1,0,2"],
  ["1,1,0","1,1,1","1,1,2"],
  ["1,2,0","1,2,1","1,2,2"],
  ["2,0,0","2,0,1","2,0,2"],
  ["2,1,0","2,1,1","2,1,2"],
  ["2,2,0","2,2,1","2,2,2"],

  // Lines through layers (Z axis)
  ["0,0,0","1,0,0","2,0,0"],
  ["0,1,0","1,1,0","2,1,0"],
  ["0,2,0","1,2,0","2,2,0"],
  ["0,0,1","1,0,1","2,0,1"],
  ["0,1,1","1,1,1","2,1,1"],
  ["0,2,1","1,2,1","2,2,1"],
  ["0,0,2","1,0,2","2,0,2"],
  ["0,1,2","1,1,2","2,1,2"],
  ["0,2,2","1,2,2","2,2,2"],

  // Diagonals in XY planes (each layer)
  ["0,0,0","0,1,1","0,2,2"],
  ["0,2,0","0,1,1","0,0,2"],
  ["1,0,0","1,1,1","1,2,2"],
  ["1,2,0","1,1,1","1,0,2"],
  ["2,0,0","2,1,1","2,2,2"],
  ["2,2,0","2,1,1","2,0,2"],

  // Diagonals through layers (XZ planes)
  ["0,0,0","1,1,0","2,2,0"],
  ["0,2,0","1,1,0","2,0,0"],
  ["0,0,1","1,1,1","2,2,1"],
  ["0,2,1","1,1,1","2,0,1"],
  ["0,0,2","1,1,2","2,2,2"],
  ["0,2,2","1,1,2","2,0,2"],

  // Diagonals through layers (YZ planes)
  ["0,0,0","1,0,1","2,0,2"],
  ["0,0,2","1,0,1","2,0,0"],
  ["0,1,0","1,1,1","2,1,2"],
  ["0,1,2","1,1,1","2,1,0"],
  ["0,2,0","1,2,1","2,2,2"],
  ["0,2,2","1,2,1","2,2,0"],

  // 4 space diagonals through the cube
  ["0,0,0","1,1,1","2,2,2"],
  ["0,2,0","1,1,1","2,0,2"],
  ["0,0,2","1,1,1","2,2,0"],
  ["0,2,2","1,1,1","2,0,0"]
];

export default function App() {
  const initialGrid = [
  [
    ["", "", ""],
    ["", "", ""],
    ["", "", ""]
      ],
  [
    ["", "", ""],
    ["", "", ""],
    ["", "", ""]
      ],
  [
    ["", "", ""],
    ["", "", ""],
    ["", "", ""]
      ],
    ];
  const [turn, setTurn] = useState("X")
  const [grid, setGrid] = useState(initialGrid)
  const [lastMove, setLastMove] = useState([])
  const [winner, setWinner] = useState(false)
  const [moves, setMoves] = useState(0)
  const [xplayer, setXplayer] = useState(false)
  const [oplayer, setOplayer] = useState(true)
  const fin = useRef(null);

  useEffect(() => {
    
    for (let pattern of winningPatterns) {
      let currentTurn = turn == "X"? "O" : "X"
      if (pattern.includes(lastMove.toString())) {
        let win = true;
        for (let array of pattern) {
          let [zz,xx,yy] = array.split(",")
          console.log("grid",grid[zz][xx][yy],"turn", currentTurn)
          if (grid[zz][xx][yy] != currentTurn) win = false;
        }

        if (win) {
          setWinner(`${currentTurn} has won!`)
          highlight(pattern);
          return;
        }

      }
    }

    
   

  }, [turn])


  const highlight = (array) => {
    for (let point of array) {
      document.getElementById(point.replaceAll(",","")).style.backgroundColor = "lightgreen";
    }

    let resetColor = setTimeout(() => {
      for (let point of array) {
        document.getElementById(point.replaceAll(",","")).style.backgroundColor = "white";
      }

    }, 5000)
  }

  const computerMove = (currentTurn,point) => {
    
    
    //delay for state update
    const move = setTimeout(() => {
      
      //check the winning patters that contain recent move
      for (let pattern of winningPatterns) {
        if (pattern.includes(point)) {
          //remove previous move
          pattern.splice(pattern.indexOf(point), 1); 
          //choose a free space from the pattern
          for (let array of pattern) {
            let [zz,xx,yy] = array.split(",")
            if (grid[zz][xx][yy] == "") {
              
              if (winner) return;
              handleClick(zz,xx,yy,currentTurn);
              
              return;
            }

          }

          

        }
      
    }
    }, 500)
  }

  const finish = (result) => {
    if (result == "tie") {
      fin.current.innerText = "It's a tie!"
    } else {
      fin.current.innerText = `${result} has won!`
    }

    fin.current.style.visibility = "visible"
  
  }

  const handleClick = (z,x,y,currentTurn) => {
    setMoves(moves + 1)
    
    setGrid(prev => {
      let na = [...prev]
      na[z][x][y] = currentTurn
      return na
      })

    setLastMove([z,x,y])
    setTurn(prev => prev == "X"? "O" : "X")                            
    let next = currentTurn == "X"? "O" : "X";
    if (next == "O" && oplayer) computerMove("O",[z,x,y].toString());
    
    //draw
    if (moves == 26) setWinner("It's a tie!")

  }

  
  return (
    
  
  <div id="game">
    
    <p id="computer">
      Computer controls:
      <label htmlFor="xplayer">X</label>
      <input type="checkbox" id="xplayer" name="xplayer" checked={xplayer} onChange={() => setXplayer(!xplayer)} />
      <label htmlFor="oplayer">O</label>
      <input type="checkbox" id="oplayer" name="oplayer" checked={oplayer} onChange={() => setOplayer(!oplayer)} />
    </p>

    <div className='board-wrapper'>
    <div className="board">

      {
        grid[0].map((row,x) => {
          return row.map((square, y) => {
            
          
          return <button 
            onClick={(e) => {
              if (e.target.innerText != "") return;
              if (winner) return;
              handleClick(0,x,y,turn);
              
              }}
            key={"0"+x+y} 
            id={"0"+x+y}
            className="square">{square}</button>

          })
        })

      }
      
  
    </div>
    </div>

    <div className='board-wrapper'>
    <div className="board">

      {
        grid[1].map((row,x) => {
          return row.map((square, y) => {
            
          
          return <button 
            onClick={(e) => {
              if (e.target.innerText != "") return;
              if (winner) return;
              handleClick(1,x,y,turn);
             
                
             
              
              }}
              key={"1"+x+y} 
              id={"1"+x+y}
            className="square">{square}</button>

          })
        })

      }
      
  
    </div>
    </div>

    <div className='board-wrapper'>
    <div className="board">

      {
        grid[2].map((row,x) => {
          return row.map((square, y) => {
            
          
          return <button 
            onClick={(e) => {
              if (e.target.innerText != "") return;
              
              if (winner) return;
              handleClick(2,x,y,turn);
              }}

              key={"2"+x+y} 
              id={"2"+x+y}
            className="square">{square}</button>

          })
        })

      }
      
  
    </div>
    </div>
    <button onClick={() => {
      setMoves(0)
      //fin.current.style.display = "none"
      setWinner(false)
      setGrid(initialGrid);
      setTurn("X");
    }


    } id="reset">{winner ? `${winner}Reset` : `Reset`}</button>
    
    
    </div>
    
    
    
    
  )

}
html,
body {
  height: 100%;
  margin: 0;
}

#computer {
  z-index: 100;
}

#game {
  position: fixed;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  background: rgba(51, 51, 51, 0.7);
  z-index: 10;

  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.board-wrapper {
  perspective: 450px; /* lower more extreme*/
  transform-style: preserve-3d;
}

.board {
  margin: auto;
  display: grid;
  grid-template-columns: repeat(3, 1fr); 
  grid-template-rows: repeat(3, 1fr); 
  gap: 5px;
  width: 400px;
  height: 200px;
  transform: rotateX(50deg); /* Tilt the board backward */
  transform-origin: top center;
  box-shadow: 0px 5px 1px rgba(117, 116, 97, 0.5);
}

button {
  width: 100%;
  height: 100%;
  font-size: 2rem;
  cursor: pointer;
  border-radius: 0 !important;
  transition: 1s;
}

#reset {
  height: 95px;
  width: 190px;
  font-size: 1.5rem;
}

.square {
  width: 100%;
  height: 100%;
  font-size: 2rem;
  font-family: monospace;
  line-height: 1;
  text-align: center;
  vertical-align: middle;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  box-sizing: border-box;
  transform: translateZ(20px);
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

If you want some 1-1 help, jump on the fcc discord and ping me. We can join the vibes voice chat and discuss the issues.

ps. I don’t know how it happened exactly but when I tested your code locally, and the computer won, it actually disabled the board and enabled the Reset button. (i don’t know if messing around with the checkboxes did something)

When the computer wins, and it’s the player’s turn, it’s ok. Same if it’s player v player.

It’s just the computer doesn’t seem to respect the rules. (Also computer is disabled for X right now, it will only play O)

Maybe if I make a longer timeout… Nope setTimeout 2000 didn’t work.

I’ve done a few tweaks to the code (removed all unused code to start with so it is a bit easier to read and I’ve created a Board component to house the repeated section returned which creates the different levels).

At this point, I want to know what all your variables are for. And how the logic works here:

  const handleClick = (z, x, y, currentTurn) => {
    setMoves(moves + 1);

    setGrid((prev) => {
      let na = [...prev];
      na[z][x][y] = currentTurn;
      return na;
    });

    setLastMove([z, x, y]);
    setTurn((prev) => (prev == "X" ? "O" : "X"));
    let next = currentTurn == "X" ? "O" : "X";
    if (next == "O" && oplayer) computerMove("O", [z, x, y].toString());

    //draw
    if (moves == 26) setWinner("It's a tie!");
  };

The ones I don’t understand much is the moves and last move variables.

also this section looks sus. to me:


    setTurn((prev) => (prev == "X" ? "O" : "X"));
    let next = currentTurn == "X" ? "O" : "X";
    if (next == "O" && oplayer) computerMove("O", [z, x, y].toString());

  };

moves just keeps track of how many moves there have been, if there’s no winner after 26 moves, it’s a draw.

lastmove keeps track of the previously clicked space co-ordinates so the computer can calculate where to move next. It checks that point against winning patterns and puts it’s move into one of those patterns.

what about here? what’s the logic here?

// just updating the current turn from X to O or vice versa
setTurn((prev) => (prev == "X" ? "O" : "X"));

//checking what the next turn will be (same logic as above 
//but I can't use `turn` because it's not updated yet
let next = currentTurn == "X" ? "O" : "X";

//checking if the next move is the computer and calling computermove
if (next == "O" && oplayer) computerMove("O", [z, x, y].toString());

  };

This is the most frustrating part of react to me. Updating a variable but I can’t use it yet because it’s not really changed yet

also this is something worth investigating.
as the moves won’t get to 26 until later unless you are using a ref for moves and not a state.

have you heard of an ‘updater’ function for react state?
You can use it or you can reorg your logic.

I haven’t tested that part fully yet. It did work fine in the normal 1d game.

Hasn’t really been triggered in my tests yet. I think it’s a battle for another day but I’ll get to it.

No, is that different from the setter, setTurn for example?

Is it like a “get current state” ?

I’m on Discord, in #programming-help-forum (I don’t have a mic)

I’ve barely used discord haha so I’m lost there

Ok now I’m in #programming-help

you are actually using an updater here:

setTurn((prev) => (prev == "X" ? "O" : "X"));

but not here

setMoves(moves + 1);

Can you give me instructions on how to reliably make the computer play after I win (or whatever the case is that causes the board not to be disabled?) I want to try something…

Ah yes, I see now. thanks. I’ll update that.

If you refresh the page this series of moves as X, from left to right, seems consistent.

1 Like

that area will be just like here basically (but a bit faster to talk thru text).
The vibes area is where we do voice chat.

So it would seem the problem stems from this line triggering the call to computerMove?

    if (next == "O" && oplayer) computerMove("O", [z, x, y].toString());

what is the conditional basing the decision on?
(next player has to be an “O” and ??)

oplayer is the state of the “Computer Controls O” checkbox.

It’s basically hardcoded to only control O for now.

oh oops… question: do you have useStrict turned on?
I noticed the player is switching every time on the 2nd render that useStrict does in my version of the code.

Not that I know of, no

I have noticed, when logging variables to the console during testing, that they will log twice, everything seemed doubled.

That doesn’t seem to be the case now…

yeah nevermind about that…

I’m trying to track the logic. Not sure why you have useEffect code and not sure why the check for the winner is part of 2 different places (it seems).