feat : add pretty san
This commit is contained in:
71
src/components/prettyMoveSan/index.tsx
Normal file
71
src/components/prettyMoveSan/index.tsx
Normal 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: "♘" },
|
||||||
|
};
|
||||||
@@ -86,7 +86,7 @@ export default function GameAnalysis() {
|
|||||||
gridTemplateRows={
|
gridTemplateRows={
|
||||||
gameEval
|
gameEval
|
||||||
? "repeat(2, auto) max-content fit-content(100%) fit-content(100%) auto"
|
? "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={{
|
size={{
|
||||||
xs: 12,
|
xs: 12,
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import { LineEval } from "@/types/eval";
|
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 { useAtomValue } from "jotai";
|
||||||
import { boardAtom } from "../../states";
|
import { boardAtom } from "../../states";
|
||||||
import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess";
|
import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess";
|
||||||
import localFont from "next/font/local";
|
|
||||||
import { useChessActions } from "@/hooks/useChessActions";
|
import { useChessActions } from "@/hooks/useChessActions";
|
||||||
|
import PrettyMoveSan from "@/components/prettyMoveSan";
|
||||||
const myFont = localFont({
|
|
||||||
src: "./chess_merida_unicode.ttf",
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
line: LineEval;
|
line: LineEval;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LineEvaluation({ line }: Props) {
|
export default function LineEvaluation({ line }: Props) {
|
||||||
const theme = useTheme();
|
|
||||||
const board = useAtomValue(boardAtom);
|
const board = useAtomValue(boardAtom);
|
||||||
const { addMoves } = useChessActions(boardAtom);
|
const { addMoves } = useChessActions(boardAtom);
|
||||||
const lineLabel = getLineEvalLabel(line);
|
const lineLabel = getLineEvalLabel(line);
|
||||||
@@ -27,23 +22,11 @@ export default function LineEvaluation({ line }: Props) {
|
|||||||
const showSkeleton = line.depth < 6;
|
const showSkeleton = line.depth < 6;
|
||||||
|
|
||||||
const uciToSan = moveLineUciToSan(board.fen());
|
const uciToSan = moveLineUciToSan(board.fen());
|
||||||
const initialTurn = board.turn();
|
const turn = board.turn();
|
||||||
const isDarkMode = theme.palette.mode === "dark";
|
|
||||||
|
|
||||||
const formatSan = (
|
const getColorFromMoveIdx = (moveIdx: number): "w" | "b" => {
|
||||||
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 moveColor = moveIdx % 2 === 0 ? turn : turn === "w" ? "b" : "w";
|
||||||
const icon = unicodeMap[firstChar][moveColor];
|
return moveColor;
|
||||||
|
|
||||||
return { icon, text: san.slice(1) };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,38 +68,28 @@ export default function LineEvaluation({ line }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
line.pv.map((uci, i) => {
|
line.pv.map((uci, i) => {
|
||||||
const san = uciToSan(uci);
|
const san = uciToSan(uci);
|
||||||
const { icon, text } = formatSan(san, i);
|
const moveColor = getColorFromMoveIdx(i);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<PrettyMoveSan
|
||||||
component="span"
|
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => {
|
san={san}
|
||||||
addMoves(line.pv.slice(0, i + 1));
|
color={moveColor}
|
||||||
}}
|
additionalText={i < line.pv.length - 1 ? "," : ""}
|
||||||
sx={{
|
boxProps={{
|
||||||
cursor: "pointer",
|
onClick: () => {
|
||||||
ml: i ? 0.5 : 0,
|
addMoves(line.pv.slice(0, i + 1));
|
||||||
transition: "opacity 0.2s ease-in-out",
|
},
|
||||||
"&:hover": {
|
sx: {
|
||||||
opacity: 0.5,
|
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>
|
</ListItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unicodeMap: Record<string, Record<"w" | "b", string>> = {
|
|
||||||
K: { w: "♚", b: "♔" },
|
|
||||||
Q: { w: "♛", b: "♕" },
|
|
||||||
R: { w: "♜", b: "♖" },
|
|
||||||
B: { w: "♝", b: "♗" },
|
|
||||||
N: { w: "♞", b: "♘" },
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useMemo } from "react";
|
|||||||
import { moveLineUciToSan } from "@/lib/chess";
|
import { moveLineUciToSan } from "@/lib/chess";
|
||||||
import { MoveClassification } from "@/types/enums";
|
import { MoveClassification } from "@/types/enums";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import PrettyMoveSan from "@/components/prettyMoveSan";
|
||||||
|
|
||||||
export default function MoveInfo() {
|
export default function MoveInfo() {
|
||||||
const position = useAtomValue(currentPositionAtom);
|
const position = useAtomValue(currentPositionAtom);
|
||||||
@@ -56,18 +57,13 @@ export default function MoveInfo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const moveClassification = position.eval?.moveClassification;
|
const moveClassification = position.eval?.moveClassification;
|
||||||
const moveLabel = moveClassification
|
|
||||||
? `${position.lastMove?.san} is ${moveClassificationLabels[moveClassification]}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const bestMoveLabel =
|
const showBestMoveLabel =
|
||||||
moveClassification === MoveClassification.Best ||
|
moveClassification !== MoveClassification.Best &&
|
||||||
moveClassification === MoveClassification.Opening ||
|
moveClassification !== MoveClassification.Opening &&
|
||||||
moveClassification === MoveClassification.Forced ||
|
moveClassification !== MoveClassification.Forced &&
|
||||||
moveClassification === MoveClassification.Splendid ||
|
moveClassification !== MoveClassification.Splendid &&
|
||||||
moveClassification === MoveClassification.Perfect
|
moveClassification !== MoveClassification.Perfect;
|
||||||
? null
|
|
||||||
: `${bestMoveSan} was the best move`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
@@ -77,27 +73,33 @@ export default function MoveInfo() {
|
|||||||
size={12}
|
size={12}
|
||||||
marginTop={0.5}
|
marginTop={0.5}
|
||||||
>
|
>
|
||||||
{moveLabel && (
|
{moveClassification && (
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
{moveClassification && (
|
<Image
|
||||||
<Image
|
src={`/icons/${moveClassification}.png`}
|
||||||
src={`/icons/${moveClassification}.png`}
|
alt="move-icon"
|
||||||
alt="move-icon"
|
width={16}
|
||||||
width={16}
|
height={16}
|
||||||
height={16}
|
style={{
|
||||||
style={{
|
maxWidth: "3.5vw",
|
||||||
maxWidth: "3.5vw",
|
maxHeight: "3.5vw",
|
||||||
maxHeight: "3.5vw",
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
)}
|
<PrettyMoveSan
|
||||||
<Typography align="center" fontSize="0.9rem">
|
typographyProps={{
|
||||||
{moveLabel}
|
fontSize: "0.9rem",
|
||||||
</Typography>
|
}}
|
||||||
|
san={position.lastMove?.san ?? ""}
|
||||||
|
color={position.lastMove?.color ?? "w"}
|
||||||
|
additionalText={
|
||||||
|
" is " + moveClassificationLabels[moveClassification]
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bestMoveLabel && (
|
{showBestMoveLabel && (
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<Image
|
<Image
|
||||||
src={"/icons/best.png"}
|
src={"/icons/best.png"}
|
||||||
@@ -109,9 +111,14 @@ export default function MoveInfo() {
|
|||||||
maxHeight: "3.5vw",
|
maxHeight: "3.5vw",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography align="center" fontSize="0.9rem">
|
<PrettyMoveSan
|
||||||
{bestMoveLabel}
|
typographyProps={{
|
||||||
</Typography>
|
fontSize: "0.9rem",
|
||||||
|
}}
|
||||||
|
san={bestMoveSan}
|
||||||
|
color={position.lastMove?.color ?? "w"}
|
||||||
|
additionalText=" was the best move"
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export default function MovesPanel() {
|
|||||||
container
|
container
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
alignItems="start"
|
alignItems="start"
|
||||||
gap={0.6}
|
gap={0.7}
|
||||||
|
paddingY={1}
|
||||||
sx={{ scrollbarWidth: "thin", overflowY: "auto" }}
|
sx={{ scrollbarWidth: "thin", overflowY: "auto" }}
|
||||||
height="100%"
|
height="100%"
|
||||||
size={6}
|
size={6}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MoveClassification } from "@/types/enums";
|
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 Image from "next/image";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { boardAtom, currentPositionAtom, gameAtom } from "../../../states";
|
import { boardAtom, currentPositionAtom, gameAtom } from "../../../states";
|
||||||
@@ -7,14 +7,21 @@ import { useChessActions } from "@/hooks/useChessActions";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { isInViewport } from "@/lib/helpers";
|
import { isInViewport } from "@/lib/helpers";
|
||||||
import { CLASSIFICATION_COLORS } from "@/constants";
|
import { CLASSIFICATION_COLORS } from "@/constants";
|
||||||
|
import PrettyMoveSan from "@/components/prettyMoveSan";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
san: string;
|
san: string;
|
||||||
moveClassification?: MoveClassification;
|
moveClassification?: MoveClassification;
|
||||||
moveIdx: number;
|
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 game = useAtomValue(gameAtom);
|
||||||
const { goToMove } = useChessActions(boardAtom);
|
const { goToMove } = useChessActions(boardAtom);
|
||||||
const position = useAtomValue(currentPositionAtom);
|
const position = useAtomValue(currentPositionAtom);
|
||||||
@@ -47,7 +54,7 @@ export default function MoveItem({ san, moveClassification, moveIdx }: Props) {
|
|||||||
width="5rem"
|
width="5rem"
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
paddingY={0.6}
|
paddingY={0.5}
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
cursor: isCurrentMove ? undefined : "pointer",
|
cursor: isCurrentMove ? undefined : "pointer",
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@@ -74,9 +81,8 @@ export default function MoveItem({ san, moveClassification, moveIdx }: Props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Typography color={color} fontSize="0.9rem" lineHeight="0.9rem">
|
|
||||||
{san}
|
<PrettyMoveSan san={san} color={moveColor} />
|
||||||
</Typography>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MoveClassification } from "@/types/enums";
|
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";
|
import MoveItem from "./moveItem";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -20,9 +20,13 @@ export default function MovesLine({ moves, moveNb }: Props) {
|
|||||||
{moveNb}.
|
{moveNb}.
|
||||||
</Typography>
|
</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>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user