diff --git a/public/icons/best.png b/public/icons/best.png new file mode 100644 index 0000000..f9bc8af Binary files /dev/null and b/public/icons/best.png differ diff --git a/public/icons/blunder.png b/public/icons/blunder.png new file mode 100644 index 0000000..783af84 Binary files /dev/null and b/public/icons/blunder.png differ diff --git a/public/icons/book.png b/public/icons/book.png new file mode 100644 index 0000000..65c8331 Binary files /dev/null and b/public/icons/book.png differ diff --git a/public/icons/excellent.png b/public/icons/excellent.png new file mode 100644 index 0000000..3e61fe1 Binary files /dev/null and b/public/icons/excellent.png differ diff --git a/public/icons/good.png b/public/icons/good.png new file mode 100644 index 0000000..0028e0f Binary files /dev/null and b/public/icons/good.png differ diff --git a/public/icons/inaccuracy.png b/public/icons/inaccuracy.png new file mode 100644 index 0000000..46f8ad0 Binary files /dev/null and b/public/icons/inaccuracy.png differ diff --git a/public/icons/mistake.png b/public/icons/mistake.png new file mode 100644 index 0000000..3151cab Binary files /dev/null and b/public/icons/mistake.png differ diff --git a/src/hooks/useCurrentMove.ts b/src/hooks/useCurrentPosition.ts similarity index 57% rename from src/hooks/useCurrentMove.ts rename to src/hooks/useCurrentPosition.ts index 441dab5..61b13d6 100644 --- a/src/hooks/useCurrentMove.ts +++ b/src/hooks/useCurrentPosition.ts @@ -1,19 +1,19 @@ import { boardAtom, - currentMoveAtom, + currentPositionAtom, engineDepthAtom, engineMultiPvAtom, gameAtom, gameEvalAtom, } from "@/sections/analysis/states"; -import { CurrentMove, MoveEval } from "@/types/eval"; +import { CurrentPosition, PositionEval } from "@/types/eval"; import { useAtom, useAtomValue } from "jotai"; import { useEffect } from "react"; import { useEngine } from "./useEngine"; import { EngineName } from "@/types/enums"; -export const useCurrentMove = (engineName?: EngineName) => { - const [currentMove, setCurrentMove] = useAtom(currentMoveAtom); +export const useCurrentPosition = (engineName?: EngineName) => { + const [currentPosition, setCurrentPosition] = useAtom(currentPositionAtom); const engine = useEngine(engineName); const gameEval = useAtomValue(gameEvalAtom); const game = useAtomValue(gameAtom); @@ -22,8 +22,8 @@ export const useCurrentMove = (engineName?: EngineName) => { const multiPv = useAtomValue(engineMultiPvAtom); useEffect(() => { - const move: CurrentMove = { - ...board.history({ verbose: true }).at(-1), + const position: CurrentPosition = { + lastMove: board.history({ verbose: true }).at(-1), }; if (gameEval) { @@ -36,15 +36,15 @@ export const useCurrentMove = (engineName?: EngineName) => { ) { const evalIndex = board.history().length; - move.eval = gameEval.moves[evalIndex]; - move.lastEval = - evalIndex > 0 ? gameEval.moves[evalIndex - 1] : undefined; + position.eval = gameEval.positions[evalIndex]; + position.lastEval = + evalIndex > 0 ? gameEval.positions[evalIndex - 1] : undefined; } } - if (!move.eval && engine?.isReady()) { - const setPartialEval = (moveEval: MoveEval) => { - setCurrentMove({ ...move, eval: moveEval }); + if (!position.eval && engine?.isReady()) { + const setPartialEval = (positionEval: PositionEval) => { + setCurrentPosition({ ...position, eval: positionEval }); }; engine.evaluatePositionWithUpdate({ @@ -55,8 +55,8 @@ export const useCurrentMove = (engineName?: EngineName) => { }); } - setCurrentMove(move); - }, [gameEval, board, game, engine, depth, multiPv, setCurrentMove]); + setCurrentPosition(position); + }, [gameEval, board, game, engine, depth, multiPv, setCurrentPosition]); - return currentMove; + return currentPosition; }; diff --git a/src/hooks/useSquareRenderer.tsx b/src/hooks/useSquareRenderer.tsx new file mode 100644 index 0000000..bec282d --- /dev/null +++ b/src/hooks/useSquareRenderer.tsx @@ -0,0 +1,73 @@ +import { showPlayerMoveIconAtom } from "@/sections/analysis/states"; +import { MoveClassification } from "@/types/enums"; +import { CurrentPosition } from "@/types/eval"; +import { useAtomValue } from "jotai"; +import Image from "next/image"; +import { CSSProperties, forwardRef, useMemo } from "react"; +import { CustomSquareProps } from "react-chessboard/dist/chessboard/types"; + +export const useSquareRenderer = (position: CurrentPosition) => { + const showPlayerMoveIcon = useAtomValue(showPlayerMoveIconAtom); + + const CustomSquareRenderer = useMemo(() => { + const fromSquare = position.lastMove?.from; + const toSquare = position.lastMove?.to; + const moveClassification = position?.eval?.moveClassification; + + if (!showPlayerMoveIcon || !moveClassification || !fromSquare || !toSquare) + return undefined; + + const squareRenderer = forwardRef( + (props, ref) => { + const { children, square, style } = props; + + const customSquareStyle: CSSProperties | undefined = + fromSquare === square || toSquare === square + ? { + position: "absolute", + width: "100%", + height: "100%", + backgroundColor: moveClassificationColors[moveClassification], + opacity: 0.5, + } + : undefined; + + return ( +
+ {children} + {customSquareStyle &&
} + {square === toSquare && ( + move-icon + )} +
+ ); + } + ); + + squareRenderer.displayName = "CustomSquareRenderer"; + + return squareRenderer; + }, [showPlayerMoveIcon, position]); + + return CustomSquareRenderer; +}; + +export const moveClassificationColors: Record = { + [MoveClassification.Best]: "#3aab18", + [MoveClassification.Book]: "#d5a47d", + [MoveClassification.Excellent]: "#3aab18", + [MoveClassification.Good]: "#81b64c", + [MoveClassification.Inaccuracy]: "#f7c631", + [MoveClassification.Mistake]: "#ffa459", + [MoveClassification.Blunder]: "#fa412d", +}; diff --git a/src/lib/engine/helpers/accuracy.ts b/src/lib/engine/helpers/accuracy.ts index d589a20..d2a6661 100644 --- a/src/lib/engine/helpers/accuracy.ts +++ b/src/lib/engine/helpers/accuracy.ts @@ -4,15 +4,15 @@ import { getStandardDeviation, getWeightedMean, } from "@/lib/helpers"; -import { Accuracy, MoveEval } from "@/types/eval"; +import { Accuracy, PositionEval } from "@/types/eval"; import { getPositionWinPercentage } from "./winPercentage"; -export const computeAccuracy = (moves: MoveEval[]): Accuracy => { - const movesWinPercentage = moves.map(getPositionWinPercentage); +export const computeAccuracy = (positions: PositionEval[]): Accuracy => { + const positionsWinPercentage = positions.map(getPositionWinPercentage); - const weights = getAccuracyWeights(movesWinPercentage); + const weights = getAccuracyWeights(positionsWinPercentage); - const movesAccuracy = getMovesAccuracy(movesWinPercentage); + const movesAccuracy = getMovesAccuracy(positionsWinPercentage); const whiteAccuracy = getPlayerAccuracy(movesAccuracy, weights, "white"); const blackAccuracy = getPlayerAccuracy(movesAccuracy, weights, "black"); diff --git a/src/lib/engine/helpers/moveClassification.ts b/src/lib/engine/helpers/moveClassification.ts index 267beab..7269d3c 100644 --- a/src/lib/engine/helpers/moveClassification.ts +++ b/src/lib/engine/helpers/moveClassification.ts @@ -1,13 +1,13 @@ -import { MoveEval } from "@/types/eval"; +import { PositionEval } from "@/types/eval"; import { getPositionWinPercentage } from "./winPercentage"; import { MoveClassification } from "@/types/enums"; import { openings } from "@/data/openings"; export const getMovesClassification = ( - rawMoves: MoveEval[], + rawMoves: PositionEval[], uciMoves: string[], fens: string[] -): MoveEval[] => { +): PositionEval[] => { const positionsWinPercentage = rawMoves.map(getPositionWinPercentage); let currentOpening: string | undefined = undefined; diff --git a/src/lib/engine/helpers/parseResults.ts b/src/lib/engine/helpers/parseResults.ts index bd40f73..abd3ead 100644 --- a/src/lib/engine/helpers/parseResults.ts +++ b/src/lib/engine/helpers/parseResults.ts @@ -1,10 +1,10 @@ -import { LineEval, MoveEval } from "@/types/eval"; +import { LineEval, PositionEval } from "@/types/eval"; export const parseEvaluationResults = ( results: string[], whiteToPlay: boolean -): MoveEval => { - const parsedResults: MoveEval = { +): PositionEval => { + const parsedResults: PositionEval = { lines: [], }; const tempResults: Record = {}; diff --git a/src/lib/engine/helpers/winPercentage.ts b/src/lib/engine/helpers/winPercentage.ts index de12fe5..8f3074b 100644 --- a/src/lib/engine/helpers/winPercentage.ts +++ b/src/lib/engine/helpers/winPercentage.ts @@ -1,13 +1,13 @@ import { ceilsNumber } from "@/lib/helpers"; -import { MoveEval } from "@/types/eval"; +import { PositionEval } from "@/types/eval"; -export const getPositionWinPercentage = (move: MoveEval): number => { - if (move.lines[0].cp !== undefined) { - return getWinPercentageFromCp(move.lines[0].cp); +export const getPositionWinPercentage = (position: PositionEval): number => { + if (position.lines[0].cp !== undefined) { + return getWinPercentageFromCp(position.lines[0].cp); } - if (move.lines[0].mate !== undefined) { - return getWinPercentageFromMate(move.lines[0].mate); + if (position.lines[0].mate !== undefined) { + return getWinPercentageFromMate(position.lines[0].mate); } throw new Error("No cp or mate in move"); diff --git a/src/lib/engine/uciEngine.ts b/src/lib/engine/uciEngine.ts index 45b57c8..d7a8425 100644 --- a/src/lib/engine/uciEngine.ts +++ b/src/lib/engine/uciEngine.ts @@ -3,7 +3,7 @@ import { EvaluateGameParams, EvaluatePositionWithUpdateParams, GameEval, - MoveEval, + PositionEval, } from "@/types/eval"; import { parseEvaluationResults } from "./helpers/parseResults"; import { computeAccuracy } from "./helpers/accuracy"; @@ -108,11 +108,11 @@ export abstract class UciEngine { await this.sendCommands(["ucinewgame", "isready"], "readyok"); this.worker.postMessage("position startpos"); - const moves: MoveEval[] = []; + const positions: PositionEval[] = []; for (const fen of fens) { const whoIsCheckmated = getWhoIsCheckmated(fen); if (whoIsCheckmated) { - moves.push({ + positions.push({ lines: [ { pv: [], @@ -125,19 +125,19 @@ export abstract class UciEngine { continue; } const result = await this.evaluatePosition(fen, depth); - moves.push(result); + positions.push(result); } - const movesWithClassification = getMovesClassification( - moves, + const positionsWithClassification = getMovesClassification( + positions, uciMoves, fens ); - const accuracy = computeAccuracy(moves); + const accuracy = computeAccuracy(positions); this.ready = true; return { - moves: movesWithClassification, + positions: positionsWithClassification, accuracy, settings: { engine: this.engineName, @@ -148,7 +148,10 @@ export abstract class UciEngine { }; } - private async evaluatePosition(fen: string, depth = 16): Promise { + private async evaluatePosition( + fen: string, + depth = 16 + ): Promise { console.log(`Evaluating position: ${fen}`); const lichessEval = await getLichessEval(fen, this.multiPv); diff --git a/src/lib/lichess.ts b/src/lib/lichess.ts index 404c03c..46ef28b 100644 --- a/src/lib/lichess.ts +++ b/src/lib/lichess.ts @@ -1,4 +1,4 @@ -import { LineEval, MoveEval } from "@/types/eval"; +import { LineEval, PositionEval } from "@/types/eval"; import { sortLines } from "./engine/helpers/parseResults"; import { LichessError, @@ -9,7 +9,7 @@ import { export const getLichessEval = async ( fen: string, multiPv = 1 -): Promise => { +): Promise => { try { const res = await fetch( `https://lichess.org/api/cloud-eval?fen=${fen}&multiPv=${multiPv}` diff --git a/src/sections/analysis/board/evaluationBar.tsx b/src/sections/analysis/board/evaluationBar.tsx index 52a078f..99f4e91 100644 --- a/src/sections/analysis/board/evaluationBar.tsx +++ b/src/sections/analysis/board/evaluationBar.tsx @@ -1,7 +1,11 @@ import { Box, Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; import { useEffect, useState } from "react"; -import { boardAtom, boardOrientationAtom, currentMoveAtom } from "../states"; +import { + boardAtom, + boardOrientationAtom, + currentPositionAtom, +} from "../states"; import { getEvaluationBarValue } from "@/lib/chess"; interface Props { @@ -15,17 +19,17 @@ export default function EvaluationBar({ height }: Props) { }); const board = useAtomValue(boardAtom); const boardOrientation = useAtomValue(boardOrientationAtom); - const currentMove = useAtomValue(currentMoveAtom); + const position = useAtomValue(currentPositionAtom); const isWhiteToPlay = board.turn() === "w"; useEffect(() => { - const bestLine = currentMove?.eval?.lines[0]; + const bestLine = position?.eval?.lines[0]; if (!bestLine || bestLine.depth < 6) return; const evalBar = getEvaluationBarValue(bestLine, isWhiteToPlay); setEvalBar(evalBar); - }, [currentMove, isWhiteToPlay]); + }, [position, isWhiteToPlay]); return ( (null); @@ -22,9 +25,9 @@ export default function Board() { const board = useAtomValue(boardAtom); const boardOrientation = useAtomValue(boardOrientationAtom); const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom); - const showPlayerMoveArrow = useAtomValue(showPlayerMoveArrowAtom); const { makeMove: makeBoardMove } = useChessActions(boardAtom); - const currentMove = useAtomValue(currentMoveAtom); + const position = useAtomValue(currentPositionAtom); + const squareRenderer = useSquareRenderer(position); const onPieceDrop = ( source: Square, @@ -45,9 +48,8 @@ export default function Board() { }; const customArrows: Arrow[] = useMemo(() => { - const arrows: Arrow[] = []; - const bestMove = currentMove?.lastEval?.bestMove; - const moveClassification = currentMove?.eval?.moveClassification; + const bestMove = position?.lastEval?.bestMove; + const moveClassification = position?.eval?.moveClassification; if ( bestMove && @@ -60,36 +62,11 @@ export default function Board() { moveClassificationColors[MoveClassification.Best], ] as Arrow; - arrows.push(bestMoveArrow); + return [bestMoveArrow]; } - if ( - currentMove.from && - currentMove.to && - showPlayerMoveArrow && - moveClassification !== MoveClassification.Best - ) { - const arrowColor = moveClassification - ? moveClassificationColors[moveClassification] - : "#ffaa00"; - const playerMoveArrow: Arrow = [ - currentMove.from, - currentMove.to, - arrowColor, - ]; - - if ( - arrows.every( - (arrow) => - arrow[0] !== playerMoveArrow[0] || arrow[1] !== playerMoveArrow[1] - ) - ) { - arrows.push(playerMoveArrow); - } - } - - return arrows; - }, [currentMove, showBestMoveArrow, showPlayerMoveArrow]); + return []; + }, [position, showBestMoveArrow]); return ( @@ -139,13 +117,3 @@ export default function Board() { ); } - -const moveClassificationColors: Record = { - [MoveClassification.Best]: "#26c2a3", - [MoveClassification.Book]: "#d5a47d", - [MoveClassification.Excellent]: "#3aab18", - [MoveClassification.Good]: "#81b64c", - [MoveClassification.Inaccuracy]: "#f7c631", - [MoveClassification.Mistake]: "#ffa459", - [MoveClassification.Blunder]: "#fa412d", -}; diff --git a/src/sections/analysis/reviewPanelBody/index.tsx b/src/sections/analysis/reviewPanelBody/index.tsx index cb8a0f8..6e7828d 100644 --- a/src/sections/analysis/reviewPanelBody/index.tsx +++ b/src/sections/analysis/reviewPanelBody/index.tsx @@ -3,7 +3,7 @@ import { Grid, List, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; import { boardAtom, engineMultiPvAtom, gameAtom } from "../states"; import LineEvaluation from "./lineEvaluation"; -import { useCurrentMove } from "@/hooks/useCurrentMove"; +import { useCurrentPosition } from "@/hooks/useCurrentPosition"; import { LineEval } from "@/types/eval"; import { EngineName } from "@/types/enums"; import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton"; @@ -13,7 +13,7 @@ import Opening from "./opening"; export default function ReviewPanelBody() { const linesNumber = useAtomValue(engineMultiPvAtom); - const move = useCurrentMove(EngineName.Stockfish16); + const position = useCurrentPosition(EngineName.Stockfish16); const game = useAtomValue(gameAtom); const board = useAtomValue(boardAtom); @@ -30,8 +30,8 @@ export default function ReviewPanelBody() { (_, i) => ({ pv: [`${i}`], depth: 0, multiPv: i + 1 }) ); - const engineLines = move?.eval?.lines?.length - ? move.eval.lines + const engineLines = position?.eval?.lines?.length + ? position.eval.lines : linesSkeleton; return ( diff --git a/src/sections/analysis/reviewPanelBody/moveInfo.tsx b/src/sections/analysis/reviewPanelBody/moveInfo.tsx index 8b48d9d..caffbff 100644 --- a/src/sections/analysis/reviewPanelBody/moveInfo.tsx +++ b/src/sections/analysis/reviewPanelBody/moveInfo.tsx @@ -1,16 +1,15 @@ -import { useCurrentMove } from "@/hooks/useCurrentMove"; import { Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; -import { boardAtom } from "../states"; +import { boardAtom, currentPositionAtom } from "../states"; import { useMemo } from "react"; import { moveLineUciToSan } from "@/lib/chess"; import { MoveClassification } from "@/types/enums"; export default function MoveInfo() { - const move = useCurrentMove(); + const position = useAtomValue(currentPositionAtom); const board = useAtomValue(boardAtom); - const bestMove = move?.lastEval?.bestMove; + const bestMove = position?.lastEval?.bestMove; const bestMoveSan = useMemo(() => { if (!bestMove) return undefined; @@ -23,9 +22,9 @@ export default function MoveInfo() { if (!bestMoveSan) return null; - const moveClassification = move.eval?.moveClassification; + const moveClassification = position.eval?.moveClassification; const moveLabel = moveClassification - ? `${move.san} is ${moveClassificationLabels[moveClassification]}` + ? `${position.lastMove?.san} is ${moveClassificationLabels[moveClassification]}` : null; const bestMoveLabel = diff --git a/src/sections/analysis/reviewPanelBody/opening.tsx b/src/sections/analysis/reviewPanelBody/opening.tsx index 76dc3ba..60aed5a 100644 --- a/src/sections/analysis/reviewPanelBody/opening.tsx +++ b/src/sections/analysis/reviewPanelBody/opening.tsx @@ -1,10 +1,10 @@ -import { useCurrentMove } from "@/hooks/useCurrentMove"; +import { useCurrentPosition } from "@/hooks/useCurrentPosition"; import { Grid, Typography } from "@mui/material"; export default function Opening() { - const move = useCurrentMove(); + const position = useCurrentPosition(); - const opening = move?.eval?.opening; + const opening = position?.eval?.opening; if (!opening) return null; return ( diff --git a/src/sections/analysis/states.ts b/src/sections/analysis/states.ts index cb8cd77..f75f0ce 100644 --- a/src/sections/analysis/states.ts +++ b/src/sections/analysis/states.ts @@ -1,15 +1,15 @@ -import { CurrentMove, GameEval } from "@/types/eval"; +import { CurrentPosition, GameEval } from "@/types/eval"; import { Chess } from "chess.js"; import { atom } from "jotai"; export const gameEvalAtom = atom(undefined); export const gameAtom = atom(new Chess()); export const boardAtom = atom(new Chess()); -export const currentMoveAtom = atom({}); +export const currentPositionAtom = atom({}); export const boardOrientationAtom = atom(true); export const showBestMoveArrowAtom = atom(true); -export const showPlayerMoveArrowAtom = atom(true); +export const showPlayerMoveIconAtom = atom(true); export const engineDepthAtom = atom(16); export const engineMultiPvAtom = atom(3); diff --git a/src/sections/engineSettings/arrowOptions.tsx b/src/sections/engineSettings/arrowOptions.tsx index 3410246..b2c4be4 100644 --- a/src/sections/engineSettings/arrowOptions.tsx +++ b/src/sections/engineSettings/arrowOptions.tsx @@ -1,7 +1,7 @@ import { Checkbox, FormControlLabel, Grid } from "@mui/material"; import { showBestMoveArrowAtom, - showPlayerMoveArrowAtom, + showPlayerMoveIconAtom, } from "../analysis/states"; import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage"; @@ -10,9 +10,9 @@ export default function ArrowOptions() { "show-arrow-best-move", showBestMoveArrowAtom ); - const [showPlayerMove, setShowPlayerMove] = useAtomLocalStorage( - "show-arrow-player-move", - showPlayerMoveArrowAtom + const [showPlayerMoveIcon, setShowPlayerMoveIcon] = useAtomLocalStorage( + "show-icon-player-move", + showPlayerMoveIconAtom ); return ( @@ -37,11 +37,11 @@ export default function ArrowOptions() { setShowPlayerMove(checked)} + checked={showPlayerMoveIcon} + onChange={(_, checked) => setShowPlayerMoveIcon(checked)} /> } - label="Show played move arrow" + label="Show played move icon" sx={{ marginX: 0 }} /> diff --git a/src/types/eval.ts b/src/types/eval.ts index 779b7bb..418db6a 100644 --- a/src/types/eval.ts +++ b/src/types/eval.ts @@ -1,7 +1,7 @@ import { Move } from "chess.js"; import { EngineName, MoveClassification } from "./enums"; -export interface MoveEval { +export interface PositionEval { bestMove?: string; moveClassification?: MoveClassification; opening?: string; @@ -29,7 +29,7 @@ export interface EngineSettings { } export interface GameEval { - moves: MoveEval[]; + positions: PositionEval[]; accuracy: Accuracy; settings: EngineSettings; } @@ -38,13 +38,14 @@ export interface EvaluatePositionWithUpdateParams { fen: string; depth?: number; multiPv?: number; - setPartialEval: (moveEval: MoveEval) => void; + setPartialEval: (positionEval: PositionEval) => void; } -export type CurrentMove = Partial & { - eval?: MoveEval; - lastEval?: MoveEval; -}; +export interface CurrentPosition { + lastMove?: Move; + eval?: PositionEval; + lastEval?: PositionEval; +} export interface EvaluateGameParams { fens: string[];