feat : play vs engine new page init

This commit is contained in:
GuillaumeSD
2024-03-18 02:48:34 +01:00
parent 1893937c4a
commit cd927ed6d7
11 changed files with 349 additions and 3 deletions

View File

@@ -32,6 +32,19 @@
"value": "same-origin"
}
]
},
{
"source": "/play",
"headers": [
{
"key": "Cross-Origin-Embedder-Policy",
"value": "require-corp"
},
{
"key": "Cross-Origin-Opener-Policy",
"value": "same-origin"
}
]
}
]
}

View File

@@ -38,6 +38,19 @@ const nextConfig = (phase) =>
},
],
},
{
source: "/play",
headers: [
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
],
},
],
});

View File

@@ -72,7 +72,7 @@ export const sortLines = (a: LineEval, b: LineEval): number => {
return (b.cp ?? 0) - (a.cp ?? 0);
};
const getResultProperty = (
export const getResultProperty = (
result: string,
property: string
): string | undefined => {

View File

@@ -5,7 +5,10 @@ import {
GameEval,
PositionEval,
} from "@/types/eval";
import { parseEvaluationResults } from "./helpers/parseResults";
import {
getResultProperty,
parseEvaluationResults,
} from "./helpers/parseResults";
import { computeAccuracy } from "./helpers/accuracy";
import { getWhoIsCheckmated } from "../chess";
import { getLichessEval } from "../lichess";
@@ -16,6 +19,7 @@ export abstract class UciEngine {
private ready = false;
private engineName: EngineName;
private multiPv = 3;
private skillLevel: number | undefined = undefined;
constructor(engineName: EngineName, enginePath: string) {
this.engineName = engineName;
@@ -50,6 +54,25 @@ export abstract class UciEngine {
this.multiPv = multiPv;
}
private async setSkillLevel(skillLevel: number, initCase = false) {
if (!initCase) {
if (skillLevel === this.skillLevel) return;
this.throwErrorIfNotReady();
}
if (skillLevel < 0 || skillLevel > 20) {
throw new Error(`Invalid SkillLevel value : ${skillLevel}`);
}
await this.sendCommands(
[`setoption name Skill Level value ${skillLevel}`, "isready"],
"readyok"
);
this.skillLevel = skillLevel;
}
private throwErrorIfNotReady() {
if (!this.ready) {
throw new Error(`${this.engineName} is not ready`);
@@ -215,4 +238,28 @@ export abstract class UciEngine {
onNewMessage
);
}
public async getEngineNextMove(
fen: string,
skillLevel: number,
depth = 16
): Promise<string> {
this.throwErrorIfNotReady();
await this.setSkillLevel(skillLevel);
console.log(`Evaluating position: ${fen}`);
const results = await this.sendCommands(
[`position fen ${fen}`, `go depth ${depth}`],
"bestmove"
);
const moveResult = results.find((result) => result.startsWith("bestmove"));
const move = getResultProperty(moveResult ?? "", "bestmove");
if (!move) {
throw new Error("No move found");
}
return move;
}
}

48
src/pages/play.tsx Normal file
View File

@@ -0,0 +1,48 @@
import Board from "@/sections/play/board";
import { CircularProgress, Divider, Grid, Typography } from "@mui/material";
export default function Play() {
return (
<Grid container gap={4} justifyContent="space-evenly" alignItems="start">
<Board />
<Grid
container
item
marginTop={{ xs: 0, lg: "2.5em" }}
justifyContent="center"
alignItems="center"
borderRadius={2}
border={1}
borderColor={"secondary.main"}
xs={12}
lg
sx={{
backgroundColor: "secondary.main",
borderColor: "primary.main",
borderWidth: 2,
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",
}}
padding={3}
rowGap={3}
style={{
maxWidth: "1100px",
}}
>
<Grid
item
container
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>
);
}

View File

@@ -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",
},
];

View 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>
);
}

View 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>
);
}

View 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;

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

View File

@@ -19,3 +19,8 @@ export enum MoveClassification {
Great = "great",
Brilliant = "brilliant",
}
export enum Color {
White = "w",
Black = "b",
}