feat : set depth & multipv

This commit is contained in:
GuillaumeSD
2024-02-24 01:20:39 +01:00
parent 89ca7d8f13
commit 6156e4a228
13 changed files with 318 additions and 142 deletions

41
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.3",
"@iconify/react": "^4.1.0",
"@mui/lab": "^5.0.0-alpha.165",
"@mui/material": "^5.13.4",
"@mui/x-data-grid": "^6.19.4",
"chess.js": "^1.0.0-beta.7",
@@ -569,6 +570,46 @@
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/lab": {
"version": "5.0.0-alpha.165",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.165.tgz",
"integrity": "sha512-8/zJStT10nh9yrAzLOPTICGhpf5YiGp/JpM0bdTP7u5AE+YT+X2u6QwMxuCrVeW8/WVLAPFg0vtzyfgPcN5T7g==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/base": "5.0.0-beta.36",
"@mui/system": "^5.15.9",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.9",
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material": ">=5.15.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "5.15.10",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.10.tgz",

View File

@@ -14,6 +14,7 @@
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.3",
"@iconify/react": "^4.1.0",
"@mui/lab": "^5.0.0-alpha.165",
"@mui/material": "^5.13.4",
"@mui/x-data-grid": "^6.19.4",
"chess.js": "^1.0.0-beta.7",

45
src/components/slider.tsx Normal file
View File

@@ -0,0 +1,45 @@
import { Grid, Slider as MuiSlider, Typography } from "@mui/material";
import { PrimitiveAtom, useAtom } from "jotai";
interface Props {
atom: PrimitiveAtom<number>;
min: number;
max: number;
label: string;
xs?: number;
}
export default function Slider({ min, max, label, atom, xs }: Props) {
const [value, setValue] = useAtom(atom);
return (
<Grid
item
container
xs={xs ?? 10}
justifyContent="center"
alignItems="center"
>
<Typography
id={`input-${label}`}
gutterBottom
textAlign="left"
width="100%"
>
{label}
</Typography>
<MuiSlider
min={min}
max={max}
marks={Array.from({ length: max - min + 1 }, (_, i) => ({
value: i + min,
label: `${i + min}`,
}))}
valueLabelDisplay="off"
value={value}
onChange={(_, value) => setValue(value as number)}
aria-labelledby={`input-${label}`}
/>
</Grid>
);
}

View File

@@ -1,32 +1,44 @@
import { boardAtom, gameAtom, gameEvalAtom } from "@/sections/analysis/states";
import { MoveEval } from "@/types/eval";
import { Move } from "chess.js";
import { useAtomValue } from "jotai";
import { useMemo } from "react";
export type CurrentMove = Partial<Move> & {
eval?: MoveEval;
lastEval?: MoveEval;
};
export const useCurrentMove = () => {
const gameEval = useAtomValue(gameEvalAtom);
const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom);
const currentEvalMove = useMemo(() => {
if (!gameEval) return undefined;
const currentMove: CurrentMove = useMemo(() => {
const move = {
...board.history({ verbose: true }).at(-1),
};
if (!gameEval) return move;
const boardHistory = board.history();
const gameHistory = game.history();
if (
boardHistory.length >= gameHistory.length ||
gameHistory.slice(0, boardHistory.length).join() !== boardHistory.join()
)
return;
boardHistory.length <= gameHistory.length &&
gameHistory.slice(0, boardHistory.length).join() === boardHistory.join()
) {
const evalIndex = board.history().length;
return {
...board.history({ verbose: true }).at(-1),
...move,
eval: gameEval.moves[evalIndex],
lastEval: evalIndex > 0 ? gameEval.moves[evalIndex - 1] : undefined,
};
}
return move;
}, [gameEval, board, game]);
return currentEvalMove;
return currentMove;
};

View File

@@ -53,6 +53,54 @@ export const useGameDatabase = (shouldFetchGames?: boolean) => {
loadGames();
}, [loadGames]);
const addGame = useCallback(
async (game: Chess) => {
if (!db) throw new Error("Database not initialized");
const gameToAdd = formatGameToDatabase(game);
const gameId = await db.add("games", gameToAdd as Game);
loadGames();
return gameId;
},
[db, loadGames]
);
const setGameEval = useCallback(
async (gameId: number, evaluation: GameEval) => {
if (!db) throw new Error("Database not initialized");
const game = await db.get("games", gameId);
if (!game) throw new Error("Game not found");
await db.put("games", { ...game, eval: evaluation });
loadGames();
},
[db, loadGames]
);
const getGame = useCallback(
async (gameId: number) => {
if (!db) return undefined;
return db.get("games", gameId);
},
[db]
);
const deleteGame = useCallback(
async (gameId: number) => {
if (!db) throw new Error("Database not initialized");
await db.delete("games", gameId);
loadGames();
},
[db, loadGames]
);
const router = useRouter();
const { gameId } = router.query;
@@ -62,43 +110,7 @@ export const useGameDatabase = (shouldFetchGames?: boolean) => {
setGameFromUrl(game);
});
}
}, [gameId, games]);
const addGame = async (game: Chess) => {
if (!db) throw new Error("Database not initialized");
const gameToAdd = formatGameToDatabase(game);
const gameId = await db.add("games", gameToAdd as Game);
loadGames();
return gameId;
};
const setGameEval = async (gameId: number, evaluation: GameEval) => {
if (!db) throw new Error("Database not initialized");
const game = await db.get("games", gameId);
if (!game) throw new Error("Game not found");
await db.put("games", { ...game, eval: evaluation });
loadGames();
};
const getGame = async (gameId: number) => {
if (!db) return undefined;
return db.get("games", gameId);
};
const deleteGame = async (gameId: number) => {
if (!db) throw new Error("Database not initialized");
await db.delete("games", gameId);
loadGames();
};
}, [gameId, setGameFromUrl, getGame]);
const isReady = db !== null;

View File

@@ -19,11 +19,11 @@ export default function GameReport() {
item
container
rowGap={2}
paddingLeft={{ xs: 0, md: 6 }}
paddingLeft={{ xs: 0, lg: 6 }}
justifyContent="center"
alignItems="center"
xs={12}
md={6}
lg={6}
>
<Grid
container

View File

@@ -1,61 +0,0 @@
import { Stockfish } from "@/lib/engine/stockfish";
import { Icon } from "@iconify/react";
import { Button, Grid } from "@mui/material";
import { useEffect, useState } from "react";
import { gameAtom, gameEvalAtom } from "./states";
import { useAtomValue, useSetAtom } from "jotai";
import { getFens } from "@/lib/chess";
import { useGameDatabase } from "@/hooks/useGameDatabase";
export default function AnalyzeButton() {
const [engine, setEngine] = useState<Stockfish | null>(null);
const [evaluationInProgress, setEvaluationInProgress] = useState(false);
const { setGameEval, gameFromUrl } = useGameDatabase();
const setEval = useSetAtom(gameEvalAtom);
const game = useAtomValue(gameAtom);
useEffect(() => {
const engine = new Stockfish();
engine.init().then(() => {
setEngine(engine);
});
return () => {
engine.shutdown();
};
}, []);
const readyToAnalyse =
engine?.isReady() && game.history().length > 0 && !evaluationInProgress;
const handleAnalyze = async () => {
const gameFens = getFens(game);
if (!engine?.isReady() || gameFens.length === 0 || evaluationInProgress)
return;
setEvaluationInProgress(true);
const newGameEval = await engine.evaluateGame(gameFens);
setEval(newGameEval);
setEvaluationInProgress(false);
if (gameFromUrl) {
setGameEval(gameFromUrl.id, newGameEval);
}
};
return (
<Grid item container xs={12} justifyContent="center" alignItems="center">
<Button
variant="contained"
size="large"
startIcon={<Icon icon="streamline:magnifying-glass-solid" />}
onClick={handleAnalyze}
disabled={!readyToAnalyse}
>
Analyse
</Button>
</Grid>
);
}

View File

@@ -6,26 +6,16 @@ import LineEvaluation from "./lineEvaluation";
import { useCurrentMove } from "@/hooks/useCurrentMove";
export default function ReviewPanelBody() {
const move = useCurrentMove();
const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom);
const move = useCurrentMove();
const getBestMoveLabel = () => {
const bestMove = move?.lastEval?.bestMove;
if (bestMove) {
return `${bestMove} was the best move`;
}
const boardHistory = board.history();
const gameHistory = game.history();
if (game.isGameOver() && boardHistory.join() === gameHistory.join()) {
return "Game is over";
}
return null;
};
const bestMove = move?.lastEval?.bestMove;
const isGameOver =
gameHistory.length > 0 && boardHistory.join() === gameHistory.join();
return (
<>
@@ -49,9 +39,21 @@ export default function ReviewPanelBody() {
</Typography>
</Grid>
{!!bestMove && (
<Grid item xs={12}>
<Typography variant="h6" align="center">
{getBestMoveLabel()}
{`${bestMove} was the best move`}
</Typography>
</Grid>
)}
{isGameOver && (
<Grid item xs={12}>
<Typography variant="h6" align="center">
Game is over
</Typography>
</Grid>
)}
<Grid item container xs={12} justifyContent="center" alignItems="center">
<List>

View File

@@ -0,0 +1,111 @@
import { Stockfish } from "@/lib/engine/stockfish";
import { Icon } from "@iconify/react";
import { Grid } from "@mui/material";
import { useEffect, useState } from "react";
import {
engineDepthAtom,
engineMultiPvAtom,
gameAtom,
gameEvalAtom,
} from "../states";
import { useAtomValue, useSetAtom } from "jotai";
import { getFens } from "@/lib/chess";
import { useGameDatabase } from "@/hooks/useGameDatabase";
import { LoadingButton } from "@mui/lab";
import Slider from "@/components/slider";
export default function AnalyzePanel() {
const [engine, setEngine] = useState<Stockfish | null>(null);
const [evaluationInProgress, setEvaluationInProgress] = useState(false);
const engineDepth = useAtomValue(engineDepthAtom);
const engineMultiPv = useAtomValue(engineMultiPvAtom);
const { setGameEval, gameFromUrl } = useGameDatabase();
const setEval = useSetAtom(gameEvalAtom);
const game = useAtomValue(gameAtom);
useEffect(() => {
const engine = new Stockfish();
engine.init().then(() => {
setEngine(engine);
});
return () => {
engine.shutdown();
};
}, []);
const readyToAnalyse =
engine?.isReady() && game.history().length > 0 && !evaluationInProgress;
const handleAnalyze = async () => {
const gameFens = getFens(game);
if (!engine?.isReady() || gameFens.length === 0 || evaluationInProgress)
return;
setEvaluationInProgress(true);
const newGameEval = await engine.evaluateGame(
gameFens,
engineDepth,
engineMultiPv
);
setEval(newGameEval);
setEvaluationInProgress(false);
if (gameFromUrl) {
setGameEval(gameFromUrl.id, newGameEval);
}
};
return (
<Grid
item
container
xs={12}
justifyContent="center"
alignItems="center"
rowGap={4}
>
<Slider label="Maximum depth" atom={engineDepthAtom} min={10} max={30} />
<Slider
label="Number of lines"
atom={engineMultiPvAtom}
min={1}
max={6}
xs={6}
/>
<Grid
item
container
xs={12}
justifyContent="center"
alignItems="center"
marginTop={1}
>
<LoadingButton
variant="contained"
size="large"
startIcon={
!evaluationInProgress && (
<Icon icon="streamline:magnifying-glass-solid" />
)
}
onClick={handleAnalyze}
disabled={!readyToAnalyse}
loading={evaluationInProgress}
loadingPosition={evaluationInProgress ? "end" : undefined}
endIcon={
evaluationInProgress && (
<Icon icon="streamline:magnifying-glass-solid" />
)
}
>
{evaluationInProgress ? "Analyzing..." : "Analyze"}
</LoadingButton>
</Grid>
</Grid>
);
}

View File

@@ -1,7 +1,7 @@
import { Icon } from "@iconify/react";
import { Grid, Typography } from "@mui/material";
import LoadGame from "./loadGame";
import AnalyzeButton from "./analyzeButton";
import AnalyzePanel from "./analyzePanel";
export default function ReviewPanelHeader() {
return (
@@ -29,7 +29,7 @@ export default function ReviewPanelHeader() {
<LoadGame />
<AnalyzeButton />
<AnalyzePanel />
</Grid>
);
}

View File

@@ -1,5 +1,5 @@
import { Grid } from "@mui/material";
import LoadGameButton from "../loadGame/loadGameButton";
import LoadGameButton from "../../loadGame/loadGameButton";
import { useCallback, useEffect } from "react";
import { useChessActions } from "@/hooks/useChess";
import {
@@ -7,7 +7,7 @@ import {
boardOrientationAtom,
gameAtom,
gameEvalAtom,
} from "./states";
} from "../states";
import { useGameDatabase } from "@/hooks/useGameDatabase";
import { useAtomValue, useSetAtom } from "jotai";
import { Chess } from "chess.js";
@@ -43,7 +43,7 @@ export default function LoadGame() {
};
loadGame();
}, [gameFromUrl, resetAndSetGamePgn, setEval]);
}, [gameFromUrl, game, resetAndSetGamePgn, setEval]);
if (gameFromUrl) return null;

View File

@@ -11,11 +11,13 @@ export default function SaveButton() {
const board = useAtomValue(boardAtom);
const gameEval = useAtomValue(gameEvalAtom);
const { addGame, setGameEval, gameFromUrl } = useGameDatabase();
const router = useRouter();
const enableSave =
!gameFromUrl && (board.history().length || game.history().length);
const handleSave = async () => {
if (gameFromUrl) return;
if (!enableSave) return;
const gameToSave = getGameToSave(game, board);
@@ -35,8 +37,16 @@ export default function SaveButton() {
};
return (
<IconButton onClick={handleSave} disabled={!!gameFromUrl}>
<>
{gameFromUrl ? (
<IconButton disabled={true}>
<Icon icon="ri:folder-check-line" />
</IconButton>
) : (
<IconButton onClick={handleSave} disabled={!enableSave}>
<Icon icon="ri:save-3-line" />
</IconButton>
)}
</>
);
}

View File

@@ -6,3 +6,6 @@ export const gameEvalAtom = atom<GameEval | undefined>(undefined);
export const gameAtom = atom(new Chess());
export const boardAtom = atom(new Chess());
export const boardOrientationAtom = atom(true);
export const engineDepthAtom = atom(16);
export const engineMultiPvAtom = atom(3);