feat : add move classification
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/hooks/usePlayerNames.ts
Normal file
18
src/hooks/usePlayerNames.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
src/sections/analysis/movesClassificationsRecap/index.tsx
Normal file
74
src/sections/analysis/movesClassificationsRecap/index.tsx
Normal 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,
|
||||||
|
];
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user