diff --git a/src/hooks/useEngine.ts b/src/hooks/useEngine.ts index b1ea406..fd78acc 100644 --- a/src/hooks/useEngine.ts +++ b/src/hooks/useEngine.ts @@ -1,15 +1,18 @@ import { Stockfish16 } from "@/lib/engine/stockfish16"; import { UciEngine } from "@/lib/engine/uciEngine"; +import { engineMultiPvAtom } from "@/sections/analysis/states"; import { EngineName } from "@/types/enums"; +import { useAtomValue } from "jotai"; import { useEffect, useState } from "react"; export const useEngine = (engineName: EngineName) => { const [engine, setEngine] = useState(null); + const multiPv = useAtomValue(engineMultiPvAtom); const pickEngine = (engine: EngineName): UciEngine => { switch (engine) { case EngineName.Stockfish16: - return new Stockfish16(); + return new Stockfish16(multiPv); } }; diff --git a/src/lib/engine/stockfish16.ts b/src/lib/engine/stockfish16.ts index 629c9cb..681e026 100644 --- a/src/lib/engine/stockfish16.ts +++ b/src/lib/engine/stockfish16.ts @@ -2,14 +2,14 @@ import { EngineName } from "@/types/enums"; import { UciEngine } from "./uciEngine"; export class Stockfish16 extends UciEngine { - constructor() { + constructor(multiPv: number) { const isWasmSupported = Stockfish16.isWasmSupported(); const enginePath = isWasmSupported ? "engines/stockfish-wasm/stockfish-nnue-16-single.js" : "engines/stockfish.js"; - super(EngineName.Stockfish16, enginePath); + super(EngineName.Stockfish16, enginePath, multiPv); } public static isWasmSupported() { diff --git a/src/lib/engine/uciEngine.ts b/src/lib/engine/uciEngine.ts index b228f08..bc4f11a 100644 --- a/src/lib/engine/uciEngine.ts +++ b/src/lib/engine/uciEngine.ts @@ -5,10 +5,11 @@ export abstract class UciEngine { private worker: Worker; private ready = false; 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.multiPv = multiPv; this.worker = new Worker(enginePath); @@ -17,7 +18,7 @@ export abstract class UciEngine { public async init(): Promise { await this.sendCommands(["uci"], "uciok"); - await this.setMultiPv(3, false); + await this.setMultiPv(this.multiPv, false); this.ready = true; console.log(`${this.engineName} initialized`); } @@ -81,24 +82,21 @@ export abstract class UciEngine { public async evaluateGame( fens: string[], depth = 16, - multiPv = 3 + multiPv = this.multiPv ): Promise { this.throwErrorIfNotReady(); this.ready = false; - await this.setMultiPv(multiPv, false); await this.sendCommands(["ucinewgame", "isready"], "readyok"); this.worker.postMessage("position startpos"); const moves: MoveEval[] = []; for (const fen of fens) { - console.log(`Evaluating position: ${fen}`); - const result = await this.evaluatePosition(fen, depth, false); + const result = await this.evaluatePosition(fen, depth, multiPv, false); moves.push(result); } this.ready = true; - console.log(moves); return { moves, accuracy: { white: 82.34, black: 67.49 }, // TODO: Calculate accuracy @@ -114,12 +112,16 @@ export abstract class UciEngine { public async evaluatePosition( fen: string, depth = 16, + multiPv = this.multiPv, checkIsReady = true ): Promise { if (checkIsReady) { this.throwErrorIfNotReady(); } + await this.setMultiPv(multiPv, checkIsReady); + + console.log(`Evaluating position: ${fen}`); const results = await this.sendCommands( [`position fen ${fen}`, `go depth ${depth}`], "bestmove" diff --git a/src/pages/database.tsx b/src/pages/database.tsx index 9bbe1dd..72daa99 100644 --- a/src/pages/database.tsx +++ b/src/pages/database.tsx @@ -60,11 +60,15 @@ export default function GameDatabase() { width: 150, }, { - field: "white", + field: "whiteLabel", headerName: "White", - width: 150, + width: 200, headerAlign: "center", align: "center", + valueGetter: (params) => + `${params.row.white.name ?? "Unknown"} (${ + params.row.white.rating ?? "?" + })`, }, { field: "result", @@ -74,11 +78,15 @@ export default function GameDatabase() { width: 100, }, { - field: "black", + field: "blackLabel", headerName: "Black", - width: 150, + width: 200, headerAlign: "center", align: "center", + valueGetter: (params) => + `${params.row.black.name ?? "Unknown"} (${ + params.row.black.rating ?? "?" + })`, }, { field: "eval", @@ -136,15 +144,14 @@ export default function GameDatabase() { ); return ( - - + + @@ -152,7 +159,7 @@ export default function GameDatabase() { - You have {0} games in your database + You have {games.length} games in your database diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 20e465d..913dc2a 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,10 +1,37 @@ +import { useChessActions } from "@/hooks/useChess"; import Board from "@/sections/analysis/board"; import ReviewPanelBody from "@/sections/analysis/reviewPanelBody"; import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader"; import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar"; +import { + boardAtom, + boardOrientationAtom, + gameAtom, + gameEvalAtom, +} from "@/sections/analysis/states"; 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() { + 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 ( { - if (!currentMove?.lastEval) return []; + const arrows: Arrow[] = []; - const bestMoveArrow = [ - currentMove.lastEval.bestMove.slice(0, 2), - currentMove.lastEval.bestMove.slice(2, 4), - "#3aab18", - ] as Arrow; + if (currentMove?.lastEval && showBestMoveArrow) { + const bestMoveArrow = [ + currentMove.lastEval.bestMove.slice(0, 2), + currentMove.lastEval.bestMove.slice(2, 4), + "#3aab18", + ] as Arrow; - if ( - !currentMove.from || - !currentMove.to || - (currentMove.from === bestMoveArrow[0] && - currentMove.to === bestMoveArrow[1]) - ) { - return [bestMoveArrow]; + arrows.push(bestMoveArrow); } - return [[currentMove.from, currentMove.to, "#ffaa00"], bestMoveArrow]; - }, [currentMove]); + if (currentMove.from && currentMove.to && showPlayerMoveArrow) { + 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 ( + No game loaded ); diff --git a/src/sections/analysis/reviewPanelHeader/playerInfo.tsx b/src/sections/analysis/reviewPanelHeader/gamePanel/playerInfo.tsx similarity index 95% rename from src/sections/analysis/reviewPanelHeader/playerInfo.tsx rename to src/sections/analysis/reviewPanelHeader/gamePanel/playerInfo.tsx index 05ac5a1..4cd9fed 100644 --- a/src/sections/analysis/reviewPanelHeader/playerInfo.tsx +++ b/src/sections/analysis/reviewPanelHeader/gamePanel/playerInfo.tsx @@ -1,7 +1,7 @@ import { useGameDatabase } from "@/hooks/useGameDatabase"; import { Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; -import { gameAtom } from "../states"; +import { gameAtom } from "../../states"; interface Props { color: "white" | "black"; diff --git a/src/sections/analysis/reviewPanelHeader/loadGame.tsx b/src/sections/analysis/reviewPanelHeader/loadGame.tsx index a69f20b..e50c16c 100644 --- a/src/sections/analysis/reviewPanelHeader/loadGame.tsx +++ b/src/sections/analysis/reviewPanelHeader/loadGame.tsx @@ -54,7 +54,7 @@ export default function LoadGame() { { - await router.push(""); + await router.push("/"); resetAndSetGamePgn(game.pgn()); }} /> diff --git a/src/sections/analysis/reviewPanelToolbar/flipBoardButton.tsx b/src/sections/analysis/reviewPanelToolbar/flipBoardButton.tsx index f26ebe3..42306d2 100644 --- a/src/sections/analysis/reviewPanelToolbar/flipBoardButton.tsx +++ b/src/sections/analysis/reviewPanelToolbar/flipBoardButton.tsx @@ -1,14 +1,16 @@ import { useSetAtom } from "jotai"; import { boardOrientationAtom } from "../states"; -import { IconButton } from "@mui/material"; +import { IconButton, Tooltip } from "@mui/material"; import { Icon } from "@iconify/react"; export default function FlipBoardButton() { const setBoardOrientation = useSetAtom(boardOrientationAtom); return ( - setBoardOrientation((prev) => !prev)}> - - + + setBoardOrientation((prev) => !prev)}> + + + ); } diff --git a/src/sections/analysis/reviewPanelToolbar/goToLastPositionButton.tsx b/src/sections/analysis/reviewPanelToolbar/goToLastPositionButton.tsx index 6ea4022..9c218ee 100644 --- a/src/sections/analysis/reviewPanelToolbar/goToLastPositionButton.tsx +++ b/src/sections/analysis/reviewPanelToolbar/goToLastPositionButton.tsx @@ -1,5 +1,5 @@ import { Icon } from "@iconify/react"; -import { IconButton } from "@mui/material"; +import { Grid, IconButton, Tooltip } from "@mui/material"; import { useAtomValue } from "jotai"; import { boardAtom, gameAtom } from "../states"; import { useChessActions } from "@/hooks/useChess"; @@ -15,14 +15,18 @@ export default function GoToLastPositionButton() { const isButtonDisabled = boardHistory >= gameHistory; return ( - { - if (isButtonDisabled) return; - boardActions.setPgn(game.pgn()); - }} - disabled={isButtonDisabled} - > - - + + + { + if (isButtonDisabled) return; + boardActions.setPgn(game.pgn()); + }} + disabled={isButtonDisabled} + > + + + + ); } diff --git a/src/sections/analysis/reviewPanelToolbar/index.tsx b/src/sections/analysis/reviewPanelToolbar/index.tsx index c31b0fc..a7e4e42 100644 --- a/src/sections/analysis/reviewPanelToolbar/index.tsx +++ b/src/sections/analysis/reviewPanelToolbar/index.tsx @@ -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 { useAtomValue } from "jotai"; -import { boardAtom } from "../states"; +import { useAtom, useAtomValue } from "jotai"; +import { + boardAtom, + showBestMoveArrowAtom, + showPlayerMoveArrowAtom, +} from "../states"; import { useChessActions } from "@/hooks/useChess"; import FlipBoardButton from "./flipBoardButton"; import NextMoveButton from "./nextMoveButton"; @@ -9,6 +20,8 @@ import GoToLastPositionButton from "./goToLastPositionButton"; import SaveButton from "./saveButton"; export default function ReviewPanelToolBar() { + const [showBestMove, setShowBestMove] = useAtom(showBestMoveArrowAtom); + const [showPlayerMove, setShowPlayerMove] = useAtom(showPlayerMoveArrowAtom); const board = useAtomValue(boardAtom); const boardActions = useChessActions(boardAtom); @@ -21,19 +34,27 @@ export default function ReviewPanelToolBar() { - boardActions.reset()} - disabled={boardHistory.length === 0} - > - - + + + boardActions.reset()} + disabled={boardHistory.length === 0} + > + + + + - boardActions.undo()} - disabled={boardHistory.length === 0} - > - - + + + boardActions.undo()} + disabled={boardHistory.length === 0} + > + + + + @@ -41,6 +62,37 @@ export default function ReviewPanelToolBar() { + + + setShowBestMove(checked)} + /> + } + label="Show best move green arrow" + sx={{ marginX: 0 }} + /> + setShowPlayerMove(checked)} + /> + } + label="Show player move yellow arrow" + sx={{ marginX: 0 }} + /> + ); } diff --git a/src/sections/analysis/reviewPanelToolbar/nextMoveButton.tsx b/src/sections/analysis/reviewPanelToolbar/nextMoveButton.tsx index 8567663..535b449 100644 --- a/src/sections/analysis/reviewPanelToolbar/nextMoveButton.tsx +++ b/src/sections/analysis/reviewPanelToolbar/nextMoveButton.tsx @@ -1,5 +1,5 @@ import { Icon } from "@iconify/react"; -import { IconButton } from "@mui/material"; +import { Grid, IconButton, Tooltip } from "@mui/material"; import { useAtomValue } from "jotai"; import { boardAtom, gameAtom } from "../states"; import { useChessActions } from "@/hooks/useChess"; @@ -32,11 +32,15 @@ export default function NextMoveButton() { }; return ( - addNextGameMoveToBoard()} - disabled={!isButtonEnabled} - > - - + + + addNextGameMoveToBoard()} + disabled={!isButtonEnabled} + > + + + + ); } diff --git a/src/sections/analysis/reviewPanelToolbar/saveButton.tsx b/src/sections/analysis/reviewPanelToolbar/saveButton.tsx index c7aecea..ff31bb7 100644 --- a/src/sections/analysis/reviewPanelToolbar/saveButton.tsx +++ b/src/sections/analysis/reviewPanelToolbar/saveButton.tsx @@ -1,6 +1,6 @@ import { useGameDatabase } from "@/hooks/useGameDatabase"; import { Icon } from "@iconify/react"; -import { IconButton } from "@mui/material"; +import { Grid, IconButton, Tooltip } from "@mui/material"; import { useAtomValue } from "jotai"; import { useRouter } from "next/router"; import { boardAtom, gameAtom, gameEvalAtom } from "../states"; @@ -39,13 +39,21 @@ export default function SaveButton() { return ( <> {gameFromUrl ? ( - - - + + + + + + + ) : ( - - - + + + + + + + )} ); diff --git a/src/sections/analysis/states.ts b/src/sections/analysis/states.ts index 66456a0..9b2afa4 100644 --- a/src/sections/analysis/states.ts +++ b/src/sections/analysis/states.ts @@ -5,7 +5,10 @@ import { atom } from "jotai"; export const gameEvalAtom = atom(undefined); export const gameAtom = atom(new Chess()); export const boardAtom = atom(new Chess()); + export const boardOrientationAtom = atom(true); +export const showBestMoveArrowAtom = atom(true); +export const showPlayerMoveArrowAtom = atom(true); export const engineDepthAtom = atom(16); export const engineMultiPvAtom = atom(3);