feat : play vs engine v0
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
51
src/sections/play/gameInProgress.tsx
Normal file
51
src/sections/play/gameInProgress.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/sections/play/gameRecap.tsx
Normal file
40
src/sections/play/gameRecap.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/sections/play/gameSettings/gameSettingsButton.tsx
Normal file
20
src/sections/play/gameSettings/gameSettingsButton.tsx
Normal 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)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
135
src/sections/play/gameSettings/gameSettingsDialog.tsx
Normal file
135
src/sections/play/gameSettings/gameSettingsDialog.tsx
Normal 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",
|
||||||
|
};
|
||||||
@@ -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[]>([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user