feat : play vs engine new page init
This commit is contained in:
@@ -32,6 +32,19 @@
|
|||||||
"value": "same-origin"
|
"value": "same-origin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/play",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cross-Origin-Embedder-Policy",
|
||||||
|
"value": "require-corp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Cross-Origin-Opener-Policy",
|
||||||
|
"value": "same-origin"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const sortLines = (a: LineEval, b: LineEval): number => {
|
|||||||
return (b.cp ?? 0) - (a.cp ?? 0);
|
return (b.cp ?? 0) - (a.cp ?? 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResultProperty = (
|
export const getResultProperty = (
|
||||||
result: string,
|
result: string,
|
||||||
property: string
|
property: string
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import {
|
|||||||
GameEval,
|
GameEval,
|
||||||
PositionEval,
|
PositionEval,
|
||||||
} from "@/types/eval";
|
} from "@/types/eval";
|
||||||
import { parseEvaluationResults } from "./helpers/parseResults";
|
import {
|
||||||
|
getResultProperty,
|
||||||
|
parseEvaluationResults,
|
||||||
|
} from "./helpers/parseResults";
|
||||||
import { computeAccuracy } from "./helpers/accuracy";
|
import { computeAccuracy } from "./helpers/accuracy";
|
||||||
import { getWhoIsCheckmated } from "../chess";
|
import { getWhoIsCheckmated } from "../chess";
|
||||||
import { getLichessEval } from "../lichess";
|
import { getLichessEval } from "../lichess";
|
||||||
@@ -16,6 +19,7 @@ export abstract class UciEngine {
|
|||||||
private ready = false;
|
private ready = false;
|
||||||
private engineName: EngineName;
|
private engineName: EngineName;
|
||||||
private multiPv = 3;
|
private multiPv = 3;
|
||||||
|
private skillLevel: number | undefined = undefined;
|
||||||
|
|
||||||
constructor(engineName: EngineName, enginePath: string) {
|
constructor(engineName: EngineName, enginePath: string) {
|
||||||
this.engineName = engineName;
|
this.engineName = engineName;
|
||||||
@@ -50,6 +54,25 @@ export abstract class UciEngine {
|
|||||||
this.multiPv = multiPv;
|
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() {
|
private throwErrorIfNotReady() {
|
||||||
if (!this.ready) {
|
if (!this.ready) {
|
||||||
throw new Error(`${this.engineName} is not ready`);
|
throw new Error(`${this.engineName} is not ready`);
|
||||||
@@ -215,4 +238,28 @@ export abstract class UciEngine {
|
|||||||
onNewMessage
|
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
48
src/pages/play.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,10 +12,11 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
const MenuOptions = [
|
const MenuOptions = [
|
||||||
|
{ text: "Play", icon: "streamline:chess-pawn", href: "/play" },
|
||||||
{ text: "Analysis", icon: "streamline:magnifying-glass-solid", href: "/" },
|
{ text: "Analysis", icon: "streamline:magnifying-glass-solid", href: "/" },
|
||||||
{
|
{
|
||||||
text: "Database",
|
text: "Database",
|
||||||
icon: "streamline:database-solid",
|
icon: "streamline:database",
|
||||||
href: "/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[]>([]);
|
||||||
@@ -19,3 +19,8 @@ export enum MoveClassification {
|
|||||||
Great = "great",
|
Great = "great",
|
||||||
Brilliant = "brilliant",
|
Brilliant = "brilliant",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Color {
|
||||||
|
White = "w",
|
||||||
|
Black = "b",
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user