feat : play vs engine v0

This commit is contained in:
GuillaumeSD
2024-03-19 03:50:53 +01:00
parent 4864bf61f5
commit 438ec60107
7 changed files with 271 additions and 20 deletions

View File

@@ -1,7 +1,14 @@
import Board from "@/sections/play/board"; import Board from "@/sections/play/board";
import { CircularProgress, Divider, Grid, Typography } from "@mui/material"; import GameInProgress from "@/sections/play/gameInProgress";
import GameRecap from "@/sections/play/gameRecap";
import GameSettingsButton from "@/sections/play/gameSettings/gameSettingsButton";
import { isGameInProgressAtom } from "@/sections/play/states";
import { Grid } from "@mui/material";
import { useAtomValue } from "jotai";
export default function Play() { export default function Play() {
const isGameInProgress = useAtomValue(isGameInProgressAtom);
return ( return (
<Grid container gap={4} justifyContent="space-evenly" alignItems="start"> <Grid container gap={4} justifyContent="space-evenly" alignItems="start">
<Board /> <Board />
@@ -29,19 +36,9 @@ export default function Play() {
maxWidth: "1100px", maxWidth: "1100px",
}} }}
> >
<Grid <GameInProgress />
item {!isGameInProgress && <GameSettingsButton />}
container <GameRecap />
xs={12}
justifyContent="center"
alignItems="center"
columnGap={2}
>
<Typography>Game in progress</Typography>
<CircularProgress size={20} color="info" />
</Grid>
<Divider sx={{ width: "90%" }} />
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@@ -7,6 +7,7 @@ import {
gameAtom, gameAtom,
playableSquaresAtom, playableSquaresAtom,
playerColorAtom, playerColorAtom,
isGameInProgressAtom,
} from "../states"; } from "../states";
import { Square } from "react-chessboard/dist/chessboard/types"; import { Square } from "react-chessboard/dist/chessboard/types";
import { useChessActions } from "@/hooks/useChessActions"; import { useChessActions } from "@/hooks/useChessActions";
@@ -23,10 +24,11 @@ export default function Board() {
const { boardSize } = useScreenSize(); const { boardSize } = useScreenSize();
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const playerColor = useAtomValue(playerColorAtom); const playerColor = useAtomValue(playerColorAtom);
const { makeMove: makeBoardMove } = useChessActions(gameAtom); const { makeMove: makeGameMove } = useChessActions(gameAtom);
const setClickedSquares = useSetAtom(clickedSquaresAtom); const setClickedSquares = useSetAtom(clickedSquaresAtom);
const setPlayableSquares = useSetAtom(playableSquaresAtom); const setPlayableSquares = useSetAtom(playableSquaresAtom);
const engineSkillLevel = useAtomValue(engineSkillLevelAtom); const engineSkillLevel = useAtomValue(engineSkillLevelAtom);
const isGameInProgress = useAtomValue(isGameInProgressAtom);
const engine = useEngine(EngineName.Stockfish16); const engine = useEngine(EngineName.Stockfish16);
const gameFen = game.fen(); const gameFen = game.fen();
@@ -34,21 +36,26 @@ export default function Board() {
useEffect(() => { useEffect(() => {
const playEngineMove = async () => { const playEngineMove = async () => {
if (!engine?.isReady() || game.turn() === playerColor || isGameFinished) { if (
!engine?.isReady() ||
game.turn() === playerColor ||
isGameFinished ||
!isGameInProgress
) {
return; return;
} }
const move = await engine.getEngineNextMove( const move = await engine.getEngineNextMove(
gameFen, gameFen,
engineSkillLevel - 1 engineSkillLevel - 1
); );
if (move) makeBoardMove(uciMoveParams(move)); if (move) makeGameMove(uciMoveParams(move));
}; };
playEngineMove(); playEngineMove();
return () => { return () => {
engine?.stopSearch(); engine?.stopSearch();
}; };
}, [gameFen, engine]); }, [gameFen, isGameInProgress]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { useEffect(() => {
setClickedSquares([]); setClickedSquares([]);
@@ -61,7 +68,7 @@ export default function Board() {
): boolean => { ): boolean => {
if (!piece || piece[0] !== playerColor) return false; if (!piece || piece[0] !== playerColor) return false;
try { try {
const result = makeBoardMove({ const result = makeGameMove({
from: source, from: source,
to: target, to: target,
promotion: piece[1]?.toLowerCase() ?? "q", promotion: piece[1]?.toLowerCase() ?? "q",
@@ -125,7 +132,7 @@ export default function Board() {
id="AnalysisBoard" id="AnalysisBoard"
position={gameFen} position={gameFen}
onPieceDrop={onPieceDrop} onPieceDrop={onPieceDrop}
boardOrientation={playerColor ? "white" : "black"} boardOrientation={playerColor === Color.White ? "white" : "black"}
customBoardStyle={{ customBoardStyle={{
borderRadius: "5px", borderRadius: "5px",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)", boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",

View File

@@ -0,0 +1,51 @@
import { Button, CircularProgress, Grid, Typography } from "@mui/material";
import { useAtom, useAtomValue } from "jotai";
import { gameAtom, isGameInProgressAtom } from "./states";
import { useEffect } from "react";
export default function GameInProgress() {
const game = useAtomValue(gameAtom);
const [isGameInProgress, setIsGameInProgress] = useAtom(isGameInProgressAtom);
useEffect(() => {
if (game.isGameOver()) setIsGameInProgress(false);
}, [game, setIsGameInProgress]);
if (!isGameInProgress) return null;
return (
<Grid
item
container
xs={12}
justifyContent="center"
alignItems="center"
gap={2}
>
<Grid
container
item
justifyContent="center"
alignItems="center"
xs={12}
gap={2}
>
<Typography>Game in progress</Typography>
<CircularProgress size={20} color="info" />
</Grid>
<Grid
item
container
justifyContent="center"
alignItems="center"
xs={12}
gap={2}
>
<Button variant="outlined" onClick={() => setIsGameInProgress(false)}>
Resign
</Button>
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,40 @@
import { useAtomValue } from "jotai";
import { gameAtom, isGameInProgressAtom, playerColorAtom } from "./states";
import { Grid, Typography } from "@mui/material";
import { Color } from "@/types/enums";
export default function GameRecap() {
const game = useAtomValue(gameAtom);
const playerColor = useAtomValue(playerColorAtom);
const isGameInProgress = useAtomValue(isGameInProgressAtom);
if (isGameInProgress) return null;
const getResultLabel = () => {
if (game.isCheckmate()) {
const winnerColor = game.turn() === "w" ? Color.Black : Color.White;
const winnerLabel = winnerColor === playerColor ? "You" : "Stockfish";
return `${winnerLabel} won by checkmate !`;
}
if (game.isDraw()) {
if (game.isInsufficientMaterial()) return "Draw by insufficient material";
if (game.isStalemate()) return "Draw by stalemate";
if (game.isThreefoldRepetition()) return "Draw by threefold repetition";
return "Draw by fifty-move rule";
}
return "You resigned";
};
return (
<Grid
item
container
xs={12}
justifyContent="center"
alignItems="center"
gap={1}
>
<Typography>{getResultLabel()}</Typography>
</Grid>
);
}

View File

@@ -0,0 +1,20 @@
import { Button } from "@mui/material";
import { useState } from "react";
import GameSettingsDialog from "./gameSettingsDialog";
export default function GameSettingsButton() {
const [openDialog, setOpenDialog] = useState(false);
return (
<>
<Button variant="contained" onClick={() => setOpenDialog(true)}>
Start game
</Button>
<GameSettingsDialog
open={openDialog}
onClose={() => setOpenDialog(false)}
/>
</>
);
}

View File

@@ -0,0 +1,135 @@
import Slider from "@/components/slider";
import { Color, EngineName } from "@/types/enums";
import {
MenuItem,
Select,
Button,
Dialog,
DialogTitle,
DialogContent,
FormControl,
InputLabel,
OutlinedInput,
DialogActions,
Typography,
Grid,
FormGroup,
FormControlLabel,
Switch,
} from "@mui/material";
import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage";
import { useAtom, useSetAtom } from "jotai";
import {
engineSkillLevelAtom,
playerColorAtom,
isGameInProgressAtom,
gameAtom,
} from "../states";
import { useChessActions } from "@/hooks/useChessActions";
interface Props {
open: boolean;
onClose: () => void;
}
export default function GameSettingsDialog({ open, onClose }: Props) {
const [skillLevel, setSkillLevel] = useAtomLocalStorage(
"engine-skill-level",
engineSkillLevelAtom
);
const [playerColor, setPlayerColor] = useAtom(playerColorAtom);
const setIsGameInProgress = useSetAtom(isGameInProgressAtom);
const { reset: resetGame } = useChessActions(gameAtom);
const handleGameStart = () => {
onClose();
resetGame();
setIsGameInProgress(true);
};
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle marginY={1} variant="h5">
Set game parameters
</DialogTitle>
<DialogContent sx={{ paddingBottom: 0 }}>
<Typography>
Stockfish 16 is the only engine available now, more engine choices
will come soon !
</Typography>
<Grid
marginTop={4}
item
container
justifyContent="center"
alignItems="center"
xs={12}
rowGap={3}
>
<Grid item container xs={12} justifyContent="center">
<FormControl variant="outlined">
<InputLabel id="dialog-select-label">Bot's engine</InputLabel>
<Select
labelId="dialog-select-label"
id="dialog-select"
displayEmpty
input={<OutlinedInput label="Engine" />}
value={EngineName.Stockfish16}
disabled={true}
sx={{ width: 200 }}
>
{Object.values(EngineName).map((engine) => (
<MenuItem key={engine} value={engine}>
{engineLabel[engine]}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Slider
label="Bot skill level"
value={skillLevel}
setValue={setSkillLevel}
min={1}
max={21}
marksFilter={2}
/>
<FormGroup>
<FormControlLabel
control={
<Switch
color="default"
checked={playerColor === Color.White}
onChange={(e) => {
setPlayerColor(
e.target.checked ? Color.White : Color.Black
);
}}
/>
}
label={
playerColor === Color.White
? "You play as White"
: "You play as Black"
}
/>
</FormGroup>
</Grid>
</DialogContent>
<DialogActions sx={{ m: 2 }}>
<Button variant="outlined" sx={{ marginRight: 2 }} onClick={onClose}>
Cancel
</Button>
<Button variant="contained" onClick={handleGameStart}>
Start game
</Button>
</DialogActions>
</Dialog>
);
}
const engineLabel: Record<EngineName, string> = {
[EngineName.Stockfish16]: "Stockfish 16",
};

View File

@@ -5,6 +5,7 @@ import { atom } from "jotai";
export const gameAtom = atom(new Chess()); export const gameAtom = atom(new Chess());
export const playerColorAtom = atom<Color>(Color.White); export const playerColorAtom = atom<Color>(Color.White);
export const engineSkillLevelAtom = atom<number>(1); export const engineSkillLevelAtom = atom<number>(1);
export const isGameInProgressAtom = atom(false);
export const clickedSquaresAtom = atom<string[]>([]); export const clickedSquaresAtom = atom<string[]>([]);
export const playableSquaresAtom = atom<string[]>([]); export const playableSquaresAtom = atom<string[]>([]);