feat : add live position evaluation
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
import { boardAtom, gameAtom, gameEvalAtom } from "@/sections/analysis/states";
|
||||
import {
|
||||
boardAtom,
|
||||
engineDepthAtom,
|
||||
engineMultiPvAtom,
|
||||
gameAtom,
|
||||
gameEvalAtom,
|
||||
} from "@/sections/analysis/states";
|
||||
import { MoveEval } from "@/types/eval";
|
||||
import { Move } from "chess.js";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEngine } from "./useEngine";
|
||||
import { EngineName } from "@/types/enums";
|
||||
|
||||
export type CurrentMove = Partial<Move> & {
|
||||
eval?: MoveEval;
|
||||
@@ -10,17 +18,20 @@ export type CurrentMove = Partial<Move> & {
|
||||
};
|
||||
|
||||
export const useCurrentMove = () => {
|
||||
const [currentMove, setCurrentMove] = useState<CurrentMove>({});
|
||||
const engine = useEngine(EngineName.Stockfish16);
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
const game = useAtomValue(gameAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
const depth = useAtomValue(engineDepthAtom);
|
||||
const multiPv = useAtomValue(engineMultiPvAtom);
|
||||
|
||||
const currentMove: CurrentMove = useMemo(() => {
|
||||
const move = {
|
||||
useEffect(() => {
|
||||
const move: CurrentMove = {
|
||||
...board.history({ verbose: true }).at(-1),
|
||||
};
|
||||
|
||||
if (!gameEval) return move;
|
||||
|
||||
if (gameEval) {
|
||||
const boardHistory = board.history();
|
||||
const gameHistory = game.history();
|
||||
|
||||
@@ -30,15 +41,27 @@ export const useCurrentMove = () => {
|
||||
) {
|
||||
const evalIndex = board.history().length;
|
||||
|
||||
return {
|
||||
...move,
|
||||
eval: gameEval.moves[evalIndex],
|
||||
lastEval: evalIndex > 0 ? gameEval.moves[evalIndex - 1] : undefined,
|
||||
};
|
||||
move.eval = gameEval.moves[evalIndex];
|
||||
move.lastEval =
|
||||
evalIndex > 0 ? gameEval.moves[evalIndex - 1] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return move;
|
||||
}, [gameEval, board, game]);
|
||||
if (!move.eval && engine?.isReady()) {
|
||||
const setPartialEval = (moveEval: MoveEval) => {
|
||||
setCurrentMove({ ...move, eval: moveEval });
|
||||
};
|
||||
|
||||
engine.evaluatePositionWithUpdate({
|
||||
fen: board.fen(),
|
||||
depth,
|
||||
multiPv,
|
||||
setPartialEval,
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentMove(move);
|
||||
}, [gameEval, board, game, engine, depth, multiPv]);
|
||||
|
||||
return currentMove;
|
||||
};
|
||||
|
||||
@@ -57,3 +57,23 @@ export const getGameToSave = (game: Chess, board: Chess): Chess => {
|
||||
|
||||
return board;
|
||||
};
|
||||
|
||||
export const moveLineUciToSan = (
|
||||
fen: string
|
||||
): ((moveUci: string) => string) => {
|
||||
const game = new Chess(fen);
|
||||
|
||||
return (moveUci: string): string => {
|
||||
try {
|
||||
const move = game.move({
|
||||
from: moveUci.slice(0, 2),
|
||||
to: moveUci.slice(2, 4),
|
||||
promotion: moveUci.slice(4, 5) || undefined,
|
||||
});
|
||||
|
||||
return move.san;
|
||||
} catch (e) {
|
||||
return moveUci;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { EngineName } from "@/types/enums";
|
||||
import { GameEval, LineEval, MoveEval } from "@/types/eval";
|
||||
import {
|
||||
EvaluatePositionWithUpdateParams,
|
||||
GameEval,
|
||||
LineEval,
|
||||
MoveEval,
|
||||
} from "@/types/eval";
|
||||
|
||||
export abstract class UciEngine {
|
||||
private worker: Worker;
|
||||
@@ -18,15 +23,15 @@ export abstract class UciEngine {
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await this.sendCommands(["uci"], "uciok");
|
||||
await this.setMultiPv(this.multiPv, false);
|
||||
await this.setMultiPv(this.multiPv, true);
|
||||
this.ready = true;
|
||||
console.log(`${this.engineName} initialized`);
|
||||
}
|
||||
|
||||
public async setMultiPv(multiPv: number, checkIsReady = true) {
|
||||
private async setMultiPv(multiPv: number, initCase = false) {
|
||||
if (!initCase) {
|
||||
if (multiPv === this.multiPv) return;
|
||||
|
||||
if (checkIsReady) {
|
||||
this.throwErrorIfNotReady();
|
||||
}
|
||||
|
||||
@@ -59,15 +64,23 @@ export abstract class UciEngine {
|
||||
return this.ready;
|
||||
}
|
||||
|
||||
private async stopSearch(): Promise<void> {
|
||||
await this.sendCommands(["stop", "isready"], "readyok");
|
||||
}
|
||||
|
||||
private async sendCommands(
|
||||
commands: string[],
|
||||
finalMessage: string
|
||||
finalMessage: string,
|
||||
onNewMessage?: (messages: string[]) => void
|
||||
): Promise<string[]> {
|
||||
return new Promise((resolve) => {
|
||||
const messages: string[] = [];
|
||||
|
||||
this.worker.onmessage = (event) => {
|
||||
const messageData: string = event.data;
|
||||
messages.push(messageData);
|
||||
onNewMessage?.(messages);
|
||||
|
||||
if (messageData.startsWith(finalMessage)) {
|
||||
resolve(messages);
|
||||
}
|
||||
@@ -85,6 +98,7 @@ export abstract class UciEngine {
|
||||
multiPv = this.multiPv
|
||||
): Promise<GameEval> {
|
||||
this.throwErrorIfNotReady();
|
||||
await this.setMultiPv(multiPv);
|
||||
this.ready = false;
|
||||
|
||||
await this.sendCommands(["ucinewgame", "isready"], "readyok");
|
||||
@@ -92,7 +106,7 @@ export abstract class UciEngine {
|
||||
|
||||
const moves: MoveEval[] = [];
|
||||
for (const fen of fens) {
|
||||
const result = await this.evaluatePosition(fen, depth, multiPv, false);
|
||||
const result = await this.evaluatePosition(fen, depth);
|
||||
moves.push(result);
|
||||
}
|
||||
|
||||
@@ -109,44 +123,46 @@ export abstract class UciEngine {
|
||||
};
|
||||
}
|
||||
|
||||
public async evaluatePosition(
|
||||
fen: string,
|
||||
depth = 16,
|
||||
multiPv = this.multiPv,
|
||||
checkIsReady = true
|
||||
): Promise<MoveEval> {
|
||||
if (checkIsReady) {
|
||||
this.throwErrorIfNotReady();
|
||||
}
|
||||
|
||||
await this.setMultiPv(multiPv, checkIsReady);
|
||||
|
||||
private async evaluatePosition(fen: string, depth = 16): Promise<MoveEval> {
|
||||
console.log(`Evaluating position: ${fen}`);
|
||||
|
||||
const results = await this.sendCommands(
|
||||
[`position fen ${fen}`, `go depth ${depth}`],
|
||||
"bestmove"
|
||||
);
|
||||
|
||||
const parsedResults = this.parseResults(results);
|
||||
const whiteToPlay = fen.split(" ")[1] === "w";
|
||||
|
||||
return this.parseResults(results, whiteToPlay);
|
||||
}
|
||||
|
||||
public async evaluatePositionWithUpdate({
|
||||
fen,
|
||||
depth = 16,
|
||||
multiPv = this.multiPv,
|
||||
setPartialEval,
|
||||
}: EvaluatePositionWithUpdateParams): Promise<void> {
|
||||
this.throwErrorIfNotReady();
|
||||
|
||||
await this.stopSearch();
|
||||
await this.setMultiPv(multiPv);
|
||||
|
||||
const whiteToPlay = fen.split(" ")[1] === "w";
|
||||
|
||||
if (!whiteToPlay) {
|
||||
const lines = parsedResults.lines.map((line) => ({
|
||||
...line,
|
||||
cp: line.cp ? -line.cp : line.cp,
|
||||
}));
|
||||
|
||||
return {
|
||||
...parsedResults,
|
||||
lines,
|
||||
const onNewMessage = (messages: string[]) => {
|
||||
const parsedResults = this.parseResults(messages, whiteToPlay);
|
||||
setPartialEval(parsedResults);
|
||||
};
|
||||
|
||||
console.log(`Evaluating position: ${fen}`);
|
||||
await this.sendCommands(
|
||||
[`position fen ${fen}`, `go depth ${depth}`],
|
||||
"bestmove",
|
||||
onNewMessage
|
||||
);
|
||||
}
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
private parseResults(results: string[]): MoveEval {
|
||||
private parseResults(results: string[], whiteToPlay: boolean): MoveEval {
|
||||
const parsedResults: MoveEval = {
|
||||
bestMove: "",
|
||||
lines: [],
|
||||
@@ -189,6 +205,13 @@ export abstract class UciEngine {
|
||||
|
||||
parsedResults.lines = Object.values(tempResults).sort(this.sortLines);
|
||||
|
||||
if (!whiteToPlay) {
|
||||
parsedResults.lines = parsedResults.lines.map((line) => ({
|
||||
...line,
|
||||
cp: line.cp ? -line.cp : line.cp,
|
||||
}));
|
||||
}
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { LineEval } from "@/types/eval";
|
||||
import { ListItem, ListItemText, Typography } from "@mui/material";
|
||||
import { ListItem, Skeleton, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom } from "./states";
|
||||
import { moveLineUciToSan } from "@/lib/chess";
|
||||
|
||||
interface Props {
|
||||
line: LineEval;
|
||||
}
|
||||
|
||||
export default function LineEvaluation({ line }: Props) {
|
||||
const board = useAtomValue(boardAtom);
|
||||
const lineLabel =
|
||||
line.cp !== undefined
|
||||
? `${line.cp / 100}`
|
||||
@@ -13,10 +17,32 @@ export default function LineEvaluation({ line }: Props) {
|
||||
? `Mate in ${Math.abs(line.mate)}`
|
||||
: "?";
|
||||
|
||||
const showSkeleton = line.depth === 0;
|
||||
|
||||
return (
|
||||
<ListItem disablePadding>
|
||||
<ListItemText primary={lineLabel} sx={{ marginRight: 2 }} />
|
||||
<Typography>{line.pv.slice(0, 7).join(", ")}</Typography>
|
||||
<Typography marginRight={2} marginY={0.5}>
|
||||
{showSkeleton ? (
|
||||
<Skeleton
|
||||
width={"2em"}
|
||||
variant="rounded"
|
||||
animation="wave"
|
||||
sx={{ color: "transparent" }}
|
||||
>
|
||||
placeholder
|
||||
</Skeleton>
|
||||
) : (
|
||||
lineLabel
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
{showSkeleton ? (
|
||||
<Skeleton width={"30em"} variant="rounded" animation="wave" />
|
||||
) : (
|
||||
line.pv.slice(0, 10).map(moveLineUciToSan(board.fen())).join(", ")
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Divider, Grid, List, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, gameAtom } from "./states";
|
||||
import { boardAtom, engineMultiPvAtom, gameAtom } from "./states";
|
||||
import LineEvaluation from "./lineEvaluation";
|
||||
import { useCurrentMove } from "@/hooks/useCurrentMove";
|
||||
import { LineEval } from "@/types/eval";
|
||||
|
||||
export default function ReviewPanelBody() {
|
||||
const linesNumber = useAtomValue(engineMultiPvAtom);
|
||||
const move = useCurrentMove();
|
||||
const game = useAtomValue(gameAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
@@ -17,6 +19,14 @@ export default function ReviewPanelBody() {
|
||||
const isGameOver =
|
||||
gameHistory.length > 0 && boardHistory.join() === gameHistory.join();
|
||||
|
||||
const linesSkeleton: LineEval[] = Array.from({ length: linesNumber }).map(
|
||||
(_, i) => ({ pv: [`${i}`], depth: 0, multiPv: i + 1 })
|
||||
);
|
||||
|
||||
const engineLines = move?.eval?.lines.length
|
||||
? move.eval.lines
|
||||
: linesSkeleton;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ width: "90%", marginY: 3 }} />
|
||||
@@ -57,8 +67,8 @@ export default function ReviewPanelBody() {
|
||||
|
||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||
<List>
|
||||
{move?.eval?.lines.map((line) => (
|
||||
<LineEvaluation key={line.pv[0]} line={line} />
|
||||
{engineLines.map((line) => (
|
||||
<LineEvaluation key={line.multiPv} line={line} />
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
|
||||
47
src/sections/analysis/reviewPanelToolbar/arrowOptions.tsx
Normal file
47
src/sections/analysis/reviewPanelToolbar/arrowOptions.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Checkbox, FormControlLabel, Grid } from "@mui/material";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import {
|
||||
gameEvalAtom,
|
||||
showBestMoveArrowAtom,
|
||||
showPlayerMoveArrowAtom,
|
||||
} from "../states";
|
||||
|
||||
export default function ArrowOptions() {
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
const [showBestMove, setShowBestMove] = useAtom(showBestMoveArrowAtom);
|
||||
const [showPlayerMove, setShowPlayerMove] = useAtom(showPlayerMoveArrowAtom);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
xs={12}
|
||||
marginY={3}
|
||||
gap={3}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={showBestMove}
|
||||
onChange={(_, checked) => setShowBestMove(checked)}
|
||||
disabled={!gameEval}
|
||||
/>
|
||||
}
|
||||
label="Show best move green arrow"
|
||||
sx={{ marginX: 0 }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={showPlayerMove}
|
||||
onChange={(_, checked) => setShowPlayerMove(checked)}
|
||||
/>
|
||||
}
|
||||
label="Show player move yellow arrow"
|
||||
sx={{ marginX: 0 }}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,15 @@
|
||||
import {
|
||||
Checkbox,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { Divider, Grid, IconButton, Tooltip } from "@mui/material";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import {
|
||||
boardAtom,
|
||||
showBestMoveArrowAtom,
|
||||
showPlayerMoveArrowAtom,
|
||||
} from "../states";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom } from "../states";
|
||||
import { useChessActions } from "@/hooks/useChess";
|
||||
import FlipBoardButton from "./flipBoardButton";
|
||||
import NextMoveButton from "./nextMoveButton";
|
||||
import GoToLastPositionButton from "./goToLastPositionButton";
|
||||
import SaveButton from "./saveButton";
|
||||
import ArrowOptions from "./arrowOptions";
|
||||
|
||||
export default function ReviewPanelToolBar() {
|
||||
const [showBestMove, setShowBestMove] = useAtom(showBestMoveArrowAtom);
|
||||
const [showPlayerMove, setShowPlayerMove] = useAtom(showPlayerMoveArrowAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
const boardActions = useChessActions(boardAtom);
|
||||
|
||||
@@ -63,36 +51,7 @@ export default function ReviewPanelToolBar() {
|
||||
<SaveButton />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
xs={12}
|
||||
marginY={3}
|
||||
gap={3}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={showBestMove}
|
||||
onChange={(_, checked) => setShowBestMove(checked)}
|
||||
/>
|
||||
}
|
||||
label="Show best move green arrow"
|
||||
sx={{ marginX: 0 }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={showPlayerMove}
|
||||
onChange={(_, checked) => setShowPlayerMove(checked)}
|
||||
/>
|
||||
}
|
||||
label="Show player move yellow arrow"
|
||||
sx={{ marginX: 0 }}
|
||||
/>
|
||||
</Grid>
|
||||
<ArrowOptions />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,3 +30,10 @@ export interface GameEval {
|
||||
accuracy: Accuracy;
|
||||
settings: EngineSettings;
|
||||
}
|
||||
|
||||
export interface EvaluatePositionWithUpdateParams {
|
||||
fen: string;
|
||||
depth?: number;
|
||||
multiPv?: number;
|
||||
setPartialEval: (moveEval: MoveEval) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user