feat : add click on engine lines

This commit is contained in:
GuillaumeSD
2025-05-11 18:21:45 +02:00
parent 74a2adbb7d
commit 6535fce2f4
22 changed files with 285 additions and 156 deletions

View File

@@ -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 (
<ListItem disablePadding>
<Typography
@@ -58,9 +86,52 @@ export default function LineEvaluation({ line }: Props) {
{showSkeleton ? (
<Skeleton variant="rounded" animation="wave" width="15em" />
) : (
line.pv.map(moveLineUciToSan(board.fen())).join(", ")
line.pv.map((uci, i) => {
const san = uciToSan(uci);
const { icon, text } = formatSan(san, i);
return (
<Box
component="span"
key={i}
onClick={() => {
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 && (
<Typography
component="span"
fontFamily={myFont.style.fontFamily}
>
{icon}
</Typography>
)}
<Typography component="span">
{text}
{i < line.pv.length - 1 && ","}
</Typography>
</Box>
);
})
)}
</Typography>
</ListItem>
);
}
const unicodeMap: Record<string, Record<"w" | "b", string>> = {
K: { w: "♚", b: "♔" },
Q: { w: "♛", b: "♕" },
R: { w: "♜", b: "♖" },
B: { w: "♝", b: "♗" },
N: { w: "♞", b: "♘" },
};

View File

@@ -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}
>
<Grid

View File

@@ -6,7 +6,7 @@ import { boardAtom, currentPositionAtom, gameAtom } from "../../../states";
import { useChessActions } from "@/hooks/useChessActions";
import { useEffect } from "react";
import { isInViewport } from "@/lib/helpers";
import { moveClassificationColors } from "@/lib/chess";
import { CLASSIFICATION_COLORS } from "@/constants";
interface Props {
san: string;
@@ -89,7 +89,7 @@ const getMoveColor = (moveClassification?: MoveClassification) => {
return undefined;
}
return moveClassificationColors[moveClassification];
return CLASSIFICATION_COLORS[moveClassification];
};
const moveClassificationsToIgnore: MoveClassification[] = [

View File

@@ -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 (

View File

@@ -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)

View File

@@ -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) => {

View File

@@ -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>(EngineName.Stockfish17Lite);
export const engineNameAtom = atom<EngineName>(DEFAULT_ENGINE);
export const engineDepthAtom = atom(14);
export const engineMultiPvAtom = atom(3);
export const evaluationProgressAtom = atom(0);

View File

@@ -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 }}
>
<Typography>
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.
</Typography>
</Grid>
@@ -105,7 +111,7 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
value={engine}
disabled={!isEngineSupported(engine)}
>
{engineLabel[engine].full}
{ENGINE_LABELS[engine].full}
</MenuItem>
))}
</Select>
@@ -183,35 +189,3 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
</Dialog>
);
}
export const engineLabel: 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",
},
};

View File

@@ -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();

View File

@@ -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) {
</DialogTitle>
<DialogContent sx={{ paddingBottom: 0 }}>
<Typography>
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.
</Typography>
<Grid
marginTop={4}
@@ -124,7 +125,7 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
value={engine}
disabled={!isEngineSupported(engine)}
>
{engineLabel[engine].full}
{ENGINE_LABELS[engine].full}
</MenuItem>
))}
</Select>

View File

@@ -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<CurrentPosition>({});
export const playerColorAtom = atom<Color>(Color.White);
export const enginePlayNameAtom = atom<EngineName>(EngineName.Stockfish17Lite);
export const enginePlayNameAtom = atom<EngineName>(DEFAULT_ENGINE);
export const engineEloAtom = atom(1320);
export const isGameInProgressAtom = atom(false);