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:
GuillaumeSD
2024-03-26 03:08:34 +01:00
parent 8355dbc4e8
commit cd514d90cf
16 changed files with 557 additions and 560 deletions

View 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>
);
}

View 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>
);
}

View 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,
});