feat : play vs engine new page init
This commit is contained in:
@@ -12,10 +12,11 @@ import {
|
||||
} from "@mui/material";
|
||||
|
||||
const MenuOptions = [
|
||||
{ text: "Play", icon: "streamline:chess-pawn", href: "/play" },
|
||||
{ text: "Analysis", icon: "streamline:magnifying-glass-solid", href: "/" },
|
||||
{
|
||||
text: "Database",
|
||||
icon: "streamline:database-solid",
|
||||
icon: "streamline:database",
|
||||
href: "/database",
|
||||
},
|
||||
];
|
||||
|
||||
112
src/sections/play/board/index.tsx
Normal file
112
src/sections/play/board/index.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Grid } from "@mui/material";
|
||||
import { Chessboard } from "react-chessboard";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import {
|
||||
clickedSquaresAtom,
|
||||
engineSkillLevelAtom,
|
||||
gameAtom,
|
||||
playerColorAtom,
|
||||
} from "../states";
|
||||
import { Square } from "react-chessboard/dist/chessboard/types";
|
||||
import { useChessActions } from "@/hooks/useChessActions";
|
||||
import { useEffect, 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";
|
||||
|
||||
export default function Board() {
|
||||
const boardRef = useRef<HTMLDivElement>(null);
|
||||
const { boardSize } = useScreenSize();
|
||||
const game = useAtomValue(gameAtom);
|
||||
const playerColor = useAtomValue(playerColorAtom);
|
||||
const { makeMove: makeBoardMove } = useChessActions(gameAtom);
|
||||
const setClickedSquares = useSetAtom(clickedSquaresAtom);
|
||||
const engineSkillLevel = useAtomValue(engineSkillLevelAtom);
|
||||
const engine = useEngine(EngineName.Stockfish16);
|
||||
|
||||
const gameFen = game.fen();
|
||||
const turn = game.turn();
|
||||
|
||||
useEffect(() => {
|
||||
const playEngineMove = async () => {
|
||||
if (!engine?.isReady() || turn === playerColor) return;
|
||||
const move = await engine.getEngineNextMove(
|
||||
gameFen,
|
||||
engineSkillLevel - 1
|
||||
);
|
||||
makeBoardMove(uciMoveParams(move));
|
||||
};
|
||||
playEngineMove();
|
||||
}, [engine, turn, engine, playerColor, engineSkillLevel]);
|
||||
|
||||
useEffect(() => {
|
||||
setClickedSquares([]);
|
||||
}, [gameFen, setClickedSquares]);
|
||||
|
||||
const onPieceDrop = (
|
||||
source: Square,
|
||||
target: Square,
|
||||
piece: string
|
||||
): boolean => {
|
||||
if (!piece || piece[0] !== playerColor) return false;
|
||||
try {
|
||||
const result = makeBoardMove({
|
||||
from: source,
|
||||
to: target,
|
||||
promotion: piece[1]?.toLowerCase() ?? "q",
|
||||
});
|
||||
|
||||
return !!result;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isPieceDraggable = ({ piece }: { piece: string }): boolean => {
|
||||
if (!piece) return false;
|
||||
return playerColor === piece[0];
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
rowGap={1}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width={boardSize}
|
||||
maxWidth="85vh"
|
||||
>
|
||||
<PlayerInfo
|
||||
color={playerColor === Color.White ? Color.Black : Color.White}
|
||||
/>
|
||||
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
ref={boardRef}
|
||||
xs={12}
|
||||
>
|
||||
<Chessboard
|
||||
id="AnalysisBoard"
|
||||
position={gameFen}
|
||||
onPieceDrop={onPieceDrop}
|
||||
boardOrientation={playerColor ? "white" : "black"}
|
||||
customBoardStyle={{
|
||||
borderRadius: "5px",
|
||||
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
isDraggablePiece={isPieceDraggable}
|
||||
customSquare={SquareRenderer}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<PlayerInfo color={playerColor} />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
20
src/sections/play/board/playerInfo.tsx
Normal file
20
src/sections/play/board/playerInfo.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { playerColorAtom } from "../states";
|
||||
import { Color } from "@/types/enums";
|
||||
|
||||
interface Props {
|
||||
color: Color;
|
||||
}
|
||||
|
||||
export default function PlayerInfo({ color }: Props) {
|
||||
const playerColor = useAtomValue(playerColorAtom);
|
||||
|
||||
const playerName = playerColor === color ? "You 🧠" : "Stockfish 🤖";
|
||||
|
||||
return (
|
||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||
<Typography variant="h6">{playerName}</Typography>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
78
src/sections/play/board/squareRenderer.tsx
Normal file
78
src/sections/play/board/squareRenderer.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { clickedSquaresAtom, gameAtom } from "../states";
|
||||
import { atom, useAtom, useAtomValue } from "jotai";
|
||||
import { CSSProperties, MouseEventHandler, forwardRef } from "react";
|
||||
import { CustomSquareProps } from "react-chessboard/dist/chessboard/types";
|
||||
|
||||
const rightClickEventSquareAtom = atom<string | null>(null);
|
||||
|
||||
const SquareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>(
|
||||
(props, ref) => {
|
||||
const { children, square, style } = props;
|
||||
const game = useAtomValue(gameAtom);
|
||||
const [clickedSquares, setClickedSquares] = useAtom(clickedSquaresAtom);
|
||||
const [rightClickEventSquare, setRightClickEventSquare] = useAtom(
|
||||
rightClickEventSquareAtom
|
||||
);
|
||||
|
||||
const lastMove = game.history({ verbose: true }).at(-1);
|
||||
const fromSquare = lastMove?.from;
|
||||
const toSquare = lastMove?.to;
|
||||
|
||||
const customSquareStyle: 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 handleSquareLeftClick: MouseEventHandler<HTMLDivElement> = () => {
|
||||
setClickedSquares([]);
|
||||
};
|
||||
|
||||
const handleSquareRightClick: MouseEventHandler<HTMLDivElement> = (
|
||||
event
|
||||
) => {
|
||||
if (event.button !== 2) return;
|
||||
|
||||
if (rightClickEventSquare !== square) {
|
||||
setRightClickEventSquare(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setClickedSquares((prev) =>
|
||||
prev.includes(square)
|
||||
? prev.filter((s) => s !== square)
|
||||
: [...prev, square]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ ...style, position: "relative" }}
|
||||
onClick={handleSquareLeftClick}
|
||||
onMouseDown={(e) => e.button === 2 && setRightClickEventSquare(square)}
|
||||
onMouseUp={handleSquareRightClick}
|
||||
>
|
||||
{children}
|
||||
{customSquareStyle && <div style={customSquareStyle} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SquareRenderer.displayName = "CustomSquareRenderer";
|
||||
|
||||
export default SquareRenderer;
|
||||
9
src/sections/play/states.ts
Normal file
9
src/sections/play/states.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Color } from "@/types/enums";
|
||||
import { Chess } from "chess.js";
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const gameAtom = atom(new Chess());
|
||||
export const playerColorAtom = atom<Color>(Color.White);
|
||||
export const engineSkillLevelAtom = atom<number>(1);
|
||||
|
||||
export const clickedSquaresAtom = atom<string[]>([]);
|
||||
Reference in New Issue
Block a user