feat : add click on engine lines
This commit is contained in:
@@ -1,56 +0,0 @@
|
|||||||
import { Piece } from "react-chessboard/dist/chessboard/types";
|
|
||||||
|
|
||||||
export const PIECE_CODES = [
|
|
||||||
"wP",
|
|
||||||
"wB",
|
|
||||||
"wN",
|
|
||||||
"wR",
|
|
||||||
"wQ",
|
|
||||||
"wK",
|
|
||||||
"bP",
|
|
||||||
"bB",
|
|
||||||
"bN",
|
|
||||||
"bR",
|
|
||||||
"bQ",
|
|
||||||
"bK",
|
|
||||||
] as const satisfies Piece[];
|
|
||||||
|
|
||||||
export const PIECE_SETS = [
|
|
||||||
"alpha",
|
|
||||||
"anarcandy",
|
|
||||||
"caliente",
|
|
||||||
"california",
|
|
||||||
"cardinal",
|
|
||||||
"cburnett",
|
|
||||||
"celtic",
|
|
||||||
"chess7",
|
|
||||||
"chessnut",
|
|
||||||
"companion",
|
|
||||||
"cooke",
|
|
||||||
"dubrovny",
|
|
||||||
"fantasy",
|
|
||||||
"firi",
|
|
||||||
"fresca",
|
|
||||||
"gioco",
|
|
||||||
"governor",
|
|
||||||
"horsey",
|
|
||||||
"icpieces",
|
|
||||||
"kiwen-suwi",
|
|
||||||
"kosal",
|
|
||||||
"leipzig",
|
|
||||||
"letter",
|
|
||||||
"maestro",
|
|
||||||
"merida",
|
|
||||||
"monarchy",
|
|
||||||
"mpchess",
|
|
||||||
"pirouetti",
|
|
||||||
"pixel",
|
|
||||||
"reillycraig",
|
|
||||||
"rhosgfx",
|
|
||||||
"riohacha",
|
|
||||||
"shapes",
|
|
||||||
"spatial",
|
|
||||||
"staunty",
|
|
||||||
"tatiana",
|
|
||||||
"xkcd",
|
|
||||||
] as const satisfies string[];
|
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Arrow,
|
Arrow,
|
||||||
CustomPieces,
|
CustomPieces,
|
||||||
CustomSquareRenderer,
|
CustomSquareRenderer,
|
||||||
|
Piece,
|
||||||
PromotionPieceOption,
|
PromotionPieceOption,
|
||||||
Square,
|
Square,
|
||||||
} from "react-chessboard/dist/chessboard/types";
|
} from "react-chessboard/dist/chessboard/types";
|
||||||
@@ -15,13 +16,12 @@ import { Chess } from "chess.js";
|
|||||||
import { getSquareRenderer } from "./squareRenderer";
|
import { getSquareRenderer } from "./squareRenderer";
|
||||||
import { CurrentPosition } from "@/types/eval";
|
import { CurrentPosition } from "@/types/eval";
|
||||||
import EvaluationBar from "./evaluationBar";
|
import EvaluationBar from "./evaluationBar";
|
||||||
import { moveClassificationColors } from "@/lib/chess";
|
import { CLASSIFICATION_COLORS } from "@/constants";
|
||||||
import { Player } from "@/types/game";
|
import { Player } from "@/types/game";
|
||||||
import PlayerHeader from "./playerHeader";
|
import PlayerHeader from "./playerHeader";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { boardHueAtom, pieceSetAtom } from "./states";
|
import { boardHueAtom, pieceSetAtom } from "./states";
|
||||||
import tinycolor from "tinycolor2";
|
import tinycolor from "tinycolor2";
|
||||||
import { PIECE_CODES } from "./constants";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -52,7 +52,7 @@ export default function Board({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const boardRef = useRef<HTMLDivElement>(null);
|
const boardRef = useRef<HTMLDivElement>(null);
|
||||||
const game = useAtomValue(gameAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
const { makeMove: makeGameMove } = useChessActions(gameAtom);
|
const { playMove } = useChessActions(gameAtom);
|
||||||
const clickedSquaresAtom = useMemo(() => atom<Square[]>([]), []);
|
const clickedSquaresAtom = useMemo(() => atom<Square[]>([]), []);
|
||||||
const setClickedSquares = useSetAtom(clickedSquaresAtom);
|
const setClickedSquares = useSetAtom(clickedSquaresAtom);
|
||||||
const playableSquaresAtom = useMemo(() => atom<Square[]>([]), []);
|
const playableSquaresAtom = useMemo(() => atom<Square[]>([]), []);
|
||||||
@@ -86,7 +86,7 @@ export default function Board({
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
if (!isPiecePlayable({ piece })) return false;
|
if (!isPiecePlayable({ piece })) return false;
|
||||||
|
|
||||||
const result = makeGameMove({
|
const result = playMove({
|
||||||
from: source,
|
from: source,
|
||||||
to: target,
|
to: target,
|
||||||
promotion: piece[1]?.toLowerCase() ?? "q",
|
promotion: piece[1]?.toLowerCase() ?? "q",
|
||||||
@@ -135,7 +135,7 @@ export default function Board({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = makeGameMove({
|
const result = playMove({
|
||||||
from: moveClickFrom,
|
from: moveClickFrom,
|
||||||
to: square,
|
to: square,
|
||||||
});
|
});
|
||||||
@@ -168,7 +168,7 @@ export default function Board({
|
|||||||
const promotionPiece = piece[1]?.toLowerCase() ?? "q";
|
const promotionPiece = piece[1]?.toLowerCase() ?? "q";
|
||||||
|
|
||||||
if (moveClickFrom && moveClickTo) {
|
if (moveClickFrom && moveClickTo) {
|
||||||
const result = makeGameMove({
|
const result = playMove({
|
||||||
from: moveClickFrom,
|
from: moveClickFrom,
|
||||||
to: moveClickTo,
|
to: moveClickTo,
|
||||||
promotion: promotionPiece,
|
promotion: promotionPiece,
|
||||||
@@ -178,7 +178,7 @@ export default function Board({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (from && to) {
|
if (from && to) {
|
||||||
const result = makeGameMove({
|
const result = playMove({
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
promotion: promotionPiece,
|
promotion: promotionPiece,
|
||||||
@@ -206,7 +206,7 @@ export default function Board({
|
|||||||
const bestMoveArrow = [
|
const bestMoveArrow = [
|
||||||
bestMove.slice(0, 2),
|
bestMove.slice(0, 2),
|
||||||
bestMove.slice(2, 4),
|
bestMove.slice(2, 4),
|
||||||
tinycolor(moveClassificationColors[MoveClassification.Best])
|
tinycolor(CLASSIFICATION_COLORS[MoveClassification.Best])
|
||||||
.spin(-boardHue)
|
.spin(-boardHue)
|
||||||
.toHexString(),
|
.toHexString(),
|
||||||
] as Arrow;
|
] as Arrow;
|
||||||
@@ -325,3 +325,18 @@ export default function Board({
|
|||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PIECE_CODES = [
|
||||||
|
"wP",
|
||||||
|
"wB",
|
||||||
|
"wN",
|
||||||
|
"wR",
|
||||||
|
"wQ",
|
||||||
|
"wK",
|
||||||
|
"bP",
|
||||||
|
"bB",
|
||||||
|
"bN",
|
||||||
|
"bR",
|
||||||
|
"bQ",
|
||||||
|
"bK",
|
||||||
|
] as const satisfies Piece[];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CustomSquareProps,
|
CustomSquareProps,
|
||||||
Square,
|
Square,
|
||||||
} from "react-chessboard/dist/chessboard/types";
|
} from "react-chessboard/dist/chessboard/types";
|
||||||
import { moveClassificationColors } from "@/lib/chess";
|
import { CLASSIFICATION_COLORS } from "@/constants";
|
||||||
import { boardHueAtom } from "./states";
|
import { boardHueAtom } from "./states";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -110,7 +110,7 @@ const previousMoveSquareStyle = (
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
backgroundColor: moveClassification
|
backgroundColor: moveClassification
|
||||||
? moveClassificationColors[moveClassification]
|
? CLASSIFICATION_COLORS[moveClassification]
|
||||||
: "#fad541",
|
: "#fad541",
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { PIECE_SETS } from "@/constants";
|
||||||
import { atomWithStorage } from "jotai/utils";
|
import { atomWithStorage } from "jotai/utils";
|
||||||
import { PIECE_SETS } from "./constants";
|
|
||||||
|
|
||||||
export const pieceSetAtom = atomWithStorage<(typeof PIECE_SETS)[number]>(
|
export const pieceSetAtom = atomWithStorage<(typeof PIECE_SETS)[number]>(
|
||||||
"pieceSet",
|
"pieceSet",
|
||||||
|
|||||||
91
src/constants.ts
Normal file
91
src/constants.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { EngineName, MoveClassification } from "./types/enums";
|
||||||
|
|
||||||
|
export const CLASSIFICATION_COLORS: Record<MoveClassification, string> = {
|
||||||
|
[MoveClassification.Book]: "#d5a47d",
|
||||||
|
[MoveClassification.Forced]: "#d5a47d",
|
||||||
|
[MoveClassification.Brilliant]: "#26c2a3",
|
||||||
|
[MoveClassification.Great]: "#4099ed",
|
||||||
|
[MoveClassification.Best]: "#3aab18",
|
||||||
|
[MoveClassification.Excellent]: "#3aab18",
|
||||||
|
[MoveClassification.Good]: "#81b64c",
|
||||||
|
[MoveClassification.Inaccuracy]: "#f7c631",
|
||||||
|
[MoveClassification.Mistake]: "#ffa459",
|
||||||
|
[MoveClassification.Blunder]: "#fa412d",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_ENGINE: EngineName = EngineName.Stockfish17Lite;
|
||||||
|
export const STRONGEST_ENGINE: EngineName = EngineName.Stockfish17;
|
||||||
|
|
||||||
|
export const ENGINE_LABELS: Record<
|
||||||
|
EngineName,
|
||||||
|
{ small: string; full: string }
|
||||||
|
> = {
|
||||||
|
[EngineName.Stockfish17]: {
|
||||||
|
full: "Stockfish 17 (75MB)",
|
||||||
|
small: "Stockfish 17",
|
||||||
|
},
|
||||||
|
[EngineName.Stockfish17Lite]: {
|
||||||
|
full: "Stockfish 17 Lite (6MB)",
|
||||||
|
small: "Stockfish 17 Lite",
|
||||||
|
},
|
||||||
|
[EngineName.Stockfish16_1]: {
|
||||||
|
full: "Stockfish 16.1 (64MB)",
|
||||||
|
small: "Stockfish 16.1",
|
||||||
|
},
|
||||||
|
[EngineName.Stockfish16_1Lite]: {
|
||||||
|
full: "Stockfish 16.1 Lite (6MB)",
|
||||||
|
small: "Stockfish 16.1 Lite",
|
||||||
|
},
|
||||||
|
[EngineName.Stockfish16NNUE]: {
|
||||||
|
full: "Stockfish 16 (40MB)",
|
||||||
|
small: "Stockfish 16",
|
||||||
|
},
|
||||||
|
[EngineName.Stockfish16]: {
|
||||||
|
full: "Stockfish 16 Lite (HCE)",
|
||||||
|
small: "Stockfish 16 Lite",
|
||||||
|
},
|
||||||
|
[EngineName.Stockfish11]: {
|
||||||
|
full: "Stockfish 11 (HCE)",
|
||||||
|
small: "Stockfish 11",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PIECE_SETS = [
|
||||||
|
"alpha",
|
||||||
|
"anarcandy",
|
||||||
|
"caliente",
|
||||||
|
"california",
|
||||||
|
"cardinal",
|
||||||
|
"cburnett",
|
||||||
|
"celtic",
|
||||||
|
"chess7",
|
||||||
|
"chessnut",
|
||||||
|
"companion",
|
||||||
|
"cooke",
|
||||||
|
"dubrovny",
|
||||||
|
"fantasy",
|
||||||
|
"firi",
|
||||||
|
"fresca",
|
||||||
|
"gioco",
|
||||||
|
"governor",
|
||||||
|
"horsey",
|
||||||
|
"icpieces",
|
||||||
|
"kiwen-suwi",
|
||||||
|
"kosal",
|
||||||
|
"leipzig",
|
||||||
|
"letter",
|
||||||
|
"maestro",
|
||||||
|
"merida",
|
||||||
|
"monarchy",
|
||||||
|
"mpchess",
|
||||||
|
"pirouetti",
|
||||||
|
"pixel",
|
||||||
|
"reillycraig",
|
||||||
|
"rhosgfx",
|
||||||
|
"riohacha",
|
||||||
|
"shapes",
|
||||||
|
"spatial",
|
||||||
|
"staunty",
|
||||||
|
"tatiana",
|
||||||
|
"xkcd",
|
||||||
|
] as const satisfies string[];
|
||||||
@@ -67,7 +67,7 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
|||||||
[copyGame, setGame]
|
[copyGame, setGame]
|
||||||
);
|
);
|
||||||
|
|
||||||
const makeMove = useCallback(
|
const playMove = useCallback(
|
||||||
(params: {
|
(params: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
@@ -92,6 +92,18 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
|||||||
[copyGame, setGame]
|
[copyGame, setGame]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addMoves = useCallback(
|
||||||
|
(moves: string[]) => {
|
||||||
|
const newGame = copyGame();
|
||||||
|
|
||||||
|
for (const move of moves) {
|
||||||
|
newGame.move(move);
|
||||||
|
}
|
||||||
|
setGame(newGame);
|
||||||
|
},
|
||||||
|
[copyGame, setGame]
|
||||||
|
);
|
||||||
|
|
||||||
const undoMove = useCallback(() => {
|
const undoMove = useCallback(() => {
|
||||||
const newGame = copyGame();
|
const newGame = copyGame();
|
||||||
const move = newGame.undo();
|
const move = newGame.undo();
|
||||||
@@ -127,9 +139,10 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
|
|||||||
return {
|
return {
|
||||||
setPgn,
|
setPgn,
|
||||||
reset,
|
reset,
|
||||||
makeMove,
|
playMove,
|
||||||
undoMove,
|
undoMove,
|
||||||
goToMove,
|
goToMove,
|
||||||
resetToStartingPosition,
|
resetToStartingPosition,
|
||||||
|
addMoves,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EvaluateGameParams, LineEval, PositionEval } from "@/types/eval";
|
|||||||
import { Game, Player } from "@/types/game";
|
import { Game, Player } from "@/types/game";
|
||||||
import { Chess, PieceSymbol, Square } from "chess.js";
|
import { Chess, PieceSymbol, Square } from "chess.js";
|
||||||
import { getPositionWinPercentage } from "./engine/helpers/winPercentage";
|
import { getPositionWinPercentage } from "./engine/helpers/winPercentage";
|
||||||
import { Color, MoveClassification } from "@/types/enums";
|
import { Color } from "@/types/enums";
|
||||||
|
|
||||||
export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => {
|
export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => {
|
||||||
const history = game.history({ verbose: true });
|
const history = game.history({ verbose: true });
|
||||||
@@ -327,15 +327,33 @@ export const getLineEvalLabel = (
|
|||||||
return "?";
|
return "?";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moveClassificationColors: Record<MoveClassification, string> = {
|
export const formatUciPv = (fen: string, uciMoves: string[]): string[] => {
|
||||||
[MoveClassification.Book]: "#d5a47d",
|
const castlingRights = fen.split(" ")[2];
|
||||||
[MoveClassification.Forced]: "#d5a47d",
|
|
||||||
[MoveClassification.Brilliant]: "#26c2a3",
|
let canWhiteCastleKingSide = castlingRights.includes("K");
|
||||||
[MoveClassification.Great]: "#4099ed",
|
let canWhiteCastleQueenSide = castlingRights.includes("Q");
|
||||||
[MoveClassification.Best]: "#3aab18",
|
let canBlackCastleKingSide = castlingRights.includes("k");
|
||||||
[MoveClassification.Excellent]: "#3aab18",
|
let canBlackCastleQueenSide = castlingRights.includes("q");
|
||||||
[MoveClassification.Good]: "#81b64c",
|
|
||||||
[MoveClassification.Inaccuracy]: "#f7c631",
|
return uciMoves.map((uci) => {
|
||||||
[MoveClassification.Mistake]: "#ffa459",
|
if (uci === "e1h1" && canWhiteCastleKingSide) {
|
||||||
[MoveClassification.Blunder]: "#fa412d",
|
canWhiteCastleKingSide = false;
|
||||||
|
return "e1g1";
|
||||||
|
}
|
||||||
|
if (uci === "e1a1" && canWhiteCastleQueenSide) {
|
||||||
|
canWhiteCastleQueenSide = false;
|
||||||
|
return "e1c1";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uci === "e8h8" && canBlackCastleKingSide) {
|
||||||
|
canBlackCastleKingSide = false;
|
||||||
|
return "e8g8";
|
||||||
|
}
|
||||||
|
if (uci === "e8a8" && canBlackCastleQueenSide) {
|
||||||
|
canBlackCastleQueenSide = false;
|
||||||
|
return "e8c8";
|
||||||
|
}
|
||||||
|
|
||||||
|
return uci;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { formatUciPv } from "@/lib/chess";
|
||||||
import { LineEval, PositionEval } from "@/types/eval";
|
import { LineEval, PositionEval } from "@/types/eval";
|
||||||
|
|
||||||
export const parseEvaluationResults = (
|
export const parseEvaluationResults = (
|
||||||
results: string[],
|
results: string[],
|
||||||
whiteToPlay: boolean
|
fen: string
|
||||||
): PositionEval => {
|
): PositionEval => {
|
||||||
const parsedResults: PositionEval = {
|
const parsedResults: PositionEval = {
|
||||||
lines: [],
|
lines: [],
|
||||||
@@ -18,7 +19,7 @@ export const parseEvaluationResults = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.startsWith("info")) {
|
if (result.startsWith("info")) {
|
||||||
const pv = getResultPv(result);
|
const pv = getResultPv(result, fen);
|
||||||
const multiPv = getResultProperty(result, "multipv");
|
const multiPv = getResultProperty(result, "multipv");
|
||||||
const depth = getResultProperty(result, "depth");
|
const depth = getResultProperty(result, "depth");
|
||||||
if (!pv || !multiPv || !depth) continue;
|
if (!pv || !multiPv || !depth) continue;
|
||||||
@@ -45,6 +46,7 @@ export const parseEvaluationResults = (
|
|||||||
|
|
||||||
parsedResults.lines = Object.values(tempResults).sort(sortLines);
|
parsedResults.lines = Object.values(tempResults).sort(sortLines);
|
||||||
|
|
||||||
|
const whiteToPlay = fen.split(" ")[1] === "w";
|
||||||
if (!whiteToPlay) {
|
if (!whiteToPlay) {
|
||||||
parsedResults.lines = parsedResults.lines.map((line) => ({
|
parsedResults.lines = parsedResults.lines.map((line) => ({
|
||||||
...line,
|
...line,
|
||||||
@@ -86,7 +88,7 @@ export const getResultProperty = (
|
|||||||
return splitResult[propertyIndex + 1];
|
return splitResult[propertyIndex + 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResultPv = (result: string): string[] | undefined => {
|
const getResultPv = (result: string, fen: string): string[] | undefined => {
|
||||||
const splitResult = result.split(" ");
|
const splitResult = result.split(" ");
|
||||||
const pvIndex = splitResult.indexOf("pv");
|
const pvIndex = splitResult.indexOf("pv");
|
||||||
|
|
||||||
@@ -94,5 +96,6 @@ const getResultPv = (result: string): string[] | undefined => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return splitResult.slice(pvIndex + 1);
|
const rawPv = splitResult.slice(pvIndex + 1);
|
||||||
|
return formatUciPv(fen, rawPv);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -317,9 +317,7 @@ export class UciEngine {
|
|||||||
"bestmove"
|
"bestmove"
|
||||||
);
|
);
|
||||||
|
|
||||||
const whiteToPlay = fen.split(" ")[1] === "w";
|
return parseEvaluationResults(results, fen);
|
||||||
|
|
||||||
return parseEvaluationResults(results, whiteToPlay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async evaluatePositionWithUpdate({
|
public async evaluatePositionWithUpdate({
|
||||||
@@ -335,11 +333,9 @@ export class UciEngine {
|
|||||||
await this.stopSearch();
|
await this.stopSearch();
|
||||||
await this.setMultiPv(multiPv);
|
await this.setMultiPv(multiPv);
|
||||||
|
|
||||||
const whiteToPlay = fen.split(" ")[1] === "w";
|
|
||||||
|
|
||||||
const onNewMessage = (messages: string[]) => {
|
const onNewMessage = (messages: string[]) => {
|
||||||
if (!setPartialEval) return;
|
if (!setPartialEval) return;
|
||||||
const parsedResults = parseEvaluationResults(messages, whiteToPlay);
|
const parsedResults = parseEvaluationResults(messages, fen);
|
||||||
setPartialEval(parsedResults);
|
setPartialEval(parsedResults);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -360,7 +356,7 @@ export class UciEngine {
|
|||||||
onNewMessage
|
onNewMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
return parseEvaluationResults(results, whiteToPlay);
|
return parseEvaluationResults(results, fen);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEngineNextMove(
|
public async getEngineNextMove(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
LichessResponse,
|
LichessResponse,
|
||||||
} from "@/types/lichess";
|
} from "@/types/lichess";
|
||||||
import { logErrorToSentry } from "./sentry";
|
import { logErrorToSentry } from "./sentry";
|
||||||
|
import { formatUciPv } from "./chess";
|
||||||
|
|
||||||
export const getLichessEval = async (
|
export const getLichessEval = async (
|
||||||
fen: string,
|
fen: string,
|
||||||
@@ -26,7 +27,7 @@ export const getLichessEval = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lines: LineEval[] = data.pvs.map((pv, index) => ({
|
const lines: LineEval[] = data.pvs.map((pv, index) => ({
|
||||||
pv: pv.moves.split(" "),
|
pv: formatUciPv(fen, pv.moves.split(" ")),
|
||||||
cp: pv.cp,
|
cp: pv.cp,
|
||||||
mate: pv.mate,
|
mate: pv.mate,
|
||||||
depth: data.depth,
|
depth: data.depth,
|
||||||
|
|||||||
Binary file not shown.
@@ -1,15 +1,23 @@
|
|||||||
import { LineEval } from "@/types/eval";
|
import { LineEval } from "@/types/eval";
|
||||||
import { ListItem, Skeleton, Typography } from "@mui/material";
|
import { Box, ListItem, Skeleton, Typography, useTheme } from "@mui/material";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { boardAtom } from "../../states";
|
import { boardAtom } from "../../states";
|
||||||
import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess";
|
import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import { useChessActions } from "@/hooks/useChessActions";
|
||||||
|
|
||||||
|
const myFont = localFont({
|
||||||
|
src: "./chess_merida_unicode.ttf",
|
||||||
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
line: LineEval;
|
line: LineEval;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LineEvaluation({ line }: Props) {
|
export default function LineEvaluation({ line }: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
const board = useAtomValue(boardAtom);
|
const board = useAtomValue(boardAtom);
|
||||||
|
const { addMoves } = useChessActions(boardAtom);
|
||||||
const lineLabel = getLineEvalLabel(line);
|
const lineLabel = getLineEvalLabel(line);
|
||||||
|
|
||||||
const isBlackCp =
|
const isBlackCp =
|
||||||
@@ -18,6 +26,26 @@ export default function LineEvaluation({ line }: Props) {
|
|||||||
|
|
||||||
const showSkeleton = line.depth < 6;
|
const showSkeleton = line.depth < 6;
|
||||||
|
|
||||||
|
const uciToSan = moveLineUciToSan(board.fen());
|
||||||
|
const initialTurn = board.turn();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
|
||||||
|
const formatSan = (
|
||||||
|
san: string,
|
||||||
|
moveIdx: number
|
||||||
|
): { icon?: string; text: string } => {
|
||||||
|
const firstChar = san.charAt(0);
|
||||||
|
|
||||||
|
const isPiece = ["K", "Q", "R", "B", "N"].includes(firstChar);
|
||||||
|
if (!isPiece) return { text: san };
|
||||||
|
|
||||||
|
const turn = isDarkMode ? initialTurn : initialTurn === "w" ? "b" : "w";
|
||||||
|
const moveColor = moveIdx % 2 === 0 ? turn : turn === "w" ? "b" : "w";
|
||||||
|
const icon = unicodeMap[firstChar][moveColor];
|
||||||
|
|
||||||
|
return { icon, text: san.slice(1) };
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -58,9 +86,52 @@ export default function LineEvaluation({ line }: Props) {
|
|||||||
{showSkeleton ? (
|
{showSkeleton ? (
|
||||||
<Skeleton variant="rounded" animation="wave" width="15em" />
|
<Skeleton variant="rounded" animation="wave" width="15em" />
|
||||||
) : (
|
) : (
|
||||||
line.pv.map(moveLineUciToSan(board.fen())).join(", ")
|
line.pv.map((uci, i) => {
|
||||||
|
const san = uciToSan(uci);
|
||||||
|
const { icon, text } = formatSan(san, i);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
key={i}
|
||||||
|
onClick={() => {
|
||||||
|
addMoves(line.pv.slice(0, i + 1));
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
ml: i ? 0.5 : 0,
|
||||||
|
transition: "opacity 0.2s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
fontFamily={myFont.style.fontFamily}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography component="span">
|
||||||
|
{text}
|
||||||
|
{i < line.pv.length - 1 && ","}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unicodeMap: Record<string, Record<"w" | "b", string>> = {
|
||||||
|
K: { w: "♚", b: "♔" },
|
||||||
|
Q: { w: "♛", b: "♕" },
|
||||||
|
R: { w: "♜", b: "♖" },
|
||||||
|
B: { w: "♝", b: "♗" },
|
||||||
|
N: { w: "♞", b: "♘" },
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useMemo } from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { capitalize } from "@/lib/helpers";
|
import { capitalize } from "@/lib/helpers";
|
||||||
import { useChessActions } from "@/hooks/useChessActions";
|
import { useChessActions } from "@/hooks/useChessActions";
|
||||||
import { moveClassificationColors } from "@/lib/chess";
|
import { CLASSIFICATION_COLORS } from "@/constants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
classification: MoveClassification;
|
classification: MoveClassification;
|
||||||
@@ -74,7 +74,7 @@ export default function ClassificationRow({ classification }: Props) {
|
|||||||
justifyContent="space-evenly"
|
justifyContent="space-evenly"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
color={moveClassificationColors[classification]}
|
color={CLASSIFICATION_COLORS[classification]}
|
||||||
size={12}
|
size={12}
|
||||||
>
|
>
|
||||||
<Grid
|
<Grid
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { boardAtom, currentPositionAtom, gameAtom } from "../../../states";
|
|||||||
import { useChessActions } from "@/hooks/useChessActions";
|
import { useChessActions } from "@/hooks/useChessActions";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { isInViewport } from "@/lib/helpers";
|
import { isInViewport } from "@/lib/helpers";
|
||||||
import { moveClassificationColors } from "@/lib/chess";
|
import { CLASSIFICATION_COLORS } from "@/constants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
san: string;
|
san: string;
|
||||||
@@ -89,7 +89,7 @@ const getMoveColor = (moveClassification?: MoveClassification) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return moveClassificationColors[moveClassification];
|
return CLASSIFICATION_COLORS[moveClassification];
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveClassificationsToIgnore: MoveClassification[] = [
|
const moveClassificationsToIgnore: MoveClassification[] = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DotProps } from "recharts";
|
import { DotProps } from "recharts";
|
||||||
import { ChartItemData } from "./types";
|
import { ChartItemData } from "./types";
|
||||||
import { moveClassificationColors } from "@/lib/chess";
|
import { CLASSIFICATION_COLORS } from "@/constants";
|
||||||
|
|
||||||
export default function CustomDot({
|
export default function CustomDot({
|
||||||
cx,
|
cx,
|
||||||
@@ -9,7 +9,7 @@ export default function CustomDot({
|
|||||||
payload,
|
payload,
|
||||||
}: DotProps & { payload?: ChartItemData }) {
|
}: DotProps & { payload?: ChartItemData }) {
|
||||||
const moveColor = payload?.moveClassification
|
const moveColor = payload?.moveClassification
|
||||||
? moveClassificationColors[payload.moveClassification]
|
? CLASSIFICATION_COLORS[payload.moveClassification]
|
||||||
: "grey";
|
: "grey";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type { ReactElement } from "react";
|
|||||||
import CustomTooltip from "./tooltip";
|
import CustomTooltip from "./tooltip";
|
||||||
import { ChartItemData } from "./types";
|
import { ChartItemData } from "./types";
|
||||||
import { PositionEval } from "@/types/eval";
|
import { PositionEval } from "@/types/eval";
|
||||||
import { moveClassificationColors } from "@/lib/chess";
|
import { CLASSIFICATION_COLORS } from "@/constants";
|
||||||
import CustomDot from "./dot";
|
import CustomDot from "./dot";
|
||||||
import { MoveClassification } from "@/types/enums";
|
import { MoveClassification } from "@/types/enums";
|
||||||
import { useChessActions } from "@/hooks/useChessActions";
|
import { useChessActions } from "@/hooks/useChessActions";
|
||||||
@@ -51,7 +51,7 @@ export default function GraphTab(props: GridProps) {
|
|||||||
}, [chartData]);
|
}, [chartData]);
|
||||||
|
|
||||||
const boardMoveColor = currentPosition.eval?.moveClassification
|
const boardMoveColor = currentPosition.eval?.moveClassification
|
||||||
? moveClassificationColors[currentPosition.eval.moveClassification]
|
? CLASSIFICATION_COLORS[currentPosition.eval.moveClassification]
|
||||||
: "grey";
|
: "grey";
|
||||||
|
|
||||||
// Render a dot only on selected classifications (always returns an element)
|
// Render a dot only on selected classifications (always returns an element)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useChessActions } from "@/hooks/useChessActions";
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
export default function NextMoveButton() {
|
export default function NextMoveButton() {
|
||||||
const { makeMove: makeBoardMove } = useChessActions(boardAtom);
|
const { playMove: playBoardMove } = useChessActions(boardAtom);
|
||||||
const game = useAtomValue(gameAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
const board = useAtomValue(boardAtom);
|
const board = useAtomValue(boardAtom);
|
||||||
|
|
||||||
@@ -27,14 +27,14 @@ export default function NextMoveButton() {
|
|||||||
.find((c) => c.fen === nextMove.after)?.comment;
|
.find((c) => c.fen === nextMove.after)?.comment;
|
||||||
|
|
||||||
if (nextMove) {
|
if (nextMove) {
|
||||||
makeBoardMove({
|
playBoardMove({
|
||||||
from: nextMove.from,
|
from: nextMove.from,
|
||||||
to: nextMove.to,
|
to: nextMove.to,
|
||||||
promotion: nextMove.promotion,
|
promotion: nextMove.promotion,
|
||||||
comment,
|
comment,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isButtonEnabled, boardHistory, game, makeBoardMove]);
|
}, [isButtonEnabled, boardHistory, game, playBoardMove]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_ENGINE } from "@/constants";
|
||||||
import { EngineName } from "@/types/enums";
|
import { EngineName } from "@/types/enums";
|
||||||
import { CurrentPosition, GameEval, SavedEvals } from "@/types/eval";
|
import { CurrentPosition, GameEval, SavedEvals } from "@/types/eval";
|
||||||
import { Chess } from "chess.js";
|
import { Chess } from "chess.js";
|
||||||
@@ -12,7 +13,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 engineNameAtom = atom<EngineName>(EngineName.Stockfish17Lite);
|
export const engineNameAtom = atom<EngineName>(DEFAULT_ENGINE);
|
||||||
export const engineDepthAtom = atom(14);
|
export const engineDepthAtom = atom(14);
|
||||||
export const engineMultiPvAtom = atom(3);
|
export const engineMultiPvAtom = atom(3);
|
||||||
export const evaluationProgressAtom = atom(0);
|
export const evaluationProgressAtom = atom(0);
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ import { isEngineSupported } from "@/lib/engine/shared";
|
|||||||
import { Stockfish16_1 } from "@/lib/engine/stockfish16_1";
|
import { Stockfish16_1 } from "@/lib/engine/stockfish16_1";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { boardHueAtom, pieceSetAtom } from "@/components/board/states";
|
import { boardHueAtom, pieceSetAtom } from "@/components/board/states";
|
||||||
import { PIECE_SETS } from "@/components/board/constants";
|
import {
|
||||||
|
DEFAULT_ENGINE,
|
||||||
|
ENGINE_LABELS,
|
||||||
|
PIECE_SETS,
|
||||||
|
STRONGEST_ENGINE,
|
||||||
|
} from "@/constants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -76,10 +81,11 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
|
|||||||
size={{ xs: 12, sm: 7, md: 8 }}
|
size={{ xs: 12, sm: 7, md: 8 }}
|
||||||
>
|
>
|
||||||
<Typography>
|
<Typography>
|
||||||
Stockfish 17 Lite is the default engine if your device support its
|
{ENGINE_LABELS[DEFAULT_ENGINE].small} is the default engine if
|
||||||
requirements. It offers the best balance between speed and
|
your device support its requirements. It offers the best balance
|
||||||
strength. Stockfish 17 is the strongest engine available, note
|
between speed and strength.{" "}
|
||||||
that it requires a one time download of 75MB.
|
{ENGINE_LABELS[STRONGEST_ENGINE].small} is the strongest engine
|
||||||
|
available, note that it requires a one time download of 75MB.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -105,7 +111,7 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
|
|||||||
value={engine}
|
value={engine}
|
||||||
disabled={!isEngineSupported(engine)}
|
disabled={!isEngineSupported(engine)}
|
||||||
>
|
>
|
||||||
{engineLabel[engine].full}
|
{ENGINE_LABELS[engine].full}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -183,35 +189,3 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const engineLabel: Record<EngineName, { small: string; full: string }> =
|
|
||||||
{
|
|
||||||
[EngineName.Stockfish17]: {
|
|
||||||
full: "Stockfish 17 (75MB)",
|
|
||||||
small: "Stockfish 17",
|
|
||||||
},
|
|
||||||
[EngineName.Stockfish17Lite]: {
|
|
||||||
full: "Stockfish 17 Lite (6MB)",
|
|
||||||
small: "Stockfish 17 Lite",
|
|
||||||
},
|
|
||||||
[EngineName.Stockfish16_1]: {
|
|
||||||
full: "Stockfish 16.1 (64MB)",
|
|
||||||
small: "Stockfish 16.1",
|
|
||||||
},
|
|
||||||
[EngineName.Stockfish16_1Lite]: {
|
|
||||||
full: "Stockfish 16.1 Lite (6MB)",
|
|
||||||
small: "Stockfish 16.1 Lite",
|
|
||||||
},
|
|
||||||
[EngineName.Stockfish16NNUE]: {
|
|
||||||
full: "Stockfish 16 (40MB)",
|
|
||||||
small: "Stockfish 16",
|
|
||||||
},
|
|
||||||
[EngineName.Stockfish16]: {
|
|
||||||
full: "Stockfish 16 Lite (HCE)",
|
|
||||||
small: "Stockfish 16 Lite",
|
|
||||||
},
|
|
||||||
[EngineName.Stockfish11]: {
|
|
||||||
full: "Stockfish 11 (HCE)",
|
|
||||||
small: "Stockfish 11",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function BoardContainer() {
|
|||||||
const game = useAtomValue(gameAtom);
|
const game = useAtomValue(gameAtom);
|
||||||
const { white, black } = usePlayersData(gameAtom);
|
const { white, black } = usePlayersData(gameAtom);
|
||||||
const playerColor = useAtomValue(playerColorAtom);
|
const playerColor = useAtomValue(playerColorAtom);
|
||||||
const { makeMove: makeGameMove } = useChessActions(gameAtom);
|
const { playMove } = useChessActions(gameAtom);
|
||||||
const engineElo = useAtomValue(engineEloAtom);
|
const engineElo = useAtomValue(engineEloAtom);
|
||||||
const isGameInProgress = useAtomValue(isGameInProgressAtom);
|
const isGameInProgress = useAtomValue(isGameInProgressAtom);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export default function BoardContainer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const move = await engine.getEngineNextMove(gameFen, engineElo);
|
const move = await engine.getEngineNextMove(gameFen, engineElo);
|
||||||
if (move) makeGameMove(uciMoveParams(move));
|
if (move) playMove(uciMoveParams(move));
|
||||||
};
|
};
|
||||||
playEngineMove();
|
playEngineMove();
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { logAnalyticsEvent } from "@/lib/firebase";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { isEngineSupported } from "@/lib/engine/shared";
|
import { isEngineSupported } from "@/lib/engine/shared";
|
||||||
import { Stockfish16_1 } from "@/lib/engine/stockfish16_1";
|
import { Stockfish16_1 } from "@/lib/engine/stockfish16_1";
|
||||||
import { engineLabel } from "@/sections/engineSettings/engineSettingsDialog";
|
import { DEFAULT_ENGINE, ENGINE_LABELS, STRONGEST_ENGINE } from "@/constants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -57,12 +57,12 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
|
|||||||
resetGame({
|
resetGame({
|
||||||
white: {
|
white: {
|
||||||
name:
|
name:
|
||||||
playerColor === Color.White ? "You" : engineLabel[engineName].small,
|
playerColor === Color.White ? "You" : ENGINE_LABELS[engineName].small,
|
||||||
rating: playerColor === Color.White ? undefined : engineElo,
|
rating: playerColor === Color.White ? undefined : engineElo,
|
||||||
},
|
},
|
||||||
black: {
|
black: {
|
||||||
name:
|
name:
|
||||||
playerColor === Color.Black ? "You" : engineLabel[engineName].small,
|
playerColor === Color.Black ? "You" : ENGINE_LABELS[engineName].small,
|
||||||
rating: playerColor === Color.Black ? undefined : engineElo,
|
rating: playerColor === Color.Black ? undefined : engineElo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -93,10 +93,11 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent sx={{ paddingBottom: 0 }}>
|
<DialogContent sx={{ paddingBottom: 0 }}>
|
||||||
<Typography>
|
<Typography>
|
||||||
Stockfish 17 Lite is the default engine if your device support its
|
{ENGINE_LABELS[DEFAULT_ENGINE].small} is the default engine if your
|
||||||
requirements. It offers the best balance between speed and strength.
|
device support its requirements. It offers the best balance between
|
||||||
Stockfish 17 is the strongest engine available, note that it requires
|
speed and strength. {ENGINE_LABELS[STRONGEST_ENGINE].small} is the
|
||||||
a one time download of 75MB.
|
strongest engine available, note that it requires a one time download
|
||||||
|
of 75MB.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid
|
<Grid
|
||||||
marginTop={4}
|
marginTop={4}
|
||||||
@@ -124,7 +125,7 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
|
|||||||
value={engine}
|
value={engine}
|
||||||
disabled={!isEngineSupported(engine)}
|
disabled={!isEngineSupported(engine)}
|
||||||
>
|
>
|
||||||
{engineLabel[engine].full}
|
{ENGINE_LABELS[engine].full}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_ENGINE } from "@/constants";
|
||||||
import { Color, EngineName } from "@/types/enums";
|
import { Color, EngineName } from "@/types/enums";
|
||||||
import { CurrentPosition } from "@/types/eval";
|
import { CurrentPosition } from "@/types/eval";
|
||||||
import { Chess } from "chess.js";
|
import { Chess } from "chess.js";
|
||||||
@@ -6,6 +7,6 @@ import { atom } from "jotai";
|
|||||||
export const gameAtom = atom(new Chess());
|
export const gameAtom = atom(new Chess());
|
||||||
export const gameDataAtom = atom<CurrentPosition>({});
|
export const gameDataAtom = atom<CurrentPosition>({});
|
||||||
export const playerColorAtom = atom<Color>(Color.White);
|
export const playerColorAtom = atom<Color>(Color.White);
|
||||||
export const enginePlayNameAtom = atom<EngineName>(EngineName.Stockfish17Lite);
|
export const enginePlayNameAtom = atom<EngineName>(DEFAULT_ENGINE);
|
||||||
export const engineEloAtom = atom(1320);
|
export const engineEloAtom = atom(1320);
|
||||||
export const isGameInProgressAtom = atom(false);
|
export const isGameInProgressAtom = atom(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user