feat : add arrow opt out options

This commit is contained in:
GuillaumeSD
2024-02-24 18:20:48 +01:00
parent 035591208f
commit 7b328d3159
16 changed files with 228 additions and 89 deletions

View File

@@ -1,15 +1,18 @@
import { Stockfish16 } from "@/lib/engine/stockfish16"; import { Stockfish16 } from "@/lib/engine/stockfish16";
import { UciEngine } from "@/lib/engine/uciEngine"; import { UciEngine } from "@/lib/engine/uciEngine";
import { engineMultiPvAtom } from "@/sections/analysis/states";
import { EngineName } from "@/types/enums"; import { EngineName } from "@/types/enums";
import { useAtomValue } from "jotai";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const useEngine = (engineName: EngineName) => { export const useEngine = (engineName: EngineName) => {
const [engine, setEngine] = useState<UciEngine | null>(null); const [engine, setEngine] = useState<UciEngine | null>(null);
const multiPv = useAtomValue(engineMultiPvAtom);
const pickEngine = (engine: EngineName): UciEngine => { const pickEngine = (engine: EngineName): UciEngine => {
switch (engine) { switch (engine) {
case EngineName.Stockfish16: case EngineName.Stockfish16:
return new Stockfish16(); return new Stockfish16(multiPv);
} }
}; };

View File

@@ -2,14 +2,14 @@ import { EngineName } from "@/types/enums";
import { UciEngine } from "./uciEngine"; import { UciEngine } from "./uciEngine";
export class Stockfish16 extends UciEngine { export class Stockfish16 extends UciEngine {
constructor() { constructor(multiPv: number) {
const isWasmSupported = Stockfish16.isWasmSupported(); const isWasmSupported = Stockfish16.isWasmSupported();
const enginePath = isWasmSupported const enginePath = isWasmSupported
? "engines/stockfish-wasm/stockfish-nnue-16-single.js" ? "engines/stockfish-wasm/stockfish-nnue-16-single.js"
: "engines/stockfish.js"; : "engines/stockfish.js";
super(EngineName.Stockfish16, enginePath); super(EngineName.Stockfish16, enginePath, multiPv);
} }
public static isWasmSupported() { public static isWasmSupported() {

View File

@@ -5,10 +5,11 @@ export abstract class UciEngine {
private worker: Worker; private worker: Worker;
private ready = false; private ready = false;
private engineName: EngineName; private engineName: EngineName;
private multiPv = 3; private multiPv: number;
constructor(engineName: EngineName, enginePath: string) { constructor(engineName: EngineName, enginePath: string, multiPv: number) {
this.engineName = engineName; this.engineName = engineName;
this.multiPv = multiPv;
this.worker = new Worker(enginePath); this.worker = new Worker(enginePath);
@@ -17,7 +18,7 @@ export abstract class UciEngine {
public async init(): Promise<void> { public async init(): Promise<void> {
await this.sendCommands(["uci"], "uciok"); await this.sendCommands(["uci"], "uciok");
await this.setMultiPv(3, false); await this.setMultiPv(this.multiPv, false);
this.ready = true; this.ready = true;
console.log(`${this.engineName} initialized`); console.log(`${this.engineName} initialized`);
} }
@@ -81,24 +82,21 @@ export abstract class UciEngine {
public async evaluateGame( public async evaluateGame(
fens: string[], fens: string[],
depth = 16, depth = 16,
multiPv = 3 multiPv = this.multiPv
): Promise<GameEval> { ): Promise<GameEval> {
this.throwErrorIfNotReady(); this.throwErrorIfNotReady();
this.ready = false; this.ready = false;
await this.setMultiPv(multiPv, false);
await this.sendCommands(["ucinewgame", "isready"], "readyok"); await this.sendCommands(["ucinewgame", "isready"], "readyok");
this.worker.postMessage("position startpos"); this.worker.postMessage("position startpos");
const moves: MoveEval[] = []; const moves: MoveEval[] = [];
for (const fen of fens) { for (const fen of fens) {
console.log(`Evaluating position: ${fen}`); const result = await this.evaluatePosition(fen, depth, multiPv, false);
const result = await this.evaluatePosition(fen, depth, false);
moves.push(result); moves.push(result);
} }
this.ready = true; this.ready = true;
console.log(moves);
return { return {
moves, moves,
accuracy: { white: 82.34, black: 67.49 }, // TODO: Calculate accuracy accuracy: { white: 82.34, black: 67.49 }, // TODO: Calculate accuracy
@@ -114,12 +112,16 @@ export abstract class UciEngine {
public async evaluatePosition( public async evaluatePosition(
fen: string, fen: string,
depth = 16, depth = 16,
multiPv = this.multiPv,
checkIsReady = true checkIsReady = true
): Promise<MoveEval> { ): Promise<MoveEval> {
if (checkIsReady) { if (checkIsReady) {
this.throwErrorIfNotReady(); this.throwErrorIfNotReady();
} }
await this.setMultiPv(multiPv, checkIsReady);
console.log(`Evaluating position: ${fen}`);
const results = await this.sendCommands( const results = await this.sendCommands(
[`position fen ${fen}`, `go depth ${depth}`], [`position fen ${fen}`, `go depth ${depth}`],
"bestmove" "bestmove"

View File

@@ -60,11 +60,15 @@ export default function GameDatabase() {
width: 150, width: 150,
}, },
{ {
field: "white", field: "whiteLabel",
headerName: "White", headerName: "White",
width: 150, width: 200,
headerAlign: "center", headerAlign: "center",
align: "center", align: "center",
valueGetter: (params) =>
`${params.row.white.name ?? "Unknown"} (${
params.row.white.rating ?? "?"
})`,
}, },
{ {
field: "result", field: "result",
@@ -74,11 +78,15 @@ export default function GameDatabase() {
width: 100, width: 100,
}, },
{ {
field: "black", field: "blackLabel",
headerName: "Black", headerName: "Black",
width: 150, width: 200,
headerAlign: "center", headerAlign: "center",
align: "center", align: "center",
valueGetter: (params) =>
`${params.row.black.name ?? "Unknown"} (${
params.row.black.rating ?? "?"
})`,
}, },
{ {
field: "eval", field: "eval",
@@ -136,15 +144,14 @@ export default function GameDatabase() {
); );
return ( return (
<Grid container rowSpacing={3} justifyContent="center" alignItems="center">
<Grid <Grid
item
container container
xs={12}
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
spacing={4} gap={4}
marginTop={6}
> >
<Grid item container xs={12} justifyContent="center" alignItems="center">
<Grid item container justifyContent="center" sx={{ maxWidth: "250px" }}> <Grid item container justifyContent="center" sx={{ maxWidth: "250px" }}>
<LoadGameButton /> <LoadGameButton />
</Grid> </Grid>
@@ -152,7 +159,7 @@ export default function GameDatabase() {
<Grid item container xs={12} justifyContent="center" alignItems="center"> <Grid item container xs={12} justifyContent="center" alignItems="center">
<Typography variant="subtitle2"> <Typography variant="subtitle2">
You have {0} games in your database You have {games.length} games in your database
</Typography> </Typography>
</Grid> </Grid>

View File

@@ -1,10 +1,37 @@
import { useChessActions } from "@/hooks/useChess";
import Board from "@/sections/analysis/board"; import Board from "@/sections/analysis/board";
import ReviewPanelBody from "@/sections/analysis/reviewPanelBody"; import ReviewPanelBody from "@/sections/analysis/reviewPanelBody";
import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader"; import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader";
import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar"; import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar";
import {
boardAtom,
boardOrientationAtom,
gameAtom,
gameEvalAtom,
} from "@/sections/analysis/states";
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import { Chess } from "chess.js";
import { useSetAtom } from "jotai";
import { useRouter } from "next/router";
import { useEffect } from "react";
export default function GameReport() { export default function GameReport() {
const boardActions = useChessActions(boardAtom);
const gameActions = useChessActions(gameAtom);
const setEval = useSetAtom(gameEvalAtom);
const setBoardOrientation = useSetAtom(boardOrientationAtom);
const router = useRouter();
const { gameId } = router.query;
useEffect(() => {
if (!gameId) {
boardActions.reset();
setEval(undefined);
setBoardOrientation(true);
gameActions.setPgn(new Chess().pgn());
}
}, [gameId]);
return ( return (
<Grid <Grid
container container

View File

@@ -1,7 +1,12 @@
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import { Chessboard } from "react-chessboard"; import { Chessboard } from "react-chessboard";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { boardAtom, boardOrientationAtom } from "../states"; import {
boardAtom,
boardOrientationAtom,
showBestMoveArrowAtom,
showPlayerMoveArrowAtom,
} from "../states";
import { Arrow, Square } from "react-chessboard/dist/chessboard/types"; import { Arrow, Square } from "react-chessboard/dist/chessboard/types";
import { useChessActions } from "@/hooks/useChess"; import { useChessActions } from "@/hooks/useChess";
import { useCurrentMove } from "@/hooks/useCurrentMove"; import { useCurrentMove } from "@/hooks/useCurrentMove";
@@ -11,6 +16,8 @@ import PlayerInfo from "./playerInfo";
export default function Board() { export default function Board() {
const board = useAtomValue(boardAtom); const board = useAtomValue(boardAtom);
const boardOrientation = useAtomValue(boardOrientationAtom); const boardOrientation = useAtomValue(boardOrientationAtom);
const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom);
const showPlayerMoveArrow = useAtomValue(showPlayerMoveArrowAtom);
const boardActions = useChessActions(boardAtom); const boardActions = useChessActions(boardAtom);
const currentMove = useCurrentMove(); const currentMove = useCurrentMove();
@@ -29,25 +36,37 @@ export default function Board() {
}; };
const customArrows: Arrow[] = useMemo(() => { const customArrows: Arrow[] = useMemo(() => {
if (!currentMove?.lastEval) return []; const arrows: Arrow[] = [];
if (currentMove?.lastEval && showBestMoveArrow) {
const bestMoveArrow = [ const bestMoveArrow = [
currentMove.lastEval.bestMove.slice(0, 2), currentMove.lastEval.bestMove.slice(0, 2),
currentMove.lastEval.bestMove.slice(2, 4), currentMove.lastEval.bestMove.slice(2, 4),
"#3aab18", "#3aab18",
] as Arrow; ] as Arrow;
if ( arrows.push(bestMoveArrow);
!currentMove.from ||
!currentMove.to ||
(currentMove.from === bestMoveArrow[0] &&
currentMove.to === bestMoveArrow[1])
) {
return [bestMoveArrow];
} }
return [[currentMove.from, currentMove.to, "#ffaa00"], bestMoveArrow]; if (currentMove.from && currentMove.to && showPlayerMoveArrow) {
}, [currentMove]); const playerMoveArrow: Arrow = [
currentMove.from,
currentMove.to,
"#ffaa00",
];
if (
arrows.every(
(arrow) =>
arrow[0] !== playerMoveArrow[0] || arrow[1] !== playerMoveArrow[1]
)
) {
arrows.push(playerMoveArrow);
}
}
return arrows;
}, [currentMove, showBestMoveArrow, showPlayerMoveArrow]);
return ( return (
<Grid <Grid
@@ -69,7 +88,7 @@ export default function Board() {
maxWidth={"80vh"} maxWidth={"80vh"}
> >
<Chessboard <Chessboard
id="BasicBoard" id="AnalysisBoard"
position={board.fen()} position={board.fen()}
onPieceDrop={onPieceDrop} onPieceDrop={onPieceDrop}
boardOrientation={boardOrientation ? "white" : "black"} boardOrientation={boardOrientation ? "white" : "black"}

View File

@@ -40,6 +40,7 @@ export default function AnalyzePanel() {
engineDepth, engineDepth,
engineMultiPv engineMultiPv
); );
console.log(newGameEval);
setEval(newGameEval); setEval(newGameEval);
setEvaluationInProgress(false); setEvaluationInProgress(false);

View File

@@ -1,7 +1,7 @@
import { Grid, Typography } from "@mui/material"; import { Grid, Typography } from "@mui/material";
import { useGameDatabase } from "@/hooks/useGameDatabase"; import { useGameDatabase } from "@/hooks/useGameDatabase";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { gameAtom } from "../states"; import { gameAtom } from "../../states";
import PlayerInfo from "./playerInfo"; import PlayerInfo from "./playerInfo";
export default function GamePanel() { export default function GamePanel() {
@@ -12,7 +12,14 @@ export default function GamePanel() {
if (!hasGameInfo) { if (!hasGameInfo) {
return ( return (
<Grid item container xs={12} justifyContent="center" alignItems="center"> <Grid
item
container
xs={12}
justifyContent="center"
alignItems="center"
marginY={1}
>
<Typography variant="h6">No game loaded</Typography> <Typography variant="h6">No game loaded</Typography>
</Grid> </Grid>
); );

View File

@@ -1,7 +1,7 @@
import { useGameDatabase } from "@/hooks/useGameDatabase"; import { useGameDatabase } from "@/hooks/useGameDatabase";
import { Grid, Typography } from "@mui/material"; import { Grid, Typography } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { gameAtom } from "../states"; import { gameAtom } from "../../states";
interface Props { interface Props {
color: "white" | "black"; color: "white" | "black";

View File

@@ -54,7 +54,7 @@ export default function LoadGame() {
<LoadGameButton <LoadGameButton
label={isGameLoaded ? "Load another game" : "Load game"} label={isGameLoaded ? "Load another game" : "Load game"}
setGame={async (game) => { setGame={async (game) => {
await router.push(""); await router.push("/");
resetAndSetGamePgn(game.pgn()); resetAndSetGamePgn(game.pgn());
}} }}
/> />

View File

@@ -1,14 +1,16 @@
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { boardOrientationAtom } from "../states"; import { boardOrientationAtom } from "../states";
import { IconButton } from "@mui/material"; import { IconButton, Tooltip } from "@mui/material";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
export default function FlipBoardButton() { export default function FlipBoardButton() {
const setBoardOrientation = useSetAtom(boardOrientationAtom); const setBoardOrientation = useSetAtom(boardOrientationAtom);
return ( return (
<Tooltip title="Flip board">
<IconButton onClick={() => setBoardOrientation((prev) => !prev)}> <IconButton onClick={() => setBoardOrientation((prev) => !prev)}>
<Icon icon="eva:flip-fill" /> <Icon icon="eva:flip-fill" />
</IconButton> </IconButton>
</Tooltip>
); );
} }

View File

@@ -1,5 +1,5 @@
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { IconButton } from "@mui/material"; import { Grid, IconButton, Tooltip } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { boardAtom, gameAtom } from "../states"; import { boardAtom, gameAtom } from "../states";
import { useChessActions } from "@/hooks/useChess"; import { useChessActions } from "@/hooks/useChess";
@@ -15,6 +15,8 @@ export default function GoToLastPositionButton() {
const isButtonDisabled = boardHistory >= gameHistory; const isButtonDisabled = boardHistory >= gameHistory;
return ( return (
<Tooltip title="Go to final position">
<Grid>
<IconButton <IconButton
onClick={() => { onClick={() => {
if (isButtonDisabled) return; if (isButtonDisabled) return;
@@ -24,5 +26,7 @@ export default function GoToLastPositionButton() {
> >
<Icon icon="ri:skip-forward-line" /> <Icon icon="ri:skip-forward-line" />
</IconButton> </IconButton>
</Grid>
</Tooltip>
); );
} }

View File

@@ -1,7 +1,18 @@
import { Divider, Grid, IconButton } from "@mui/material"; import {
Checkbox,
Divider,
FormControlLabel,
Grid,
IconButton,
Tooltip,
} from "@mui/material";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { boardAtom } from "../states"; import {
boardAtom,
showBestMoveArrowAtom,
showPlayerMoveArrowAtom,
} from "../states";
import { useChessActions } from "@/hooks/useChess"; import { useChessActions } from "@/hooks/useChess";
import FlipBoardButton from "./flipBoardButton"; import FlipBoardButton from "./flipBoardButton";
import NextMoveButton from "./nextMoveButton"; import NextMoveButton from "./nextMoveButton";
@@ -9,6 +20,8 @@ import GoToLastPositionButton from "./goToLastPositionButton";
import SaveButton from "./saveButton"; import SaveButton from "./saveButton";
export default function ReviewPanelToolBar() { export default function ReviewPanelToolBar() {
const [showBestMove, setShowBestMove] = useAtom(showBestMoveArrowAtom);
const [showPlayerMove, setShowPlayerMove] = useAtom(showPlayerMoveArrowAtom);
const board = useAtomValue(boardAtom); const board = useAtomValue(boardAtom);
const boardActions = useChessActions(boardAtom); const boardActions = useChessActions(boardAtom);
@@ -21,19 +34,27 @@ export default function ReviewPanelToolBar() {
<Grid container item justifyContent="center" alignItems="center" xs={12}> <Grid container item justifyContent="center" alignItems="center" xs={12}>
<FlipBoardButton /> <FlipBoardButton />
<Tooltip title="Reset board">
<Grid>
<IconButton <IconButton
onClick={() => boardActions.reset()} onClick={() => boardActions.reset()}
disabled={boardHistory.length === 0} disabled={boardHistory.length === 0}
> >
<Icon icon="ri:skip-back-line" /> <Icon icon="ri:skip-back-line" />
</IconButton> </IconButton>
</Grid>
</Tooltip>
<Tooltip title="Go to previous move">
<Grid>
<IconButton <IconButton
onClick={() => boardActions.undo()} onClick={() => boardActions.undo()}
disabled={boardHistory.length === 0} disabled={boardHistory.length === 0}
> >
<Icon icon="ri:arrow-left-s-line" height={30} /> <Icon icon="ri:arrow-left-s-line" height={30} />
</IconButton> </IconButton>
</Grid>
</Tooltip>
<NextMoveButton /> <NextMoveButton />
@@ -41,6 +62,37 @@ export default function ReviewPanelToolBar() {
<SaveButton /> <SaveButton />
</Grid> </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>
</> </>
); );
} }

View File

@@ -1,5 +1,5 @@
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { IconButton } from "@mui/material"; import { Grid, IconButton, Tooltip } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { boardAtom, gameAtom } from "../states"; import { boardAtom, gameAtom } from "../states";
import { useChessActions } from "@/hooks/useChess"; import { useChessActions } from "@/hooks/useChess";
@@ -32,11 +32,15 @@ export default function NextMoveButton() {
}; };
return ( return (
<Tooltip title="Go to next move">
<Grid>
<IconButton <IconButton
onClick={() => addNextGameMoveToBoard()} onClick={() => addNextGameMoveToBoard()}
disabled={!isButtonEnabled} disabled={!isButtonEnabled}
> >
<Icon icon="ri:arrow-right-s-line" height={30} /> <Icon icon="ri:arrow-right-s-line" height={30} />
</IconButton> </IconButton>
</Grid>
</Tooltip>
); );
} }

View File

@@ -1,6 +1,6 @@
import { useGameDatabase } from "@/hooks/useGameDatabase"; import { useGameDatabase } from "@/hooks/useGameDatabase";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { IconButton } from "@mui/material"; import { Grid, IconButton, Tooltip } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { boardAtom, gameAtom, gameEvalAtom } from "../states"; import { boardAtom, gameAtom, gameEvalAtom } from "../states";
@@ -39,13 +39,21 @@ export default function SaveButton() {
return ( return (
<> <>
{gameFromUrl ? ( {gameFromUrl ? (
<Tooltip title="Game saved in database">
<Grid>
<IconButton disabled={true}> <IconButton disabled={true}>
<Icon icon="ri:folder-check-line" /> <Icon icon="ri:folder-check-line" />
</IconButton> </IconButton>
</Grid>
</Tooltip>
) : ( ) : (
<Tooltip title="Save game">
<Grid>
<IconButton onClick={handleSave} disabled={!enableSave}> <IconButton onClick={handleSave} disabled={!enableSave}>
<Icon icon="ri:save-3-line" /> <Icon icon="ri:save-3-line" />
</IconButton> </IconButton>
</Grid>
</Tooltip>
)} )}
</> </>
); );

View File

@@ -5,7 +5,10 @@ import { atom } from "jotai";
export const gameEvalAtom = atom<GameEval | undefined>(undefined); export const gameEvalAtom = atom<GameEval | undefined>(undefined);
export const gameAtom = atom(new Chess()); export const gameAtom = atom(new Chess());
export const boardAtom = atom(new Chess()); export const boardAtom = atom(new Chess());
export const boardOrientationAtom = atom(true); export const boardOrientationAtom = atom(true);
export const showBestMoveArrowAtom = atom(true);
export const showPlayerMoveArrowAtom = atom(true);
export const engineDepthAtom = atom(16); export const engineDepthAtom = atom(16);
export const engineMultiPvAtom = atom(3); export const engineMultiPvAtom = atom(3);