skip to Main Content

I’m trying to build a React web-app that allows people to solve chess puzzles. I used a chessground library to render the chessboard and chess.js library to handle chess logic.

I created a component Board.tsx to use chessground library in my React project. Here’s its code:

import React from 'react';
import { Chessground } from 'chessground';
import { Config } from 'chessground/config';
import { Chess } from 'chess.js';
import './Board.css';
import * as cg from 'chessground/types';

interface BoardProperties {
  fen: cg.FEN; // chess position in Forsyth notation
  orientation: cg.Color; // board orientation: 'white' | 'black'
  game?: Chess;
  turnColor?: cg.Color;
  coordinates?: boolean;
  viewOnly?: boolean;
  disableContextMenu?: boolean;
  highlight?: {
    lastMove?: boolean;
    check?: boolean;
  };
  animation?: {
    enabled?: boolean;
    duration?: number;
  };
  draggable?: {
    enabled?: boolean;
    distance?: number;
    autoDistance?: boolean;
    showGhost?: boolean;
    deleteOnDropOff?: boolean;
  };
  events?: {
    change?: () => void;
    move?: (orig: cg.Key, dest: cg.Key, capturedPiece?: cg.Piece) => void;
    dropNewPiece?: (piece: cg.Piece, key: cg.Key) => void;
    select?: (key: cg.Key) => void;
    insert?: (elements: cg.Elements) => void;
  };
}

class Board extends React.Component<BoardProperties> {
  private boardRef: React.RefObject<HTMLDivElement>;
  private groundInstance: ReturnType<typeof Chessground> | null;

  constructor(props: BoardProperties) {
    super(props);
    this.boardRef = React.createRef();
    this.groundInstance = null;
  }

  // Get allowed moves
  getLegalMoves() {
    const dests = new Map();
    const { game } = this.props;
    
    if (game) {
      let allLegalMoves = game.moves({ verbose: true });

      allLegalMoves.forEach((move) => {
        if (!dests.has(move.from)) {
          dests.set(move.from, []);
        }
        dests.get(move.from).push(move.to);
      });
    }
    return dests;
  }

  // Make a move 
  makeMove(from: cg.Key, to: cg.Key) {
    if (this.groundInstance) {
      this.groundInstance.move(from, to);
      console.log(`Move executed: ${from} -> ${to}`);
    }
  }

  componentDidMount() {
    if (this.boardRef.current) {
      const config: Config = {
        fen: this.props.fen,
        orientation: this.props.orientation,
        coordinates: this.props.coordinates ?? true,
        turnColor: this.props.turnColor || 'white',
        viewOnly: this.props.viewOnly || false,
        disableContextMenu: this.props.disableContextMenu || true,
        animation: this.props.animation || { enabled: true, duration: 500 },
        movable: {
          color: 'both',
          free: false,
          dests: this.getLegalMoves(),
          showDests: true,
        },
        highlight: this.props.highlight || {},
        events: this.props.events || {},
      };

      this.groundInstance = Chessground(this.boardRef.current, config);
    }
  }

  componentDidUpdate(prevProps: BoardProperties) {
    if (prevProps.fen !== this.props.fen) {
      if (this.groundInstance) {
        this.groundInstance.set({ fen: this.props.fen,  movable: {
            color: 'both',
            free: false,
            dests: this.getLegalMoves(),
            showDests: true,
          },});
      }
    }
  }

  componentWillUnmount() {
    if (this.groundInstance) {
      this.groundInstance.destroy();
    }
  }

  render() {
    return (
      <div>
        <div ref={this.boardRef} style={{ width: '400px', height: '400px' }}></div>
      </div>
    );
  }
}

export default Board;

I also created a React component PuzzlesPage.js to implement my idea. I’ll show its code before I explain how it works:

import Board from "./board/Board.tsx";
import { useEffect, useState, useRef } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { Chess } from 'chess.js';

const DEFAULT_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';

export default function PuzzlesPage() {
    const [fen, setFen] = useState(DEFAULT_FEN);
    const [boardOrientation, setBoardOrientation] = useState('white');
    const [correctMoves, setCorrectMoves] = useState([]);
    const [game, setGame] = useState(new Chess(fen));
    const [currentMoveIndex, setCurrentMoveIndex] = useState(0);
    const [isUserTurn, setIsUserTurn] = useState(false);
    const [isComputerMove, setIsComputerMove] = useState(false); 

    const boardRef = useRef(null);

    const makeAIMove = () => {
        console.log('makeAIMove called');
        console.log('currentMoveIndex:', currentMoveIndex);
        console.log('correctMoves:', correctMoves);

        if (currentMoveIndex < correctMoves.length) {
            const move = correctMoves[currentMoveIndex];
            console.log('Computer move:', move);
            const [orig, dest] = [move.slice(0, 2), move.slice(2, 4)];

            if (game.move({ from: orig, to: dest })) {
                console.log('Computer move is valid');
                setFen(game.fen());
                boardRef.current.makeMove(orig, dest);
                setCurrentMoveIndex((prevIndex) => prevIndex + 1);
                setIsUserTurn(true);
                setIsComputerMove(false);
                console.log('User turn set to true, computer move executed');
            } else {
                console.log('Invalid computer move');
            }
        } else {
            console.log('currentMoveIndex ', currentMoveIndex, ' is >= correctMoves.length ', correctMoves.length);
        }
    };


    const handleUserMove = (orig, dest) => {
        console.log('User move:', orig, dest);
        console.log('isUserTurn:', isUserTurn);
        console.log('isComputerMove:', isComputerMove);
        
        if (!isUserTurn || currentMoveIndex >= correctMoves.length) {
            console.log('Not user's turn or moves are done');
            return;
        }

        const correctMove = correctMoves[currentMoveIndex];
        console.log('Expected user move:', correctMove);
        if (orig + dest === correctMove) {
            console.log('User move is correct');
            if (game.move({ from: orig, to: dest })) {
                setFen(game.fen());
                setCurrentMoveIndex((prevIndex) => prevIndex + 1);
                setIsUserTurn(false);
                setIsComputerMove(true);
                console.log('User turn set to false, computer turn will follow');

                
                if (currentMoveIndex + 1 < correctMoves.length) {
                    console.log('Preparing for next computer move');
                    setTimeout(() => makeAIMove(), 500);
                } else {
                    console.log('Puzzle solved');
                    toast.success('Puzzle solved!');
                }
            }
        } else {
            console.log('User move is incorrect');
            toast.error('User move is incorrect');
            boardRef.current.groundInstance.set({ fen: game.fen() });
        }
    };


    function getRandomPuzzle() {
        console.log('Fetching new puzzle');
        fetch('http://localhost:5000/api/random-puzzle')
            .then((response) => response.json())
            .then((data) => {
                console.log('Puzzle received:', data);
                setFen(data.fen);

                
                if (data.moves && data.moves.trim()) {
                    const moves = data.moves.split(' ').filter(move => move); 
                    console.log('Parsed moves:', moves);
                    setCorrectMoves(moves);
                } else {
                    console.error('No valid moves found in the response');
                    setCorrectMoves([]);
                }

                setGame(new Chess(data.fen));
                setBoardOrientation(data.fen.split(' ')[1] === 'b' ? 'white' : 'black');
                setCurrentMoveIndex(0);
                setIsUserTurn(false);
                setIsComputerMove(false); 
            })
            .catch((error) => {
                console.error('Error fetching puzzle:', error);
            });
    }

    
    useEffect(() => {
        if (correctMoves.length > 0) {
            console.log('First computer move after puzzle load');
            setTimeout(() => makeAIMove(), 500);
        }
    }, [correctMoves]);

    return (
        <div>
            <Board
                ref={boardRef}
                fen={fen}
                game={game}
                orientation={boardOrientation}
                events={{
                    move: (orig, dest) => {
                        
                        console.log('Move event triggered:', orig, dest);
                        console.log('isUserTurn before handling:', isUserTurn);
                        console.log('isComputerMove before handling:', isComputerMove);
                        
                        
                        if (isComputerMove) {
                            console.log('Ignoring computer move event:', orig, dest);
                            setIsComputerMove(false); 
                            return;
                        }
                        
                        handleUserMove(orig, dest); 
                    },
                }}
            />
            <button
                onClick={getRandomPuzzle}
                style={{ marginTop: '20px', padding: '10px 20px', fontSize: '16px' }}
            >
                New puzzle
            </button>
            <ToastContainer />
        </div>
    );
}

How it works:

When user presses the button, a new puzzle is loaded from server. Here’s an example of server’s response:

{
    "id": 3684723,
    "puzzle_id": "ZgWE3",
    "fen": "8/5pk1/8/3rq2p/6p1/6P1/1pQ2PP1/1R4K1 w - - 0 43",
    "moves": "b1b2 e5e1 g1h2 d5d1 c2d1 e1d1",
    "rating": 1681,
    "rating_deviation": 84,
    "popularity": 79,
    "nb_plays": 39,
    "themes": "crushing endgame long quietMove",
    "game_url": "https://lichess.org/5kJUutVK#85",
    "opening_tags": null
}

Then it converts "moves" to an array of moves, and in useEffect calls makeAIMove() after correctMoves array has been created. When makeAIMove() has been called, if makes the first move from correctMoves and starts waiting for user’s moves. When user makes his move, it checks if this move goes after computer’s move in correctMoves in handleUserMove() and if so it calls makeAIMove() again etc. until computer and user make all moves from correctMoves.

The problem:

Both computer’s moves made by boardRef.current.makeMove(orig, dest); and user’s moves make by himself call events.move event in Board component. I need to handle this event only when user makes a move to get his move’s coordinates. I use boolean flags isUserTurn and isComputerMove to decide which move called the event. But the problem is that flags isUserTurn and isComputerMove are not updating everywhere in component and because of that somewhere in my code (for example, at events.move) they contain old values.

I can show you console logs that may help you to understand my problem:

*New puzzle button has been pressed*

Fetching new puzzle
PuzzlesPage.js:90 Puzzle received: {id: 4823889, puzzle_id: 'qmZAy', fen: '8/p1p2p2/8/2p1kP2/4P3/1PrP3R/3K4/8 b - - 2 34', moves: 'c3b3 d3d4 c5d4 h3b3', rating: 875, …}
PuzzlesPage.js:96 Parsed moves: (4) ['c3b3', 'd3d4', 'c5d4', 'h3b3']
PuzzlesPage.js:117 First computer move after puzzle load
PuzzlesPage.js:21 makeAIMove called

*Then computer makes its first move*

currentMoveIndex: 0
PuzzlesPage.js:23 correctMoves: (4) ['c3b3', 'd3d4', 'c5d4', 'h3b3']
PuzzlesPage.js:27 Computer move: c3b3
PuzzlesPage.js:31 Computer move is valid
Board.tsx:73 Move executed: c3 -> b3
PuzzlesPage.js:37 User turn set to true, computer move executed
PuzzlesPage.js:132 Move event triggered: c3 b3 

*Here it should decide that event was called by AI move*

PuzzlesPage.js:133 isUserTurn before handling: false              (Why?)
PuzzlesPage.js:134 isComputerMove before handling: false 
PuzzlesPage.js:48 User move: c3 b3

*It decided that it was user's turn and took coords and called handleUserMove()*

PuzzlesPage.js:49 isUserTurn: false              (Why?)
PuzzlesPage.js:50 isComputerMove: false
PuzzlesPage.js:53 Not user's turn or moves are done

*Then user makes a random move at board*

Move event triggered: h3 h6
PuzzlesPage.js:133 isUserTurn before handling: false
PuzzlesPage.js:134 isComputerMove before handling: false
PuzzlesPage.js:48 User move: h3 h6 

*It decided that it was user's turn and took coords and called handleUserMove() but it's too late*

PuzzlesPage.js:49 isUserTurn: false
PuzzlesPage.js:50 isComputerMove: false
PuzzlesPage.js:53 Not user's turn or moves are done

There’s may be another problem that causes that behavior but I can’t find another reasons except for the wrong value updating.

2

Answers


  1. Your events.move function has a stale closure over the state values in your PuzzlesPage component. The events.move function only knows about the variables/state in its surrounding scope when it was created. Since you only register this function with your Chessground instance in componentDidMount you’re only ever registering the first events.move function created in the first render of PuzzlesPage (which only knows about the initial state variables created in that render’s scope).

    You have a few options to fix this:

    • Use the Chessground React wrapper: https://github.com/react-chess/chessground so that a fresh config object is used with the latest function when the config changes

    • Reset your Chessground instance to use the latest events object:

      componentDidUpdate(prevProps) {
          // ... existing logic ...
          if (prevProps.events !== this.props.events) {
            this.groundInstance.set({ events: this.props.events });
          }
      }
      

      Note that this triggers on each rerender as your events function changes on each rerender (which you can change by memoizing your callback).

    Login or Signup to reply.
  2. I agree with Nick that you have stale state values. It appears to me that neither isUserTurn nor ismComputerMove are used to drive changes in the UI, so they don’t really need to be stateful. If that is so, a fix would be to track them as refs instead of state.

    const isUserTurnRef = useRef(false)
    const isComputerMove = useRef(false)
    

    set them like so isUserTurnRef.current = true/false

    read them like so if(isComputerMove.current){....

    Is there ever a situation where both of these are true or false at the same time? It feels like they could be redundant.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search