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
335 stars 101 forks source link

Is it possible to adapt it to a player vs. player game? #142

Closed Konstantin9658 closed 3 months ago

Konstantin9658 commented 3 months ago

Good afternoon I ask for help in explaining how to adapt the game against another player. The connection occurs via a web socket, and I understand that after the first move (white), the second player still has a game state that is as if the first player never left. I thought that it would be enough to update the board for the second player and the game would switch the course itself, but it turned out not... It seems that it is enough to update the game state in the event, but I don’t understand how to do this in my case.

I miscalculated, but where...?

import { useUser } from "@mercdev.com/auth";
import { Chess, ChessInstance, PieceColor, ShortMove, Square } from "chess.js";
import clsx from "clsx";
import { useEffect, useMemo, useState } from "react";
import { Chessboard } from "react-chessboard";
import {
  BoardOrientation,
  CustomPieces,
  CustomSquareStyles,
} from "react-chessboard/dist/chessboard/types";
import { Modal } from "react-overlays";
import useBus from "use-bus";

import { moveBattleChess } from "@/api/pvp/game/battle-chess";
import { useGameInfoCurrent } from "@/api/pvp/pvp";
import { VersusBox } from "@/components/versus-box";
import { useModalFade } from "@/modals/use-modal-fade";
import { useModalStore } from "@/store/modal";
import { PvpGameName, PvpGameReward, usePvpStore } from "@/store/pvp";

import {
  BOARD_STYLE,
  DARK_SQUARE_STYLE,
  DROP_SQUARE_STYLE,
  LIGHT_SQUARE_STYLE,
  PIECES,
} from "./constants";
import ChessBlack from "./images/chess-black.svg?react";
import ChessWhite from "./images/chess-white.svg?react";
import { PieceImage } from "./piece-image";
import classes from "./styles.module.scss";
import { SquareColor } from "./types";
import { getHighlitedStyle } from "./utils";

const GameChess = () => {
  const isChessGameVisible = useModalStore((state) => state.isChessGameVisible);
  const setChessGameVisible = useModalStore(
    (state) => state.setChessGameVisible
  );

  const onHide = () => setChessGameVisible(false);

  const Fade = useModalFade(classes.visible);

  return (
    <Modal
      show={isChessGameVisible}
      transition={Fade}
      className={classes.modal}
    >
      <ModalVisual onHide={onHide} />
    </Modal>
  );
};

const Game = ({
  board,
  // gameRef,
  side,
  gameId,
  onWinHandle,
  onLoseHandle,
}: {
  side: BoardOrientation;
  board: string;
  // gameRef: ChessInstance;
  gameId: string;
  onWinHandle: (v: boolean) => void;
  onLoseHandle: (v: boolean) => void;
}) => {
  // const game = useRef(new Chess());

  const [isCheck, setCheck] = useState(false);
  const [isCheckmate, setCheckmate] = useState(false);
  const [currentTurn, setCurrentTurn] = useState<PieceColor>("w");
  const [statusMessage, setStatusMessage] = useState("");
  const [squareStyles, setSquareStyles] = useState<CustomSquareStyles>({});
  const [squareStyleInCheck, setSquareStyleInCheck] =
    useState<CustomSquareStyles>({});

  const [game, setGame] = useState<ChessInstance>(new Chess());

  useBus("@pvp/BATTLE_CHESS_MOVE", () => {
    setCurrentTurn((prev) => (prev === "w" ? "b" : "w"));
    game.load(board);
    game.turn();
  });

  const customPieces = useMemo(() => {
    const pieceComponents: CustomPieces = {};
    for (const piece of PIECES) {
      pieceComponents[piece] = () => <PieceImage piece={piece} />;
    }

    return pieceComponents;
  }, []);

  const orientation: PieceColor = useMemo(
    () => (side === "white" ? "w" : "b"),
    [side]
  );

  const onMouseOverSquare = async (square: Square) => {
    if (currentTurn !== orientation) return setSquareStyles({});

    const newSquares: CustomSquareStyles = {};

    const moves = game.moves({
      square,
      verbose: true,
    });

    // deduplication of unnecessary rerenders when the previous state is already equal to the current one
    if (!Object.keys(squareStyles).length && !moves.length) return;

    if (moves.length)
      newSquares[square] = getHighlitedStyle("green", false, "transparent", {
        topLeft: square === "a8",
        topRight: square === "h8",
        bottomLeft: square === "a1",
        bottomRight: square === "h1",
      });

    for (const move of moves) {
      const fromSquare = game.get(square);
      const toSquare = game.get(move.to);

      if (!fromSquare) continue;

      let color: SquareColor = "light-green";
      let bordered = false;

      // если есть фигура которую можно "съесть"
      if (toSquare && fromSquare.color !== toSquare.color) {
        color = "light-red";
        bordered = true;
      }

      if (toSquare && square === move.to && fromSquare.color !== toSquare.color)
        bordered = true;

      newSquares[move.to] = getHighlitedStyle(color, bordered, "#ed4159", {
        topLeft: move.to === "a8",
        topRight: move.to === "h8",
        bottomLeft: move.to === "a1",
        bottomRight: move.to === "h1",
      });
    }

    setSquareStyles(newSquares);
  };

  function makeAMove(move: ShortMove) {
    const gameCopy = { ...game };
    const result = gameCopy.move(move);
    setGame(gameCopy);
    return result;
  }

  const onDrop = (sourceSquare: Square, targetSquare: Square) => {
    const move = makeAMove({
      from: sourceSquare,
      to: targetSquare,
      promotion: "q",
    });
    if (!move) return false;

    const currentMove = `${sourceSquare} ${targetSquare}`;

    if (game.in_checkmate()) {
      setCheckmate(true);
    } else if (game.in_check()) {
      setCheck(true);
    } else {
      setCheck(false);
      setCheckmate(false);
    }

    moveBattleChess(gameId, game.fen(), currentMove);

    return true;
  };

  useEffect(() => {
    if (!isCheck) setSquareStyleInCheck({});
    if (isCheck) {
      setStatusMessage("Check");

      const king = game
        .board()
        .flat()
        .find((square) => square?.type === "k" && square.color === game.turn());

      if (king) {
        const color: SquareColor = "light-red";
        const bordered = true;
        const borderColor = "#ed4159";

        const square: CustomSquareStyles = {};

        square[king.square] = getHighlitedStyle(color, bordered, borderColor);

        setSquareStyleInCheck(square);
      }
    }

    if (isCheckmate) {
      if (currentTurn === orientation) {
        setStatusMessage("Checkmate, you lose");
        onLoseHandle(true);
      } else {
        setStatusMessage("You win");
        onWinHandle(true);
      }

      const king = game
        .board()
        .flat()
        .find((square) => square?.type === "k" && square.color === game.turn());

      if (king) {
        const color: SquareColor = "red";

        const square: CustomSquareStyles = {};

        square[king.square] = getHighlitedStyle(color);

        setSquareStyleInCheck(square);
      }
    }
  }, [
    currentTurn,
    game,
    isCheck,
    isCheckmate,
    onLoseHandle,
    onWinHandle,
    orientation,
  ]);

  return (
    <div className={classes.game}>
      <div
        className={clsx(
          classes.game__badge,
          (isCheck || (isCheckmate && currentTurn === orientation)) &&
            classes.game__badge_checkOrCheckmate,
          currentTurn !== orientation && isCheckmate && classes.game__badge_win,
          currentTurn !== orientation && classes.game__badge_opponentsTurn
        )}
      >
        {statusMessage}
      </div>
      <div
        className={clsx(
          classes.game__badge,
          currentTurn === orientation && classes.game__badge_youTurn
        )}
      >
        {currentTurn === orientation ? "You turn" : "Opponents turn"}
      </div>
      <div className={classes["modal__game-board"]}>
        <Chessboard
          position={board}
          boardWidth={540}
          isDraggablePiece={({ piece }) => piece[0] === orientation}
          boardOrientation={side}
          arePiecesDraggable={!isCheckmate}
          showBoardNotation={false}
          customPieces={customPieces}
          customBoardStyle={BOARD_STYLE}
          customDropSquareStyle={DROP_SQUARE_STYLE}
          customDarkSquareStyle={DARK_SQUARE_STYLE}
          customLightSquareStyle={LIGHT_SQUARE_STYLE}
          customSquareStyles={{ ...squareStyles, ...squareStyleInCheck }}
          onPieceDrop={onDrop}
          onMouseOverSquare={onMouseOverSquare}
        />
      </div>
    </div>
  );
};

const ModalVisual = ({ onHide }: { onHide: () => void }) => {
  const [gameId, setGameId] = useState("");
  const [creator, setCreator] = useState("");
  const [board, setBoard] = useState<string>("");
  const [isYouWin, setYouWin] = useState<boolean | undefined>();
  const [isYouLose, setYouLose] = useState<boolean | undefined>();

  // const game = useRef(new Chess());

  const setGameName = usePvpStore((state) => state.setPvpGameName);
  const setGameReward = usePvpStore((state) => state.setPvpGameReward);

  const { data: gameInfo } = useGameInfoCurrent();
  const { user } = useUser();

  const currentSide = useMemo(
    () =>
      creator === user?.email
        ? ("white" as BoardOrientation)
        : ("black" as BoardOrientation),
    [creator, user?.email]
  );

  const icon = {
    player1: creator === user?.email ? <ChessWhite /> : <ChessBlack />,
    player2: creator !== user?.email ? <ChessWhite /> : <ChessBlack />,
  };

  const gameName = usePvpStore((state) => state.pvpGameName);
  const reward = usePvpStore((state) => state.pvpGameReward);

  useEffect(() => {
    if (!gameInfo || !user) return;

    const { id: gameId, type, creatorUserEmail, progress } = gameInfo;

    setGameId(gameId);
    setCreator(creatorUserEmail);
    setGameName(PvpGameName[type]);
    setGameReward(PvpGameReward[type]);
    if (!progress?.board || typeof progress.board !== "string") return;
    setBoard(progress.board);
  }, [creator, gameInfo, setGameName, setGameReward, user]);

  return (
    <>
      <div className={classes.background} />
      <div className={classes.content}>
        <div className={classes.content__inner}>
          <Game
            gameId={gameId}
            side={currentSide}
            board={board}
            onWinHandle={setYouWin}
            onLoseHandle={setYouLose}
          />
          <VersusBox
            title={gameName}
            reward={reward}
            isYouWin={isYouWin}
            isYouLose={isYouLose}
            icon={icon}
            onHide={onHide}
          />
        </div>
      </div>
    </>
  );
};

export default GameChess;
Konstantin9658 commented 3 months ago

Sorry, I found my own mistake after I released this issue. The thing was that I didn’t do one important thing in order to update the game state

 useEffect(() => {
    game.load(board);
  }, [board, game]);

And that's it, the game works as I expected