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:
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 {
|
||||
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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
Reference in New Issue
Block a user