From 035591208f9b88279b1bdb731c9d4d9b5e6dbc6c Mon Sep 17 00:00:00 2001 From: GuillaumeSD Date: Sat, 24 Feb 2024 16:32:22 +0100 Subject: [PATCH] feat : add game panel info --- src/components/slider.tsx | 14 +++- src/hooks/useEngine.ts | 28 ++++++++ src/hooks/useGameDatabase.ts | 12 ++-- src/lib/chess.ts | 12 +++- src/lib/engine/stockfish16.ts | 23 +++++++ src/lib/engine/{stockfish.ts => uciEngine.ts} | 37 +++++------ .../analysis/{board.tsx => board/index.tsx} | 21 ++---- src/sections/analysis/board/playerInfo.tsx | 32 ++++++++++ .../reviewPanelHeader/analyzePanel.tsx | 29 ++++----- .../analysis/reviewPanelHeader/gamePanel.tsx | 64 +++++++++++++++++++ .../analysis/reviewPanelHeader/index.tsx | 19 +++++- .../analysis/reviewPanelHeader/loadGame.tsx | 12 +++- .../analysis/reviewPanelHeader/playerInfo.tsx | 38 +++++++++++ src/sections/loadGame/loadGameButton.tsx | 5 +- src/types/enums.ts | 2 +- src/types/eval.ts | 4 +- src/types/game.ts | 11 +++- 17 files changed, 290 insertions(+), 73 deletions(-) create mode 100644 src/hooks/useEngine.ts create mode 100644 src/lib/engine/stockfish16.ts rename src/lib/engine/{stockfish.ts => uciEngine.ts} (88%) rename src/sections/analysis/{board.tsx => board/index.tsx} (72%) create mode 100644 src/sections/analysis/board/playerInfo.tsx create mode 100644 src/sections/analysis/reviewPanelHeader/gamePanel.tsx create mode 100644 src/sections/analysis/reviewPanelHeader/playerInfo.tsx diff --git a/src/components/slider.tsx b/src/components/slider.tsx index 0f423af..7b4157f 100644 --- a/src/components/slider.tsx +++ b/src/components/slider.tsx @@ -7,9 +7,17 @@ interface Props { max: number; label: string; xs?: number; + marksFilter?: number; } -export default function Slider({ min, max, label, atom, xs }: Props) { +export default function Slider({ + min, + max, + label, + atom, + xs, + marksFilter = 1, +}: Props) { const [value, setValue] = useAtom(atom); return ( @@ -28,13 +36,15 @@ export default function Slider({ min, max, label, atom, xs }: Props) { > {label} + ({ value: i + min, label: `${i + min}`, - }))} + })).filter((_, i) => i % marksFilter === 0)} + step={1} valueLabelDisplay="off" value={value} onChange={(_, value) => setValue(value as number)} diff --git a/src/hooks/useEngine.ts b/src/hooks/useEngine.ts new file mode 100644 index 0000000..b1ea406 --- /dev/null +++ b/src/hooks/useEngine.ts @@ -0,0 +1,28 @@ +import { Stockfish16 } from "@/lib/engine/stockfish16"; +import { UciEngine } from "@/lib/engine/uciEngine"; +import { EngineName } from "@/types/enums"; +import { useEffect, useState } from "react"; + +export const useEngine = (engineName: EngineName) => { + const [engine, setEngine] = useState(null); + + const pickEngine = (engine: EngineName): UciEngine => { + switch (engine) { + case EngineName.Stockfish16: + return new Stockfish16(); + } + }; + + useEffect(() => { + const engine = pickEngine(engineName); + engine.init().then(() => { + setEngine(engine); + }); + + return () => { + engine.shutdown(); + }; + }, []); + + return engine; +}; diff --git a/src/hooks/useGameDatabase.ts b/src/hooks/useGameDatabase.ts index 0ad89e7..566747c 100644 --- a/src/hooks/useGameDatabase.ts +++ b/src/hooks/useGameDatabase.ts @@ -105,10 +105,14 @@ export const useGameDatabase = (shouldFetchGames?: boolean) => { const { gameId } = router.query; useEffect(() => { - if (typeof gameId === "string") { - getGame(parseInt(gameId)).then((game) => { - setGameFromUrl(game); - }); + switch (typeof gameId) { + case "string": + getGame(parseInt(gameId)).then((game) => { + setGameFromUrl(game); + }); + break; + default: + setGameFromUrl(undefined); } }, [gameId, setGameFromUrl, getGame]); diff --git a/src/lib/chess.ts b/src/lib/chess.ts index f6cb33e..70ddaef 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -21,9 +21,17 @@ export const formatGameToDatabase = (game: Chess): Omit => { site: headers.Site, date: headers.Date, round: headers.Round, - white: headers.White, - black: headers.Black, + white: { + name: headers.White, + rating: headers.WhiteElo ? Number(headers.WhiteElo) : undefined, + }, + black: { + name: headers.Black, + rating: headers.BlackElo ? Number(headers.BlackElo) : undefined, + }, result: headers.Result, + termination: headers.Termination, + timeControl: headers.TimeControl, }; }; diff --git a/src/lib/engine/stockfish16.ts b/src/lib/engine/stockfish16.ts new file mode 100644 index 0000000..629c9cb --- /dev/null +++ b/src/lib/engine/stockfish16.ts @@ -0,0 +1,23 @@ +import { EngineName } from "@/types/enums"; +import { UciEngine } from "./uciEngine"; + +export class Stockfish16 extends UciEngine { + constructor() { + const isWasmSupported = Stockfish16.isWasmSupported(); + + const enginePath = isWasmSupported + ? "engines/stockfish-wasm/stockfish-nnue-16-single.js" + : "engines/stockfish.js"; + + super(EngineName.Stockfish16, enginePath); + } + + public static isWasmSupported() { + return ( + typeof WebAssembly === "object" && + WebAssembly.validate( + Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00) + ) + ); + } +} diff --git a/src/lib/engine/stockfish.ts b/src/lib/engine/uciEngine.ts similarity index 88% rename from src/lib/engine/stockfish.ts rename to src/lib/engine/uciEngine.ts index 8485c51..b228f08 100644 --- a/src/lib/engine/stockfish.ts +++ b/src/lib/engine/uciEngine.ts @@ -1,37 +1,30 @@ -import { Engine } from "@/types/enums"; +import { EngineName } from "@/types/enums"; import { GameEval, LineEval, MoveEval } from "@/types/eval"; -export class Stockfish { +export abstract class UciEngine { private worker: Worker; private ready = false; + private engineName: EngineName; + private multiPv = 3; - constructor() { - this.worker = new Worker( - this.isWasmSupported() - ? "engines/stockfish-wasm/stockfish-nnue-16-single.js" - : "engines/stockfish.js" - ); + constructor(engineName: EngineName, enginePath: string) { + this.engineName = engineName; - console.log("Stockfish created"); - } + this.worker = new Worker(enginePath); - public isWasmSupported() { - return ( - typeof WebAssembly === "object" && - WebAssembly.validate( - Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00) - ) - ); + console.log(`${engineName} created`); } public async init(): Promise { await this.sendCommands(["uci"], "uciok"); await this.setMultiPv(3, false); this.ready = true; - console.log("Stockfish initialized"); + console.log(`${this.engineName} initialized`); } public async setMultiPv(multiPv: number, checkIsReady = true) { + if (multiPv === this.multiPv) return; + if (checkIsReady) { this.throwErrorIfNotReady(); } @@ -44,11 +37,13 @@ export class Stockfish { [`setoption name MultiPV value ${multiPv}`, "isready"], "readyok" ); + + this.multiPv = multiPv; } private throwErrorIfNotReady() { if (!this.ready) { - throw new Error("Stockfish is not ready"); + throw new Error(`${this.engineName} is not ready`); } } @@ -56,7 +51,7 @@ export class Stockfish { this.ready = false; this.worker.postMessage("quit"); this.worker.terminate(); - console.log("Stockfish shutdown"); + console.log(`${this.engineName} shutdown`); } public isReady(): boolean { @@ -108,7 +103,7 @@ export class Stockfish { moves, accuracy: { white: 82.34, black: 67.49 }, // TODO: Calculate accuracy settings: { - name: Engine.Stockfish16, + engine: this.engineName, date: new Date().toISOString(), depth, multiPv, diff --git a/src/sections/analysis/board.tsx b/src/sections/analysis/board/index.tsx similarity index 72% rename from src/sections/analysis/board.tsx rename to src/sections/analysis/board/index.tsx index 3744370..dca9ae4 100644 --- a/src/sections/analysis/board.tsx +++ b/src/sections/analysis/board/index.tsx @@ -1,15 +1,15 @@ -import { Grid, Typography } from "@mui/material"; +import { Grid } from "@mui/material"; import { Chessboard } from "react-chessboard"; import { useAtomValue } from "jotai"; -import { boardAtom, boardOrientationAtom, gameAtom } from "./states"; +import { boardAtom, boardOrientationAtom } from "../states"; import { Arrow, Square } from "react-chessboard/dist/chessboard/types"; import { useChessActions } from "@/hooks/useChess"; import { useCurrentMove } from "@/hooks/useCurrentMove"; import { useMemo } from "react"; +import PlayerInfo from "./playerInfo"; export default function Board() { const board = useAtomValue(boardAtom); - const game = useAtomValue(gameAtom); const boardOrientation = useAtomValue(boardOrientationAtom); const boardActions = useChessActions(boardAtom); const currentMove = useCurrentMove(); @@ -49,9 +49,6 @@ export default function Board() { return [[currentMove.from, currentMove.to, "#ffaa00"], bestMoveArrow]; }, [currentMove]); - const whiteLabel = game.header()["White"] || "White Player (?)"; - const blackLabel = game.header()["Black"] || "Black Player (?)"; - return ( - - - {boardOrientation ? blackLabel : whiteLabel} - - + - - - {boardOrientation ? whiteLabel : blackLabel} - - + ); } diff --git a/src/sections/analysis/board/playerInfo.tsx b/src/sections/analysis/board/playerInfo.tsx new file mode 100644 index 0000000..24bbc9e --- /dev/null +++ b/src/sections/analysis/board/playerInfo.tsx @@ -0,0 +1,32 @@ +import { useGameDatabase } from "@/hooks/useGameDatabase"; +import { Grid, Typography } from "@mui/material"; +import { useAtomValue } from "jotai"; +import { gameAtom } from "../states"; + +interface Props { + color: "white" | "black"; +} + +export default function PlayerInfo({ color }: Props) { + const { gameFromUrl } = useGameDatabase(); + const game = useAtomValue(gameAtom); + + const playerName = + gameFromUrl?.[color]?.name || + game.header()[color === "white" ? "White" : "Black"]; + + return ( + + + {playerName || (color === "white" ? "White" : "Black")} + + + ); +} diff --git a/src/sections/analysis/reviewPanelHeader/analyzePanel.tsx b/src/sections/analysis/reviewPanelHeader/analyzePanel.tsx index e6dc5cd..b6f5961 100644 --- a/src/sections/analysis/reviewPanelHeader/analyzePanel.tsx +++ b/src/sections/analysis/reviewPanelHeader/analyzePanel.tsx @@ -1,7 +1,6 @@ -import { Stockfish } from "@/lib/engine/stockfish"; import { Icon } from "@iconify/react"; import { Grid } from "@mui/material"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { engineDepthAtom, engineMultiPvAtom, @@ -13,9 +12,11 @@ import { getFens } from "@/lib/chess"; import { useGameDatabase } from "@/hooks/useGameDatabase"; import { LoadingButton } from "@mui/lab"; import Slider from "@/components/slider"; +import { useEngine } from "@/hooks/useEngine"; +import { EngineName } from "@/types/enums"; export default function AnalyzePanel() { - const [engine, setEngine] = useState(null); + const engine = useEngine(EngineName.Stockfish16); const [evaluationInProgress, setEvaluationInProgress] = useState(false); const engineDepth = useAtomValue(engineDepthAtom); const engineMultiPv = useAtomValue(engineMultiPvAtom); @@ -23,24 +24,14 @@ export default function AnalyzePanel() { 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) + if (!engine?.isReady() || gameFens.length === 0 || evaluationInProgress) { return; + } setEvaluationInProgress(true); @@ -67,7 +58,13 @@ export default function AnalyzePanel() { alignItems="center" rowGap={4} > - + + No game loaded + + ); + } + + return ( + + + + + + vs + + + + + + + + Site : {gameFromUrl?.site || game.header().Site || "?"} + + + + Date : {gameFromUrl?.date || game.header().Date || "?"} + + + + Result :{" "} + {gameFromUrl?.termination || game.header().Termination || "?"} + + + + ); +} diff --git a/src/sections/analysis/reviewPanelHeader/index.tsx b/src/sections/analysis/reviewPanelHeader/index.tsx index 90e6562..7bc1536 100644 --- a/src/sections/analysis/reviewPanelHeader/index.tsx +++ b/src/sections/analysis/reviewPanelHeader/index.tsx @@ -1,7 +1,8 @@ import { Icon } from "@iconify/react"; -import { Grid, Typography } from "@mui/material"; -import LoadGame from "./loadGame"; +import { Divider, Grid, Typography } from "@mui/material"; import AnalyzePanel from "./analyzePanel"; +import GamePanel from "./gamePanel"; +import LoadGame from "./loadGame"; export default function ReviewPanelHeader() { return ( @@ -27,7 +28,19 @@ export default function ReviewPanelHeader() { - + + + + + + diff --git a/src/sections/analysis/reviewPanelHeader/loadGame.tsx b/src/sections/analysis/reviewPanelHeader/loadGame.tsx index c328d1d..a69f20b 100644 --- a/src/sections/analysis/reviewPanelHeader/loadGame.tsx +++ b/src/sections/analysis/reviewPanelHeader/loadGame.tsx @@ -11,8 +11,10 @@ import { import { useGameDatabase } from "@/hooks/useGameDatabase"; import { useAtomValue, useSetAtom } from "jotai"; import { Chess } from "chess.js"; +import { useRouter } from "next/router"; export default function LoadGame() { + const router = useRouter(); const game = useAtomValue(gameAtom); const gameActions = useChessActions(gameAtom); const boardActions = useChessActions(boardAtom); @@ -45,11 +47,17 @@ export default function LoadGame() { loadGame(); }, [gameFromUrl, game, resetAndSetGamePgn, setEval]); - if (gameFromUrl) return null; + const isGameLoaded = gameFromUrl !== undefined || !!game.header().White; return ( - resetAndSetGamePgn(game.pgn())} /> + { + await router.push(""); + resetAndSetGamePgn(game.pgn()); + }} + /> ); } diff --git a/src/sections/analysis/reviewPanelHeader/playerInfo.tsx b/src/sections/analysis/reviewPanelHeader/playerInfo.tsx new file mode 100644 index 0000000..05ac5a1 --- /dev/null +++ b/src/sections/analysis/reviewPanelHeader/playerInfo.tsx @@ -0,0 +1,38 @@ +import { useGameDatabase } from "@/hooks/useGameDatabase"; +import { Grid, Typography } from "@mui/material"; +import { useAtomValue } from "jotai"; +import { gameAtom } from "../states"; + +interface Props { + color: "white" | "black"; +} + +export default function PlayerInfo({ color }: Props) { + const { gameFromUrl } = useGameDatabase(); + const game = useAtomValue(gameAtom); + + const rating = + gameFromUrl?.[color]?.rating || + game.header()[color === "white" ? "WhiteElo" : "BlackElo"]; + + const playerName = + gameFromUrl?.[color]?.name || + game.header()[color === "white" ? "White" : "Black"]; + + return ( + + + {playerName || (color === "white" ? "White" : "Black")} + + + {rating ? `(${rating})` : "(?)"} + + ); +} diff --git a/src/sections/loadGame/loadGameButton.tsx b/src/sections/loadGame/loadGameButton.tsx index d5fe062..f1a7bbd 100644 --- a/src/sections/loadGame/loadGameButton.tsx +++ b/src/sections/loadGame/loadGameButton.tsx @@ -5,15 +5,16 @@ import { Chess } from "chess.js"; interface Props { setGame?: (game: Chess) => void; + label?: string; } -export default function LoadGameButton({ setGame }: Props) { +export default function LoadGameButton({ setGame, label }: Props) { const [openDialog, setOpenDialog] = useState(false); return ( <>