feat : add board interactions
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Chess } from "chess.js";
|
import { Chess, Move } from "chess.js";
|
||||||
import { PrimitiveAtom, useAtom } from "jotai";
|
import { PrimitiveAtom, useAtom } from "jotai";
|
||||||
|
|
||||||
export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
||||||
@@ -20,10 +20,16 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
|||||||
return newGame;
|
return newGame;
|
||||||
};
|
};
|
||||||
|
|
||||||
const move = (move: { from: string; to: string; promotion?: string }) => {
|
const move = (move: {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
promotion?: string;
|
||||||
|
}): Move | null => {
|
||||||
const newGame = copyGame();
|
const newGame = copyGame();
|
||||||
newGame.move(move);
|
const result = newGame.move(move);
|
||||||
setGame(newGame);
|
setGame(newGame);
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const undo = () => {
|
const undo = () => {
|
||||||
|
|||||||
31
src/hooks/useCurrentMove.ts
Normal file
31
src/hooks/useCurrentMove.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { boardAtom, gameAtom, gameEvalAtom } from "@/sections/analysis/states";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export const useCurrentMove = () => {
|
||||||
|
const gameEval = useAtomValue(gameEvalAtom);
|
||||||
|
const game = useAtomValue(gameAtom);
|
||||||
|
const board = useAtomValue(boardAtom);
|
||||||
|
|
||||||
|
const currentEvalMove = useMemo(() => {
|
||||||
|
if (!gameEval) return undefined;
|
||||||
|
|
||||||
|
const boardHistory = board.history();
|
||||||
|
const gameHistory = game.history();
|
||||||
|
|
||||||
|
if (
|
||||||
|
boardHistory.length >= gameHistory.length ||
|
||||||
|
gameHistory.slice(0, boardHistory.length).join() !== boardHistory.join()
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const evalIndex = board.history().length;
|
||||||
|
return {
|
||||||
|
...board.history({ verbose: true }).at(-1),
|
||||||
|
eval: gameEval.moves[evalIndex],
|
||||||
|
lastEval: evalIndex > 0 ? gameEval.moves[evalIndex - 1] : undefined,
|
||||||
|
};
|
||||||
|
}, [gameEval, board, game]);
|
||||||
|
|
||||||
|
return currentEvalMove;
|
||||||
|
};
|
||||||
@@ -55,9 +55,11 @@ export const useGameDatabase = (shouldFetchGames?: boolean) => {
|
|||||||
if (!db) throw new Error("Database not initialized");
|
if (!db) throw new Error("Database not initialized");
|
||||||
|
|
||||||
const gameToAdd = formatGameToDatabase(game);
|
const gameToAdd = formatGameToDatabase(game);
|
||||||
await db.add("games", gameToAdd as Game);
|
const gameId = await db.add("games", gameToAdd as Game);
|
||||||
|
|
||||||
loadGames();
|
loadGames();
|
||||||
|
|
||||||
|
return gameId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setGameEval = async (gameId: number, evaluation: GameEval) => {
|
const setGameEval = async (gameId: number, evaluation: GameEval) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Engine } from "@/types/enums";
|
||||||
import { GameEval, LineEval, MoveEval } from "@/types/eval";
|
import { GameEval, LineEval, MoveEval } from "@/types/eval";
|
||||||
|
|
||||||
export class Stockfish {
|
export class Stockfish {
|
||||||
@@ -25,14 +26,32 @@ export class Stockfish {
|
|||||||
|
|
||||||
public async init(): Promise<void> {
|
public async init(): Promise<void> {
|
||||||
await this.sendCommands(["uci"], "uciok");
|
await this.sendCommands(["uci"], "uciok");
|
||||||
await this.sendCommands(
|
await this.setMultiPv(3, false);
|
||||||
["setoption name MultiPV value 3", "isready"],
|
|
||||||
"readyok"
|
|
||||||
);
|
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
console.log("Stockfish initialized");
|
console.log("Stockfish initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setMultiPv(multiPv: number, checkIsReady = true) {
|
||||||
|
if (checkIsReady) {
|
||||||
|
this.throwErrorIfNotReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (multiPv < 1 || multiPv > 6) {
|
||||||
|
throw new Error(`Invalid MultiPV value : ${multiPv}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sendCommands(
|
||||||
|
[`setoption name MultiPV value ${multiPv}`, "isready"],
|
||||||
|
"readyok"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private throwErrorIfNotReady() {
|
||||||
|
if (!this.ready) {
|
||||||
|
throw new Error("Stockfish is not ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public shutdown(): void {
|
public shutdown(): void {
|
||||||
this.ready = false;
|
this.ready = false;
|
||||||
this.worker.postMessage("quit");
|
this.worker.postMessage("quit");
|
||||||
@@ -64,30 +83,53 @@ export class Stockfish {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async evaluateGame(fens: string[], depth = 16): Promise<GameEval> {
|
public async evaluateGame(
|
||||||
|
fens: string[],
|
||||||
|
depth = 16,
|
||||||
|
multiPv = 3
|
||||||
|
): Promise<GameEval> {
|
||||||
|
this.throwErrorIfNotReady();
|
||||||
this.ready = false;
|
this.ready = false;
|
||||||
console.log("Evaluating game");
|
|
||||||
|
await this.setMultiPv(multiPv, false);
|
||||||
await this.sendCommands(["ucinewgame", "isready"], "readyok");
|
await this.sendCommands(["ucinewgame", "isready"], "readyok");
|
||||||
this.worker.postMessage("position startpos");
|
this.worker.postMessage("position startpos");
|
||||||
|
|
||||||
const moves: MoveEval[] = [];
|
const moves: MoveEval[] = [];
|
||||||
for (const fen of fens) {
|
for (const fen of fens) {
|
||||||
console.log(`Evaluating position: ${fen}`);
|
console.log(`Evaluating position: ${fen}`);
|
||||||
const result = await this.evaluatePosition(fen, depth);
|
const result = await this.evaluatePosition(fen, depth, false);
|
||||||
moves.push(result);
|
moves.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
console.log("Game evaluated");
|
|
||||||
console.log(moves);
|
console.log(moves);
|
||||||
return { moves, accuracy: { white: 82.34, black: 67.49 } }; // TODO: Calculate accuracy
|
return {
|
||||||
|
moves,
|
||||||
|
accuracy: { white: 82.34, black: 67.49 }, // TODO: Calculate accuracy
|
||||||
|
settings: {
|
||||||
|
name: Engine.Stockfish16,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
depth,
|
||||||
|
multiPv,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async evaluatePosition(fen: string, depth = 16): Promise<MoveEval> {
|
public async evaluatePosition(
|
||||||
|
fen: string,
|
||||||
|
depth = 16,
|
||||||
|
checkIsReady = true
|
||||||
|
): Promise<MoveEval> {
|
||||||
|
if (checkIsReady) {
|
||||||
|
this.throwErrorIfNotReady();
|
||||||
|
}
|
||||||
|
|
||||||
const results = await this.sendCommands(
|
const results = await this.sendCommands(
|
||||||
[`position fen ${fen}`, `go depth ${depth}`],
|
[`position fen ${fen}`, `go depth ${depth}`],
|
||||||
"bestmove"
|
"bestmove"
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.parseResults(results);
|
return this.parseResults(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useCallback, useMemo } from "react";
|
|||||||
import { red } from "@mui/material/colors";
|
import { red } from "@mui/material/colors";
|
||||||
import LoadGameButton from "@/sections/loadGame/loadGameButton";
|
import LoadGameButton from "@/sections/loadGame/loadGameButton";
|
||||||
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const gridLocaleText: GridLocaleText = {
|
const gridLocaleText: GridLocaleText = {
|
||||||
...GRID_DEFAULT_LOCALE_TEXT,
|
...GRID_DEFAULT_LOCALE_TEXT,
|
||||||
@@ -20,6 +21,9 @@ const gridLocaleText: GridLocaleText = {
|
|||||||
|
|
||||||
export default function GameDatabase() {
|
export default function GameDatabase() {
|
||||||
const { games, deleteGame } = useGameDatabase(true);
|
const { games, deleteGame } = useGameDatabase(true);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
console.log(games);
|
||||||
|
|
||||||
const handleDeleteGameRow = useCallback(
|
const handleDeleteGameRow = useCallback(
|
||||||
(id: GridRowId) => async () => {
|
(id: GridRowId) => async () => {
|
||||||
@@ -77,7 +81,29 @@ export default function GameDatabase() {
|
|||||||
align: "center",
|
align: "center",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "openEvaluation",
|
||||||
|
type: "actions",
|
||||||
|
headerName: "Analyze",
|
||||||
|
width: 100,
|
||||||
|
cellClassName: "actions",
|
||||||
|
getActions: ({ id }) => {
|
||||||
|
return [
|
||||||
|
<GridActionsCellItem
|
||||||
|
icon={
|
||||||
|
<Icon icon="streamline:magnifying-glass-solid" width="20px" />
|
||||||
|
}
|
||||||
|
label="Open Evaluation"
|
||||||
|
onClick={() =>
|
||||||
|
router.push({ pathname: "/", query: { gameId: id } })
|
||||||
|
}
|
||||||
|
color="inherit"
|
||||||
|
key={`${id}-open-eval-button`}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "delete",
|
||||||
type: "actions",
|
type: "actions",
|
||||||
headerName: "Delete",
|
headerName: "Delete",
|
||||||
width: 100,
|
width: 100,
|
||||||
@@ -88,7 +114,7 @@ export default function GameDatabase() {
|
|||||||
icon={
|
icon={
|
||||||
<Icon icon="mdi:delete-outline" color={red[400]} width="20px" />
|
<Icon icon="mdi:delete-outline" color={red[400]} width="20px" />
|
||||||
}
|
}
|
||||||
label="Supprimer"
|
label="Delete"
|
||||||
onClick={handleDeleteGameRow(id)}
|
onClick={handleDeleteGameRow(id)}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
key={`${id}-delete-button`}
|
key={`${id}-delete-button`}
|
||||||
@@ -97,7 +123,7 @@ export default function GameDatabase() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[handleDeleteGameRow]
|
[handleDeleteGameRow, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,29 +5,47 @@ import { useEffect, useState } from "react";
|
|||||||
import { gameAtom, gameEvalAtom } from "./states";
|
import { gameAtom, gameEvalAtom } from "./states";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { getFens } from "@/lib/chess";
|
import { getFens } from "@/lib/chess";
|
||||||
|
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export default function AnalyzeButton() {
|
export default function AnalyzeButton() {
|
||||||
const [engine, setEngine] = useState<Stockfish | null>(null);
|
const [engine, setEngine] = useState<Stockfish | null>(null);
|
||||||
|
const [evaluationInProgress, setEvaluationInProgress] = useState(false);
|
||||||
|
const { setGameEval } = useGameDatabase();
|
||||||
const setEval = useSetAtom(gameEvalAtom);
|
const setEval = useSetAtom(gameEvalAtom);
|
||||||
const game = useAtomValue(gameAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
|
const router = useRouter();
|
||||||
|
const { gameId } = router.query;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const engine = new Stockfish();
|
const engine = new Stockfish();
|
||||||
engine.init();
|
engine.init().then(() => {
|
||||||
setEngine(engine);
|
setEngine(engine);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
engine.shutdown();
|
engine.shutdown();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const readyToAnalyse = engine?.isReady() && game.history().length > 0;
|
const readyToAnalyse =
|
||||||
|
engine?.isReady() && game.history().length > 0 && !evaluationInProgress;
|
||||||
|
|
||||||
const handleAnalyze = async () => {
|
const handleAnalyze = async () => {
|
||||||
const gameFens = getFens(game);
|
const gameFens = getFens(game);
|
||||||
if (engine?.isReady() && gameFens.length) {
|
if (!engine?.isReady() || gameFens.length === 0 || evaluationInProgress)
|
||||||
const newGameEval = await engine.evaluateGame(gameFens);
|
return;
|
||||||
setEval(newGameEval);
|
|
||||||
|
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 { Grid, Typography } from "@mui/material";
|
||||||
import { Chessboard } from "react-chessboard";
|
import { Chessboard } from "react-chessboard";
|
||||||
import { useAtomValue } from "jotai";
|
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() {
|
export default function Board() {
|
||||||
const board = useAtomValue(boardAtom);
|
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 (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
@@ -16,15 +62,33 @@ export default function Board() {
|
|||||||
xs={12}
|
xs={12}
|
||||||
md={6}
|
md={6}
|
||||||
>
|
>
|
||||||
<Typography variant="h4" align="center">
|
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||||
White Player (?)
|
<Typography variant="h4" align="center">
|
||||||
</Typography>
|
{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">
|
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||||
Black Player (?)
|
<Typography variant="h4" align="center">
|
||||||
</Typography>
|
{boardOrientation ? whiteLabel : blackLabel}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,61 @@
|
|||||||
import { Grid } from "@mui/material";
|
import { Grid } from "@mui/material";
|
||||||
import LoadGameButton from "../loadGame/loadGameButton";
|
import LoadGameButton from "../loadGame/loadGameButton";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useChessActions } from "@/hooks/useChess";
|
import { useChessActions } from "@/hooks/useChess";
|
||||||
import { boardAtom, gameAtom } from "./states";
|
import {
|
||||||
|
boardAtom,
|
||||||
|
boardOrientationAtom,
|
||||||
|
gameAtom,
|
||||||
|
gameEvalAtom,
|
||||||
|
} from "./states";
|
||||||
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
import { useGameDatabase } from "@/hooks/useGameDatabase";
|
||||||
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import { Chess } from "chess.js";
|
||||||
|
|
||||||
export default function LoadGame() {
|
export default function LoadGame() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { gameId } = router.query;
|
const { gameId } = router.query;
|
||||||
|
const game = useAtomValue(gameAtom);
|
||||||
const gameActions = useChessActions(gameAtom);
|
const gameActions = useChessActions(gameAtom);
|
||||||
const boardActions = useChessActions(boardAtom);
|
const boardActions = useChessActions(boardAtom);
|
||||||
const { getGame } = useGameDatabase();
|
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(() => {
|
useEffect(() => {
|
||||||
const loadGame = async () => {
|
const loadGame = async () => {
|
||||||
if (typeof gameId !== "string") return;
|
if (typeof gameId !== "string") return;
|
||||||
|
|
||||||
const game = await getGame(parseInt(gameId));
|
const gamefromDb = await getGame(parseInt(gameId));
|
||||||
if (!game) return;
|
if (!gamefromDb) return;
|
||||||
|
|
||||||
boardActions.reset();
|
const gamefromDbChess = new Chess();
|
||||||
gameActions.setPgn(game.pgn);
|
gamefromDbChess.loadPgn(gamefromDb.pgn);
|
||||||
|
if (game.history().join() === gamefromDbChess.history().join()) return;
|
||||||
|
|
||||||
|
resetAndSetGamePgn(gamefromDb.pgn);
|
||||||
|
setEval(gamefromDb.eval);
|
||||||
};
|
};
|
||||||
|
|
||||||
loadGame();
|
loadGame();
|
||||||
}, [gameId]);
|
}, [gameId, getGame, game, resetAndSetGamePgn, setEval]);
|
||||||
|
|
||||||
if (!router.isReady || gameId) return null;
|
if (!router.isReady || gameId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||||
<LoadGameButton
|
<LoadGameButton setGame={(game) => resetAndSetGamePgn(game.pgn())} />
|
||||||
setGame={(game) => {
|
|
||||||
boardActions.reset();
|
|
||||||
gameActions.setPgn(game.pgn());
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { Divider, Grid, List, Typography } from "@mui/material";
|
import { Divider, Grid, List, Typography } from "@mui/material";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { boardAtom, gameEvalAtom } from "./states";
|
import { boardAtom, gameAtom } from "./states";
|
||||||
import LineEvaluation from "./lineEvaluation";
|
import LineEvaluation from "./lineEvaluation";
|
||||||
|
import { useCurrentMove } from "@/hooks/useCurrentMove";
|
||||||
|
|
||||||
export default function ReviewPanelBody() {
|
export default function ReviewPanelBody() {
|
||||||
const gameEval = useAtomValue(gameEvalAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
if (!gameEval) return null;
|
|
||||||
|
|
||||||
const board = useAtomValue(boardAtom);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -35,12 +50,12 @@ export default function ReviewPanelBody() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Typography variant="h6" align="center">
|
<Typography variant="h6" align="center">
|
||||||
{moveEval ? `${moveEval.bestMove} is the best move` : "Game is over"}
|
{getBestMoveLabel()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||||
<List>
|
<List>
|
||||||
{moveEval?.lines.map((line) => (
|
{move?.eval?.lines.map((line) => (
|
||||||
<LineEvaluation key={line.pv[0]} line={line} />
|
<LineEvaluation key={line.pv[0]} line={line} />
|
||||||
))}
|
))}
|
||||||
</List>
|
</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 gameEvalAtom = atom<GameEval | undefined>(undefined);
|
||||||
export const gameAtom = atom(new Chess());
|
export const gameAtom = atom(new Chess());
|
||||||
export const boardAtom = 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" }}>
|
<Box sx={{ flexGrow: 1, display: "flex" }}>
|
||||||
<AppBar
|
<AppBar
|
||||||
position="static"
|
position="static"
|
||||||
sx={{
|
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
enableColorOnDark
|
||||||
backgroundColor: "primary.main",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ export enum GameOrigin {
|
|||||||
ChessCom = "chesscom",
|
ChessCom = "chesscom",
|
||||||
Lichess = "lichess",
|
Lichess = "lichess",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Engine {
|
||||||
|
Stockfish16 = "stockfish_16",
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Engine } from "./enums";
|
||||||
|
|
||||||
export interface MoveEval {
|
export interface MoveEval {
|
||||||
bestMove: string;
|
bestMove: string;
|
||||||
lines: LineEval[];
|
lines: LineEval[];
|
||||||
@@ -14,7 +16,15 @@ export interface Accuracy {
|
|||||||
black: number;
|
black: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EngineSettings {
|
||||||
|
name: Engine;
|
||||||
|
depth: number;
|
||||||
|
multiPv: number;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameEval {
|
export interface GameEval {
|
||||||
moves: MoveEval[];
|
moves: MoveEval[];
|
||||||
accuracy: Accuracy;
|
accuracy: Accuracy;
|
||||||
|
settings: EngineSettings;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user