Clariity / react-chessboard

The React Chessboard Library used at ChessOpenings.co.uk. Inspired and adapted from the unmaintained Chessboard.jsx.
https://react-chessboard.vercel.app
MIT License
348 stars 105 forks source link

Positioning issue if FEN string changes completely #117

Closed Makavto closed 11 months ago

Makavto commented 11 months ago

Hi! Here is an example of issue:

https://github.com/Clariity/react-chessboard/assets/93901579/9f04b2f2-6d34-4ef5-8078-b37636946ddd

If I change FEN to a couple of moves it's ok, but if I change it to the start of the game it messes things up :(

Is there any way to fix it?

Manukyanq commented 11 months ago

Hi @Makavto! Can you please provide code how FEN position's change works in your component, and check which value for start position do you set, when you click on first move. Are you sure that start position value is a valid FEN? Also please tell me which version of react-chessboard do you use?

Makavto commented 11 months ago

I am using the last version, 4.3.2 I am sure that FEN position is always correct, chess.js validates it for me. I checked the getPositionObject function and it return correct position object, the problem is somewhere in transform: translate():

https://github.com/Clariity/react-chessboard/assets/93901579/21492f71-7336-46dd-85b7-4472c943c63b

Here is my ChessBoard.tsx:

interface IChessBoardProps {
  startingFen?: string;
  makeMove: (moveCode: string) => void;
  boardOrientation: 'black' | 'white',
  isMovingBlocked?: boolean
}

type ISquares = {
  [key in Square]?: any
}

const ChessBoard = memo(function ChessBoard({startingFen, boardOrientation, makeMove, isMovingBlocked}: IChessBoardProps) {

  const [game, setGame] = useState(new Chess(startingFen));
  const [moveFrom, setMoveFrom] = useState<Square | null>(null);
  const [moveTo, setMoveTo] = useState<Square | null>(null);
  const [optionSquares, setOptionSquares] = useState<ISquares>({});
  const [dangerSquares, setDangerSquares] = useState<ISquares>({});
  const [showPromotionDialog, setShowPromotionDialog] = useState(false);

  const checkDangerSquares = () => {
    setDangerSquares({});
    if (game.isCheck() || game.isCheckmate()) {
      const board = game.board();
      const turn = game.turn();
      let newDangerSquares: ISquares = {};
      for (let row of board) {
        for (let square of row) {
          if (square?.type === 'k' && (square.color === 'b' && turn === 'b' || square.color === 'w' && turn === 'w')) {
            newDangerSquares[square.square] = {
              boxShadow: 'inset 0 0 5px 5px #B15653'
            }
          }
        }
      }
      setDangerSquares(newDangerSquares);
    }
  }

  useEffect(() => {
    if (!!startingFen) {
      game.load(startingFen);
      checkDangerSquares();
    }
  }, [startingFen])

  const getMoveOptions = (square: Square) => {
    if (game.get(square).color === 'b' && boardOrientation === 'white' || game.get(square).color === 'w' && boardOrientation === 'black') {
      return;
    }
    const moves = game.moves({
      verbose: true,
      square
    });

    if (moves.length === 0) {
      setOptionSquares({});
      return false;
    }

    const newSquares: ISquares = {};
    moves.map((move) => {
      newSquares[move.to] = {
        background:
          game.get(move.to) &&
          game.get(move.to).color !== game.get(square).color
            ? "radial-gradient(circle, rgba(0,0,0,.1) 85%, transparent 85%)"
            : "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
        borderRadius: "50%",
      };
      return move;
    });
    newSquares[square] = {
      background: "rgba(255, 255, 0, 0.4)",
    };
    setOptionSquares(newSquares);
    return true;
  }

  const onSquareClick = (square: Square) => {
    if (isMovingBlocked) {
      return;
    }
    if (!moveFrom) {
      const hasMoveOptions = getMoveOptions(square);
      if (hasMoveOptions) setMoveFrom(square);
      return;
    }

    // to square
    if (!moveTo) {
      // check if valid move before showing dialog
      const moves = game.moves({
        verbose: true,
        square: moveFrom,
      });
      const foundMove = moves.find(
        (m) => m.from === moveFrom && m.to === square
      );
      // not a valid move
      if (!foundMove) {
        // check if clicked on new piece
        const hasMoveOptions = getMoveOptions(square);
        // if new piece, setMoveFrom, otherwise clear moveFrom
        setMoveFrom(hasMoveOptions ? square : null);
        return;
      }

      // valid move
      setMoveTo(square);

      // if promotion move
      if (
        (foundMove.color === "w" &&
          foundMove.piece === "p" &&
          square[1] === "8") ||
        (foundMove.color === "b" &&
          foundMove.piece === "p" &&
          square[1] === "1")
      ) {
        setShowPromotionDialog(true);
        return;
      }

      // is normal move
      const move = game.move({
        from: moveFrom,
        to: square,
        promotion: "q",
      });

      checkDangerSquares();
      makeMove(move.san);

      // if invalid, setMoveFrom and getMoveOptions
      if (move === null) {
        const hasMoveOptions = getMoveOptions(square);
        if (hasMoveOptions) setMoveFrom(square);
        return;
      }

      setMoveFrom(null);
      setMoveTo(null);
      setOptionSquares({});
      return;
    }
  }

  const onPromotionPieceSelect = (piece?: PromotionPieceOption) => {
    // if no piece passed then user has cancelled dialog, don't make move and reset
    if (piece && moveFrom && moveTo) {
      const move = game.move({
        from: moveFrom,
        to: moveTo,
        promotion: piece[1].toLowerCase() ?? "q",
      });
      setGame(game);
      checkDangerSquares();
      makeMove(move.san);
    }

    setMoveFrom(null);
    setMoveTo(null);
    setShowPromotionDialog(false);
    setOptionSquares({});
    return true;
  }

  return (
    <div className={styles.board}>
      <Chessboard
        animationDuration={200}
        arePiecesDraggable={false}
        position={game.fen()}
        boardOrientation={boardOrientation}
        onSquareClick={onSquareClick}
        customSquareStyles={{
          ...optionSquares,
          ...dangerSquares,
        }}
        onPromotionPieceSelect={onPromotionPieceSelect}
        promotionToSquare={moveTo}
        showPromotionDialog={showPromotionDialog}
        areArrowsAllowed={true}
        customSquare={CustomSquare}
      />
    </div>
  );
})

const CustomSquare = React.forwardRef(({squareColor, children, style}: CustomSquareProps, ref) => {
  return (
    <div style={style} className={`${squareColor === 'black' ? styles.darkSquare : styles.lightSquare}`}>
      {children}
    </div>
  )
})

export default ChessBoard

And here is how I use it in GamePage.tsx:

const getPositionAfterMove = (positionBefore: string, moveCode: string) => {
    const game = new Chess(positionBefore);
    game.move(moveCode);
    return game.fen();
  }

...some code here

<ChessBoard
                startingFen={activeMove ? getPositionAfterMove(activeMove.positionBefore, activeMove.moveCode) : undefined}
                isMovingBlocked={activeMove?.number !== chessGame.gameMoves[chessGame.gameMoves.length - 1].moveNumber}
                makeMove={onMakeMove}
                boardOrientation={chessGame.blackPlayerId === user?.id ? 'black' : 'white'}
              />
Manukyanq commented 11 months ago

I guess the problem is here in useEffect: The game.load() method change's Chess objects, but doesn't make your Chessboard component rerender

  useEffect(() => {
    if (!!startingFen) {
      game.load(startingFen);
      checkDangerSquares();
      // here will be setGame() call which will force your component rerender `Chessboard`
    }
  }, [startingFen])
Manukyanq commented 11 months ago

I highly reccomend you to use approach with useMemo like in this example, where the external FEN changes work fine https://react-chessboard.vercel.app/?path=/docs/example-chessboard--analysis-board

  const MyChessBoard = (props: ChessBoardProps) = {
    const game = useMemo(() => new Chess(), []);
    const [chessBoardPosition, setChessBoardPosition] = useState(game.fen());

    useEffect(() => {
      if (!!props.startingFen) {
        game.load(startingFen);
        checkDangerSquares();
        setChessBoardPosition(game.fen)
      }
    }, [startingFen])

  return (
    <ChessBoard position={chessBoardPosition}/>
    )
Makavto commented 11 months ago

I've tried it with setGame() like this:

useEffect(() => {
    if (!!startingFen) {
      setGame(new Chess(startingFen));
      checkDangerSquares();
    }
  }, [startingFen])

and it didn't work either :(

I will try useMemo approach

Manukyanq commented 11 months ago

I've only now noticed,that you use custom squares JSX. Can you please also try to comment customSquare={CustomSquare} line and check, is the issue still appears? Make sure your custom style squares have right css position props

Makavto commented 11 months ago

Yes! You are right, the problem was in custom squares! I will edit styles for them. Thanks a lot for your help!