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:
105
src/components/board/evaluationBar.tsx
Normal file
105
src/components/board/evaluationBar.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Box, Grid, Typography } from "@mui/material";
|
||||
import { PrimitiveAtom, atom, useAtomValue } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getEvaluationBarValue } from "@/lib/chess";
|
||||
import { Color } from "@/types/enums";
|
||||
import { CurrentPosition } from "@/types/eval";
|
||||
|
||||
interface Props {
|
||||
height: number;
|
||||
boardOrientation?: Color;
|
||||
currentPositionAtom?: PrimitiveAtom<CurrentPosition>;
|
||||
}
|
||||
|
||||
export default function EvaluationBar({
|
||||
height,
|
||||
boardOrientation,
|
||||
currentPositionAtom = atom({}),
|
||||
}: Props) {
|
||||
const [evalBar, setEvalBar] = useState({
|
||||
whiteBarPercentage: 50,
|
||||
label: "0.0",
|
||||
});
|
||||
const position = useAtomValue(currentPositionAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const bestLine = position?.eval?.lines[0];
|
||||
if (!position.eval || !bestLine || bestLine.depth < 6) return;
|
||||
|
||||
const evalBar = getEvaluationBarValue(position.eval);
|
||||
setEvalBar(evalBar);
|
||||
}, [position]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="2rem"
|
||||
height={height}
|
||||
border="1px solid black"
|
||||
borderRadius="5px"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor:
|
||||
boardOrientation === Color.White ? "#424242" : "white",
|
||||
transition: "height 1s",
|
||||
}}
|
||||
height={`${
|
||||
boardOrientation === Color.White
|
||||
? 100 - evalBar.whiteBarPercentage
|
||||
: evalBar.whiteBarPercentage
|
||||
}%`}
|
||||
width="100%"
|
||||
borderRadius={
|
||||
evalBar.whiteBarPercentage === 100 ? "5px" : "5px 5px 0 0"
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
color={boardOrientation === Color.White ? "white" : "black"}
|
||||
textAlign="center"
|
||||
width="100%"
|
||||
>
|
||||
{(evalBar.whiteBarPercentage < 50 &&
|
||||
boardOrientation === Color.White) ||
|
||||
(evalBar.whiteBarPercentage >= 50 && boardOrientation === Color.Black)
|
||||
? evalBar.label
|
||||
: ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor:
|
||||
boardOrientation === Color.White ? "white" : "#424242",
|
||||
transition: "height 1s",
|
||||
}}
|
||||
height={`${
|
||||
boardOrientation === Color.White
|
||||
? evalBar.whiteBarPercentage
|
||||
: 100 - evalBar.whiteBarPercentage
|
||||
}%`}
|
||||
width={"100%"}
|
||||
display="flex"
|
||||
alignItems="flex-end"
|
||||
borderRadius={
|
||||
evalBar.whiteBarPercentage === 100 ? "5px" : "0 0 5px 5px"
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
color={boardOrientation === Color.White ? "black" : "white"}
|
||||
textAlign="center"
|
||||
width="100%"
|
||||
>
|
||||
{(evalBar.whiteBarPercentage >= 50 &&
|
||||
boardOrientation === Color.White) ||
|
||||
(evalBar.whiteBarPercentage < 50 && boardOrientation === Color.Black)
|
||||
? evalBar.label
|
||||
: ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
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,
|
||||
});
|
||||
Reference in New Issue
Block a user