From 6535fce2f47e7162001a75a3e9c16759ee663070 Mon Sep 17 00:00:00 2001 From: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Sun, 11 May 2025 18:21:45 +0200 Subject: [PATCH] feat : add click on engine lines --- src/components/board/constants.ts | 56 ----------- src/components/board/index.tsx | 31 ++++-- src/components/board/squareRenderer.tsx | 4 +- src/components/board/states.ts | 2 +- src/constants.ts | 91 ++++++++++++++++++ src/hooks/useChessActions.ts | 17 +++- src/lib/chess.ts | 42 +++++--- src/lib/engine/helpers/parseResults.ts | 11 ++- src/lib/engine/uciEngine.ts | 10 +- src/lib/lichess.ts | 3 +- .../analysisTab/chess_merida_unicode.ttf | Bin 0 -> 42644 bytes .../panelBody/analysisTab/lineEvaluation.tsx | 75 ++++++++++++++- .../classificationRow.tsx | 4 +- .../classificationTab/movesPanel/moveItem.tsx | 4 +- .../analysis/panelBody/graphTab/dot.tsx | 4 +- .../analysis/panelBody/graphTab/index.tsx | 4 +- .../analysis/panelToolbar/nextMoveButton.tsx | 6 +- src/sections/analysis/states.ts | 3 +- .../engineSettings/engineSettingsDialog.tsx | 50 +++------- src/sections/play/board.tsx | 4 +- .../play/gameSettings/gameSettingsDialog.tsx | 17 ++-- src/sections/play/states.ts | 3 +- 22 files changed, 285 insertions(+), 156 deletions(-) delete mode 100644 src/components/board/constants.ts create mode 100644 src/constants.ts create mode 100644 src/sections/analysis/panelBody/analysisTab/chess_merida_unicode.ttf diff --git a/src/components/board/constants.ts b/src/components/board/constants.ts deleted file mode 100644 index f17e0ac..0000000 --- a/src/components/board/constants.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Piece } from "react-chessboard/dist/chessboard/types"; - -export const PIECE_CODES = [ - "wP", - "wB", - "wN", - "wR", - "wQ", - "wK", - "bP", - "bB", - "bN", - "bR", - "bQ", - "bK", -] as const satisfies Piece[]; - -export const PIECE_SETS = [ - "alpha", - "anarcandy", - "caliente", - "california", - "cardinal", - "cburnett", - "celtic", - "chess7", - "chessnut", - "companion", - "cooke", - "dubrovny", - "fantasy", - "firi", - "fresca", - "gioco", - "governor", - "horsey", - "icpieces", - "kiwen-suwi", - "kosal", - "leipzig", - "letter", - "maestro", - "merida", - "monarchy", - "mpchess", - "pirouetti", - "pixel", - "reillycraig", - "rhosgfx", - "riohacha", - "shapes", - "spatial", - "staunty", - "tatiana", - "xkcd", -] as const satisfies string[]; diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx index 6f5a727..8084535 100644 --- a/src/components/board/index.tsx +++ b/src/components/board/index.tsx @@ -5,6 +5,7 @@ import { Arrow, CustomPieces, CustomSquareRenderer, + Piece, PromotionPieceOption, Square, } from "react-chessboard/dist/chessboard/types"; @@ -15,13 +16,12 @@ import { Chess } from "chess.js"; import { getSquareRenderer } from "./squareRenderer"; import { CurrentPosition } from "@/types/eval"; import EvaluationBar from "./evaluationBar"; -import { moveClassificationColors } from "@/lib/chess"; +import { CLASSIFICATION_COLORS } from "@/constants"; import { Player } from "@/types/game"; import PlayerHeader from "./playerHeader"; import Image from "next/image"; import { boardHueAtom, pieceSetAtom } from "./states"; import tinycolor from "tinycolor2"; -import { PIECE_CODES } from "./constants"; export interface Props { id: string; @@ -52,7 +52,7 @@ export default function Board({ }: Props) { const boardRef = useRef(null); const game = useAtomValue(gameAtom); - const { makeMove: makeGameMove } = useChessActions(gameAtom); + const { playMove } = useChessActions(gameAtom); const clickedSquaresAtom = useMemo(() => atom([]), []); const setClickedSquares = useSetAtom(clickedSquaresAtom); const playableSquaresAtom = useMemo(() => atom([]), []); @@ -86,7 +86,7 @@ export default function Board({ ): boolean => { if (!isPiecePlayable({ piece })) return false; - const result = makeGameMove({ + const result = playMove({ from: source, to: target, promotion: piece[1]?.toLowerCase() ?? "q", @@ -135,7 +135,7 @@ export default function Board({ return; } - const result = makeGameMove({ + const result = playMove({ from: moveClickFrom, to: square, }); @@ -168,7 +168,7 @@ export default function Board({ const promotionPiece = piece[1]?.toLowerCase() ?? "q"; if (moveClickFrom && moveClickTo) { - const result = makeGameMove({ + const result = playMove({ from: moveClickFrom, to: moveClickTo, promotion: promotionPiece, @@ -178,7 +178,7 @@ export default function Board({ } if (from && to) { - const result = makeGameMove({ + const result = playMove({ from, to, promotion: promotionPiece, @@ -206,7 +206,7 @@ export default function Board({ const bestMoveArrow = [ bestMove.slice(0, 2), bestMove.slice(2, 4), - tinycolor(moveClassificationColors[MoveClassification.Best]) + tinycolor(CLASSIFICATION_COLORS[MoveClassification.Best]) .spin(-boardHue) .toHexString(), ] as Arrow; @@ -325,3 +325,18 @@ export default function Board({ ); } + +export const PIECE_CODES = [ + "wP", + "wB", + "wN", + "wR", + "wQ", + "wK", + "bP", + "bB", + "bN", + "bR", + "bQ", + "bK", +] as const satisfies Piece[]; diff --git a/src/components/board/squareRenderer.tsx b/src/components/board/squareRenderer.tsx index fb3808f..4e4454c 100644 --- a/src/components/board/squareRenderer.tsx +++ b/src/components/board/squareRenderer.tsx @@ -7,7 +7,7 @@ import { CustomSquareProps, Square, } from "react-chessboard/dist/chessboard/types"; -import { moveClassificationColors } from "@/lib/chess"; +import { CLASSIFICATION_COLORS } from "@/constants"; import { boardHueAtom } from "./states"; export interface Props { @@ -110,7 +110,7 @@ const previousMoveSquareStyle = ( width: "100%", height: "100%", backgroundColor: moveClassification - ? moveClassificationColors[moveClassification] + ? CLASSIFICATION_COLORS[moveClassification] : "#fad541", opacity: 0.5, }); diff --git a/src/components/board/states.ts b/src/components/board/states.ts index 4ffce8e..a2065f9 100644 --- a/src/components/board/states.ts +++ b/src/components/board/states.ts @@ -1,5 +1,5 @@ +import { PIECE_SETS } from "@/constants"; import { atomWithStorage } from "jotai/utils"; -import { PIECE_SETS } from "./constants"; export const pieceSetAtom = atomWithStorage<(typeof PIECE_SETS)[number]>( "pieceSet", diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..021989b --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,91 @@ +import { EngineName, MoveClassification } from "./types/enums"; + +export const CLASSIFICATION_COLORS: Record = { + [MoveClassification.Book]: "#d5a47d", + [MoveClassification.Forced]: "#d5a47d", + [MoveClassification.Brilliant]: "#26c2a3", + [MoveClassification.Great]: "#4099ed", + [MoveClassification.Best]: "#3aab18", + [MoveClassification.Excellent]: "#3aab18", + [MoveClassification.Good]: "#81b64c", + [MoveClassification.Inaccuracy]: "#f7c631", + [MoveClassification.Mistake]: "#ffa459", + [MoveClassification.Blunder]: "#fa412d", +}; + +export const DEFAULT_ENGINE: EngineName = EngineName.Stockfish17Lite; +export const STRONGEST_ENGINE: EngineName = EngineName.Stockfish17; + +export const ENGINE_LABELS: Record< + EngineName, + { small: string; full: string } +> = { + [EngineName.Stockfish17]: { + full: "Stockfish 17 (75MB)", + small: "Stockfish 17", + }, + [EngineName.Stockfish17Lite]: { + full: "Stockfish 17 Lite (6MB)", + small: "Stockfish 17 Lite", + }, + [EngineName.Stockfish16_1]: { + full: "Stockfish 16.1 (64MB)", + small: "Stockfish 16.1", + }, + [EngineName.Stockfish16_1Lite]: { + full: "Stockfish 16.1 Lite (6MB)", + small: "Stockfish 16.1 Lite", + }, + [EngineName.Stockfish16NNUE]: { + full: "Stockfish 16 (40MB)", + small: "Stockfish 16", + }, + [EngineName.Stockfish16]: { + full: "Stockfish 16 Lite (HCE)", + small: "Stockfish 16 Lite", + }, + [EngineName.Stockfish11]: { + full: "Stockfish 11 (HCE)", + small: "Stockfish 11", + }, +}; + +export const PIECE_SETS = [ + "alpha", + "anarcandy", + "caliente", + "california", + "cardinal", + "cburnett", + "celtic", + "chess7", + "chessnut", + "companion", + "cooke", + "dubrovny", + "fantasy", + "firi", + "fresca", + "gioco", + "governor", + "horsey", + "icpieces", + "kiwen-suwi", + "kosal", + "leipzig", + "letter", + "maestro", + "merida", + "monarchy", + "mpchess", + "pirouetti", + "pixel", + "reillycraig", + "rhosgfx", + "riohacha", + "shapes", + "spatial", + "staunty", + "tatiana", + "xkcd", +] as const satisfies string[]; diff --git a/src/hooks/useChessActions.ts b/src/hooks/useChessActions.ts index 3efdfd2..b7d4e92 100644 --- a/src/hooks/useChessActions.ts +++ b/src/hooks/useChessActions.ts @@ -67,7 +67,7 @@ export const useChessActions = (chessAtom: PrimitiveAtom) => { [copyGame, setGame] ); - const makeMove = useCallback( + const playMove = useCallback( (params: { from: string; to: string; @@ -92,6 +92,18 @@ export const useChessActions = (chessAtom: PrimitiveAtom) => { [copyGame, setGame] ); + const addMoves = useCallback( + (moves: string[]) => { + const newGame = copyGame(); + + for (const move of moves) { + newGame.move(move); + } + setGame(newGame); + }, + [copyGame, setGame] + ); + const undoMove = useCallback(() => { const newGame = copyGame(); const move = newGame.undo(); @@ -127,9 +139,10 @@ export const useChessActions = (chessAtom: PrimitiveAtom) => { return { setPgn, reset, - makeMove, + playMove, undoMove, goToMove, resetToStartingPosition, + addMoves, }; }; diff --git a/src/lib/chess.ts b/src/lib/chess.ts index a9a9baf..2f9907e 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -2,7 +2,7 @@ import { EvaluateGameParams, LineEval, PositionEval } from "@/types/eval"; import { Game, Player } from "@/types/game"; import { Chess, PieceSymbol, Square } from "chess.js"; import { getPositionWinPercentage } from "./engine/helpers/winPercentage"; -import { Color, MoveClassification } from "@/types/enums"; +import { Color } from "@/types/enums"; export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => { const history = game.history({ verbose: true }); @@ -327,15 +327,33 @@ export const getLineEvalLabel = ( return "?"; }; -export const moveClassificationColors: Record = { - [MoveClassification.Book]: "#d5a47d", - [MoveClassification.Forced]: "#d5a47d", - [MoveClassification.Brilliant]: "#26c2a3", - [MoveClassification.Great]: "#4099ed", - [MoveClassification.Best]: "#3aab18", - [MoveClassification.Excellent]: "#3aab18", - [MoveClassification.Good]: "#81b64c", - [MoveClassification.Inaccuracy]: "#f7c631", - [MoveClassification.Mistake]: "#ffa459", - [MoveClassification.Blunder]: "#fa412d", +export const formatUciPv = (fen: string, uciMoves: string[]): string[] => { + const castlingRights = fen.split(" ")[2]; + + let canWhiteCastleKingSide = castlingRights.includes("K"); + let canWhiteCastleQueenSide = castlingRights.includes("Q"); + let canBlackCastleKingSide = castlingRights.includes("k"); + let canBlackCastleQueenSide = castlingRights.includes("q"); + + return uciMoves.map((uci) => { + if (uci === "e1h1" && canWhiteCastleKingSide) { + canWhiteCastleKingSide = false; + return "e1g1"; + } + if (uci === "e1a1" && canWhiteCastleQueenSide) { + canWhiteCastleQueenSide = false; + return "e1c1"; + } + + if (uci === "e8h8" && canBlackCastleKingSide) { + canBlackCastleKingSide = false; + return "e8g8"; + } + if (uci === "e8a8" && canBlackCastleQueenSide) { + canBlackCastleQueenSide = false; + return "e8c8"; + } + + return uci; + }); }; diff --git a/src/lib/engine/helpers/parseResults.ts b/src/lib/engine/helpers/parseResults.ts index d2eee90..6784dea 100644 --- a/src/lib/engine/helpers/parseResults.ts +++ b/src/lib/engine/helpers/parseResults.ts @@ -1,8 +1,9 @@ +import { formatUciPv } from "@/lib/chess"; import { LineEval, PositionEval } from "@/types/eval"; export const parseEvaluationResults = ( results: string[], - whiteToPlay: boolean + fen: string ): PositionEval => { const parsedResults: PositionEval = { lines: [], @@ -18,7 +19,7 @@ export const parseEvaluationResults = ( } if (result.startsWith("info")) { - const pv = getResultPv(result); + const pv = getResultPv(result, fen); const multiPv = getResultProperty(result, "multipv"); const depth = getResultProperty(result, "depth"); if (!pv || !multiPv || !depth) continue; @@ -45,6 +46,7 @@ export const parseEvaluationResults = ( parsedResults.lines = Object.values(tempResults).sort(sortLines); + const whiteToPlay = fen.split(" ")[1] === "w"; if (!whiteToPlay) { parsedResults.lines = parsedResults.lines.map((line) => ({ ...line, @@ -86,7 +88,7 @@ export const getResultProperty = ( return splitResult[propertyIndex + 1]; }; -const getResultPv = (result: string): string[] | undefined => { +const getResultPv = (result: string, fen: string): string[] | undefined => { const splitResult = result.split(" "); const pvIndex = splitResult.indexOf("pv"); @@ -94,5 +96,6 @@ const getResultPv = (result: string): string[] | undefined => { return undefined; } - return splitResult.slice(pvIndex + 1); + const rawPv = splitResult.slice(pvIndex + 1); + return formatUciPv(fen, rawPv); }; diff --git a/src/lib/engine/uciEngine.ts b/src/lib/engine/uciEngine.ts index d9babdc..ff6e0be 100644 --- a/src/lib/engine/uciEngine.ts +++ b/src/lib/engine/uciEngine.ts @@ -317,9 +317,7 @@ export class UciEngine { "bestmove" ); - const whiteToPlay = fen.split(" ")[1] === "w"; - - return parseEvaluationResults(results, whiteToPlay); + return parseEvaluationResults(results, fen); } public async evaluatePositionWithUpdate({ @@ -335,11 +333,9 @@ export class UciEngine { await this.stopSearch(); await this.setMultiPv(multiPv); - const whiteToPlay = fen.split(" ")[1] === "w"; - const onNewMessage = (messages: string[]) => { if (!setPartialEval) return; - const parsedResults = parseEvaluationResults(messages, whiteToPlay); + const parsedResults = parseEvaluationResults(messages, fen); setPartialEval(parsedResults); }; @@ -360,7 +356,7 @@ export class UciEngine { onNewMessage ); - return parseEvaluationResults(results, whiteToPlay); + return parseEvaluationResults(results, fen); } public async getEngineNextMove( diff --git a/src/lib/lichess.ts b/src/lib/lichess.ts index cef22d6..222dd0c 100644 --- a/src/lib/lichess.ts +++ b/src/lib/lichess.ts @@ -7,6 +7,7 @@ import { LichessResponse, } from "@/types/lichess"; import { logErrorToSentry } from "./sentry"; +import { formatUciPv } from "./chess"; export const getLichessEval = async ( fen: string, @@ -26,7 +27,7 @@ export const getLichessEval = async ( } const lines: LineEval[] = data.pvs.map((pv, index) => ({ - pv: pv.moves.split(" "), + pv: formatUciPv(fen, pv.moves.split(" ")), cp: pv.cp, mate: pv.mate, depth: data.depth, diff --git a/src/sections/analysis/panelBody/analysisTab/chess_merida_unicode.ttf b/src/sections/analysis/panelBody/analysisTab/chess_merida_unicode.ttf new file mode 100644 index 0000000000000000000000000000000000000000..674af6bc0feeeda4281254e74a00eb5b0ed20690 GIT binary patch literal 42644 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 literal 0 HcmV?d00001 diff --git a/src/sections/analysis/panelBody/analysisTab/lineEvaluation.tsx b/src/sections/analysis/panelBody/analysisTab/lineEvaluation.tsx index 4cf23b8..6297934 100644 --- a/src/sections/analysis/panelBody/analysisTab/lineEvaluation.tsx +++ b/src/sections/analysis/panelBody/analysisTab/lineEvaluation.tsx @@ -1,15 +1,23 @@ import { LineEval } from "@/types/eval"; -import { ListItem, Skeleton, Typography } from "@mui/material"; +import { Box, ListItem, Skeleton, Typography, useTheme } from "@mui/material"; import { useAtomValue } from "jotai"; import { boardAtom } from "../../states"; import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess"; +import localFont from "next/font/local"; +import { useChessActions } from "@/hooks/useChessActions"; + +const myFont = localFont({ + src: "./chess_merida_unicode.ttf", +}); interface Props { line: LineEval; } export default function LineEvaluation({ line }: Props) { + const theme = useTheme(); const board = useAtomValue(boardAtom); + const { addMoves } = useChessActions(boardAtom); const lineLabel = getLineEvalLabel(line); const isBlackCp = @@ -18,6 +26,26 @@ export default function LineEvaluation({ line }: Props) { const showSkeleton = line.depth < 6; + const uciToSan = moveLineUciToSan(board.fen()); + const initialTurn = board.turn(); + const isDarkMode = theme.palette.mode === "dark"; + + const formatSan = ( + san: string, + moveIdx: number + ): { icon?: string; text: string } => { + const firstChar = san.charAt(0); + + const isPiece = ["K", "Q", "R", "B", "N"].includes(firstChar); + if (!isPiece) return { text: san }; + + const turn = isDarkMode ? initialTurn : initialTurn === "w" ? "b" : "w"; + const moveColor = moveIdx % 2 === 0 ? turn : turn === "w" ? "b" : "w"; + const icon = unicodeMap[firstChar][moveColor]; + + return { icon, text: san.slice(1) }; + }; + return ( ) : ( - line.pv.map(moveLineUciToSan(board.fen())).join(", ") + line.pv.map((uci, i) => { + const san = uciToSan(uci); + const { icon, text } = formatSan(san, i); + + return ( + { + addMoves(line.pv.slice(0, i + 1)); + }} + sx={{ + cursor: "pointer", + ml: i ? 0.5 : 0, + transition: "opacity 0.2s ease-in-out", + "&:hover": { + opacity: 0.5, + }, + }} + > + {icon && ( + + {icon} + + )} + + + {text} + {i < line.pv.length - 1 && ","} + + + ); + }) )} ); } + +const unicodeMap: Record> = { + K: { w: "♚", b: "♔" }, + Q: { w: "♛", b: "♕" }, + R: { w: "♜", b: "♖" }, + B: { w: "♝", b: "♗" }, + N: { w: "♞", b: "♘" }, +}; diff --git a/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/classificationRow.tsx b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/classificationRow.tsx index e30936f..cf783e6 100644 --- a/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/classificationRow.tsx +++ b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/classificationRow.tsx @@ -6,7 +6,7 @@ import { useMemo } from "react"; import Image from "next/image"; import { capitalize } from "@/lib/helpers"; import { useChessActions } from "@/hooks/useChessActions"; -import { moveClassificationColors } from "@/lib/chess"; +import { CLASSIFICATION_COLORS } from "@/constants"; interface Props { classification: MoveClassification; @@ -74,7 +74,7 @@ export default function ClassificationRow({ classification }: Props) { justifyContent="space-evenly" alignItems="center" wrap="nowrap" - color={moveClassificationColors[classification]} + color={CLASSIFICATION_COLORS[classification]} size={12} > { return undefined; } - return moveClassificationColors[moveClassification]; + return CLASSIFICATION_COLORS[moveClassification]; }; const moveClassificationsToIgnore: MoveClassification[] = [ diff --git a/src/sections/analysis/panelBody/graphTab/dot.tsx b/src/sections/analysis/panelBody/graphTab/dot.tsx index 1a81efd..9876686 100644 --- a/src/sections/analysis/panelBody/graphTab/dot.tsx +++ b/src/sections/analysis/panelBody/graphTab/dot.tsx @@ -1,6 +1,6 @@ import { DotProps } from "recharts"; import { ChartItemData } from "./types"; -import { moveClassificationColors } from "@/lib/chess"; +import { CLASSIFICATION_COLORS } from "@/constants"; export default function CustomDot({ cx, @@ -9,7 +9,7 @@ export default function CustomDot({ payload, }: DotProps & { payload?: ChartItemData }) { const moveColor = payload?.moveClassification - ? moveClassificationColors[payload.moveClassification] + ? CLASSIFICATION_COLORS[payload.moveClassification] : "grey"; return ( diff --git a/src/sections/analysis/panelBody/graphTab/index.tsx b/src/sections/analysis/panelBody/graphTab/index.tsx index 2a84be2..71a98b1 100644 --- a/src/sections/analysis/panelBody/graphTab/index.tsx +++ b/src/sections/analysis/panelBody/graphTab/index.tsx @@ -21,7 +21,7 @@ import type { ReactElement } from "react"; import CustomTooltip from "./tooltip"; import { ChartItemData } from "./types"; import { PositionEval } from "@/types/eval"; -import { moveClassificationColors } from "@/lib/chess"; +import { CLASSIFICATION_COLORS } from "@/constants"; import CustomDot from "./dot"; import { MoveClassification } from "@/types/enums"; import { useChessActions } from "@/hooks/useChessActions"; @@ -51,7 +51,7 @@ export default function GraphTab(props: GridProps) { }, [chartData]); const boardMoveColor = currentPosition.eval?.moveClassification - ? moveClassificationColors[currentPosition.eval.moveClassification] + ? CLASSIFICATION_COLORS[currentPosition.eval.moveClassification] : "grey"; // Render a dot only on selected classifications (always returns an element) diff --git a/src/sections/analysis/panelToolbar/nextMoveButton.tsx b/src/sections/analysis/panelToolbar/nextMoveButton.tsx index bad16d0..8691f44 100644 --- a/src/sections/analysis/panelToolbar/nextMoveButton.tsx +++ b/src/sections/analysis/panelToolbar/nextMoveButton.tsx @@ -6,7 +6,7 @@ import { useChessActions } from "@/hooks/useChessActions"; import { useCallback, useEffect } from "react"; export default function NextMoveButton() { - const { makeMove: makeBoardMove } = useChessActions(boardAtom); + const { playMove: playBoardMove } = useChessActions(boardAtom); const game = useAtomValue(gameAtom); const board = useAtomValue(boardAtom); @@ -27,14 +27,14 @@ export default function NextMoveButton() { .find((c) => c.fen === nextMove.after)?.comment; if (nextMove) { - makeBoardMove({ + playBoardMove({ from: nextMove.from, to: nextMove.to, promotion: nextMove.promotion, comment, }); } - }, [isButtonEnabled, boardHistory, game, makeBoardMove]); + }, [isButtonEnabled, boardHistory, game, playBoardMove]); useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { diff --git a/src/sections/analysis/states.ts b/src/sections/analysis/states.ts index ff59887..17b5203 100644 --- a/src/sections/analysis/states.ts +++ b/src/sections/analysis/states.ts @@ -1,3 +1,4 @@ +import { DEFAULT_ENGINE } from "@/constants"; import { EngineName } from "@/types/enums"; import { CurrentPosition, GameEval, SavedEvals } from "@/types/eval"; import { Chess } from "chess.js"; @@ -12,7 +13,7 @@ export const boardOrientationAtom = atom(true); export const showBestMoveArrowAtom = atom(true); export const showPlayerMoveIconAtom = atom(true); -export const engineNameAtom = atom(EngineName.Stockfish17Lite); +export const engineNameAtom = atom(DEFAULT_ENGINE); export const engineDepthAtom = atom(14); export const engineMultiPvAtom = atom(3); export const evaluationProgressAtom = atom(0); diff --git a/src/sections/engineSettings/engineSettingsDialog.tsx b/src/sections/engineSettings/engineSettingsDialog.tsx index ca0ddc3..4cf19fb 100644 --- a/src/sections/engineSettings/engineSettingsDialog.tsx +++ b/src/sections/engineSettings/engineSettingsDialog.tsx @@ -26,7 +26,12 @@ import { isEngineSupported } from "@/lib/engine/shared"; import { Stockfish16_1 } from "@/lib/engine/stockfish16_1"; import { useAtom } from "jotai"; import { boardHueAtom, pieceSetAtom } from "@/components/board/states"; -import { PIECE_SETS } from "@/components/board/constants"; +import { + DEFAULT_ENGINE, + ENGINE_LABELS, + PIECE_SETS, + STRONGEST_ENGINE, +} from "@/constants"; interface Props { open: boolean; @@ -76,10 +81,11 @@ export default function EngineSettingsDialog({ open, onClose }: Props) { size={{ xs: 12, sm: 7, md: 8 }} > - Stockfish 17 Lite is the default engine if your device support its - requirements. It offers the best balance between speed and - strength. Stockfish 17 is the strongest engine available, note - that it requires a one time download of 75MB. + {ENGINE_LABELS[DEFAULT_ENGINE].small} is the default engine if + your device support its requirements. It offers the best balance + between speed and strength.{" "} + {ENGINE_LABELS[STRONGEST_ENGINE].small} is the strongest engine + available, note that it requires a one time download of 75MB. @@ -105,7 +111,7 @@ export default function EngineSettingsDialog({ open, onClose }: Props) { value={engine} disabled={!isEngineSupported(engine)} > - {engineLabel[engine].full} + {ENGINE_LABELS[engine].full} ))} @@ -183,35 +189,3 @@ export default function EngineSettingsDialog({ open, onClose }: Props) { ); } - -export const engineLabel: Record = - { - [EngineName.Stockfish17]: { - full: "Stockfish 17 (75MB)", - small: "Stockfish 17", - }, - [EngineName.Stockfish17Lite]: { - full: "Stockfish 17 Lite (6MB)", - small: "Stockfish 17 Lite", - }, - [EngineName.Stockfish16_1]: { - full: "Stockfish 16.1 (64MB)", - small: "Stockfish 16.1", - }, - [EngineName.Stockfish16_1Lite]: { - full: "Stockfish 16.1 Lite (6MB)", - small: "Stockfish 16.1 Lite", - }, - [EngineName.Stockfish16NNUE]: { - full: "Stockfish 16 (40MB)", - small: "Stockfish 16", - }, - [EngineName.Stockfish16]: { - full: "Stockfish 16 Lite (HCE)", - small: "Stockfish 16 Lite", - }, - [EngineName.Stockfish11]: { - full: "Stockfish 11 (HCE)", - small: "Stockfish 11", - }, - }; diff --git a/src/sections/play/board.tsx b/src/sections/play/board.tsx index 742705d..4fdac6e 100644 --- a/src/sections/play/board.tsx +++ b/src/sections/play/board.tsx @@ -23,7 +23,7 @@ export default function BoardContainer() { const game = useAtomValue(gameAtom); const { white, black } = usePlayersData(gameAtom); const playerColor = useAtomValue(playerColorAtom); - const { makeMove: makeGameMove } = useChessActions(gameAtom); + const { playMove } = useChessActions(gameAtom); const engineElo = useAtomValue(engineEloAtom); const isGameInProgress = useAtomValue(isGameInProgressAtom); @@ -41,7 +41,7 @@ export default function BoardContainer() { return; } const move = await engine.getEngineNextMove(gameFen, engineElo); - if (move) makeGameMove(uciMoveParams(move)); + if (move) playMove(uciMoveParams(move)); }; playEngineMove(); diff --git a/src/sections/play/gameSettings/gameSettingsDialog.tsx b/src/sections/play/gameSettings/gameSettingsDialog.tsx index 8102d7d..8524316 100644 --- a/src/sections/play/gameSettings/gameSettingsDialog.tsx +++ b/src/sections/play/gameSettings/gameSettingsDialog.tsx @@ -32,7 +32,7 @@ import { logAnalyticsEvent } from "@/lib/firebase"; import { useEffect } from "react"; import { isEngineSupported } from "@/lib/engine/shared"; import { Stockfish16_1 } from "@/lib/engine/stockfish16_1"; -import { engineLabel } from "@/sections/engineSettings/engineSettingsDialog"; +import { DEFAULT_ENGINE, ENGINE_LABELS, STRONGEST_ENGINE } from "@/constants"; interface Props { open: boolean; @@ -57,12 +57,12 @@ export default function GameSettingsDialog({ open, onClose }: Props) { resetGame({ white: { name: - playerColor === Color.White ? "You" : engineLabel[engineName].small, + playerColor === Color.White ? "You" : ENGINE_LABELS[engineName].small, rating: playerColor === Color.White ? undefined : engineElo, }, black: { name: - playerColor === Color.Black ? "You" : engineLabel[engineName].small, + playerColor === Color.Black ? "You" : ENGINE_LABELS[engineName].small, rating: playerColor === Color.Black ? undefined : engineElo, }, }); @@ -93,10 +93,11 @@ export default function GameSettingsDialog({ open, onClose }: Props) { - Stockfish 17 Lite is the default engine if your device support its - requirements. It offers the best balance between speed and strength. - Stockfish 17 is the strongest engine available, note that it requires - a one time download of 75MB. + {ENGINE_LABELS[DEFAULT_ENGINE].small} is the default engine if your + device support its requirements. It offers the best balance between + speed and strength. {ENGINE_LABELS[STRONGEST_ENGINE].small} is the + strongest engine available, note that it requires a one time download + of 75MB. - {engineLabel[engine].full} + {ENGINE_LABELS[engine].full} ))} diff --git a/src/sections/play/states.ts b/src/sections/play/states.ts index edfe070..d12ec93 100644 --- a/src/sections/play/states.ts +++ b/src/sections/play/states.ts @@ -1,3 +1,4 @@ +import { DEFAULT_ENGINE } from "@/constants"; import { Color, EngineName } from "@/types/enums"; import { CurrentPosition } from "@/types/eval"; import { Chess } from "chess.js"; @@ -6,6 +7,6 @@ import { atom } from "jotai"; export const gameAtom = atom(new Chess()); export const gameDataAtom = atom({}); export const playerColorAtom = atom(Color.White); -export const enginePlayNameAtom = atom(EngineName.Stockfish17Lite); +export const enginePlayNameAtom = atom(DEFAULT_ENGINE); export const engineEloAtom = atom(1320); export const isGameInProgressAtom = atom(false);