diff --git a/package-lock.json b/package-lock.json index db3f3a5..65981e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "jotai": "^2.6.4", "next": "13.5.6", "react": "18.2.0", - "react-chessboard": "^4.4.0", + "react-chessboard": "^4.5.0", "react-dom": "18.2.0" }, "devDependencies": { @@ -4922,9 +4922,9 @@ } }, "node_modules/react-chessboard": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/react-chessboard/-/react-chessboard-4.4.0.tgz", - "integrity": "sha512-UJuFVju9pEcPJvH76HQyccm4uHyU755/Uf/3e1QfMHGWLFCBeEMSUkbzhlvxSL8Mutj/4wRxBVyJMyjhtnX0xg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-chessboard/-/react-chessboard-4.5.0.tgz", + "integrity": "sha512-YS91zFMZlaW05vIFOAkM2KnR5wp/sqBCzglUe1GJrXOtVkxzDT7Tr6cetm2txMGmSwdFQgurIi2/wKV1YQs34w==", "dependencies": { "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", diff --git a/package.json b/package.json index 1a411ae..01eea22 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "jotai": "^2.6.4", "next": "13.5.6", "react": "18.2.0", - "react-chessboard": "^4.4.0", + "react-chessboard": "^4.5.0", "react-dom": "18.2.0" }, "devDependencies": { diff --git a/src/sections/analysis/board/evaluationBar.tsx b/src/components/board/evaluationBar.tsx similarity index 61% rename from src/sections/analysis/board/evaluationBar.tsx rename to src/components/board/evaluationBar.tsx index 6c4bb64..2779f41 100644 --- a/src/sections/analysis/board/evaluationBar.tsx +++ b/src/components/board/evaluationBar.tsx @@ -1,35 +1,34 @@ import { Box, Grid, Typography } from "@mui/material"; -import { useAtomValue } from "jotai"; +import { PrimitiveAtom, atom, useAtomValue } from "jotai"; import { useEffect, useState } from "react"; -import { - boardAtom, - boardOrientationAtom, - currentPositionAtom, -} from "../states"; import { getEvaluationBarValue } from "@/lib/chess"; +import { Color } from "@/types/enums"; +import { CurrentPosition } from "@/types/eval"; interface Props { height: number; + boardOrientation?: Color; + currentPositionAtom?: PrimitiveAtom; } -export default function EvaluationBar({ height }: Props) { +export default function EvaluationBar({ + height, + boardOrientation, + currentPositionAtom = atom({}), +}: Props) { const [evalBar, setEvalBar] = useState({ whiteBarPercentage: 50, label: "0.0", }); - const board = useAtomValue(boardAtom); - const boardOrientation = useAtomValue(boardOrientationAtom); const position = useAtomValue(currentPositionAtom); - const isWhiteToPlay = board.turn() === "w"; - useEffect(() => { const bestLine = position?.eval?.lines[0]; if (!position.eval || !bestLine || bestLine.depth < 6) return; const evalBar = getEvaluationBarValue(position.eval); setEvalBar(evalBar); - }, [position, isWhiteToPlay]); + }, [position]); return ( - {(evalBar.whiteBarPercentage < 50 && boardOrientation) || - (evalBar.whiteBarPercentage >= 50 && !boardOrientation) + {(evalBar.whiteBarPercentage < 50 && + boardOrientation === Color.White) || + (evalBar.whiteBarPercentage >= 50 && boardOrientation === Color.Black) ? evalBar.label : ""} @@ -71,11 +72,12 @@ export default function EvaluationBar({ height }: Props) { - {(evalBar.whiteBarPercentage >= 50 && boardOrientation) || - (evalBar.whiteBarPercentage < 50 && !boardOrientation) + {(evalBar.whiteBarPercentage >= 50 && + boardOrientation === Color.White) || + (evalBar.whiteBarPercentage < 50 && boardOrientation === Color.Black) ? evalBar.label : ""} diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx new file mode 100644 index 0000000..0170c3e --- /dev/null +++ b/src/components/board/index.tsx @@ -0,0 +1,284 @@ +import { Grid, Typography } from "@mui/material"; +import { Chessboard } from "react-chessboard"; +import { PrimitiveAtom, atom, useAtomValue, useSetAtom } from "jotai"; +import { + Arrow, + CustomSquareRenderer, + PromotionPieceOption, + Square, +} from "react-chessboard/dist/chessboard/types"; +import { useChessActions } from "@/hooks/useChessActions"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Color, MoveClassification } from "@/types/enums"; +import { Chess } from "chess.js"; +import { getSquareRenderer, moveClassificationColors } from "./squareRenderer"; +import { CurrentPosition } from "@/types/eval"; +import EvaluationBar from "./evaluationBar"; + +export interface Props { + id: string; + canPlay?: Color | boolean; + gameAtom: PrimitiveAtom; + boardSize?: number; + whitePlayer?: string; + blackPlayer?: string; + boardOrientation?: Color; + currentPositionAtom?: PrimitiveAtom; + showBestMoveArrow?: boolean; + showPlayerMoveIconAtom?: PrimitiveAtom; + showEvaluationBar?: boolean; +} + +export default function Board({ + id: boardId, + canPlay, + gameAtom, + boardSize, + whitePlayer, + blackPlayer, + boardOrientation = Color.White, + currentPositionAtom = atom({}), + showBestMoveArrow = false, + showPlayerMoveIconAtom, + showEvaluationBar = false, +}: Props) { + const boardRef = useRef(null); + const game = useAtomValue(gameAtom); + const { makeMove: makeGameMove } = useChessActions(gameAtom); + const clickedSquaresAtom = useMemo(() => atom([]), []); + const setClickedSquares = useSetAtom(clickedSquaresAtom); + const playableSquaresAtom = useMemo(() => atom([]), []); + const setPlayableSquares = useSetAtom(playableSquaresAtom); + const position = useAtomValue(currentPositionAtom); + const [showPromotionDialog, setShowPromotionDialog] = useState(false); + const [moveClickFrom, setMoveClickFrom] = useState(null); + const [moveClickTo, setMoveClickTo] = useState(null); + + const gameFen = game.fen(); + + useEffect(() => { + setClickedSquares([]); + }, [gameFen, setClickedSquares]); + + const isPieceDraggable = useCallback( + ({ piece }: { piece: string }): boolean => { + if (game.isGameOver() || !canPlay) return false; + if (canPlay === true || canPlay === piece[0]) return true; + return false; + }, + [canPlay, game] + ); + + const onPieceDrop = ( + source: Square, + target: Square, + piece: string + ): boolean => { + if (!isPieceDraggable({ piece })) return false; + + const result = makeGameMove({ + from: source, + to: target, + promotion: piece[1]?.toLowerCase() ?? "q", + }); + + return !!result; + }; + + const resetMoveClick = (square?: Square) => { + setMoveClickFrom(square ?? null); + setMoveClickTo(null); + setShowPromotionDialog(false); + if (square) { + const moves = game.moves({ square, verbose: true }); + setPlayableSquares(moves.map((m) => m.to)); + } else { + setPlayableSquares([]); + } + }; + + const handleSquareLeftClick = (square: Square) => { + setClickedSquares([]); + + if (!moveClickFrom) { + resetMoveClick(square); + return; + } + + const validMoves = game.moves({ square: moveClickFrom, verbose: true }); + const move = validMoves.find((m) => m.to === square); + + if (!move) { + resetMoveClick(square); + return; + } + + setMoveClickTo(square); + + if ( + move.piece === "p" && + ((move.color === "w" && square[1] === "8") || + (move.color === "b" && square[1] === "1")) + ) { + setShowPromotionDialog(true); + return; + } + + const result = makeGameMove({ + from: moveClickFrom, + to: square, + }); + + resetMoveClick(result ? undefined : square); + }; + + const handleSquareRightClick = (square: Square) => { + setClickedSquares((prev) => + prev.includes(square) + ? prev.filter((s) => s !== square) + : [...prev, square] + ); + }; + + const handlePieceDragBegin = (_: string, square: Square) => { + resetMoveClick(square); + }; + + const handlePieceDragEnd = () => { + resetMoveClick(); + }; + + const onPromotionPieceSelect = (piece?: PromotionPieceOption) => { + if (piece && moveClickFrom && moveClickTo) { + const result = makeGameMove({ + from: moveClickFrom, + to: moveClickTo, + promotion: piece[1]?.toLowerCase() ?? "q", + }); + resetMoveClick(); + return !!result; + } + + return false; + }; + + const customArrows: Arrow[] = useMemo(() => { + const bestMove = position?.lastEval?.bestMove; + const moveClassification = position?.eval?.moveClassification; + + if ( + bestMove && + showBestMoveArrow && + moveClassification !== MoveClassification.Book + ) { + const bestMoveArrow = [ + bestMove.slice(0, 2), + bestMove.slice(2, 4), + moveClassificationColors[MoveClassification.Best], + ] as Arrow; + + return [bestMoveArrow]; + } + + return []; + }, [position, showBestMoveArrow]); + + const SquareRenderer: CustomSquareRenderer = useMemo(() => { + return getSquareRenderer({ + currentPositionAtom: currentPositionAtom, + clickedSquaresAtom, + playableSquaresAtom, + showPlayerMoveIconAtom, + }); + }, [ + currentPositionAtom, + clickedSquaresAtom, + playableSquaresAtom, + showPlayerMoveIconAtom, + ]); + + return ( + + {showEvaluationBar && ( + + )} + + + + + {boardOrientation === Color.White ? blackPlayer : whitePlayer} + + + + + + + + + + {boardOrientation === Color.White ? whitePlayer : blackPlayer} + + + + + ); +} diff --git a/src/components/board/squareRenderer.tsx b/src/components/board/squareRenderer.tsx new file mode 100644 index 0000000..6db24d6 --- /dev/null +++ b/src/components/board/squareRenderer.tsx @@ -0,0 +1,118 @@ +import { CurrentPosition } from "@/types/eval"; +import { MoveClassification } from "@/types/enums"; +import { PrimitiveAtom, atom, useAtomValue } from "jotai"; +import Image from "next/image"; +import { CSSProperties, forwardRef } from "react"; +import { + CustomSquareProps, + Square, +} from "react-chessboard/dist/chessboard/types"; + +export interface Props { + currentPositionAtom: PrimitiveAtom; + clickedSquaresAtom: PrimitiveAtom; + playableSquaresAtom: PrimitiveAtom; + showPlayerMoveIconAtom?: PrimitiveAtom; +} + +export function getSquareRenderer({ + currentPositionAtom, + clickedSquaresAtom, + playableSquaresAtom, + showPlayerMoveIconAtom = atom(false), +}: Props) { + const squareRenderer = forwardRef( + (props, ref) => { + const { children, square, style } = props; + const showPlayerMoveIcon = useAtomValue(showPlayerMoveIconAtom); + const position = useAtomValue(currentPositionAtom); + const clickedSquares = useAtomValue(clickedSquaresAtom); + const playableSquares = useAtomValue(playableSquaresAtom); + + const fromSquare = position.lastMove?.from; + const toSquare = position.lastMove?.to; + const moveClassification = position?.eval?.moveClassification; + + const highlightSquareStyle: CSSProperties | undefined = + clickedSquares.includes(square) + ? rightClickSquareStyle + : fromSquare === square || toSquare === square + ? previousMoveSquareStyle(moveClassification) + : undefined; + + const playableSquareStyle: CSSProperties | undefined = + playableSquares.includes(square) ? playableSquareStyles : undefined; + + return ( +
+ {children} + {highlightSquareStyle &&
} + {playableSquareStyle &&
} + {moveClassification && showPlayerMoveIcon && square === toSquare && ( + move-icon + )} +
+ ); + } + ); + + squareRenderer.displayName = "SquareRenderer"; + + return squareRenderer; +} + +export const moveClassificationColors: Record = { + [MoveClassification.Book]: "#d5a47d", + [MoveClassification.Brilliant]: "#26c2a3", + [MoveClassification.Great]: "#4099ed", + [MoveClassification.Best]: "#3aab18", + [MoveClassification.Excellent]: "#3aab18", + [MoveClassification.Good]: "#81b64c", + [MoveClassification.Inaccuracy]: "#f7c631", + [MoveClassification.Mistake]: "#ffa459", + [MoveClassification.Blunder]: "#fa412d", +}; + +const rightClickSquareStyle: CSSProperties = { + position: "absolute", + width: "100%", + height: "100%", + backgroundColor: "#eb6150", + opacity: "0.8", +}; + +const playableSquareStyles: CSSProperties = { + position: "absolute", + width: "100%", + height: "100%", + backgroundColor: "rgba(0,0,0,.14)", + padding: "35%", + backgroundClip: "content-box", + borderRadius: "50%", + boxSizing: "border-box", +}; + +const previousMoveSquareStyle = ( + moveClassification?: MoveClassification +): CSSProperties => ({ + position: "absolute", + width: "100%", + height: "100%", + backgroundColor: moveClassification + ? moveClassificationColors[moveClassification] + : "#fad541", + opacity: 0.5, +}); diff --git a/src/hooks/useGameData.ts b/src/hooks/useGameData.ts index ea25f60..d1d668a 100644 --- a/src/hooks/useGameData.ts +++ b/src/hooks/useGameData.ts @@ -1,15 +1,11 @@ -import { Chess, Move } from "chess.js"; +import { CurrentPosition } from "@/types/eval"; +import { Chess } from "chess.js"; import { PrimitiveAtom, useAtom, useAtomValue } from "jotai"; import { useEffect } from "react"; -export interface GameData { - history: Move[]; - lastMove: Move | undefined; -} - export const useGameData = ( gameAtom: PrimitiveAtom, - gameDataAtom: PrimitiveAtom + gameDataAtom: PrimitiveAtom ) => { const game = useAtomValue(gameAtom); const [gameData, setGameData] = useAtom(gameDataAtom); @@ -17,7 +13,7 @@ export const useGameData = ( useEffect(() => { const history = game.history({ verbose: true }); const lastMove = history.at(-1); - setGameData({ history, lastMove }); + setGameData({ lastMove }); }, [game]); // eslint-disable-line react-hooks/exhaustive-deps return gameData; diff --git a/src/sections/analysis/board/index.tsx b/src/sections/analysis/board/index.tsx index c8780fa..201ba90 100644 --- a/src/sections/analysis/board/index.tsx +++ b/src/sections/analysis/board/index.tsx @@ -1,95 +1,24 @@ -import { Grid } from "@mui/material"; -import { Chessboard } from "react-chessboard"; -import { useAtomValue, useSetAtom } from "jotai"; +import { useAtomValue } from "jotai"; import { boardAtom, boardOrientationAtom, - clickedSquaresAtom, currentPositionAtom, - playableSquaresAtom, + gameAtom, showBestMoveArrowAtom, + showPlayerMoveIconAtom, } from "../states"; -import { Arrow, Square } from "react-chessboard/dist/chessboard/types"; -import { useChessActions } from "@/hooks/useChessActions"; -import { useEffect, useMemo, useRef } from "react"; -import PlayerInfo from "./playerInfo"; -import EvaluationBar from "./evaluationBar"; +import { useMemo } from "react"; import { useScreenSize } from "@/hooks/useScreenSize"; -import { MoveClassification } from "@/types/enums"; -import SquareRenderer, { moveClassificationColors } from "./squareRenderer"; +import { Color } from "@/types/enums"; +import Board from "@/components/board"; +import { useGameDatabase } from "@/hooks/useGameDatabase"; -export default function Board() { - const boardRef = useRef(null); +export default function BoardContainer() { const screenSize = useScreenSize(); - const board = useAtomValue(boardAtom); const boardOrientation = useAtomValue(boardOrientationAtom); const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom); - const { makeMove: makeBoardMove } = useChessActions(boardAtom); - const position = useAtomValue(currentPositionAtom); - const setClickedSquares = useSetAtom(clickedSquaresAtom); - const setPlayableSquares = useSetAtom(playableSquaresAtom); - - const boardFen = board.fen(); - - useEffect(() => { - setClickedSquares([]); - }, [boardFen, setClickedSquares]); - - const onPieceDrop = ( - source: Square, - target: Square, - piece: string - ): boolean => { - const result = makeBoardMove({ - from: source, - to: target, - promotion: piece[1]?.toLowerCase() ?? "q", - }); - - return !!result; - }; - - const handleSquareLeftClick = () => { - setClickedSquares([]); - }; - - const handleSquareRightClick = (square: Square) => { - setClickedSquares((prev) => - prev.includes(square) - ? prev.filter((s) => s !== square) - : [...prev, square] - ); - }; - - const handlePieceDragBegin = (_: string, square: Square) => { - const moves = board.moves({ square, verbose: true }); - setPlayableSquares(moves.map((m) => m.to)); - }; - - const handlePieceDragEnd = () => { - setPlayableSquares([]); - }; - - const customArrows: Arrow[] = useMemo(() => { - const bestMove = position?.lastEval?.bestMove; - const moveClassification = position?.eval?.moveClassification; - - if ( - bestMove && - showBestMoveArrow && - moveClassification !== MoveClassification.Book - ) { - const bestMoveArrow = [ - bestMove.slice(0, 2), - bestMove.slice(2, 4), - moveClassificationColors[MoveClassification.Best], - ] as Arrow; - - return [bestMoveArrow]; - } - - return []; - }, [position, showBestMoveArrow]); + const { gameFromUrl } = useGameDatabase(); + const game = useAtomValue(gameAtom); const boardSize = useMemo(() => { const width = screenSize.width; @@ -104,55 +33,22 @@ export default function Board() { }, [screenSize]); return ( - - - - - - - - - - - - - + ); } diff --git a/src/sections/analysis/board/playerInfo.tsx b/src/sections/analysis/board/playerInfo.tsx deleted file mode 100644 index 4bff709..0000000 --- a/src/sections/analysis/board/playerInfo.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useGameDatabase } from "@/hooks/useGameDatabase"; -import { Grid, Typography } from "@mui/material"; -import { useAtomValue } from "jotai"; -import { gameAtom } from "../states"; - -interface Props { - color: "white" | "black"; -} - -export default function PlayerInfo({ color }: Props) { - const { gameFromUrl } = useGameDatabase(); - const game = useAtomValue(gameAtom); - - const playerName = - gameFromUrl?.[color]?.name || - game.header()[color === "white" ? "White" : "Black"]; - - return ( - - - {playerName || (color === "white" ? "White" : "Black")} - - - ); -} diff --git a/src/sections/analysis/board/squareRenderer.tsx b/src/sections/analysis/board/squareRenderer.tsx deleted file mode 100644 index 22eec3f..0000000 --- a/src/sections/analysis/board/squareRenderer.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { - clickedSquaresAtom, - currentPositionAtom, - playableSquaresAtom, - showPlayerMoveIconAtom, -} from "../states"; -import { MoveClassification } from "@/types/enums"; -import { useAtomValue } from "jotai"; -import Image from "next/image"; -import { CSSProperties, forwardRef } from "react"; -import { CustomSquareProps } from "react-chessboard/dist/chessboard/types"; - -const SquareRenderer = forwardRef( - (props, ref) => { - const { children, square, style } = props; - const showPlayerMoveIcon = useAtomValue(showPlayerMoveIconAtom); - const position = useAtomValue(currentPositionAtom); - const clickedSquares = useAtomValue(clickedSquaresAtom); - const playableSquares = useAtomValue(playableSquaresAtom); - - const fromSquare = position.lastMove?.from; - const toSquare = position.lastMove?.to; - const moveClassification = position?.eval?.moveClassification; - - const highlightSquareStyle: CSSProperties | undefined = - clickedSquares.includes(square) - ? { - position: "absolute", - width: "100%", - height: "100%", - backgroundColor: "#eb6150", - opacity: "0.8", - } - : fromSquare === square || toSquare === square - ? { - position: "absolute", - width: "100%", - height: "100%", - backgroundColor: moveClassification - ? moveClassificationColors[moveClassification] - : "#fad541", - opacity: 0.5, - } - : undefined; - - const playableSquareStyle: CSSProperties | undefined = - playableSquares.includes(square) - ? { - position: "absolute", - width: "100%", - height: "100%", - backgroundColor: "rgba(0,0,0,.14)", - padding: "35%", - backgroundClip: "content-box", - borderRadius: "50%", - boxSizing: "border-box", - } - : undefined; - - return ( -
- {children} - {highlightSquareStyle &&
} - {playableSquareStyle &&
} - {moveClassification && showPlayerMoveIcon && square === toSquare && ( - move-icon - )} -
- ); - } -); - -SquareRenderer.displayName = "CustomSquareRenderer"; - -export default SquareRenderer; - -export const moveClassificationColors: Record = { - [MoveClassification.Book]: "#d5a47d", - [MoveClassification.Brilliant]: "#26c2a3", - [MoveClassification.Great]: "#4099ed", - [MoveClassification.Best]: "#3aab18", - [MoveClassification.Excellent]: "#3aab18", - [MoveClassification.Good]: "#81b64c", - [MoveClassification.Inaccuracy]: "#f7c631", - [MoveClassification.Mistake]: "#ffa459", - [MoveClassification.Blunder]: "#fa412d", -}; diff --git a/src/sections/play/board.tsx b/src/sections/play/board.tsx new file mode 100644 index 0000000..abf0921 --- /dev/null +++ b/src/sections/play/board.tsx @@ -0,0 +1,87 @@ +import { useAtomValue } from "jotai"; +import { + engineSkillLevelAtom, + gameAtom, + playerColorAtom, + isGameInProgressAtom, + gameDataAtom, +} from "./states"; +import { useChessActions } from "@/hooks/useChessActions"; +import { useEffect, useMemo } from "react"; +import { useScreenSize } from "@/hooks/useScreenSize"; +import { Color, EngineName } from "@/types/enums"; +import { useEngine } from "@/hooks/useEngine"; +import { uciMoveParams } from "@/lib/chess"; +import Board from "@/components/board"; +import { useGameData } from "@/hooks/useGameData"; + +export default function BoardContainer() { + const screenSize = useScreenSize(); + const engine = useEngine(EngineName.Stockfish16); + const game = useAtomValue(gameAtom); + const playerColor = useAtomValue(playerColorAtom); + const { makeMove: makeGameMove } = useChessActions(gameAtom); + const engineSkillLevel = useAtomValue(engineSkillLevelAtom); + const isGameInProgress = useAtomValue(isGameInProgressAtom); + + const gameFen = game.fen(); + const isGameFinished = game.isGameOver(); + + useEffect(() => { + const playEngineMove = async () => { + if ( + !engine?.isReady() || + game.turn() === playerColor || + isGameFinished || + !isGameInProgress + ) { + return; + } + const move = await engine.getEngineNextMove( + gameFen, + engineSkillLevel - 1 + ); + if (move) makeGameMove(uciMoveParams(move)); + }; + playEngineMove(); + + return () => { + engine?.stopSearch(); + }; + }, [gameFen, isGameInProgress]); // eslint-disable-line react-hooks/exhaustive-deps + + const boardSize = useMemo(() => { + const width = screenSize.width; + const height = screenSize.height; + + // 900 is the md layout breakpoint + if (window?.innerWidth < 900) { + return Math.min(width, height - 150); + } + + return Math.min(width - 300, height * 0.85); + }, [screenSize]); + + useGameData(gameAtom, gameDataAtom); + + return ( + + ); +} diff --git a/src/sections/play/board/index.tsx b/src/sections/play/board/index.tsx deleted file mode 100644 index 2e5f3a1..0000000 --- a/src/sections/play/board/index.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Grid } from "@mui/material"; -import { Chessboard } from "react-chessboard"; -import { useAtomValue, useSetAtom } from "jotai"; -import { - clickedSquaresAtom, - engineSkillLevelAtom, - gameAtom, - playableSquaresAtom, - playerColorAtom, - isGameInProgressAtom, - gameDataAtom, -} from "../states"; -import { Square } from "react-chessboard/dist/chessboard/types"; -import { useChessActions } from "@/hooks/useChessActions"; -import { useEffect, useMemo, useRef } from "react"; -import PlayerInfo from "./playerInfo"; -import { useScreenSize } from "@/hooks/useScreenSize"; -import { Color, EngineName } from "@/types/enums"; -import SquareRenderer from "./squareRenderer"; -import { useEngine } from "@/hooks/useEngine"; -import { uciMoveParams } from "@/lib/chess"; -import { useGameData } from "@/hooks/useGameData"; - -export default function Board() { - const boardRef = useRef(null); - const screenSize = useScreenSize(); - const engine = useEngine(EngineName.Stockfish16); - const game = useAtomValue(gameAtom); - const playerColor = useAtomValue(playerColorAtom); - const { makeMove: makeGameMove } = useChessActions(gameAtom); - const setClickedSquares = useSetAtom(clickedSquaresAtom); - const setPlayableSquares = useSetAtom(playableSquaresAtom); - const engineSkillLevel = useAtomValue(engineSkillLevelAtom); - const isGameInProgress = useAtomValue(isGameInProgressAtom); - useGameData(gameAtom, gameDataAtom); - - const gameFen = game.fen(); - const isGameFinished = game.isGameOver(); - - useEffect(() => { - const playEngineMove = async () => { - if ( - !engine?.isReady() || - game.turn() === playerColor || - isGameFinished || - !isGameInProgress - ) { - return; - } - const move = await engine.getEngineNextMove( - gameFen, - engineSkillLevel - 1 - ); - if (move) makeGameMove(uciMoveParams(move)); - }; - playEngineMove(); - - return () => { - engine?.stopSearch(); - }; - }, [gameFen, isGameInProgress]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - setClickedSquares([]); - }, [gameFen, setClickedSquares]); - - const onPieceDrop = ( - source: Square, - target: Square, - piece: string - ): boolean => { - if (!piece || piece[0] !== playerColor || !isGameInProgress) return false; - - const result = makeGameMove({ - from: source, - to: target, - promotion: piece[1]?.toLowerCase() ?? "q", - }); - - return !!result; - }; - - const isPieceDraggable = ({ piece }: { piece: string }): boolean => { - if (!piece) return false; - return playerColor === piece[0]; - }; - - const handleSquareLeftClick = () => { - setClickedSquares([]); - }; - - const handleSquareRightClick = (square: Square) => { - setClickedSquares((prev) => - prev.includes(square) - ? prev.filter((s) => s !== square) - : [...prev, square] - ); - }; - - const handlePieceDragBegin = (_: string, square: Square) => { - const moves = game.moves({ square, verbose: true }); - setPlayableSquares(moves.map((m) => m.to)); - }; - - const handlePieceDragEnd = () => { - setPlayableSquares([]); - }; - - const boardSize = useMemo(() => { - const width = screenSize.width; - const height = screenSize.height; - - // 900 is the md layout breakpoint - if (window?.innerWidth < 900) { - return Math.min(width, height - 150); - } - - return Math.min(width - 300, height * 0.85); - }, [screenSize]); - - return ( - - - - - - - - - - ); -} diff --git a/src/sections/play/board/playerInfo.tsx b/src/sections/play/board/playerInfo.tsx deleted file mode 100644 index 307a8eb..0000000 --- a/src/sections/play/board/playerInfo.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Grid, Typography } from "@mui/material"; -import { useAtomValue } from "jotai"; -import { engineSkillLevelAtom, playerColorAtom } from "../states"; -import { Color } from "@/types/enums"; - -interface Props { - color: Color; -} - -export default function PlayerInfo({ color }: Props) { - const playerColor = useAtomValue(playerColorAtom); - const skillLevel = useAtomValue(engineSkillLevelAtom); - - const playerName = - playerColor === color ? "You 🧠" : `Stockfish level ${skillLevel} 🤖`; - - return ( - - {playerName} - - ); -} diff --git a/src/sections/play/board/squareRenderer.tsx b/src/sections/play/board/squareRenderer.tsx deleted file mode 100644 index f165304..0000000 --- a/src/sections/play/board/squareRenderer.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - clickedSquaresAtom, - gameDataAtom, - playableSquaresAtom, -} from "../states"; -import { useAtomValue } from "jotai"; -import { CSSProperties, forwardRef } from "react"; -import { CustomSquareProps } from "react-chessboard/dist/chessboard/types"; - -const SquareRenderer = forwardRef( - (props, ref) => { - const { children, square, style } = props; - const clickedSquares = useAtomValue(clickedSquaresAtom); - const playableSquares = useAtomValue(playableSquaresAtom); - const gameData = useAtomValue(gameDataAtom); - - const fromSquare = gameData.lastMove?.from; - const toSquare = gameData.lastMove?.to; - - const highlightSquareStyle: CSSProperties | undefined = - clickedSquares.includes(square) - ? { - position: "absolute", - width: "100%", - height: "100%", - backgroundColor: "#eb6150", - opacity: "0.8", - } - : fromSquare === square || toSquare === square - ? { - position: "absolute", - width: "100%", - height: "100%", - backgroundColor: "#fad541", - opacity: 0.5, - } - : undefined; - - const playableSquareStyle: CSSProperties | undefined = - playableSquares.includes(square) - ? { - position: "absolute", - width: "100%", - height: "100%", - backgroundColor: "rgba(0,0,0,.14)", - padding: "35%", - backgroundClip: "content-box", - borderRadius: "50%", - boxSizing: "border-box", - } - : undefined; - - return ( -
- {children} - {highlightSquareStyle &&
} - {playableSquareStyle &&
} -
- ); - } -); - -SquareRenderer.displayName = "CustomSquareRenderer"; - -export default SquareRenderer; diff --git a/src/sections/play/gameRecap.tsx b/src/sections/play/gameRecap.tsx index 7969aee..2115ca8 100644 --- a/src/sections/play/gameRecap.tsx +++ b/src/sections/play/gameRecap.tsx @@ -1,10 +1,5 @@ import { useAtomValue } from "jotai"; -import { - gameAtom, - gameDataAtom, - isGameInProgressAtom, - playerColorAtom, -} from "./states"; +import { gameAtom, isGameInProgressAtom, playerColorAtom } from "./states"; import { Button, Grid, Typography } from "@mui/material"; import { Color } from "@/types/enums"; import { setGameHeaders } from "@/lib/chess"; @@ -13,13 +8,12 @@ import { useRouter } from "next/router"; export default function GameRecap() { const game = useAtomValue(gameAtom); - const gameData = useAtomValue(gameDataAtom); const playerColor = useAtomValue(playerColorAtom); const isGameInProgress = useAtomValue(isGameInProgressAtom); const { addGame } = useGameDatabase(); const router = useRouter(); - if (isGameInProgress || !gameData.history.length) return null; + if (isGameInProgress || !game.history().length) return null; const getResultLabel = () => { if (game.isCheckmate()) { diff --git a/src/sections/play/gameSettings/gameSettingsButton.tsx b/src/sections/play/gameSettings/gameSettingsButton.tsx index cae7af2..0a97ef4 100644 --- a/src/sections/play/gameSettings/gameSettingsButton.tsx +++ b/src/sections/play/gameSettings/gameSettingsButton.tsx @@ -1,17 +1,17 @@ import { Button } from "@mui/material"; import { useState } from "react"; import GameSettingsDialog from "./gameSettingsDialog"; +import { gameAtom } from "../states"; import { useAtomValue } from "jotai"; -import { gameDataAtom } from "../states"; export default function GameSettingsButton() { const [openDialog, setOpenDialog] = useState(false); - const gameData = useAtomValue(gameDataAtom); + const game = useAtomValue(gameAtom); return ( <> ({ - history: [], - lastMove: undefined, -}); +export const gameDataAtom = atom({}); export const playerColorAtom = atom(Color.White); export const engineSkillLevelAtom = atom(1); export const isGameInProgressAtom = atom(false); - -export const clickedSquaresAtom = atom([]); -export const playableSquaresAtom = atom([]);