diff --git a/src/hooks/useGameDatabase.ts b/src/hooks/useGameDatabase.ts index 5c1d6ba..8eb7405 100644 --- a/src/hooks/useGameDatabase.ts +++ b/src/hooks/useGameDatabase.ts @@ -1,4 +1,7 @@ -import { Game, GameEval } from "@/types/game"; +import { formatGameToDatabase } from "@/lib/chess"; +import { GameEval } from "@/types/eval"; +import { Game } from "@/types/game"; +import { Chess } from "chess.js"; import { openDB, DBSchema, IDBPDatabase } from "idb"; import { atom, useAtom } from "jotai"; import { useCallback, useEffect, useState } from "react"; @@ -48,10 +51,11 @@ export const useGameDatabase = (shouldFetchGames?: boolean) => { loadGames(); }, [loadGames]); - const addGame = async (game: Omit) => { + const addGame = async (game: Chess) => { if (!db) throw new Error("Database not initialized"); - await db.add("games", game as Game); + const gameToAdd = formatGameToDatabase(game); + await db.add("games", gameToAdd as Game); loadGames(); }; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index d636604..bc03e47 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useState } from "react"; type SetValue = Dispatch>; -export function useLocalStorage( +export function useLocalStorage( key: string, initialValue: T ): [T | null, SetValue] { @@ -15,7 +15,7 @@ export function useLocalStorage( } else { setStoredValue(initialValue); } - }, [key]); + }, [key, initialValue]); const setValue: SetValue = (value) => { if (storedValue === null) diff --git a/src/lib/chess.ts b/src/lib/chess.ts index e792650..47d5a58 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -1,20 +1,22 @@ import { Game } from "@/types/game"; import { Chess } from "chess.js"; -export const pgnToFens = (pgn: string): string[] => { - const game = new Chess(); - game.loadPgn(pgn); +export const getFens = (game: Chess): string[] => { return game.history({ verbose: true }).map((move) => move.before); }; -export const getGameFromPgn = (pgn: string): Omit => { +export const getGameFromPgn = (pgn: string): Chess => { const game = new Chess(); game.loadPgn(pgn); + return game; +}; + +export const formatGameToDatabase = (game: Chess): Omit => { const headers: Record = game.header(); return { - pgn, + pgn: game.pgn(), event: headers.Event, site: headers.Site, date: headers.Date, diff --git a/src/lib/engine/stockfish.ts b/src/lib/engine/stockfish.ts index 8acc7e0..a08fe3d 100644 --- a/src/lib/engine/stockfish.ts +++ b/src/lib/engine/stockfish.ts @@ -1,4 +1,4 @@ -import { GameEval, LineEval, MoveEval } from "@/types/game"; +import { GameEval, LineEval, MoveEval } from "@/types/eval"; export class Stockfish { private worker: Worker; @@ -80,7 +80,7 @@ export class Stockfish { this.ready = true; console.log("Game evaluated"); console.log(moves); - return { moves, whiteAccuracy: 82.34, blackAccuracy: 67.49 }; + return { moves, accuracy: { white: 82.34, black: 67.49 } }; // TODO: Calculate accuracy } public async evaluatePosition(fen: string, depth = 16): Promise { diff --git a/src/pages/game-database.tsx b/src/pages/database.tsx similarity index 98% rename from src/pages/game-database.tsx rename to src/pages/database.tsx index 30b9ce5..68da31b 100644 --- a/src/pages/game-database.tsx +++ b/src/pages/database.tsx @@ -114,12 +114,14 @@ export default function GameDatabase() { + You have {0} games in your database - + + (null); + const setEval = useSetAtom(gameEvalAtom); + const game = useAtomValue(gameAtom); + + useEffect(() => { + const engine = new Stockfish(); + engine.init(); + setEngine(engine); + + return () => { + engine.shutdown(); + }; + }, []); + + const readyToAnalyse = engine?.isReady() && game.history().length > 0; + + const handleAnalyze = async () => { + const gameFens = getFens(game); + if (engine?.isReady() && gameFens.length) { + const newGameEval = await engine.evaluateGame(gameFens); + setEval(newGameEval); + } + }; + + return ( + + + + ); +} diff --git a/src/sections/gameReport/board.tsx b/src/sections/analysis/board.tsx similarity index 99% rename from src/sections/gameReport/board.tsx rename to src/sections/analysis/board.tsx index 6dc5e8a..ef90a03 100644 --- a/src/sections/gameReport/board.tsx +++ b/src/sections/analysis/board.tsx @@ -19,7 +19,9 @@ export default function Board() { White Player (?) + + Black Player (?) diff --git a/src/sections/analysis/lineEvaluation.tsx b/src/sections/analysis/lineEvaluation.tsx new file mode 100644 index 0000000..a32e081 --- /dev/null +++ b/src/sections/analysis/lineEvaluation.tsx @@ -0,0 +1,22 @@ +import { LineEval } from "@/types/eval"; +import { ListItem, ListItemText, Typography } from "@mui/material"; + +interface Props { + line: LineEval; +} + +export default function LineEvaluation({ line }: Props) { + const lineLabel = + line.cp !== undefined + ? `${line.cp / 100}` + : line.mate + ? `Mate in ${Math.abs(line.mate)}` + : "?"; + + return ( + + + {line.pv.slice(0, 7).join(", ")} + + ); +} diff --git a/src/sections/analysis/loadGame.tsx b/src/sections/analysis/loadGame.tsx new file mode 100644 index 0000000..adab299 --- /dev/null +++ b/src/sections/analysis/loadGame.tsx @@ -0,0 +1,42 @@ +import { Grid } from "@mui/material"; +import LoadGameButton from "../loadGame/loadGameButton"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; +import { useChessActions } from "@/hooks/useChess"; +import { boardAtom, gameAtom } from "./states"; +import { useGameDatabase } from "@/hooks/useGameDatabase"; + +export default function LoadGame() { + const router = useRouter(); + const { gameId } = router.query; + const gameActions = useChessActions(gameAtom); + const boardActions = useChessActions(boardAtom); + const { getGame } = useGameDatabase(); + + useEffect(() => { + const loadGame = async () => { + if (typeof gameId !== "string") return; + + const game = await getGame(parseInt(gameId)); + if (!game) return; + + boardActions.reset(); + gameActions.setPgn(game.pgn); + }; + + loadGame(); + }, [gameId]); + + if (!router.isReady || gameId) return null; + + return ( + + { + boardActions.reset(); + gameActions.setPgn(game.pgn()); + }} + /> + + ); +} diff --git a/src/sections/gameReport/reviewPanelBody.tsx b/src/sections/analysis/reviewPanelBody.tsx similarity index 67% rename from src/sections/gameReport/reviewPanelBody.tsx rename to src/sections/analysis/reviewPanelBody.tsx index 7ee78c4..5671b94 100644 --- a/src/sections/gameReport/reviewPanelBody.tsx +++ b/src/sections/analysis/reviewPanelBody.tsx @@ -1,20 +1,14 @@ import { Icon } from "@iconify/react"; -import { - Divider, - Grid, - List, - ListItem, - ListItemText, - Typography, -} from "@mui/material"; +import { Divider, Grid, List, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; import { boardAtom, gameEvalAtom } from "./states"; +import LineEvaluation from "./lineEvaluation"; export default function ReviewPanelBody() { - const board = useAtomValue(boardAtom); const gameEval = useAtomValue(gameEvalAtom); if (!gameEval) return null; + const board = useAtomValue(boardAtom); const evalIndex = board.history().length; const moveEval = gameEval.moves[evalIndex]; @@ -36,7 +30,7 @@ export default function ReviewPanelBody() { height={30} /> - Bilan de la partie + Game Review @@ -47,17 +41,7 @@ export default function ReviewPanelBody() { {moveEval?.lines.map((line) => ( - - - {line.pv.slice(0, 7).join(", ")} - + ))} diff --git a/src/sections/analysis/reviewPanelHeader.tsx b/src/sections/analysis/reviewPanelHeader.tsx new file mode 100644 index 0000000..d7b9f27 --- /dev/null +++ b/src/sections/analysis/reviewPanelHeader.tsx @@ -0,0 +1,35 @@ +import { Icon } from "@iconify/react"; +import { Grid, Typography } from "@mui/material"; +import LoadGame from "./loadGame"; +import AnalyzeButton from "./analyzeButton"; + +export default function ReviewPanelHeader() { + return ( + + + + + Game Report + + + + + + + + ); +} diff --git a/src/sections/gameReport/reviewPanelToolbar.tsx b/src/sections/analysis/reviewPanelToolbar.tsx similarity index 100% rename from src/sections/gameReport/reviewPanelToolbar.tsx rename to src/sections/analysis/reviewPanelToolbar.tsx diff --git a/src/sections/gameReport/states.ts b/src/sections/analysis/states.ts similarity index 84% rename from src/sections/gameReport/states.ts rename to src/sections/analysis/states.ts index c8b20f6..59bee2d 100644 --- a/src/sections/gameReport/states.ts +++ b/src/sections/analysis/states.ts @@ -1,4 +1,4 @@ -import { GameEval } from "@/types/game"; +import { GameEval } from "@/types/eval"; import { Chess } from "chess.js"; import { atom } from "jotai"; diff --git a/src/sections/gameReport/reviewPanelHeader.tsx b/src/sections/gameReport/reviewPanelHeader.tsx deleted file mode 100644 index 8b6acc5..0000000 --- a/src/sections/gameReport/reviewPanelHeader.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect, useState } from "react"; -import SelectGameOrigin from "./selectGame/selectGameOrigin"; -import { Stockfish } from "@/lib/engine/stockfish"; -import { Icon } from "@iconify/react"; -import { Button, Typography } from "@mui/material"; -import { useAtomValue, useSetAtom } from "jotai"; -import { boardAtom, gameAtom, gameEvalAtom } from "./states"; -import { useChessActions } from "@/hooks/useChess"; -import { gameInputPgnAtom } from "./selectGame/gameInput.states"; -import { pgnToFens } from "@/lib/chess"; - -export default function ReviewPanelHeader() { - const [engine, setEngine] = useState(null); - const setEval = useSetAtom(gameEvalAtom); - const boardActions = useChessActions(boardAtom); - const gameActions = useChessActions(gameAtom); - const pgnInput = useAtomValue(gameInputPgnAtom); - - useEffect(() => { - const engine = new Stockfish(); - engine.init(); - setEngine(engine); - - return () => { - engine.shutdown(); - }; - }, []); - - const handleAnalyse = async () => { - boardActions.reset(); - gameActions.setPgn(pgnInput); - const gameFens = pgnToFens(pgnInput); - if (engine?.isReady() && gameFens.length) { - const newGameEval = await engine.evaluateGame(gameFens); - setEval(newGameEval); - } - }; - - return ( - <> - - - Game Report - - - - - - - ); -} diff --git a/src/sections/gameReport/selectGame/gameInput.states.ts b/src/sections/gameReport/selectGame/gameInput.states.ts deleted file mode 100644 index a7e1ca0..0000000 --- a/src/sections/gameReport/selectGame/gameInput.states.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from "jotai"; - -export const gameInputPgnAtom = atom(""); diff --git a/src/sections/gameReport/selectGame/inputGame.tsx b/src/sections/gameReport/selectGame/inputGame.tsx deleted file mode 100644 index 71fb7be..0000000 --- a/src/sections/gameReport/selectGame/inputGame.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { TextField } from "@mui/material"; -import { useAtom } from "jotai"; -import { gameInputPgnAtom } from "./gameInput.states"; - -export default function InputGame() { - const [pgn, setPgn] = useAtom(gameInputPgnAtom); - - return ( - { - setPgn(e.target.value); - }} - /> - ); -} diff --git a/src/sections/gameReport/selectGame/selectGameOrigin.tsx b/src/sections/gameReport/selectGame/selectGameOrigin.tsx deleted file mode 100644 index 464fdd1..0000000 --- a/src/sections/gameReport/selectGame/selectGameOrigin.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { GameOrigin } from "@/types/enums"; -import { FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material"; -import InputGame from "./inputGame"; -import { useState } from "react"; - -export default function SelectGameOrigin() { - const [gameOrigin, setGameOrigin] = useState(GameOrigin.Pgn); - - return ( - - - Game Origin - - - - - - ); -} - -const gameOriginLabel: Record = { - [GameOrigin.Pgn]: "PGN", - [GameOrigin.ChessCom]: "Chess.com", - [GameOrigin.Lichess]: "Lichess", -}; diff --git a/src/sections/layout/NavBar.tsx b/src/sections/layout/NavBar.tsx index 3ec32a4..b535adf 100644 --- a/src/sections/layout/NavBar.tsx +++ b/src/sections/layout/NavBar.tsx @@ -26,7 +26,10 @@ export default function NavBar({ darkMode, switchDarkMode }: Props) { theme.zIndex.drawer + 1 }} + sx={{ + zIndex: (theme) => theme.zIndex.drawer + 1, + backgroundColor: "primary.main", + }} > {MenuOptions.map(({ text, icon, href }) => ( - + diff --git a/src/sections/loadGame/loadGameButton.tsx b/src/sections/loadGame/loadGameButton.tsx index 5f1f18b..d5fe062 100644 --- a/src/sections/loadGame/loadGameButton.tsx +++ b/src/sections/loadGame/loadGameButton.tsx @@ -1,17 +1,26 @@ import { Button } from "@mui/material"; import { useState } from "react"; import NewGameDialog from "./loadGameDialog"; +import { Chess } from "chess.js"; -export default function LoadGameButton() { +interface Props { + setGame?: (game: Chess) => void; +} + +export default function LoadGameButton({ setGame }: Props) { const [openDialog, setOpenDialog] = useState(false); return ( <> - setOpenDialog(false)} /> + setOpenDialog(false)} + setGame={setGame} + /> ); } diff --git a/src/sections/loadGame/loadGameDialog.tsx b/src/sections/loadGame/loadGameDialog.tsx index 6a641e8..c846303 100644 --- a/src/sections/loadGame/loadGameDialog.tsx +++ b/src/sections/loadGame/loadGameDialog.tsx @@ -16,14 +16,16 @@ import { TextField, Typography, } from "@mui/material"; +import { Chess } from "chess.js"; import { useState } from "react"; interface Props { open: boolean; onClose: () => void; + setGame?: (game: Chess) => void; } -export default function NewGameDialog({ open, onClose }: Props) { +export default function NewGameDialog({ open, onClose, setGame }: Props) { const [pgn, setPgn] = useState(""); const [parsingError, setParsingError] = useState(""); const { addGame } = useGameDatabase(); @@ -34,7 +36,13 @@ export default function NewGameDialog({ open, onClose }: Props) { try { const gameToAdd = getGameFromPgn(pgn); - addGame(gameToAdd); + + if (setGame) { + setGame(gameToAdd); + } else { + addGame(gameToAdd); + } + handleClose(); } catch (error) { console.error(error); diff --git a/src/types/eval.ts b/src/types/eval.ts new file mode 100644 index 0000000..f5619b0 --- /dev/null +++ b/src/types/eval.ts @@ -0,0 +1,20 @@ +export interface MoveEval { + bestMove: string; + lines: LineEval[]; +} + +export interface LineEval { + pv: string[]; + cp?: number; + mate?: number; +} + +export interface Accuracy { + white: number; + black: number; +} + +export interface GameEval { + moves: MoveEval[]; + accuracy: Accuracy; +} diff --git a/src/types/game.ts b/src/types/game.ts index ebcebba..ae48f60 100644 --- a/src/types/game.ts +++ b/src/types/game.ts @@ -1,19 +1,4 @@ -export interface MoveEval { - bestMove: string; - lines: LineEval[]; -} - -export interface LineEval { - pv: string[]; - cp?: number; - mate?: number; -} - -export interface GameEval { - moves: MoveEval[]; - whiteAccuracy: number; - blackAccuracy: number; -} +import { GameEval } from "./eval"; export interface Game { id: number;