From d9b322d9fa47924a018487d6ddc0c8766bd0d128 Mon Sep 17 00:00:00 2001 From: GuillaumeSD Date: Fri, 29 Mar 2024 03:53:16 +0100 Subject: [PATCH] feat : add move classification --- src/components/board/squareRenderer.tsx | 8 +- src/hooks/useChessActions.ts | 23 +++- src/hooks/usePlayerNames.ts | 18 +++ src/pages/index.tsx | 3 + src/sections/analysis/board/index.tsx | 13 +- .../analysis/hooks/useCurrentPosition.ts | 2 +- .../classificationRow.tsx | 130 ++++++++++++++++++ .../movesClassificationsRecap/index.tsx | 74 ++++++++++ .../analysis/reviewPanelHeader/index.tsx | 2 +- 9 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 src/hooks/usePlayerNames.ts create mode 100644 src/sections/analysis/movesClassificationsRecap/classificationRow.tsx create mode 100644 src/sections/analysis/movesClassificationsRecap/index.tsx diff --git a/src/components/board/squareRenderer.tsx b/src/components/board/squareRenderer.tsx index 6db24d6..6738c53 100644 --- a/src/components/board/squareRenderer.tsx +++ b/src/components/board/squareRenderer.tsx @@ -52,12 +52,12 @@ export function getSquareRenderer({ move-icon) => { setGame(newGame); }, [copyGame, setGame]); - return { setPgn, reset, makeMove, undoMove }; + const goToMove = useCallback( + (moveIdx: number, game: Chess) => { + if (moveIdx < 0) return; + + const newGame = new Chess(); + newGame.loadPgn(game.pgn()); + + const movesNb = game.history().length; + if (moveIdx > movesNb) return; + + let lastMove: Move | null = null; + for (let i = movesNb; i > moveIdx; i--) { + lastMove = newGame.undo(); + } + + setGame(newGame); + playSoundFromMove(lastMove); + }, + [setGame] + ); + + return { setPgn, reset, makeMove, undoMove, goToMove }; }; diff --git a/src/hooks/usePlayerNames.ts b/src/hooks/usePlayerNames.ts new file mode 100644 index 0000000..5f4085d --- /dev/null +++ b/src/hooks/usePlayerNames.ts @@ -0,0 +1,18 @@ +import { Chess } from "chess.js"; +import { PrimitiveAtom, useAtomValue } from "jotai"; +import { useGameDatabase } from "./useGameDatabase"; + +export const usePlayersNames = (gameAtom: PrimitiveAtom) => { + const game = useAtomValue(gameAtom); + const { gameFromUrl } = useGameDatabase(); + + const whiteName = + gameFromUrl?.white?.name || game.header()["White"] || "White"; + const blackName = + gameFromUrl?.black?.name || game.header()["Black"] || "Black"; + + return { + whiteName, + blackName, + }; +}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index c749432..ac3ce3f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,6 @@ import { useChessActions } from "@/hooks/useChessActions"; import Board from "@/sections/analysis/board"; +import MovesClassificationsRecap from "@/sections/analysis/movesClassificationsRecap"; import ReviewPanelBody from "@/sections/analysis/reviewPanelBody"; import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader"; import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar"; @@ -73,6 +74,8 @@ export default function GameReport() { {isLgOrGreater ? : } + + ); } diff --git a/src/sections/analysis/board/index.tsx b/src/sections/analysis/board/index.tsx index 201ba90..ec278b3 100644 --- a/src/sections/analysis/board/index.tsx +++ b/src/sections/analysis/board/index.tsx @@ -11,14 +11,13 @@ import { useMemo } from "react"; import { useScreenSize } from "@/hooks/useScreenSize"; import { Color } from "@/types/enums"; import Board from "@/components/board"; -import { useGameDatabase } from "@/hooks/useGameDatabase"; +import { usePlayersNames } from "@/hooks/usePlayerNames"; export default function BoardContainer() { const screenSize = useScreenSize(); const boardOrientation = useAtomValue(boardOrientationAtom); const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom); - const { gameFromUrl } = useGameDatabase(); - const game = useAtomValue(gameAtom); + const { whiteName, blackName } = usePlayersNames(gameAtom); const boardSize = useMemo(() => { const width = screenSize.width; @@ -38,12 +37,8 @@ export default function BoardContainer() { boardSize={boardSize} canPlay={true} gameAtom={boardAtom} - whitePlayer={ - gameFromUrl?.white?.name || game.header()["White"] || "White" - } - blackPlayer={ - gameFromUrl?.black?.name || game.header()["Black"] || "Black" - } + whitePlayer={whiteName} + blackPlayer={blackName} boardOrientation={boardOrientation ? Color.White : Color.Black} currentPositionAtom={currentPositionAtom} showBestMoveArrow={showBestMoveArrow} diff --git a/src/sections/analysis/hooks/useCurrentPosition.ts b/src/sections/analysis/hooks/useCurrentPosition.ts index 787b7f9..fdae87c 100644 --- a/src/sections/analysis/hooks/useCurrentPosition.ts +++ b/src/sections/analysis/hooks/useCurrentPosition.ts @@ -34,7 +34,7 @@ export const useCurrentPosition = (engineName?: EngineName) => { boardHistory.length <= gameHistory.length && gameHistory.slice(0, boardHistory.length).join() === boardHistory.join() ) { - const evalIndex = board.history().length; + const evalIndex = boardHistory.length; position.eval = gameEval.positions[evalIndex]; position.lastEval = diff --git a/src/sections/analysis/movesClassificationsRecap/classificationRow.tsx b/src/sections/analysis/movesClassificationsRecap/classificationRow.tsx new file mode 100644 index 0000000..4c24cf8 --- /dev/null +++ b/src/sections/analysis/movesClassificationsRecap/classificationRow.tsx @@ -0,0 +1,130 @@ +import { Color, MoveClassification } from "@/types/enums"; +import { Grid, Typography } from "@mui/material"; +import { useAtomValue } from "jotai"; +import { boardAtom, gameAtom, gameEvalAtom } from "../states"; +import { useMemo } from "react"; +import { moveClassificationColors } from "@/components/board/squareRenderer"; +import Image from "next/image"; +import { capitalize } from "@/lib/helpers"; +import { useChessActions } from "@/hooks/useChessActions"; + +interface Props { + classification: MoveClassification; +} + +export default function ClassificationRow({ classification }: Props) { + const gameEval = useAtomValue(gameEvalAtom); + const board = useAtomValue(boardAtom); + const game = useAtomValue(gameAtom); + const { goToMove } = useChessActions(boardAtom); + + const whiteNb = useMemo(() => { + if (!gameEval) return 0; + return gameEval.positions.filter( + (position, idx) => + idx % 2 !== 0 && position.moveClassification === classification + ).length; + }, [gameEval, classification]); + + const blackNb = useMemo(() => { + if (!gameEval) return 0; + return gameEval.positions.filter( + (position, idx) => + idx % 2 === 0 && position.moveClassification === classification + ).length; + }, [gameEval, classification]); + + const handleClick = (color: Color) => { + if ( + !gameEval || + (color === Color.White && !whiteNb) || + (color === Color.Black && !blackNb) + ) { + return; + } + + const filterColor = (idx: number) => + (idx % 2 !== 0 && color === Color.White) || + (idx % 2 === 0 && color === Color.Black); + const moveIdx = board.history().length; + + const nextPositionIdx = gameEval.positions.findIndex( + (position, idx) => + filterColor(idx) && + position.moveClassification === classification && + idx > moveIdx + ); + + if (nextPositionIdx > 0) { + goToMove(nextPositionIdx, game); + } else { + const firstPositionIdx = gameEval.positions.findIndex( + (position, idx) => + filterColor(idx) && position.moveClassification === classification + ); + if (firstPositionIdx > 0 && firstPositionIdx !== moveIdx) { + goToMove(firstPositionIdx, game); + } + } + }; + + if (!gameEval?.positions.length) return null; + + return ( + + handleClick(Color.White)} + > + {whiteNb} + + + + move-icon + + {capitalize(classification)} + + + handleClick(Color.Black)} + > + {blackNb} + + + ); +} diff --git a/src/sections/analysis/movesClassificationsRecap/index.tsx b/src/sections/analysis/movesClassificationsRecap/index.tsx new file mode 100644 index 0000000..1de67d3 --- /dev/null +++ b/src/sections/analysis/movesClassificationsRecap/index.tsx @@ -0,0 +1,74 @@ +import { usePlayersNames } from "@/hooks/usePlayerNames"; +import { Grid, Typography } from "@mui/material"; +import { gameAtom, gameEvalAtom } from "../states"; +import { MoveClassification } from "@/types/enums"; +import ClassificationRow from "./classificationRow"; +import { useAtomValue } from "jotai"; + +export default function MovesClassificationsRecap() { + const { whiteName, blackName } = usePlayersNames(gameAtom); + const gameEval = useAtomValue(gameEvalAtom); + + if (!gameEval?.positions.length) return null; + + return ( + + + + {whiteName} + + + + + + {blackName} + + + + {sortedMoveClassfications.map((classification) => ( + + ))} + + ); +} + +export const sortedMoveClassfications = [ + MoveClassification.Brilliant, + MoveClassification.Great, + MoveClassification.Best, + MoveClassification.Excellent, + MoveClassification.Good, + MoveClassification.Book, + MoveClassification.Inaccuracy, + MoveClassification.Mistake, + MoveClassification.Blunder, +]; diff --git a/src/sections/analysis/reviewPanelHeader/index.tsx b/src/sections/analysis/reviewPanelHeader/index.tsx index 7ee22b1..a669cf2 100644 --- a/src/sections/analysis/reviewPanelHeader/index.tsx +++ b/src/sections/analysis/reviewPanelHeader/index.tsx @@ -27,7 +27,7 @@ export default function ReviewPanelHeader() { alignItems="center" columnGap={1} > - + Game Report