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

@@ -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<CurrentPosition>;
}
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 (
<Grid
@@ -44,11 +43,12 @@ export default function EvaluationBar({ height }: Props) {
>
<Box
sx={{
backgroundColor: boardOrientation ? "#424242" : "white",
backgroundColor:
boardOrientation === Color.White ? "#424242" : "white",
transition: "height 1s",
}}
height={`${
boardOrientation
boardOrientation === Color.White
? 100 - evalBar.whiteBarPercentage
: evalBar.whiteBarPercentage
}%`}
@@ -58,12 +58,13 @@ export default function EvaluationBar({ height }: Props) {
}
>
<Typography
color={boardOrientation ? "white" : "black"}
color={boardOrientation === Color.White ? "white" : "black"}
textAlign="center"
width="100%"
>
{(evalBar.whiteBarPercentage < 50 && boardOrientation) ||
(evalBar.whiteBarPercentage >= 50 && !boardOrientation)
{(evalBar.whiteBarPercentage < 50 &&
boardOrientation === Color.White) ||
(evalBar.whiteBarPercentage >= 50 && boardOrientation === Color.Black)
? evalBar.label
: ""}
</Typography>
@@ -71,11 +72,12 @@ export default function EvaluationBar({ height }: Props) {
<Box
sx={{
backgroundColor: boardOrientation ? "white" : "#424242",
backgroundColor:
boardOrientation === Color.White ? "white" : "#424242",
transition: "height 1s",
}}
height={`${
boardOrientation
boardOrientation === Color.White
? evalBar.whiteBarPercentage
: 100 - evalBar.whiteBarPercentage
}%`}
@@ -87,12 +89,13 @@ export default function EvaluationBar({ height }: Props) {
}
>
<Typography
color={boardOrientation ? "black" : "white"}
color={boardOrientation === Color.White ? "black" : "white"}
textAlign="center"
width="100%"
>
{(evalBar.whiteBarPercentage >= 50 && boardOrientation) ||
(evalBar.whiteBarPercentage < 50 && !boardOrientation)
{(evalBar.whiteBarPercentage >= 50 &&
boardOrientation === Color.White) ||
(evalBar.whiteBarPercentage < 50 && boardOrientation === Color.Black)
? evalBar.label
: ""}
</Typography>

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

View File

@@ -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<Chess>,
gameDataAtom: PrimitiveAtom<GameData>
gameDataAtom: PrimitiveAtom<CurrentPosition>
) => {
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;

View File

@@ -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<HTMLDivElement>(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 (
<Grid
item
container
justifyContent="center"
alignItems="center"
wrap="nowrap"
width={boardSize}
>
<EvaluationBar height={boardRef?.current?.offsetHeight || boardSize} />
<Grid
item
container
rowGap={1}
justifyContent="center"
alignItems="center"
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>
<Board
id="AnalysisBoard"
boardSize={boardSize}
canPlay={true}
gameAtom={boardAtom}
whitePlayer={
gameFromUrl?.white?.name || game.header()["White"] || "White"
}
blackPlayer={
gameFromUrl?.black?.name || game.header()["Black"] || "Black"
}
boardOrientation={boardOrientation ? Color.White : Color.Black}
currentPositionAtom={currentPositionAtom}
showBestMoveArrow={showBestMoveArrow}
showPlayerMoveIconAtom={showPlayerMoveIconAtom}
showEvaluationBar={true}
/>
);
}

View File

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

View File

@@ -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",
};

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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()) {

View File

@@ -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 (
<>
<Button variant="contained" onClick={() => setOpenDialog(true)}>
{gameData.history.length ? "Start new game" : "Start game"}
{game.history().length ? "Start new game" : "Start game"}
</Button>
<GameSettingsDialog

View File

@@ -1,16 +1,10 @@
import { GameData } from "@/hooks/useGameData";
import { Color } from "@/types/enums";
import { CurrentPosition } from "@/types/eval";
import { Chess } from "chess.js";
import { atom } from "jotai";
export const gameAtom = atom(new Chess());
export const gameDataAtom = atom<GameData>({
history: [],
lastMove: undefined,
});
export const gameDataAtom = atom<CurrentPosition>({});
export const playerColorAtom = atom<Color>(Color.White);
export const engineSkillLevelAtom = atom<number>(1);
export const isGameInProgressAtom = atom(false);
export const clickedSquaresAtom = atom<string[]>([]);
export const playableSquaresAtom = atom<string[]>([]);