feat : add move classification

This commit is contained in:
GuillaumeSD
2024-03-29 03:53:16 +01:00
parent 714ed1039e
commit d9b322d9fa
9 changed files with 257 additions and 16 deletions

View File

@@ -52,12 +52,12 @@ export function getSquareRenderer({
<Image <Image
src={`/icons/${moveClassification}.png`} src={`/icons/${moveClassification}.png`}
alt="move-icon" alt="move-icon"
width={40} width={35}
height={40} height={35}
style={{ style={{
position: "absolute", position: "absolute",
top: "max(-15px, -1.8vw)", top: "max(-12px, -1.8vw)",
right: "max(-15px, -1.8vw)", right: "max(-12px, -1.8vw)",
maxWidth: "3.6vw", maxWidth: "3.6vw",
maxHeight: "3.6vw", maxHeight: "3.6vw",
zIndex: 100, zIndex: 100,

View File

@@ -60,5 +60,26 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
setGame(newGame); setGame(newGame);
}, [copyGame, setGame]); }, [copyGame, setGame]);
return { setPgn, reset, makeMove, undoMove }; const goToMove = useCallback(
(moveIdx: number, game: Chess) => {
if (moveIdx < 0) return;
const newGame = new Chess();
newGame.loadPgn(game.pgn());
const movesNb = game.history().length;
if (moveIdx > movesNb) return;
let lastMove: Move | null = null;
for (let i = movesNb; i > moveIdx; i--) {
lastMove = newGame.undo();
}
setGame(newGame);
playSoundFromMove(lastMove);
},
[setGame]
);
return { setPgn, reset, makeMove, undoMove, goToMove };
}; };

View File

@@ -0,0 +1,18 @@
import { Chess } from "chess.js";
import { PrimitiveAtom, useAtomValue } from "jotai";
import { useGameDatabase } from "./useGameDatabase";
export const usePlayersNames = (gameAtom: PrimitiveAtom<Chess>) => {
const game = useAtomValue(gameAtom);
const { gameFromUrl } = useGameDatabase();
const whiteName =
gameFromUrl?.white?.name || game.header()["White"] || "White";
const blackName =
gameFromUrl?.black?.name || game.header()["Black"] || "Black";
return {
whiteName,
blackName,
};
};

View File

@@ -1,5 +1,6 @@
import { useChessActions } from "@/hooks/useChessActions"; import { useChessActions } from "@/hooks/useChessActions";
import Board from "@/sections/analysis/board"; import Board from "@/sections/analysis/board";
import MovesClassificationsRecap from "@/sections/analysis/movesClassificationsRecap";
import ReviewPanelBody from "@/sections/analysis/reviewPanelBody"; import ReviewPanelBody from "@/sections/analysis/reviewPanelBody";
import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader"; import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader";
import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar"; import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar";
@@ -73,6 +74,8 @@ export default function GameReport() {
{isLgOrGreater ? <ReviewPanelToolBar /> : <ReviewPanelHeader />} {isLgOrGreater ? <ReviewPanelToolBar /> : <ReviewPanelHeader />}
</Grid> </Grid>
<MovesClassificationsRecap />
</Grid> </Grid>
); );
} }

View File

@@ -11,14 +11,13 @@ import { useMemo } from "react";
import { useScreenSize } from "@/hooks/useScreenSize"; import { useScreenSize } from "@/hooks/useScreenSize";
import { Color } from "@/types/enums"; import { Color } from "@/types/enums";
import Board from "@/components/board"; import Board from "@/components/board";
import { useGameDatabase } from "@/hooks/useGameDatabase"; import { usePlayersNames } from "@/hooks/usePlayerNames";
export default function BoardContainer() { export default function BoardContainer() {
const screenSize = useScreenSize(); const screenSize = useScreenSize();
const boardOrientation = useAtomValue(boardOrientationAtom); const boardOrientation = useAtomValue(boardOrientationAtom);
const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom); const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom);
const { gameFromUrl } = useGameDatabase(); const { whiteName, blackName } = usePlayersNames(gameAtom);
const game = useAtomValue(gameAtom);
const boardSize = useMemo(() => { const boardSize = useMemo(() => {
const width = screenSize.width; const width = screenSize.width;
@@ -38,12 +37,8 @@ export default function BoardContainer() {
boardSize={boardSize} boardSize={boardSize}
canPlay={true} canPlay={true}
gameAtom={boardAtom} gameAtom={boardAtom}
whitePlayer={ whitePlayer={whiteName}
gameFromUrl?.white?.name || game.header()["White"] || "White" blackPlayer={blackName}
}
blackPlayer={
gameFromUrl?.black?.name || game.header()["Black"] || "Black"
}
boardOrientation={boardOrientation ? Color.White : Color.Black} boardOrientation={boardOrientation ? Color.White : Color.Black}
currentPositionAtom={currentPositionAtom} currentPositionAtom={currentPositionAtom}
showBestMoveArrow={showBestMoveArrow} showBestMoveArrow={showBestMoveArrow}

View File

@@ -34,7 +34,7 @@ export const useCurrentPosition = (engineName?: EngineName) => {
boardHistory.length <= gameHistory.length && boardHistory.length <= gameHistory.length &&
gameHistory.slice(0, boardHistory.length).join() === boardHistory.join() gameHistory.slice(0, boardHistory.length).join() === boardHistory.join()
) { ) {
const evalIndex = board.history().length; const evalIndex = boardHistory.length;
position.eval = gameEval.positions[evalIndex]; position.eval = gameEval.positions[evalIndex];
position.lastEval = position.lastEval =

View File

@@ -0,0 +1,130 @@
import { Color, MoveClassification } from "@/types/enums";
import { Grid, Typography } from "@mui/material";
import { useAtomValue } from "jotai";
import { boardAtom, gameAtom, gameEvalAtom } from "../states";
import { useMemo } from "react";
import { moveClassificationColors } from "@/components/board/squareRenderer";
import Image from "next/image";
import { capitalize } from "@/lib/helpers";
import { useChessActions } from "@/hooks/useChessActions";
interface Props {
classification: MoveClassification;
}
export default function ClassificationRow({ classification }: Props) {
const gameEval = useAtomValue(gameEvalAtom);
const board = useAtomValue(boardAtom);
const game = useAtomValue(gameAtom);
const { goToMove } = useChessActions(boardAtom);
const whiteNb = useMemo(() => {
if (!gameEval) return 0;
return gameEval.positions.filter(
(position, idx) =>
idx % 2 !== 0 && position.moveClassification === classification
).length;
}, [gameEval, classification]);
const blackNb = useMemo(() => {
if (!gameEval) return 0;
return gameEval.positions.filter(
(position, idx) =>
idx % 2 === 0 && position.moveClassification === classification
).length;
}, [gameEval, classification]);
const handleClick = (color: Color) => {
if (
!gameEval ||
(color === Color.White && !whiteNb) ||
(color === Color.Black && !blackNb)
) {
return;
}
const filterColor = (idx: number) =>
(idx % 2 !== 0 && color === Color.White) ||
(idx % 2 === 0 && color === Color.Black);
const moveIdx = board.history().length;
const nextPositionIdx = gameEval.positions.findIndex(
(position, idx) =>
filterColor(idx) &&
position.moveClassification === classification &&
idx > moveIdx
);
if (nextPositionIdx > 0) {
goToMove(nextPositionIdx, game);
} else {
const firstPositionIdx = gameEval.positions.findIndex(
(position, idx) =>
filterColor(idx) && position.moveClassification === classification
);
if (firstPositionIdx > 0 && firstPositionIdx !== moveIdx) {
goToMove(firstPositionIdx, game);
}
}
};
if (!gameEval?.positions.length) return null;
return (
<Grid
container
item
justifyContent="space-evenly"
alignItems="center"
xs={12}
wrap="nowrap"
color={moveClassificationColors[classification]}
>
<Grid
container
item
justifyContent="center"
alignItems="center"
width={"3rem"}
style={{ cursor: whiteNb ? "pointer" : "default" }}
onClick={() => handleClick(Color.White)}
>
{whiteNb}
</Grid>
<Grid
container
item
justifyContent="start"
alignItems="center"
width={"7rem"}
gap={1}
>
<Image
src={`/icons/${classification}.png`}
alt="move-icon"
width={20}
height={20}
style={{
maxWidth: "3.6vw",
maxHeight: "3.6vw",
}}
/>
<Typography align="center">{capitalize(classification)}</Typography>
</Grid>
<Grid
container
item
justifyContent="center"
alignItems="center"
width={"3rem"}
style={{ cursor: blackNb ? "pointer" : "default" }}
onClick={() => handleClick(Color.Black)}
>
{blackNb}
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,74 @@
import { usePlayersNames } from "@/hooks/usePlayerNames";
import { Grid, Typography } from "@mui/material";
import { gameAtom, gameEvalAtom } from "../states";
import { MoveClassification } from "@/types/enums";
import ClassificationRow from "./classificationRow";
import { useAtomValue } from "jotai";
export default function MovesClassificationsRecap() {
const { whiteName, blackName } = usePlayersNames(gameAtom);
const gameEval = useAtomValue(gameEvalAtom);
if (!gameEval?.positions.length) return null;
return (
<Grid
container
item
justifyContent="center"
alignItems="center"
borderRadius={2}
border={1}
borderColor={"secondary.main"}
sx={{
backgroundColor: "secondary.main",
borderColor: "primary.main",
borderWidth: 2,
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",
}}
marginTop={{ xs: 0, lg: "2.5em" }}
paddingY={3}
rowGap={2}
xs
style={{ maxWidth: "50rem" }}
>
<Grid
item
container
alignItems="center"
justifyContent="space-evenly"
wrap="nowrap"
xs={12}
>
<Typography width="12rem" align="center">
{whiteName}
</Typography>
<Typography width="7rem" />
<Typography width="12rem" align="center">
{blackName}
</Typography>
</Grid>
{sortedMoveClassfications.map((classification) => (
<ClassificationRow
key={classification}
classification={classification}
/>
))}
</Grid>
);
}
export const sortedMoveClassfications = [
MoveClassification.Brilliant,
MoveClassification.Great,
MoveClassification.Best,
MoveClassification.Excellent,
MoveClassification.Good,
MoveClassification.Book,
MoveClassification.Inaccuracy,
MoveClassification.Mistake,
MoveClassification.Blunder,
];

View File

@@ -27,7 +27,7 @@ export default function ReviewPanelHeader() {
alignItems="center" alignItems="center"
columnGap={1} columnGap={1}
> >
<Icon icon="ph:file-magnifying-glass-fill" height={28} /> <Icon icon="streamline:clipboard-check" height={24} />
<Typography variant="h5" align="center"> <Typography variant="h5" align="center">
Game Report Game Report