feat : add board interactions
This commit is contained in:
@@ -5,29 +5,47 @@ import { useEffect, useState } from "react";
|
||||
import { gameAtom, gameEvalAtom } from "./states";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { getFens } from "@/lib/chess";
|
||||
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function AnalyzeButton() {
|
||||
const [engine, setEngine] = useState<Stockfish | null>(null);
|
||||
const [evaluationInProgress, setEvaluationInProgress] = useState(false);
|
||||
const { setGameEval } = useGameDatabase();
|
||||
const setEval = useSetAtom(gameEvalAtom);
|
||||
const game = useAtomValue(gameAtom);
|
||||
const router = useRouter();
|
||||
const { gameId } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
const engine = new Stockfish();
|
||||
engine.init();
|
||||
setEngine(engine);
|
||||
engine.init().then(() => {
|
||||
setEngine(engine);
|
||||
});
|
||||
|
||||
return () => {
|
||||
engine.shutdown();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const readyToAnalyse = engine?.isReady() && game.history().length > 0;
|
||||
const readyToAnalyse =
|
||||
engine?.isReady() && game.history().length > 0 && !evaluationInProgress;
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
const gameFens = getFens(game);
|
||||
if (engine?.isReady() && gameFens.length) {
|
||||
const newGameEval = await engine.evaluateGame(gameFens);
|
||||
setEval(newGameEval);
|
||||
if (!engine?.isReady() || gameFens.length === 0 || evaluationInProgress)
|
||||
return;
|
||||
|
||||
setEvaluationInProgress(true);
|
||||
|
||||
const newGameEval = await engine.evaluateGame(gameFens);
|
||||
setEval(newGameEval);
|
||||
|
||||
setEvaluationInProgress(false);
|
||||
|
||||
if (typeof gameId === "string") {
|
||||
setGameEval(parseInt(gameId), newGameEval);
|
||||
console.log("Game Eval saved to database");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,56 @@
|
||||
import { Grid, Typography } from "@mui/material";
|
||||
import { Chessboard } from "react-chessboard";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom } from "./states";
|
||||
import { boardAtom, boardOrientationAtom, gameAtom } from "./states";
|
||||
import { Arrow, Square } from "react-chessboard/dist/chessboard/types";
|
||||
import { useChessActions } from "@/hooks/useChess";
|
||||
import { useCurrentMove } from "@/hooks/useCurrentMove";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function Board() {
|
||||
const board = useAtomValue(boardAtom);
|
||||
const game = useAtomValue(gameAtom);
|
||||
const boardOrientation = useAtomValue(boardOrientationAtom);
|
||||
const boardActions = useChessActions(boardAtom);
|
||||
const currentMove = useCurrentMove();
|
||||
|
||||
const onPieceDrop = (source: Square, target: Square): boolean => {
|
||||
try {
|
||||
const result = boardActions.move({
|
||||
from: source,
|
||||
to: target,
|
||||
promotion: "q", // TODO: Let the user choose the promotion
|
||||
});
|
||||
|
||||
return !!result;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const customArrows: Arrow[] = useMemo(() => {
|
||||
if (!currentMove?.lastEval) return [];
|
||||
|
||||
const bestMoveArrow = [
|
||||
currentMove.lastEval.bestMove.slice(0, 2),
|
||||
currentMove.lastEval.bestMove.slice(2, 4),
|
||||
"#3aab18",
|
||||
] as Arrow;
|
||||
|
||||
if (
|
||||
!currentMove.from ||
|
||||
!currentMove.to ||
|
||||
(currentMove.from === bestMoveArrow[0] &&
|
||||
currentMove.to === bestMoveArrow[1])
|
||||
) {
|
||||
return [bestMoveArrow];
|
||||
}
|
||||
|
||||
return [[currentMove.from, currentMove.to, "#ffaa00"], bestMoveArrow];
|
||||
}, [currentMove]);
|
||||
|
||||
const whiteLabel = game.header()["White"] || "White Player (?)";
|
||||
const blackLabel = game.header()["Black"] || "Black Player (?)";
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -16,15 +62,33 @@ export default function Board() {
|
||||
xs={12}
|
||||
md={6}
|
||||
>
|
||||
<Typography variant="h4" align="center">
|
||||
White Player (?)
|
||||
</Typography>
|
||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||
<Typography variant="h4" align="center">
|
||||
{boardOrientation ? blackLabel : whiteLabel}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Chessboard id="BasicBoard" position={board.fen()} />
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
maxWidth={"80vh"}
|
||||
>
|
||||
<Chessboard
|
||||
id="BasicBoard"
|
||||
position={board.fen()}
|
||||
onPieceDrop={onPieceDrop}
|
||||
boardOrientation={boardOrientation ? "white" : "black"}
|
||||
customArrows={customArrows}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Typography variant="h4" align="center">
|
||||
Black Player (?)
|
||||
</Typography>
|
||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||
<Typography variant="h4" align="center">
|
||||
{boardOrientation ? whiteLabel : blackLabel}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,61 @@
|
||||
import { Grid } from "@mui/material";
|
||||
import LoadGameButton from "../loadGame/loadGameButton";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useChessActions } from "@/hooks/useChess";
|
||||
import { boardAtom, gameAtom } from "./states";
|
||||
import {
|
||||
boardAtom,
|
||||
boardOrientationAtom,
|
||||
gameAtom,
|
||||
gameEvalAtom,
|
||||
} from "./states";
|
||||
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { Chess } from "chess.js";
|
||||
|
||||
export default function LoadGame() {
|
||||
const router = useRouter();
|
||||
const { gameId } = router.query;
|
||||
const game = useAtomValue(gameAtom);
|
||||
const gameActions = useChessActions(gameAtom);
|
||||
const boardActions = useChessActions(boardAtom);
|
||||
const { getGame } = useGameDatabase();
|
||||
const setEval = useSetAtom(gameEvalAtom);
|
||||
const setBoardOrientation = useSetAtom(boardOrientationAtom);
|
||||
|
||||
const resetAndSetGamePgn = useCallback(
|
||||
(pgn: string) => {
|
||||
boardActions.reset();
|
||||
setEval(undefined);
|
||||
setBoardOrientation(true);
|
||||
gameActions.setPgn(pgn);
|
||||
},
|
||||
[boardActions, gameActions, setEval, setBoardOrientation]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadGame = async () => {
|
||||
if (typeof gameId !== "string") return;
|
||||
|
||||
const game = await getGame(parseInt(gameId));
|
||||
if (!game) return;
|
||||
const gamefromDb = await getGame(parseInt(gameId));
|
||||
if (!gamefromDb) return;
|
||||
|
||||
boardActions.reset();
|
||||
gameActions.setPgn(game.pgn);
|
||||
const gamefromDbChess = new Chess();
|
||||
gamefromDbChess.loadPgn(gamefromDb.pgn);
|
||||
if (game.history().join() === gamefromDbChess.history().join()) return;
|
||||
|
||||
resetAndSetGamePgn(gamefromDb.pgn);
|
||||
setEval(gamefromDb.eval);
|
||||
};
|
||||
|
||||
loadGame();
|
||||
}, [gameId]);
|
||||
}, [gameId, getGame, game, resetAndSetGamePgn, setEval]);
|
||||
|
||||
if (!router.isReady || gameId) return null;
|
||||
|
||||
return (
|
||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||
<LoadGameButton
|
||||
setGame={(game) => {
|
||||
boardActions.reset();
|
||||
gameActions.setPgn(game.pgn());
|
||||
}}
|
||||
/>
|
||||
<LoadGameButton setGame={(game) => resetAndSetGamePgn(game.pgn())} />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Divider, Grid, List, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, gameEvalAtom } from "./states";
|
||||
import { boardAtom, gameAtom } from "./states";
|
||||
import LineEvaluation from "./lineEvaluation";
|
||||
import { useCurrentMove } from "@/hooks/useCurrentMove";
|
||||
|
||||
export default function ReviewPanelBody() {
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
if (!gameEval) return null;
|
||||
|
||||
const game = useAtomValue(gameAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
const evalIndex = board.history().length;
|
||||
const moveEval = gameEval.moves[evalIndex];
|
||||
|
||||
const move = useCurrentMove();
|
||||
|
||||
const getBestMoveLabel = () => {
|
||||
const bestMove = move?.lastEval?.bestMove;
|
||||
if (bestMove) {
|
||||
return `${bestMove} was the best move`;
|
||||
}
|
||||
|
||||
const boardHistory = board.history();
|
||||
const gameHistory = game.history();
|
||||
|
||||
if (game.isGameOver() && boardHistory.join() === gameHistory.join()) {
|
||||
return "Game is over";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -35,12 +50,12 @@ export default function ReviewPanelBody() {
|
||||
</Grid>
|
||||
|
||||
<Typography variant="h6" align="center">
|
||||
{moveEval ? `${moveEval.bestMove} is the best move` : "Game is over"}
|
||||
{getBestMoveLabel()}
|
||||
</Typography>
|
||||
|
||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||
<List>
|
||||
{moveEval?.lines.map((line) => (
|
||||
{move?.eval?.lines.map((line) => (
|
||||
<LineEvaluation key={line.pv[0]} line={line} />
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Divider, Grid, IconButton } from "@mui/material";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, gameAtom } from "./states";
|
||||
import { useChessActions } from "@/hooks/useChess";
|
||||
|
||||
export default function ReviewPanelToolBar() {
|
||||
const game = useAtomValue(gameAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
const boardActions = useChessActions(boardAtom);
|
||||
|
||||
const addNextMoveToGame = () => {
|
||||
const nextMoveIndex = board.history().length;
|
||||
const nextMove = game.history({ verbose: true })[nextMoveIndex];
|
||||
|
||||
if (nextMove) {
|
||||
boardActions.move({
|
||||
from: nextMove.from,
|
||||
to: nextMove.to,
|
||||
promotion: nextMove.promotion,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ width: "90%", marginY: 3 }} />
|
||||
|
||||
<Grid container item justifyContent="center" alignItems="center" xs={12}>
|
||||
<IconButton>
|
||||
<Icon icon="eva:flip-fill" />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => boardActions.reset()}>
|
||||
<Icon icon="ri:skip-back-line" />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => boardActions.undo()}>
|
||||
<Icon icon="ri:arrow-left-s-line" height={30} />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => addNextMoveToGame()}>
|
||||
<Icon icon="ri:arrow-right-s-line" height={30} />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<Icon icon="ri:skip-forward-line" />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<Icon icon="ri:save-3-line" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
src/sections/analysis/reviewPanelToolbar/flipBoardButton.tsx
Normal file
14
src/sections/analysis/reviewPanelToolbar/flipBoardButton.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useSetAtom } from "jotai";
|
||||
import { boardOrientationAtom } from "../states";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
export default function FlipBoardButton() {
|
||||
const setBoardOrientation = useSetAtom(boardOrientationAtom);
|
||||
|
||||
return (
|
||||
<IconButton onClick={() => setBoardOrientation((prev) => !prev)}>
|
||||
<Icon icon="eva:flip-fill" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Icon } from "@iconify/react";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, gameAtom } from "../states";
|
||||
import { useChessActions } from "@/hooks/useChess";
|
||||
|
||||
export default function GoToLastPositionButton() {
|
||||
const boardActions = useChessActions(boardAtom);
|
||||
const game = useAtomValue(gameAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
|
||||
const gameHistory = game.history();
|
||||
const boardHistory = board.history();
|
||||
|
||||
const isButtonDisabled = boardHistory >= gameHistory;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (isButtonDisabled) return;
|
||||
boardActions.setPgn(game.pgn());
|
||||
}}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
<Icon icon="ri:skip-forward-line" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
46
src/sections/analysis/reviewPanelToolbar/index.tsx
Normal file
46
src/sections/analysis/reviewPanelToolbar/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Divider, Grid, IconButton } from "@mui/material";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom } from "../states";
|
||||
import { useChessActions } from "@/hooks/useChess";
|
||||
import FlipBoardButton from "./flipBoardButton";
|
||||
import NextMoveButton from "./nextMoveButton";
|
||||
import GoToLastPositionButton from "./goToLastPositionButton";
|
||||
import SaveButton from "./saveButton";
|
||||
|
||||
export default function ReviewPanelToolBar() {
|
||||
const board = useAtomValue(boardAtom);
|
||||
const boardActions = useChessActions(boardAtom);
|
||||
|
||||
const boardHistory = board.history();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ width: "90%", marginY: 3 }} />
|
||||
|
||||
<Grid container item justifyContent="center" alignItems="center" xs={12}>
|
||||
<FlipBoardButton />
|
||||
|
||||
<IconButton
|
||||
onClick={() => boardActions.reset()}
|
||||
disabled={boardHistory.length === 0}
|
||||
>
|
||||
<Icon icon="ri:skip-back-line" />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={() => boardActions.undo()}
|
||||
disabled={boardHistory.length === 0}
|
||||
>
|
||||
<Icon icon="ri:arrow-left-s-line" height={30} />
|
||||
</IconButton>
|
||||
|
||||
<NextMoveButton />
|
||||
|
||||
<GoToLastPositionButton />
|
||||
|
||||
<SaveButton />
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
42
src/sections/analysis/reviewPanelToolbar/nextMoveButton.tsx
Normal file
42
src/sections/analysis/reviewPanelToolbar/nextMoveButton.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Icon } from "@iconify/react";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { boardAtom, gameAtom } from "../states";
|
||||
import { useChessActions } from "@/hooks/useChess";
|
||||
|
||||
export default function NextMoveButton() {
|
||||
const boardActions = useChessActions(boardAtom);
|
||||
const game = useAtomValue(gameAtom);
|
||||
const board = useAtomValue(boardAtom);
|
||||
|
||||
const gameHistory = game.history();
|
||||
const boardHistory = board.history();
|
||||
|
||||
const isButtonEnabled =
|
||||
boardHistory.length < gameHistory.length &&
|
||||
gameHistory.slice(0, boardHistory.length).join() === boardHistory.join();
|
||||
|
||||
const addNextGameMoveToBoard = () => {
|
||||
if (!isButtonEnabled) return;
|
||||
|
||||
const nextMoveIndex = boardHistory.length;
|
||||
const nextMove = game.history({ verbose: true })[nextMoveIndex];
|
||||
|
||||
if (nextMove) {
|
||||
boardActions.move({
|
||||
from: nextMove.from,
|
||||
to: nextMove.to,
|
||||
promotion: nextMove.promotion,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => addNextGameMoveToBoard()}
|
||||
disabled={!isButtonEnabled}
|
||||
>
|
||||
<Icon icon="ri:arrow-right-s-line" height={30} />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
36
src/sections/analysis/reviewPanelToolbar/saveButton.tsx
Normal file
36
src/sections/analysis/reviewPanelToolbar/saveButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useRouter } from "next/router";
|
||||
import { gameAtom } from "../states";
|
||||
|
||||
export default function SaveButton() {
|
||||
const game = useAtomValue(gameAtom);
|
||||
const { addGame } = useGameDatabase();
|
||||
|
||||
const router = useRouter();
|
||||
const { gameId } = router.query;
|
||||
|
||||
const isButtonEnabled = router.isReady && typeof gameId === undefined;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
if (!isButtonEnabled) return;
|
||||
const gameId = await addGame(game);
|
||||
router.replace(
|
||||
{
|
||||
query: { gameId: gameId },
|
||||
pathname: router.pathname,
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true, scroll: false }
|
||||
);
|
||||
}}
|
||||
disabled={!isButtonEnabled}
|
||||
>
|
||||
<Icon icon="ri:save-3-line" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -5,3 +5,4 @@ import { atom } from "jotai";
|
||||
export const gameEvalAtom = atom<GameEval | undefined>(undefined);
|
||||
export const gameAtom = atom(new Chess());
|
||||
export const boardAtom = atom(new Chess());
|
||||
export const boardOrientationAtom = atom(true);
|
||||
|
||||
@@ -26,10 +26,8 @@ export default function NavBar({ darkMode, switchDarkMode }: Props) {
|
||||
<Box sx={{ flexGrow: 1, display: "flex" }}>
|
||||
<AppBar
|
||||
position="static"
|
||||
sx={{
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
backgroundColor: "primary.main",
|
||||
}}
|
||||
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
enableColorOnDark
|
||||
>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
|
||||
Reference in New Issue
Block a user