From 3d0d1c41a8c724b6bf4f66701ed432d2ff9e6715 Mon Sep 17 00:00:00 2001 From: GuillaumeSD Date: Sat, 6 Apr 2024 01:38:06 +0200 Subject: [PATCH] Squashed commit of the following: commit 4810de3b94b0ec0d7e9b8570de58f85792dffa80 Author: GuillaumeSD Date: Sat Apr 6 01:37:42 2024 +0200 fix : lint commit 59e0b571e6089da6c086ab6340ec6a966b2e9739 Author: GuillaumeSD Date: Sat Apr 6 01:36:17 2024 +0200 feat : UI refacto commit 56806a89dca5c7fb2c229b5a57404f9a856fac09 Author: GuillaumeSD Date: Fri Apr 5 03:56:08 2024 +0200 feat : add moves list commit 9e3d2347882074c38ab183e642ecef8153dbfcde Author: GuillaumeSD Date: Thu Apr 4 02:18:52 2024 +0200 feat : init branch, wip --- src/hooks/useChessActions.ts | 12 ++- src/hooks/usePlayerNames.ts | 8 ++ src/lib/engine/helpers/accuracy.ts | 2 +- src/lib/engine/helpers/winPercentage.ts | 2 +- src/lib/helpers.ts | 36 ++------ src/lib/math.ts | 33 +++++++ src/pages/index.tsx | 39 ++++++--- src/sections/analysis/board/index.tsx | 9 +- .../analysis/hooks/useCurrentPosition.ts | 16 ++-- .../classificationRow.tsx | 5 +- .../movesClassificationsRecap/index.tsx | 22 ++--- .../reviewPanelBody/movesPanel/index.tsx | 62 +++++++++++++ .../reviewPanelBody/movesPanel/moveItem.tsx | 86 +++++++++++++++++++ .../reviewPanelBody/movesPanel/movesLine.tsx | 27 ++++++ .../analysis/reviewPanelHeader/gamePanel.tsx | 44 ++++++++++ .../reviewPanelHeader/gamePanel/index.tsx | 62 ------------- .../gamePanel/playerInfo.tsx | 38 -------- src/types/eval.ts | 1 + 18 files changed, 328 insertions(+), 176 deletions(-) create mode 100644 src/lib/math.ts rename src/sections/analysis/{ => reviewPanelBody}/movesClassificationsRecap/classificationRow.tsx (96%) rename src/sections/analysis/{ => reviewPanelBody}/movesClassificationsRecap/index.tsx (73%) create mode 100644 src/sections/analysis/reviewPanelBody/movesPanel/index.tsx create mode 100644 src/sections/analysis/reviewPanelBody/movesPanel/moveItem.tsx create mode 100644 src/sections/analysis/reviewPanelBody/movesPanel/movesLine.tsx create mode 100644 src/sections/analysis/reviewPanelHeader/gamePanel.tsx delete mode 100644 src/sections/analysis/reviewPanelHeader/gamePanel/index.tsx delete mode 100644 src/sections/analysis/reviewPanelHeader/gamePanel/playerInfo.tsx diff --git a/src/hooks/useChessActions.ts b/src/hooks/useChessActions.ts index 6582ef1..07eb18c 100644 --- a/src/hooks/useChessActions.ts +++ b/src/hooks/useChessActions.ts @@ -1,5 +1,9 @@ import { setGameHeaders } from "@/lib/chess"; -import { playIllegalMoveSound, playSoundFromMove } from "@/lib/sounds"; +import { + playGameEndSound, + playIllegalMoveSound, + playSoundFromMove, +} from "@/lib/sounds"; import { Chess, Move } from "chess.js"; import { PrimitiveAtom, useAtom } from "jotai"; import { useCallback } from "react"; @@ -76,7 +80,11 @@ export const useChessActions = (chessAtom: PrimitiveAtom) => { } setGame(newGame); - playSoundFromMove(lastMove); + if (lastMove) { + playSoundFromMove(lastMove); + } else { + playGameEndSound(); + } }, [setGame] ); diff --git a/src/hooks/usePlayerNames.ts b/src/hooks/usePlayerNames.ts index 5f4085d..4dd40ae 100644 --- a/src/hooks/usePlayerNames.ts +++ b/src/hooks/usePlayerNames.ts @@ -11,8 +11,16 @@ export const usePlayersNames = (gameAtom: PrimitiveAtom) => { const blackName = gameFromUrl?.black?.name || game.header()["Black"] || "Black"; + const whiteElo = + gameFromUrl?.white?.rating || game.header()["WhiteElo"] || "?"; + + const blackElo = + gameFromUrl?.black?.rating || game.header()["BlackElo"] || "?"; + return { whiteName, blackName, + whiteElo, + blackElo, }; }; diff --git a/src/lib/engine/helpers/accuracy.ts b/src/lib/engine/helpers/accuracy.ts index d2a6661..240c5ac 100644 --- a/src/lib/engine/helpers/accuracy.ts +++ b/src/lib/engine/helpers/accuracy.ts @@ -3,7 +3,7 @@ import { getHarmonicMean, getStandardDeviation, getWeightedMean, -} from "@/lib/helpers"; +} from "@/lib/math"; import { Accuracy, PositionEval } from "@/types/eval"; import { getPositionWinPercentage } from "./winPercentage"; diff --git a/src/lib/engine/helpers/winPercentage.ts b/src/lib/engine/helpers/winPercentage.ts index d20ab62..6c6813c 100644 --- a/src/lib/engine/helpers/winPercentage.ts +++ b/src/lib/engine/helpers/winPercentage.ts @@ -1,4 +1,4 @@ -import { ceilsNumber } from "@/lib/helpers"; +import { ceilsNumber } from "@/lib/math"; import { LineEval, PositionEval } from "@/types/eval"; export const getPositionWinPercentage = (position: PositionEval): number => { diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 4a6d4e1..af7f49a 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -6,36 +6,10 @@ export const capitalize = (s: string) => { return s.charAt(0).toUpperCase() + s.slice(1); }; -export const ceilsNumber = (number: number, min: number, max: number) => { - if (number > max) return max; - if (number < min) return min; - return number; -}; - -export const getHarmonicMean = (array: number[]) => { - const sum = array.reduce((acc, curr) => acc + 1 / curr, 0); - return array.length / sum; -}; - -export const getStandardDeviation = (array: number[]) => { - const n = array.length; - const mean = array.reduce((a, b) => a + b) / n; - return Math.sqrt( - array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n +export const isInViewport = (element: HTMLElement) => { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) ); }; - -export const getWeightedMean = (array: number[], weights: number[]) => { - if (array.length > weights.length) - throw new Error("Weights array is too short"); - - const weightedSum = array.reduce( - (acc, curr, index) => acc + curr * weights[index], - 0 - ); - const weightSum = weights - .slice(0, array.length) - .reduce((acc, curr) => acc + curr, 0); - - return weightedSum / weightSum; -}; diff --git a/src/lib/math.ts b/src/lib/math.ts new file mode 100644 index 0000000..c2f0448 --- /dev/null +++ b/src/lib/math.ts @@ -0,0 +1,33 @@ +export const ceilsNumber = (number: number, min: number, max: number) => { + if (number > max) return max; + if (number < min) return min; + return number; +}; + +export const getHarmonicMean = (array: number[]) => { + const sum = array.reduce((acc, curr) => acc + 1 / curr, 0); + return array.length / sum; +}; + +export const getStandardDeviation = (array: number[]) => { + const n = array.length; + const mean = array.reduce((a, b) => a + b) / n; + return Math.sqrt( + array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n + ); +}; + +export const getWeightedMean = (array: number[], weights: number[]) => { + if (array.length > weights.length) + throw new Error("Weights array is too short"); + + const weightedSum = array.reduce( + (acc, curr, index) => acc + curr * weights[index], + 0 + ); + const weightSum = weights + .slice(0, array.length) + .reduce((acc, curr) => acc + curr, 0); + + return weightedSum / weightSum; +}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ac3ce3f..2da0929 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,6 +1,6 @@ import { useChessActions } from "@/hooks/useChessActions"; import Board from "@/sections/analysis/board"; -import MovesClassificationsRecap from "@/sections/analysis/movesClassificationsRecap"; +import MovesClassificationsRecap from "@/sections/analysis/reviewPanelBody/movesClassificationsRecap"; import ReviewPanelBody from "@/sections/analysis/reviewPanelBody"; import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader"; import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar"; @@ -15,6 +15,7 @@ import { Chess } from "chess.js"; import { useSetAtom } from "jotai"; import { useRouter } from "next/router"; import { useEffect } from "react"; +import MovesPanel from "@/sections/analysis/reviewPanelBody/movesPanel"; export default function GameReport() { const theme = useTheme(); @@ -22,7 +23,7 @@ export default function GameReport() { const { reset: resetBoard } = useChessActions(boardAtom); const { setPgn: setGamePgn } = useChessActions(gameAtom); - const setEval = useSetAtom(gameEvalAtom); + const setGameEval = useSetAtom(gameEvalAtom); const setBoardOrientation = useSetAtom(boardOrientationAtom); const router = useRouter(); @@ -31,20 +32,19 @@ export default function GameReport() { useEffect(() => { if (!gameId) { resetBoard(); - setEval(undefined); + setGameEval(undefined); setBoardOrientation(true); setGamePgn(new Chess().pgn()); } - }, [gameId, setEval, setBoardOrientation, resetBoard, setGamePgn]); + }, [gameId, setGameEval, setBoardOrientation, resetBoard, setGamePgn]); return ( - + {isLgOrGreater ? : } - + - + + + + + + + + + {isLgOrGreater ? : } - - ); } diff --git a/src/sections/analysis/board/index.tsx b/src/sections/analysis/board/index.tsx index ec278b3..bfb313d 100644 --- a/src/sections/analysis/board/index.tsx +++ b/src/sections/analysis/board/index.tsx @@ -17,7 +17,8 @@ export default function BoardContainer() { const screenSize = useScreenSize(); const boardOrientation = useAtomValue(boardOrientationAtom); const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom); - const { whiteName, blackName } = usePlayersNames(gameAtom); + const { whiteName, whiteElo, blackName, blackElo } = + usePlayersNames(gameAtom); const boardSize = useMemo(() => { const width = screenSize.width; @@ -28,7 +29,7 @@ export default function BoardContainer() { return Math.min(width, height - 150); } - return Math.min(width - 600, height * 0.95); + return Math.min(width - 700, height * 0.95); }, [screenSize]); return ( @@ -37,8 +38,8 @@ export default function BoardContainer() { boardSize={boardSize} canPlay={true} gameAtom={boardAtom} - whitePlayer={whiteName} - blackPlayer={blackName} + whitePlayer={`${whiteName} (${whiteElo})`} + blackPlayer={`${blackName} (${blackElo})`} 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 fdae87c..f24d2a4 100644 --- a/src/sections/analysis/hooks/useCurrentPosition.ts +++ b/src/sections/analysis/hooks/useCurrentPosition.ts @@ -26,14 +26,16 @@ export const useCurrentPosition = (engineName?: EngineName) => { lastMove: board.history({ verbose: true }).at(-1), }; - if (gameEval) { - const boardHistory = board.history(); - const gameHistory = game.history(); + const boardHistory = board.history(); + const gameHistory = game.history(); - if ( - boardHistory.length <= gameHistory.length && - gameHistory.slice(0, boardHistory.length).join() === boardHistory.join() - ) { + if ( + boardHistory.length <= gameHistory.length && + gameHistory.slice(0, boardHistory.length).join() === boardHistory.join() + ) { + position.currentMoveIdx = boardHistory.length; + + if (gameEval) { const evalIndex = boardHistory.length; position.eval = gameEval.positions[evalIndex]; diff --git a/src/sections/analysis/movesClassificationsRecap/classificationRow.tsx b/src/sections/analysis/reviewPanelBody/movesClassificationsRecap/classificationRow.tsx similarity index 96% rename from src/sections/analysis/movesClassificationsRecap/classificationRow.tsx rename to src/sections/analysis/reviewPanelBody/movesClassificationsRecap/classificationRow.tsx index 4c24cf8..389eeb5 100644 --- a/src/sections/analysis/movesClassificationsRecap/classificationRow.tsx +++ b/src/sections/analysis/reviewPanelBody/movesClassificationsRecap/classificationRow.tsx @@ -1,7 +1,7 @@ import { Color, MoveClassification } from "@/types/enums"; import { Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; -import { boardAtom, gameAtom, gameEvalAtom } from "../states"; +import { boardAtom, gameAtom, gameEvalAtom } from "../../states"; import { useMemo } from "react"; import { moveClassificationColors } from "@/components/board/squareRenderer"; import Image from "next/image"; @@ -68,8 +68,6 @@ export default function ClassificationRow({ classification }: Props) { } }; - if (!gameEval?.positions.length) return null; - return ( - + {whiteName} - + {blackName} diff --git a/src/sections/analysis/reviewPanelBody/movesPanel/index.tsx b/src/sections/analysis/reviewPanelBody/movesPanel/index.tsx new file mode 100644 index 0000000..8ddc34f --- /dev/null +++ b/src/sections/analysis/reviewPanelBody/movesPanel/index.tsx @@ -0,0 +1,62 @@ +import { Grid } from "@mui/material"; +import MovesLine from "./movesLine"; +import { useMemo } from "react"; +import { useAtomValue } from "jotai"; +import { gameAtom, gameEvalAtom } from "../../states"; +import { MoveClassification } from "@/types/enums"; + +export default function MovesPanel() { + const game = useAtomValue(gameAtom); + const gameEval = useAtomValue(gameEvalAtom); + + const gameMoves = useMemo(() => { + const history = game.history(); + if (!history.length) return undefined; + + const moves: { san: string; moveClassification?: MoveClassification }[][] = + []; + + for (let i = 0; i < history.length; i += 2) { + const items = [ + { + san: history[i], + moveClassification: gameEval?.positions[i + 1]?.moveClassification, + }, + ]; + + if (history[i + 1]) { + items.push({ + san: history[i + 1], + moveClassification: gameEval?.positions[i + 2]?.moveClassification, + }); + } + + moves.push(items); + } + + return moves; + }, [game, gameEval]); + + if (!gameMoves) return null; + + return ( + + {gameMoves?.map((moves, idx) => ( + san).join()}-${idx}`} + moves={moves} + moveNb={idx + 1} + /> + ))} + + ); +} diff --git a/src/sections/analysis/reviewPanelBody/movesPanel/moveItem.tsx b/src/sections/analysis/reviewPanelBody/movesPanel/moveItem.tsx new file mode 100644 index 0000000..837041d --- /dev/null +++ b/src/sections/analysis/reviewPanelBody/movesPanel/moveItem.tsx @@ -0,0 +1,86 @@ +import { MoveClassification } from "@/types/enums"; +import { Grid, Typography } from "@mui/material"; +import { moveClassificationColors } from "@/components/board/squareRenderer"; +import Image from "next/image"; +import { useAtomValue } from "jotai"; +import { boardAtom, currentPositionAtom, gameAtom } from "../../states"; +import { useChessActions } from "@/hooks/useChessActions"; +import { useEffect } from "react"; +import { isInViewport } from "@/lib/helpers"; + +interface Props { + san: string; + moveClassification?: MoveClassification; + moveIdx: number; +} + +export default function MoveItem({ san, moveClassification, moveIdx }: Props) { + const game = useAtomValue(gameAtom); + const { goToMove } = useChessActions(boardAtom); + const position = useAtomValue(currentPositionAtom); + const color = getMoveColor(moveClassification); + + const isCurrentMove = position?.currentMoveIdx === moveIdx; + + useEffect(() => { + if (!isCurrentMove) return; + const moveItem = document.getElementById(`move-${moveIdx}`); + if (!moveItem || !isInViewport(moveItem)) return; + moveItem.scrollIntoView({ behavior: "smooth", block: "center" }); + }, [isCurrentMove, moveIdx]); + + const handleClick = () => { + if (isCurrentMove) return; + goToMove(moveIdx, game); + }; + + return ( + + {color && ( + move-icon + )} + {san} + + ); +} + +const getMoveColor = (moveClassification?: MoveClassification) => { + if ( + !moveClassification || + moveClassificationsToIgnore.includes(moveClassification) + ) { + return undefined; + } + + return moveClassificationColors[moveClassification]; +}; + +const moveClassificationsToIgnore: MoveClassification[] = [ + MoveClassification.Good, + MoveClassification.Excellent, +]; diff --git a/src/sections/analysis/reviewPanelBody/movesPanel/movesLine.tsx b/src/sections/analysis/reviewPanelBody/movesPanel/movesLine.tsx new file mode 100644 index 0000000..4662f5c --- /dev/null +++ b/src/sections/analysis/reviewPanelBody/movesPanel/movesLine.tsx @@ -0,0 +1,27 @@ +import { MoveClassification } from "@/types/enums"; +import { Grid, Typography } from "@mui/material"; +import MoveItem from "./moveItem"; + +interface Props { + moves: { san: string; moveClassification?: MoveClassification }[]; + moveNb: number; +} + +export default function MovesLine({ moves, moveNb }: Props) { + return ( + + {moveNb}. + + + + + + ); +} diff --git a/src/sections/analysis/reviewPanelHeader/gamePanel.tsx b/src/sections/analysis/reviewPanelHeader/gamePanel.tsx new file mode 100644 index 0000000..cabe588 --- /dev/null +++ b/src/sections/analysis/reviewPanelHeader/gamePanel.tsx @@ -0,0 +1,44 @@ +import { Grid, Typography } from "@mui/material"; +import { useGameDatabase } from "@/hooks/useGameDatabase"; +import { useAtomValue } from "jotai"; +import { gameAtom } from "../states"; + +export default function GamePanel() { + const { gameFromUrl } = useGameDatabase(); + const game = useAtomValue(gameAtom); + + const hasGameInfo = gameFromUrl !== undefined || !!game.header().White; + + if (!hasGameInfo) return null; + + return ( + + + + 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/gamePanel/index.tsx b/src/sections/analysis/reviewPanelHeader/gamePanel/index.tsx deleted file mode 100644 index 0bc3f3e..0000000 --- a/src/sections/analysis/reviewPanelHeader/gamePanel/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Grid, Typography } from "@mui/material"; -import { useGameDatabase } from "@/hooks/useGameDatabase"; -import { useAtomValue } from "jotai"; -import { gameAtom } from "../../states"; -import PlayerInfo from "./playerInfo"; - -export default function GamePanel() { - const { gameFromUrl } = useGameDatabase(); - const game = useAtomValue(gameAtom); - - const hasGameInfo = gameFromUrl !== undefined || !!game.header().White; - - if (!hasGameInfo) return null; - - 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/gamePanel/playerInfo.tsx b/src/sections/analysis/reviewPanelHeader/gamePanel/playerInfo.tsx deleted file mode 100644 index 820cdab..0000000 --- a/src/sections/analysis/reviewPanelHeader/gamePanel/playerInfo.tsx +++ /dev/null @@ -1,38 +0,0 @@ -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/types/eval.ts b/src/types/eval.ts index 3be012e..54f66c4 100644 --- a/src/types/eval.ts +++ b/src/types/eval.ts @@ -45,6 +45,7 @@ export interface CurrentPosition { lastMove?: Move; eval?: PositionEval; lastEval?: PositionEval; + currentMoveIdx?: number; } export interface EvaluateGameParams {