feat : add pretty san

This commit is contained in:
GuillaumeSD
2025-06-02 02:37:05 +02:00
parent f782b55b5a
commit d04c4d99df
8 changed files with 153 additions and 99 deletions

View File

@@ -0,0 +1,71 @@
import {
Box,
BoxProps,
Typography,
TypographyProps,
useTheme,
} from "@mui/material";
import localFont from "next/font/local";
import { useMemo } from "react";
const chessFont = localFont({
src: "./chess_merida_unicode.ttf",
});
interface Props {
san: string;
color: "w" | "b";
additionalText?: string;
typographyProps?: TypographyProps;
boxProps?: BoxProps;
}
export default function PrettyMoveSan({
san,
color,
additionalText,
typographyProps,
boxProps,
}: Props) {
const theme = useTheme();
const isDarkMode = theme.palette.mode === "dark";
const { icon, text } = useMemo(() => {
const firstChar = san.charAt(0);
const isPiece = ["K", "Q", "R", "B", "N"].includes(firstChar);
if (!isPiece) return { text: san };
const pieceColor = isDarkMode ? color : color === "w" ? "b" : "w";
const icon = unicodeMap[firstChar][pieceColor];
return { icon, text: san.slice(1) };
}, [san, color, isDarkMode]);
return (
<Box component="span" {...boxProps}>
{icon && (
<Typography
component="span"
fontFamily={chessFont.style.fontFamily}
{...typographyProps}
>
{icon}
</Typography>
)}
<Typography component="span" {...typographyProps}>
{text}
{additionalText}
</Typography>
</Box>
);
}
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

@@ -86,7 +86,7 @@ export default function GameAnalysis() {
gridTemplateRows={
gameEval
? "repeat(2, auto) max-content fit-content(100%) fit-content(100%) auto"
: "repeat(3, auto) fit-content(100%)"
: "repeat(2, auto) max-content fit-content(100%)"
}
size={{
xs: 12,

View File

@@ -1,21 +1,16 @@
import { LineEval } from "@/types/eval";
import { Box, ListItem, Skeleton, Typography, useTheme } from "@mui/material";
import { ListItem, Skeleton, Typography } 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",
});
import PrettyMoveSan from "@/components/prettyMoveSan";
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);
@@ -27,23 +22,11 @@ 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 turn = board.turn();
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 getColorFromMoveIdx = (moveIdx: number): "w" | "b" => {
const moveColor = moveIdx % 2 === 0 ? turn : turn === "w" ? "b" : "w";
const icon = unicodeMap[firstChar][moveColor];
return { icon, text: san.slice(1) };
return moveColor;
};
return (
@@ -85,38 +68,28 @@ export default function LineEvaluation({ line }: Props) {
) : (
line.pv.map((uci, i) => {
const san = uciToSan(uci);
const { icon, text } = formatSan(san, i);
const moveColor = getColorFromMoveIdx(i);
return (
<Box
component="span"
<PrettyMoveSan
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,
san={san}
color={moveColor}
additionalText={i < line.pv.length - 1 ? "," : ""}
boxProps={{
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>
/>
);
})
)}
@@ -124,11 +97,3 @@ export default function LineEvaluation({ line }: Props) {
</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

@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { moveLineUciToSan } from "@/lib/chess";
import { MoveClassification } from "@/types/enums";
import Image from "next/image";
import PrettyMoveSan from "@/components/prettyMoveSan";
export default function MoveInfo() {
const position = useAtomValue(currentPositionAtom);
@@ -56,18 +57,13 @@ export default function MoveInfo() {
}
const moveClassification = position.eval?.moveClassification;
const moveLabel = moveClassification
? `${position.lastMove?.san} is ${moveClassificationLabels[moveClassification]}`
: null;
const bestMoveLabel =
moveClassification === MoveClassification.Best ||
moveClassification === MoveClassification.Opening ||
moveClassification === MoveClassification.Forced ||
moveClassification === MoveClassification.Splendid ||
moveClassification === MoveClassification.Perfect
? null
: `${bestMoveSan} was the best move`;
const showBestMoveLabel =
moveClassification !== MoveClassification.Best &&
moveClassification !== MoveClassification.Opening &&
moveClassification !== MoveClassification.Forced &&
moveClassification !== MoveClassification.Splendid &&
moveClassification !== MoveClassification.Perfect;
return (
<Grid
@@ -77,27 +73,33 @@ export default function MoveInfo() {
size={12}
marginTop={0.5}
>
{moveLabel && (
{moveClassification && (
<Stack direction="row" alignItems="center" spacing={1}>
{moveClassification && (
<Image
src={`/icons/${moveClassification}.png`}
alt="move-icon"
width={16}
height={16}
style={{
maxWidth: "3.5vw",
maxHeight: "3.5vw",
}}
/>
)}
<Typography align="center" fontSize="0.9rem">
{moveLabel}
</Typography>
<Image
src={`/icons/${moveClassification}.png`}
alt="move-icon"
width={16}
height={16}
style={{
maxWidth: "3.5vw",
maxHeight: "3.5vw",
}}
/>
<PrettyMoveSan
typographyProps={{
fontSize: "0.9rem",
}}
san={position.lastMove?.san ?? ""}
color={position.lastMove?.color ?? "w"}
additionalText={
" is " + moveClassificationLabels[moveClassification]
}
/>
</Stack>
)}
{bestMoveLabel && (
{showBestMoveLabel && (
<Stack direction="row" alignItems="center" spacing={1}>
<Image
src={"/icons/best.png"}
@@ -109,9 +111,14 @@ export default function MoveInfo() {
maxHeight: "3.5vw",
}}
/>
<Typography align="center" fontSize="0.9rem">
{bestMoveLabel}
</Typography>
<PrettyMoveSan
typographyProps={{
fontSize: "0.9rem",
}}
san={bestMoveSan}
color={position.lastMove?.color ?? "w"}
additionalText=" was the best move"
/>
</Stack>
)}

View File

@@ -42,7 +42,8 @@ export default function MovesPanel() {
container
justifyContent="center"
alignItems="start"
gap={0.6}
gap={0.7}
paddingY={1}
sx={{ scrollbarWidth: "thin", overflowY: "auto" }}
height="100%"
size={6}

View File

@@ -1,5 +1,5 @@
import { MoveClassification } from "@/types/enums";
import { Grid2 as Grid, Typography } from "@mui/material";
import { Grid2 as Grid } from "@mui/material";
import Image from "next/image";
import { useAtomValue } from "jotai";
import { boardAtom, currentPositionAtom, gameAtom } from "../../../states";
@@ -7,14 +7,21 @@ import { useChessActions } from "@/hooks/useChessActions";
import { useEffect } from "react";
import { isInViewport } from "@/lib/helpers";
import { CLASSIFICATION_COLORS } from "@/constants";
import PrettyMoveSan from "@/components/prettyMoveSan";
interface Props {
san: string;
moveClassification?: MoveClassification;
moveIdx: number;
moveColor: "w" | "b";
}
export default function MoveItem({ san, moveClassification, moveIdx }: Props) {
export default function MoveItem({
san,
moveClassification,
moveIdx,
moveColor,
}: Props) {
const game = useAtomValue(gameAtom);
const { goToMove } = useChessActions(boardAtom);
const position = useAtomValue(currentPositionAtom);
@@ -47,7 +54,7 @@ export default function MoveItem({ san, moveClassification, moveIdx }: Props) {
width="5rem"
wrap="nowrap"
onClick={handleClick}
paddingY={0.6}
paddingY={0.5}
sx={(theme) => ({
cursor: isCurrentMove ? undefined : "pointer",
backgroundColor:
@@ -74,9 +81,8 @@ export default function MoveItem({ san, moveClassification, moveIdx }: Props) {
}}
/>
)}
<Typography color={color} fontSize="0.9rem" lineHeight="0.9rem">
{san}
</Typography>
<PrettyMoveSan san={san} color={moveColor} />
</Grid>
);
}

View File

@@ -1,5 +1,5 @@
import { MoveClassification } from "@/types/enums";
import { Grid2 as Grid, Typography } from "@mui/material";
import { Box, Grid2 as Grid, Typography } from "@mui/material";
import MoveItem from "./moveItem";
interface Props {
@@ -20,9 +20,13 @@ export default function MovesLine({ moves, moveNb }: Props) {
{moveNb}.
</Typography>
<MoveItem {...moves[0]} moveIdx={(moveNb - 1) * 2 + 1} />
<MoveItem {...moves[0]} moveIdx={(moveNb - 1) * 2 + 1} moveColor="w" />
<MoveItem {...moves[1]} moveIdx={(moveNb - 1) * 2 + 2} />
{moves[1] ? (
<MoveItem {...moves[1]} moveIdx={(moveNb - 1) * 2 + 2} moveColor="b" />
) : (
<Box width="5rem" />
)}
</Grid>
);
}