0

Using react-chessboard with chess.js. I modified the example code at: https://www.npmjs.com/package/react-chessboard.

My code:

import { Chessboard } from "react-chessboard";
import { useState } from "react";
import { Chess } from "chess.js";

const Board = () =>{
    const [game, setGame] = useState(new Chess());
    function makeAMove(move) {
        const gameCopy = new Chess();
        gameCopy.loadPgn(game.pgn());
        gameCopy.move(move);
        setGame(gameCopy); 
    }

    function onDrop(sourceSquare, targetSquare) {
        makeAMove({
            from: sourceSquare,
            to: targetSquare,
        });
        
        if(game.isGameOver()){
            if(game.isStalemate() || game.isThreefoldRepetition()){
                alert("Stalemate")
            } 
            if(game.turn() == "b"){
                alert("White won")
            }
            else if(game.turn() == "w"){
                alert("Black won")
            }
        }
    }

    return (
    <>
        <div style = {{width: '50%',alignItems: 'center', marginLeft: '25%',justifyContent: 'center'}}>
            <Chessboard position={game.fen()} onPieceDrop={onDrop} id="BasicBoard"/>
        </div>
    </>
  );
}

export default Board;

Why is the isGameOver() one move behind for me? If white checkmates black then the "White won" alert pops up only after black tries to make another move after being checkmated and vice versa.

Cam
  • 21
  • 3

3 Answers3

1

It's because makeAMove updates the state but the local instance of game that you check is still the same that it was before the state update. Only on the next render does it get the updated game from the state. Check out The useState set method is not reflecting a change immediately to get a better understanding of why this happens.

You can fix this by running the check on the gameCopy constant when you write it to the state but a more appropriate solution is to move your checks into an effect.

You'll need to import useEffect from React:

import { useState, useEffect } from "react";

Then change your code like this:

    function onDrop(sourceSquare, targetSquare) {
        makeAMove({
            from: sourceSquare,
            to: targetSquare,
        });
     }
        
     useEffect(function() {
        if(game.isGameOver()){
            if(game.isStalemate() || game.isThreefoldRepetition()){
                alert("Stalemate");
            } 
            if(game.turn() == "b"){
                alert("White won");
            }
            else if(game.turn() == "w"){
                alert("Black won");
            }
        }
    }, [ game ]);

The effect has the game constant as a dependency, which means that it runs every time game changes and will always run the checks and the latest state.

Lennholm
  • 7,205
  • 1
  • 21
  • 30
0

I would consider storing game in a ref instead of creating a new one every time you want to make a move

import { Chessboard } from "react-chessboard";
import { useState, useReducer } from "react";
import { Chess } from "chess.js";

const Board = () => {
  const game = useRef();
  const [_, rerender] = useReducer((x) => x + 1, 0);

  useEffect(() => {
    if (!ref.current) {
      ref.current = new Chess();
    }
  }, []);

  function onDrop(sourceSquare, targetSquare) {
    game.current.move({
      from: sourceSquare,
      to: targetSquare,
    });

    rerender()

    if (game.current.isGameOver()) {
      if (game.current.isStalemate() || game.current.isThreefoldRepetition()) {
        alert("Stalemate");
      }
      if (game.current.turn() == "b") {
        alert("White won");
      } else if (game.current.turn() == "w") {
        alert("Black won");
      }
    }
  }

  return (
    <>
      <div
        style={{
          width: "50%",
          alignItems: "center",
          marginLeft: "25%",
          justifyContent: "center",
        }}
      >
        <Chessboard
          position={game.current.fen()}
          onPieceDrop={onDrop}
          id="BasicBoard"
        />
      </div>
    </>
  );
};

export default Board;
Konrad
  • 21,590
  • 4
  • 28
  • 64
0

Because calling setGame does not immediately change the value of game. It queues a change that will resolve after render (explained in the new React docs site).

One way to solve this would be to inline your makeAMove function and use the updated game in the rest of your event handler, like this:

function onDrop(sourceSquare, targetSquare) {
  const updatedGame = new Chess();
  updatedGame.loadPgn(game.pgn());
  updatedGame.move({
    from: sourceSquare,
    to: targetSquare,
  });
  setGame(updatedGame);

  if (updatedGame.isGameOver()) {
    if (updatedGame.isStalemate() || updatedGame.isThreefoldRepetition()) {
      alert('Stalemate');
    }
    if (updatedGame.turn() == 'b') {
      alert('White won');
    } else if (updatedGame.turn() == 'w') {
      alert('Black won');
    }
  }
}
Valentin
  • 10,769
  • 2
  • 17
  • 27