feat : add move classification
This commit is contained in:
@@ -52,12 +52,12 @@ export function getSquareRenderer({
|
||||
<Image
|
||||
src={`/icons/${moveClassification}.png`}
|
||||
alt="move-icon"
|
||||
width={40}
|
||||
height={40}
|
||||
width={35}
|
||||
height={35}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "max(-15px, -1.8vw)",
|
||||
right: "max(-15px, -1.8vw)",
|
||||
top: "max(-12px, -1.8vw)",
|
||||
right: "max(-12px, -1.8vw)",
|
||||
maxWidth: "3.6vw",
|
||||
maxHeight: "3.6vw",
|
||||
zIndex: 100,
|
||||
|
||||
@@ -60,5 +60,26 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
||||
setGame(newGame);
|
||||
}, [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 Board from "@/sections/analysis/board";
|
||||
import MovesClassificationsRecap from "@/sections/analysis/movesClassificationsRecap";
|
||||
import ReviewPanelBody from "@/sections/analysis/reviewPanelBody";
|
||||
import ReviewPanelHeader from "@/sections/analysis/reviewPanelHeader";
|
||||
import ReviewPanelToolBar from "@/sections/analysis/reviewPanelToolbar";
|
||||
@@ -73,6 +74,8 @@ export default function GameReport() {
|
||||
|
||||
{isLgOrGreater ? <ReviewPanelToolBar /> : <ReviewPanelHeader />}
|
||||
</Grid>
|
||||
|
||||
<MovesClassificationsRecap />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,13 @@ import { useMemo } from "react";
|
||||
import { useScreenSize } from "@/hooks/useScreenSize";
|
||||
import { Color } from "@/types/enums";
|
||||
import Board from "@/components/board";
|
||||
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||
import { usePlayersNames } from "@/hooks/usePlayerNames";
|
||||
|
||||
export default function BoardContainer() {
|
||||
const screenSize = useScreenSize();
|
||||
const boardOrientation = useAtomValue(boardOrientationAtom);
|
||||
const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom);
|
||||
const { gameFromUrl } = useGameDatabase();
|
||||
const game = useAtomValue(gameAtom);
|
||||
const { whiteName, blackName } = usePlayersNames(gameAtom);
|
||||
|
||||
const boardSize = useMemo(() => {
|
||||
const width = screenSize.width;
|
||||
@@ -38,12 +37,8 @@ export default function BoardContainer() {
|
||||
boardSize={boardSize}
|
||||
canPlay={true}
|
||||
gameAtom={boardAtom}
|
||||
whitePlayer={
|
||||
gameFromUrl?.white?.name || game.header()["White"] || "White"
|
||||
}
|
||||
blackPlayer={
|
||||
gameFromUrl?.black?.name || game.header()["Black"] || "Black"
|
||||
}
|
||||
whitePlayer={whiteName}
|
||||
blackPlayer={blackName}
|
||||
boardOrientation={boardOrientation ? Color.White : Color.Black}
|
||||
currentPositionAtom={currentPositionAtom}
|
||||
showBestMoveArrow={showBestMoveArrow}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const useCurrentPosition = (engineName?: EngineName) => {
|
||||
boardHistory.length <= gameHistory.length &&
|
||||
gameHistory.slice(0, boardHistory.length).join() === boardHistory.join()
|
||||
) {
|
||||
const evalIndex = board.history().length;
|
||||
const evalIndex = boardHistory.length;
|
||||
|
||||
position.eval = gameEval.positions[evalIndex];
|
||||
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"
|
||||
columnGap={1}
|
||||
>
|
||||
<Icon icon="ph:file-magnifying-glass-fill" height={28} />
|
||||
<Icon icon="streamline:clipboard-check" height={24} />
|
||||
|
||||
<Typography variant="h5" align="center">
|
||||
Game Report
|
||||
|
||||
Reference in New Issue
Block a user