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,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[]>([]);