feat : add piece drag hint

This commit is contained in:
GuillaumeSD
2024-03-19 01:56:35 +01:00
parent cd927ed6d7
commit 4864bf61f5
7 changed files with 113 additions and 82 deletions

View File

@@ -90,7 +90,7 @@ export abstract class UciEngine {
return this.ready; return this.ready;
} }
private async stopSearch(): Promise<void> { public async stopSearch(): Promise<void> {
await this.sendCommands(["stop", "isready"], "readyok"); await this.sendCommands(["stop", "isready"], "readyok");
} }
@@ -243,7 +243,7 @@ export abstract class UciEngine {
fen: string, fen: string,
skillLevel: number, skillLevel: number,
depth = 16 depth = 16
): Promise<string> { ): Promise<string | undefined> {
this.throwErrorIfNotReady(); this.throwErrorIfNotReady();
await this.setSkillLevel(skillLevel); await this.setSkillLevel(skillLevel);
@@ -260,6 +260,6 @@ export abstract class UciEngine {
throw new Error("No move found"); throw new Error("No move found");
} }
return move; return move === "(none)" ? undefined : move;
} }
} }

View File

@@ -6,6 +6,7 @@ import {
boardOrientationAtom, boardOrientationAtom,
clickedSquaresAtom, clickedSquaresAtom,
currentPositionAtom, currentPositionAtom,
playableSquaresAtom,
showBestMoveArrowAtom, showBestMoveArrowAtom,
} from "../states"; } from "../states";
import { Arrow, Square } from "react-chessboard/dist/chessboard/types"; import { Arrow, Square } from "react-chessboard/dist/chessboard/types";
@@ -26,6 +27,7 @@ export default function Board() {
const { makeMove: makeBoardMove } = useChessActions(boardAtom); const { makeMove: makeBoardMove } = useChessActions(boardAtom);
const position = useAtomValue(currentPositionAtom); const position = useAtomValue(currentPositionAtom);
const setClickedSquares = useSetAtom(clickedSquaresAtom); const setClickedSquares = useSetAtom(clickedSquaresAtom);
const setPlayableSquares = useSetAtom(playableSquaresAtom);
const boardFen = board.fen(); const boardFen = board.fen();
@@ -51,6 +53,27 @@ export default function Board() {
} }
}; };
const handleSquareLeftClick = () => {
setClickedSquares([]);
};
const handleSquareRightClick = (square: Square) => {
setClickedSquares((prev) =>
prev.includes(square)
? prev.filter((s) => s !== square)
: [...prev, square]
);
};
const handlePieceDragBegin = (_: string, square: Square) => {
const moves = board.moves({ square, verbose: true });
setPlayableSquares(moves.map((m) => m.to));
};
const handlePieceDragEnd = () => {
setPlayableSquares([]);
};
const customArrows: Arrow[] = useMemo(() => { const customArrows: Arrow[] = useMemo(() => {
const bestMove = position?.lastEval?.bestMove; const bestMove = position?.lastEval?.bestMove;
const moveClassification = position?.eval?.moveClassification; const moveClassification = position?.eval?.moveClassification;
@@ -113,6 +136,10 @@ export default function Board() {
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)", boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",
}} }}
customSquare={SquareRenderer} customSquare={SquareRenderer}
onSquareClick={handleSquareLeftClick}
onSquareRightClick={handleSquareRightClick}
onPieceDragBegin={handlePieceDragBegin}
onPieceDragEnd={handlePieceDragEnd}
/> />
</Grid> </Grid>

View File

@@ -1,31 +1,28 @@
import { import {
clickedSquaresAtom, clickedSquaresAtom,
currentPositionAtom, currentPositionAtom,
playableSquaresAtom,
showPlayerMoveIconAtom, showPlayerMoveIconAtom,
} from "../states"; } from "../states";
import { MoveClassification } from "@/types/enums"; import { MoveClassification } from "@/types/enums";
import { atom, useAtom, useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import Image from "next/image"; import Image from "next/image";
import { CSSProperties, MouseEventHandler, forwardRef } from "react"; import { CSSProperties, forwardRef } from "react";
import { CustomSquareProps } from "react-chessboard/dist/chessboard/types"; import { CustomSquareProps } from "react-chessboard/dist/chessboard/types";
const rightClickEventSquareAtom = atom<string | null>(null);
const SquareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>( const SquareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>(
(props, ref) => { (props, ref) => {
const { children, square, style } = props; const { children, square, style } = props;
const showPlayerMoveIcon = useAtomValue(showPlayerMoveIconAtom); const showPlayerMoveIcon = useAtomValue(showPlayerMoveIconAtom);
const position = useAtomValue(currentPositionAtom); const position = useAtomValue(currentPositionAtom);
const [clickedSquares, setClickedSquares] = useAtom(clickedSquaresAtom); const clickedSquares = useAtomValue(clickedSquaresAtom);
const [rightClickEventSquare, setRightClickEventSquare] = useAtom( const playableSquares = useAtomValue(playableSquaresAtom);
rightClickEventSquareAtom
);
const fromSquare = position.lastMove?.from; const fromSquare = position.lastMove?.from;
const toSquare = position.lastMove?.to; const toSquare = position.lastMove?.to;
const moveClassification = position?.eval?.moveClassification; const moveClassification = position?.eval?.moveClassification;
const customSquareStyle: CSSProperties | undefined = const highlightSquareStyle: CSSProperties | undefined =
clickedSquares.includes(square) clickedSquares.includes(square)
? { ? {
position: "absolute", position: "absolute",
@@ -46,37 +43,25 @@ const SquareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>(
} }
: undefined; : undefined;
const handleSquareLeftClick: MouseEventHandler<HTMLDivElement> = () => { const playableSquareStyle: CSSProperties | undefined =
setClickedSquares([]); playableSquares.includes(square)
}; ? {
position: "absolute",
const handleSquareRightClick: MouseEventHandler<HTMLDivElement> = ( width: "100%",
event height: "100%",
) => { backgroundColor: "rgba(0,0,0,.14)",
if (event.button !== 2) return; padding: "35%",
backgroundClip: "content-box",
if (rightClickEventSquare !== square) { borderRadius: "50%",
setRightClickEventSquare(null); boxSizing: "border-box",
return; }
} : undefined;
setClickedSquares((prev) =>
prev.includes(square)
? prev.filter((s) => s !== square)
: [...prev, square]
);
};
return ( return (
<div <div ref={ref} style={{ ...style, position: "relative" }}>
ref={ref}
style={{ ...style, position: "relative" }}
onClick={handleSquareLeftClick}
onMouseDown={(e) => e.button === 2 && setRightClickEventSquare(square)}
onMouseUp={handleSquareRightClick}
>
{children} {children}
{customSquareStyle && <div style={customSquareStyle} />} {highlightSquareStyle && <div style={highlightSquareStyle} />}
{playableSquareStyle && <div style={playableSquareStyle} />}
{moveClassification && showPlayerMoveIcon && square === toSquare && ( {moveClassification && showPlayerMoveIcon && square === toSquare && (
<Image <Image
src={`/icons/${moveClassification}.png`} src={`/icons/${moveClassification}.png`}

View File

@@ -11,6 +11,7 @@ export const boardOrientationAtom = atom(true);
export const showBestMoveArrowAtom = atom(true); export const showBestMoveArrowAtom = atom(true);
export const showPlayerMoveIconAtom = atom(true); export const showPlayerMoveIconAtom = atom(true);
export const clickedSquaresAtom = atom<string[]>([]); export const clickedSquaresAtom = atom<string[]>([]);
export const playableSquaresAtom = atom<string[]>([]);
export const engineDepthAtom = atom(16); export const engineDepthAtom = atom(16);
export const engineMultiPvAtom = atom(3); export const engineMultiPvAtom = atom(3);

View File

@@ -5,6 +5,7 @@ import {
clickedSquaresAtom, clickedSquaresAtom,
engineSkillLevelAtom, engineSkillLevelAtom,
gameAtom, gameAtom,
playableSquaresAtom,
playerColorAtom, playerColorAtom,
} from "../states"; } from "../states";
import { Square } from "react-chessboard/dist/chessboard/types"; import { Square } from "react-chessboard/dist/chessboard/types";
@@ -24,23 +25,30 @@ export default function Board() {
const playerColor = useAtomValue(playerColorAtom); const playerColor = useAtomValue(playerColorAtom);
const { makeMove: makeBoardMove } = useChessActions(gameAtom); const { makeMove: makeBoardMove } = useChessActions(gameAtom);
const setClickedSquares = useSetAtom(clickedSquaresAtom); const setClickedSquares = useSetAtom(clickedSquaresAtom);
const setPlayableSquares = useSetAtom(playableSquaresAtom);
const engineSkillLevel = useAtomValue(engineSkillLevelAtom); const engineSkillLevel = useAtomValue(engineSkillLevelAtom);
const engine = useEngine(EngineName.Stockfish16); const engine = useEngine(EngineName.Stockfish16);
const gameFen = game.fen(); const gameFen = game.fen();
const turn = game.turn(); const isGameFinished = game.isGameOver();
useEffect(() => { useEffect(() => {
const playEngineMove = async () => { const playEngineMove = async () => {
if (!engine?.isReady() || turn === playerColor) return; if (!engine?.isReady() || game.turn() === playerColor || isGameFinished) {
return;
}
const move = await engine.getEngineNextMove( const move = await engine.getEngineNextMove(
gameFen, gameFen,
engineSkillLevel - 1 engineSkillLevel - 1
); );
makeBoardMove(uciMoveParams(move)); if (move) makeBoardMove(uciMoveParams(move));
}; };
playEngineMove(); playEngineMove();
}, [engine, turn, engine, playerColor, engineSkillLevel]);
return () => {
engine?.stopSearch();
};
}, [gameFen, engine]);
useEffect(() => { useEffect(() => {
setClickedSquares([]); setClickedSquares([]);
@@ -70,6 +78,27 @@ export default function Board() {
return playerColor === piece[0]; return playerColor === piece[0];
}; };
const handleSquareLeftClick = () => {
setClickedSquares([]);
};
const handleSquareRightClick = (square: Square) => {
setClickedSquares((prev) =>
prev.includes(square)
? prev.filter((s) => s !== square)
: [...prev, square]
);
};
const handlePieceDragBegin = (_: string, square: Square) => {
const moves = game.moves({ square, verbose: true });
setPlayableSquares(moves.map((m) => m.to));
};
const handlePieceDragEnd = () => {
setPlayableSquares([]);
};
return ( return (
<Grid <Grid
item item
@@ -103,6 +132,10 @@ export default function Board() {
}} }}
isDraggablePiece={isPieceDraggable} isDraggablePiece={isPieceDraggable}
customSquare={SquareRenderer} customSquare={SquareRenderer}
onSquareClick={handleSquareLeftClick}
onSquareRightClick={handleSquareRightClick}
onPieceDragBegin={handlePieceDragBegin}
onPieceDragEnd={handlePieceDragEnd}
/> />
</Grid> </Grid>

View File

@@ -1,24 +1,20 @@
import { clickedSquaresAtom, gameAtom } from "../states"; import { clickedSquaresAtom, gameAtom, playableSquaresAtom } from "../states";
import { atom, useAtom, useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { CSSProperties, MouseEventHandler, forwardRef } from "react"; import { CSSProperties, forwardRef } from "react";
import { CustomSquareProps } from "react-chessboard/dist/chessboard/types"; import { CustomSquareProps } from "react-chessboard/dist/chessboard/types";
const rightClickEventSquareAtom = atom<string | null>(null);
const SquareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>( const SquareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>(
(props, ref) => { (props, ref) => {
const { children, square, style } = props; const { children, square, style } = props;
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const [clickedSquares, setClickedSquares] = useAtom(clickedSquaresAtom); const clickedSquares = useAtomValue(clickedSquaresAtom);
const [rightClickEventSquare, setRightClickEventSquare] = useAtom( const playableSquares = useAtomValue(playableSquaresAtom);
rightClickEventSquareAtom
);
const lastMove = game.history({ verbose: true }).at(-1); const lastMove = game.history({ verbose: true }).at(-1);
const fromSquare = lastMove?.from; const fromSquare = lastMove?.from;
const toSquare = lastMove?.to; const toSquare = lastMove?.to;
const customSquareStyle: CSSProperties | undefined = const highlightSquareStyle: CSSProperties | undefined =
clickedSquares.includes(square) clickedSquares.includes(square)
? { ? {
position: "absolute", position: "absolute",
@@ -37,37 +33,25 @@ const SquareRenderer = forwardRef<HTMLDivElement, CustomSquareProps>(
} }
: undefined; : undefined;
const handleSquareLeftClick: MouseEventHandler<HTMLDivElement> = () => { const playableSquareStyle: CSSProperties | undefined =
setClickedSquares([]); playableSquares.includes(square)
}; ? {
position: "absolute",
const handleSquareRightClick: MouseEventHandler<HTMLDivElement> = ( width: "100%",
event height: "100%",
) => { backgroundColor: "rgba(0,0,0,.14)",
if (event.button !== 2) return; padding: "35%",
backgroundClip: "content-box",
if (rightClickEventSquare !== square) { borderRadius: "50%",
setRightClickEventSquare(null); boxSizing: "border-box",
return; }
} : undefined;
setClickedSquares((prev) =>
prev.includes(square)
? prev.filter((s) => s !== square)
: [...prev, square]
);
};
return ( return (
<div <div ref={ref} style={{ ...style, position: "relative" }}>
ref={ref}
style={{ ...style, position: "relative" }}
onClick={handleSquareLeftClick}
onMouseDown={(e) => e.button === 2 && setRightClickEventSquare(square)}
onMouseUp={handleSquareRightClick}
>
{children} {children}
{customSquareStyle && <div style={customSquareStyle} />} {highlightSquareStyle && <div style={highlightSquareStyle} />}
{playableSquareStyle && <div style={playableSquareStyle} />}
</div> </div>
); );
} }

View File

@@ -7,3 +7,4 @@ export const playerColorAtom = atom<Color>(Color.White);
export const engineSkillLevelAtom = atom<number>(1); export const engineSkillLevelAtom = atom<number>(1);
export const clickedSquaresAtom = atom<string[]>([]); export const clickedSquaresAtom = atom<string[]>([]);
export const playableSquaresAtom = atom<string[]>([]);