Squashed commit of the following:
commit dbb5ce37add830b04e3cb977ca5caa9ae9429001 Author: GuillaumeSD <gsd.lfny@gmail.com> Date: Tue Mar 26 03:07:38 2024 +0100 feat : add move click commit a6d1d10d452a1e556b6e2ecb1fd12ada135b96d0 Author: GuillaumeSD <gsd.lfny@gmail.com> Date: Tue Mar 26 01:44:49 2024 +0100 feat : board refacto done commit 55f7d6dbac4cb135796cf66120de613e0bf34462 Author: GuillaumeSD <gsd.lfny@gmail.com> Date: Sun Mar 24 04:00:35 2024 +0100 feat : add click to move
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -21,7 +21,7 @@
|
|||||||
"jotai": "^2.6.4",
|
"jotai": "^2.6.4",
|
||||||
"next": "13.5.6",
|
"next": "13.5.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-chessboard": "^4.4.0",
|
"react-chessboard": "^4.5.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -4922,9 +4922,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-chessboard": {
|
"node_modules/react-chessboard": {
|
||||||
"version": "4.4.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-chessboard/-/react-chessboard-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-chessboard/-/react-chessboard-4.5.0.tgz",
|
||||||
"integrity": "sha512-UJuFVju9pEcPJvH76HQyccm4uHyU755/Uf/3e1QfMHGWLFCBeEMSUkbzhlvxSL8Mutj/4wRxBVyJMyjhtnX0xg==",
|
"integrity": "sha512-YS91zFMZlaW05vIFOAkM2KnR5wp/sqBCzglUe1GJrXOtVkxzDT7Tr6cetm2txMGmSwdFQgurIi2/wKV1YQs34w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"jotai": "^2.6.4",
|
"jotai": "^2.6.4",
|
||||||
"next": "13.5.6",
|
"next": "13.5.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-chessboard": "^4.4.0",
|
"react-chessboard": "^4.5.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import { Box, Grid, Typography } from "@mui/material";
|
import { Box, Grid, Typography } from "@mui/material";
|
||||||
import { useAtomValue } from "jotai";
|
import { PrimitiveAtom, atom, useAtomValue } from "jotai";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
|
||||||
boardAtom,
|
|
||||||
boardOrientationAtom,
|
|
||||||
currentPositionAtom,
|
|
||||||
} from "../states";
|
|
||||||
import { getEvaluationBarValue } from "@/lib/chess";
|
import { getEvaluationBarValue } from "@/lib/chess";
|
||||||
|
import { Color } from "@/types/enums";
|
||||||
|
import { CurrentPosition } from "@/types/eval";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
height: number;
|
height: number;
|
||||||
|
boardOrientation?: Color;
|
||||||
|
currentPositionAtom?: PrimitiveAtom<CurrentPosition>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EvaluationBar({ height }: Props) {
|
export default function EvaluationBar({
|
||||||
|
height,
|
||||||
|
boardOrientation,
|
||||||
|
currentPositionAtom = atom({}),
|
||||||
|
}: Props) {
|
||||||
const [evalBar, setEvalBar] = useState({
|
const [evalBar, setEvalBar] = useState({
|
||||||
whiteBarPercentage: 50,
|
whiteBarPercentage: 50,
|
||||||
label: "0.0",
|
label: "0.0",
|
||||||
});
|
});
|
||||||
const board = useAtomValue(boardAtom);
|
|
||||||
const boardOrientation = useAtomValue(boardOrientationAtom);
|
|
||||||
const position = useAtomValue(currentPositionAtom);
|
const position = useAtomValue(currentPositionAtom);
|
||||||
|
|
||||||
const isWhiteToPlay = board.turn() === "w";
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bestLine = position?.eval?.lines[0];
|
const bestLine = position?.eval?.lines[0];
|
||||||
if (!position.eval || !bestLine || bestLine.depth < 6) return;
|
if (!position.eval || !bestLine || bestLine.depth < 6) return;
|
||||||
|
|
||||||
const evalBar = getEvaluationBarValue(position.eval);
|
const evalBar = getEvaluationBarValue(position.eval);
|
||||||
setEvalBar(evalBar);
|
setEvalBar(evalBar);
|
||||||
}, [position, isWhiteToPlay]);
|
}, [position]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
@@ -44,11 +43,12 @@ export default function EvaluationBar({ height }: Props) {
|
|||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: boardOrientation ? "#424242" : "white",
|
backgroundColor:
|
||||||
|
boardOrientation === Color.White ? "#424242" : "white",
|
||||||
transition: "height 1s",
|
transition: "height 1s",
|
||||||
}}
|
}}
|
||||||
height={`${
|
height={`${
|
||||||
boardOrientation
|
boardOrientation === Color.White
|
||||||
? 100 - evalBar.whiteBarPercentage
|
? 100 - evalBar.whiteBarPercentage
|
||||||
: evalBar.whiteBarPercentage
|
: evalBar.whiteBarPercentage
|
||||||
}%`}
|
}%`}
|
||||||
@@ -58,12 +58,13 @@ export default function EvaluationBar({ height }: Props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
color={boardOrientation ? "white" : "black"}
|
color={boardOrientation === Color.White ? "white" : "black"}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
width="100%"
|
width="100%"
|
||||||
>
|
>
|
||||||
{(evalBar.whiteBarPercentage < 50 && boardOrientation) ||
|
{(evalBar.whiteBarPercentage < 50 &&
|
||||||
(evalBar.whiteBarPercentage >= 50 && !boardOrientation)
|
boardOrientation === Color.White) ||
|
||||||
|
(evalBar.whiteBarPercentage >= 50 && boardOrientation === Color.Black)
|
||||||
? evalBar.label
|
? evalBar.label
|
||||||
: ""}
|
: ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -71,11 +72,12 @@ export default function EvaluationBar({ height }: Props) {
|
|||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: boardOrientation ? "white" : "#424242",
|
backgroundColor:
|
||||||
|
boardOrientation === Color.White ? "white" : "#424242",
|
||||||
transition: "height 1s",
|
transition: "height 1s",
|
||||||
}}
|
}}
|
||||||
height={`${
|
height={`${
|
||||||
boardOrientation
|
boardOrientation === Color.White
|
||||||
? evalBar.whiteBarPercentage
|
? evalBar.whiteBarPercentage
|
||||||
: 100 - evalBar.whiteBarPercentage
|
: 100 - evalBar.whiteBarPercentage
|
||||||
}%`}
|
}%`}
|
||||||
@@ -87,12 +89,13 @@ export default function EvaluationBar({ height }: Props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
color={boardOrientation ? "black" : "white"}
|
color={boardOrientation === Color.White ? "black" : "white"}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
width="100%"
|
width="100%"
|
||||||
>
|
>
|
||||||
{(evalBar.whiteBarPercentage >= 50 && boardOrientation) ||
|
{(evalBar.whiteBarPercentage >= 50 &&
|
||||||
(evalBar.whiteBarPercentage < 50 && !boardOrientation)
|
boardOrientation === Color.White) ||
|
||||||
|
(evalBar.whiteBarPercentage < 50 && boardOrientation === Color.Black)
|
||||||
? evalBar.label
|
? evalBar.label
|
||||||
: ""}
|
: ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
284
src/components/board/index.tsx
Normal file
284
src/components/board/index.tsx
Normal file
@@ -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<Chess>;
|
||||||
|
boardSize?: number;
|
||||||
|
whitePlayer?: string;
|
||||||
|
blackPlayer?: string;
|
||||||
|
boardOrientation?: Color;
|
||||||
|
currentPositionAtom?: PrimitiveAtom<CurrentPosition>;
|
||||||
|
showBestMoveArrow?: boolean;
|
||||||
|
showPlayerMoveIconAtom?: PrimitiveAtom<boolean>;
|
||||||
|
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<HTMLDivElement>(null);
|
||||||
|
const game = useAtomValue(gameAtom);
|
||||||
|
const { makeMove: makeGameMove } = useChessActions(gameAtom);
|
||||||
|
const clickedSquaresAtom = useMemo(() => atom<Square[]>([]), []);
|
||||||
|
const setClickedSquares = useSetAtom(clickedSquaresAtom);
|
||||||
|
const playableSquaresAtom = useMemo(() => atom<Square[]>([]), []);
|
||||||
|
const setPlayableSquares = useSetAtom(playableSquaresAtom);
|
||||||
|
const position = useAtomValue(currentPositionAtom);
|
||||||
|
const [showPromotionDialog, setShowPromotionDialog] = useState(false);
|
||||||
|
const [moveClickFrom, setMoveClickFrom] = useState<Square | null>(null);
|
||||||
|
const [moveClickTo, setMoveClickTo] = useState<Square | null>(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 (
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
wrap="nowrap"
|
||||||
|
width={boardSize}
|
||||||
|
>
|
||||||
|
{showEvaluationBar && (
|
||||||
|
<EvaluationBar
|
||||||
|
height={boardRef?.current?.offsetHeight || boardSize || 400}
|
||||||
|
boardOrientation={boardOrientation}
|
||||||
|
currentPositionAtom={currentPositionAtom}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
rowGap={1}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
paddingLeft={showEvaluationBar ? 2 : 0}
|
||||||
|
xs
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
xs={12}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{boardOrientation === Color.White ? blackPlayer : whitePlayer}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
ref={boardRef}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
|
<Chessboard
|
||||||
|
id={`${boardId}-${canPlay}`}
|
||||||
|
position={gameFen}
|
||||||
|
onPieceDrop={onPieceDrop}
|
||||||
|
boardOrientation={
|
||||||
|
boardOrientation === Color.White ? "white" : "black"
|
||||||
|
}
|
||||||
|
customBoardStyle={{
|
||||||
|
borderRadius: "5px",
|
||||||
|
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",
|
||||||
|
}}
|
||||||
|
customArrows={customArrows}
|
||||||
|
isDraggablePiece={isPieceDraggable}
|
||||||
|
customSquare={SquareRenderer}
|
||||||
|
onSquareClick={handleSquareLeftClick}
|
||||||
|
onSquareRightClick={handleSquareRightClick}
|
||||||
|
onPieceDragBegin={handlePieceDragBegin}
|
||||||
|
onPieceDragEnd={handlePieceDragEnd}
|
||||||
|
onPromotionPieceSelect={onPromotionPieceSelect}
|
||||||
|
showPromotionDialog={showPromotionDialog}
|
||||||
|
promotionToSquare={moveClickTo}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
xs={12}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{boardOrientation === Color.White ? whitePlayer : blackPlayer}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
src/components/board/squareRenderer.tsx
Normal file
118
src/components/board/squareRenderer.tsx
Normal file
@@ -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<CurrentPosition>;
|
||||||
|
clickedSquaresAtom: PrimitiveAtom<Square[]>;
|
||||||
|
playableSquaresAtom: PrimitiveAtom<Square[]>;
|
||||||
|
showPlayerMoveIconAtom?: PrimitiveAtom<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSquareRenderer({
|
||||||
|
currentPositionAtom,
|
||||||
|
clickedSquaresAtom,
|
||||||
|
playableSquaresAtom,
|
||||||
|
showPlayerMoveIconAtom = atom(false),
|
||||||
|
}: Props) {
|
||||||
|
const squareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>(
|
||||||
|
(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 (
|
||||||
|
<div ref={ref} style={{ ...style, position: "relative" }}>
|
||||||
|
{children}
|
||||||
|
{highlightSquareStyle && <div style={highlightSquareStyle} />}
|
||||||
|
{playableSquareStyle && <div style={playableSquareStyle} />}
|
||||||
|
{moveClassification && showPlayerMoveIcon && square === toSquare && (
|
||||||
|
<Image
|
||||||
|
src={`/icons/${moveClassification}.png`}
|
||||||
|
alt="move-icon"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "max(-15px, -1.8vw)",
|
||||||
|
right: "max(-15px, -1.8vw)",
|
||||||
|
maxWidth: "3.6vw",
|
||||||
|
maxHeight: "3.6vw",
|
||||||
|
zIndex: 100,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
squareRenderer.displayName = "SquareRenderer";
|
||||||
|
|
||||||
|
return squareRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moveClassificationColors: Record<MoveClassification, string> = {
|
||||||
|
[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,
|
||||||
|
});
|
||||||
@@ -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 { PrimitiveAtom, useAtom, useAtomValue } from "jotai";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export interface GameData {
|
|
||||||
history: Move[];
|
|
||||||
lastMove: Move | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGameData = (
|
export const useGameData = (
|
||||||
gameAtom: PrimitiveAtom<Chess>,
|
gameAtom: PrimitiveAtom<Chess>,
|
||||||
gameDataAtom: PrimitiveAtom<GameData>
|
gameDataAtom: PrimitiveAtom<CurrentPosition>
|
||||||
) => {
|
) => {
|
||||||
const game = useAtomValue(gameAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
const [gameData, setGameData] = useAtom(gameDataAtom);
|
const [gameData, setGameData] = useAtom(gameDataAtom);
|
||||||
@@ -17,7 +13,7 @@ export const useGameData = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const history = game.history({ verbose: true });
|
const history = game.history({ verbose: true });
|
||||||
const lastMove = history.at(-1);
|
const lastMove = history.at(-1);
|
||||||
setGameData({ history, lastMove });
|
setGameData({ lastMove });
|
||||||
}, [game]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [game]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return gameData;
|
return gameData;
|
||||||
|
|||||||
@@ -1,95 +1,24 @@
|
|||||||
import { Grid } from "@mui/material";
|
import { useAtomValue } from "jotai";
|
||||||
import { Chessboard } from "react-chessboard";
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
|
||||||
import {
|
import {
|
||||||
boardAtom,
|
boardAtom,
|
||||||
boardOrientationAtom,
|
boardOrientationAtom,
|
||||||
clickedSquaresAtom,
|
|
||||||
currentPositionAtom,
|
currentPositionAtom,
|
||||||
playableSquaresAtom,
|
gameAtom,
|
||||||
showBestMoveArrowAtom,
|
showBestMoveArrowAtom,
|
||||||
|
showPlayerMoveIconAtom,
|
||||||
} from "../states";
|
} from "../states";
|
||||||
import { Arrow, Square } from "react-chessboard/dist/chessboard/types";
|
import { useMemo } from "react";
|
||||||
import { useChessActions } from "@/hooks/useChessActions";
|
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
|
||||||
import PlayerInfo from "./playerInfo";
|
|
||||||
import EvaluationBar from "./evaluationBar";
|
|
||||||
import { useScreenSize } from "@/hooks/useScreenSize";
|
import { useScreenSize } from "@/hooks/useScreenSize";
|
||||||
import { MoveClassification } from "@/types/enums";
|
import { Color } from "@/types/enums";
|
||||||
import SquareRenderer, { moveClassificationColors } from "./squareRenderer";
|
import Board from "@/components/board";
|
||||||
|
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||||
|
|
||||||
export default function Board() {
|
export default function BoardContainer() {
|
||||||
const boardRef = useRef<HTMLDivElement>(null);
|
|
||||||
const screenSize = useScreenSize();
|
const screenSize = useScreenSize();
|
||||||
const board = useAtomValue(boardAtom);
|
|
||||||
const boardOrientation = useAtomValue(boardOrientationAtom);
|
const boardOrientation = useAtomValue(boardOrientationAtom);
|
||||||
const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom);
|
const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom);
|
||||||
const { makeMove: makeBoardMove } = useChessActions(boardAtom);
|
const { gameFromUrl } = useGameDatabase();
|
||||||
const position = useAtomValue(currentPositionAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
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 boardSize = useMemo(() => {
|
const boardSize = useMemo(() => {
|
||||||
const width = screenSize.width;
|
const width = screenSize.width;
|
||||||
@@ -104,55 +33,22 @@ export default function Board() {
|
|||||||
}, [screenSize]);
|
}, [screenSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Board
|
||||||
item
|
id="AnalysisBoard"
|
||||||
container
|
boardSize={boardSize}
|
||||||
justifyContent="center"
|
canPlay={true}
|
||||||
alignItems="center"
|
gameAtom={boardAtom}
|
||||||
wrap="nowrap"
|
whitePlayer={
|
||||||
width={boardSize}
|
gameFromUrl?.white?.name || game.header()["White"] || "White"
|
||||||
>
|
}
|
||||||
<EvaluationBar height={boardRef?.current?.offsetHeight || boardSize} />
|
blackPlayer={
|
||||||
|
gameFromUrl?.black?.name || game.header()["Black"] || "Black"
|
||||||
<Grid
|
}
|
||||||
item
|
boardOrientation={boardOrientation ? Color.White : Color.Black}
|
||||||
container
|
currentPositionAtom={currentPositionAtom}
|
||||||
rowGap={1}
|
showBestMoveArrow={showBestMoveArrow}
|
||||||
justifyContent="center"
|
showPlayerMoveIconAtom={showPlayerMoveIconAtom}
|
||||||
alignItems="center"
|
showEvaluationBar={true}
|
||||||
paddingLeft={2}
|
/>
|
||||||
xs
|
|
||||||
>
|
|
||||||
<PlayerInfo color={boardOrientation ? "black" : "white"} />
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
container
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
ref={boardRef}
|
|
||||||
xs={12}
|
|
||||||
>
|
|
||||||
<Chessboard
|
|
||||||
id="AnalysisBoard"
|
|
||||||
position={boardFen}
|
|
||||||
onPieceDrop={onPieceDrop}
|
|
||||||
boardOrientation={boardOrientation ? "white" : "black"}
|
|
||||||
customArrows={customArrows}
|
|
||||||
customBoardStyle={{
|
|
||||||
borderRadius: "5px",
|
|
||||||
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",
|
|
||||||
}}
|
|
||||||
customSquare={SquareRenderer}
|
|
||||||
onSquareClick={handleSquareLeftClick}
|
|
||||||
onSquareRightClick={handleSquareRightClick}
|
|
||||||
onPieceDragBegin={handlePieceDragBegin}
|
|
||||||
onPieceDragEnd={handlePieceDragEnd}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<PlayerInfo color={boardOrientation ? "white" : "black"} />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
|
||||||
<Typography variant="h6">
|
|
||||||
{playerName || (color === "white" ? "White" : "Black")}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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<HTMLDivElement, CustomSquareProps>(
|
|
||||||
(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 (
|
|
||||||
<div ref={ref} style={{ ...style, position: "relative" }}>
|
|
||||||
{children}
|
|
||||||
{highlightSquareStyle && <div style={highlightSquareStyle} />}
|
|
||||||
{playableSquareStyle && <div style={playableSquareStyle} />}
|
|
||||||
{moveClassification && showPlayerMoveIcon && square === toSquare && (
|
|
||||||
<Image
|
|
||||||
src={`/icons/${moveClassification}.png`}
|
|
||||||
alt="move-icon"
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "max(-15px, -1.8vw)",
|
|
||||||
right: "max(-15px, -1.8vw)",
|
|
||||||
maxWidth: "3.6vw",
|
|
||||||
maxHeight: "3.6vw",
|
|
||||||
zIndex: 100,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
SquareRenderer.displayName = "CustomSquareRenderer";
|
|
||||||
|
|
||||||
export default SquareRenderer;
|
|
||||||
|
|
||||||
export const moveClassificationColors: Record<MoveClassification, string> = {
|
|
||||||
[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",
|
|
||||||
};
|
|
||||||
87
src/sections/play/board.tsx
Normal file
87
src/sections/play/board.tsx
Normal file
@@ -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 (
|
||||||
|
<Board
|
||||||
|
id="PlayBoard"
|
||||||
|
canPlay={isGameInProgress ? playerColor : false}
|
||||||
|
gameAtom={gameAtom}
|
||||||
|
boardSize={boardSize}
|
||||||
|
whitePlayer={
|
||||||
|
playerColor === Color.White
|
||||||
|
? "You 🧠"
|
||||||
|
: `Stockfish level ${engineSkillLevel} 🤖`
|
||||||
|
}
|
||||||
|
blackPlayer={
|
||||||
|
playerColor === Color.Black
|
||||||
|
? "You 🧠"
|
||||||
|
: `Stockfish level ${engineSkillLevel} 🤖`
|
||||||
|
}
|
||||||
|
boardOrientation={playerColor}
|
||||||
|
currentPositionAtom={gameDataAtom}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<HTMLDivElement>(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 (
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
container
|
|
||||||
rowGap={1}
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
width={boardSize}
|
|
||||||
>
|
|
||||||
<PlayerInfo
|
|
||||||
color={playerColor === Color.White ? Color.Black : Color.White}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
container
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
ref={boardRef}
|
|
||||||
xs={12}
|
|
||||||
>
|
|
||||||
<Chessboard
|
|
||||||
id="PlayBoard"
|
|
||||||
position={gameFen}
|
|
||||||
onPieceDrop={onPieceDrop}
|
|
||||||
boardOrientation={playerColor === Color.White ? "white" : "black"}
|
|
||||||
customBoardStyle={{
|
|
||||||
borderRadius: "5px",
|
|
||||||
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",
|
|
||||||
}}
|
|
||||||
isDraggablePiece={isPieceDraggable}
|
|
||||||
customSquare={SquareRenderer}
|
|
||||||
onSquareClick={handleSquareLeftClick}
|
|
||||||
onSquareRightClick={handleSquareRightClick}
|
|
||||||
onPieceDragBegin={handlePieceDragBegin}
|
|
||||||
onPieceDragEnd={handlePieceDragEnd}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<PlayerInfo color={playerColor} />
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
|
||||||
<Typography variant="h6">{playerName}</Typography>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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<HTMLDivElement, CustomSquareProps>(
|
|
||||||
(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 (
|
|
||||||
<div ref={ref} style={{ ...style, position: "relative" }}>
|
|
||||||
{children}
|
|
||||||
{highlightSquareStyle && <div style={highlightSquareStyle} />}
|
|
||||||
{playableSquareStyle && <div style={playableSquareStyle} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
SquareRenderer.displayName = "CustomSquareRenderer";
|
|
||||||
|
|
||||||
export default SquareRenderer;
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import {
|
import { gameAtom, isGameInProgressAtom, playerColorAtom } from "./states";
|
||||||
gameAtom,
|
|
||||||
gameDataAtom,
|
|
||||||
isGameInProgressAtom,
|
|
||||||
playerColorAtom,
|
|
||||||
} from "./states";
|
|
||||||
import { Button, Grid, Typography } from "@mui/material";
|
import { Button, Grid, Typography } from "@mui/material";
|
||||||
import { Color } from "@/types/enums";
|
import { Color } from "@/types/enums";
|
||||||
import { setGameHeaders } from "@/lib/chess";
|
import { setGameHeaders } from "@/lib/chess";
|
||||||
@@ -13,13 +8,12 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
export default function GameRecap() {
|
export default function GameRecap() {
|
||||||
const game = useAtomValue(gameAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
const gameData = useAtomValue(gameDataAtom);
|
|
||||||
const playerColor = useAtomValue(playerColorAtom);
|
const playerColor = useAtomValue(playerColorAtom);
|
||||||
const isGameInProgress = useAtomValue(isGameInProgressAtom);
|
const isGameInProgress = useAtomValue(isGameInProgressAtom);
|
||||||
const { addGame } = useGameDatabase();
|
const { addGame } = useGameDatabase();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
if (isGameInProgress || !gameData.history.length) return null;
|
if (isGameInProgress || !game.history().length) return null;
|
||||||
|
|
||||||
const getResultLabel = () => {
|
const getResultLabel = () => {
|
||||||
if (game.isCheckmate()) {
|
if (game.isCheckmate()) {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import GameSettingsDialog from "./gameSettingsDialog";
|
import GameSettingsDialog from "./gameSettingsDialog";
|
||||||
|
import { gameAtom } from "../states";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { gameDataAtom } from "../states";
|
|
||||||
|
|
||||||
export default function GameSettingsButton() {
|
export default function GameSettingsButton() {
|
||||||
const [openDialog, setOpenDialog] = useState(false);
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
const gameData = useAtomValue(gameDataAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button variant="contained" onClick={() => setOpenDialog(true)}>
|
<Button variant="contained" onClick={() => setOpenDialog(true)}>
|
||||||
{gameData.history.length ? "Start new game" : "Start game"}
|
{game.history().length ? "Start new game" : "Start game"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<GameSettingsDialog
|
<GameSettingsDialog
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import { GameData } from "@/hooks/useGameData";
|
|
||||||
import { Color } from "@/types/enums";
|
import { Color } from "@/types/enums";
|
||||||
|
import { CurrentPosition } from "@/types/eval";
|
||||||
import { Chess } from "chess.js";
|
import { Chess } from "chess.js";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
|
|
||||||
export const gameAtom = atom(new Chess());
|
export const gameAtom = atom(new Chess());
|
||||||
export const gameDataAtom = atom<GameData>({
|
export const gameDataAtom = atom<CurrentPosition>({});
|
||||||
history: [],
|
|
||||||
lastMove: undefined,
|
|
||||||
});
|
|
||||||
export const playerColorAtom = atom<Color>(Color.White);
|
export const playerColorAtom = atom<Color>(Color.White);
|
||||||
export const engineSkillLevelAtom = atom<number>(1);
|
export const engineSkillLevelAtom = atom<number>(1);
|
||||||
export const isGameInProgressAtom = atom(false);
|
export const isGameInProgressAtom = atom(false);
|
||||||
|
|
||||||
export const clickedSquaresAtom = atom<string[]>([]);
|
|
||||||
export const playableSquaresAtom = atom<string[]>([]);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user