commit dfc79cf287823383a25a650d5788ee5250b1c316
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date: Sun May 11 01:32:35 2025 +0200
fix : style
commit bccfa5a3358302c2f037cc2dcfbd0a1df5e2974e
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date: Sun May 11 01:01:12 2025 +0200
feat : players clocks v1
commit 5f65009f200686433904710d5f9ceb1ba166fa9d
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date: Sat May 10 21:58:02 2025 +0200
fix : merge issues
commit f93dc6104e2d3fbb60088f578c2d1f13bf6519e9
Merge: a9f3728 fea1f3f
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date: Sat May 10 21:53:11 2025 +0200
Merge branch 'main' into feat/add-players-clocks
commit a9f372808ef403dfb823c4cf93c837412cc55c53
Author: GuillaumeSD <gsd.lfny@gmail.com>
Date: Mon Jan 6 23:10:28 2025 +0100
fix : rename
commit aedf9c252023bebe4da4327b7526371fa75b7b3e
Author: GuillaumeSD <gsd.lfny@gmail.com>
Date: Sun Jan 5 17:30:27 2025 +0100
feat : add players clocks
342 lines
8.8 KiB
TypeScript
342 lines
8.8 KiB
TypeScript
import { EvaluateGameParams, LineEval, PositionEval } from "@/types/eval";
|
|
import { Game, Player } from "@/types/game";
|
|
import { Chess, PieceSymbol, Square } from "chess.js";
|
|
import { getPositionWinPercentage } from "./engine/helpers/winPercentage";
|
|
import { Color, MoveClassification } from "@/types/enums";
|
|
|
|
export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => {
|
|
const history = game.history({ verbose: true });
|
|
|
|
const fens = history.map((move) => move.before);
|
|
fens.push(history[history.length - 1].after);
|
|
|
|
const uciMoves = history.map(
|
|
(move) => move.from + move.to + (move.promotion || "")
|
|
);
|
|
|
|
return { fens, uciMoves };
|
|
};
|
|
|
|
export const getGameFromPgn = (pgn: string): Chess => {
|
|
const game = new Chess();
|
|
game.loadPgn(pgn);
|
|
|
|
return game;
|
|
};
|
|
|
|
export const formatGameToDatabase = (game: Chess): Omit<Game, "id"> => {
|
|
const headers: Record<string, string | undefined> = game.getHeaders();
|
|
|
|
return {
|
|
pgn: game.pgn(),
|
|
event: headers.Event,
|
|
site: headers.Site,
|
|
date: headers.Date,
|
|
round: headers.Round ?? "?",
|
|
white: {
|
|
name: headers.White || "White",
|
|
rating: headers.WhiteElo ? Number(headers.WhiteElo) : undefined,
|
|
},
|
|
black: {
|
|
name: headers.Black || "Black",
|
|
rating: headers.BlackElo ? Number(headers.BlackElo) : undefined,
|
|
},
|
|
result: headers.Result,
|
|
termination: headers.Termination,
|
|
timeControl: headers.TimeControl,
|
|
};
|
|
};
|
|
|
|
export const getGameToSave = (game: Chess, board: Chess): Chess => {
|
|
if (game.history().length) return game;
|
|
return setGameHeaders(board);
|
|
};
|
|
|
|
export const setGameHeaders = (
|
|
game: Chess,
|
|
params: { white?: Player; black?: Player; resigned?: Color } = {}
|
|
): Chess => {
|
|
game.setHeader("Event", "Chesskit Game");
|
|
game.setHeader("Site", "Chesskit.org");
|
|
game.setHeader(
|
|
"Date",
|
|
new Date().toISOString().split("T")[0].replaceAll("-", ".")
|
|
);
|
|
|
|
const { white, black, resigned } = params;
|
|
|
|
if (white?.name) game.setHeader("White", white.name);
|
|
if (black?.name) game.setHeader("Black", black.name);
|
|
|
|
if (white?.rating) game.setHeader("WhiteElo", `${white.rating}`);
|
|
if (black?.rating) game.setHeader("BlackElo", `${black.rating}`);
|
|
|
|
const whiteNameToUse = game.getHeaders().White || "White";
|
|
const blackNameToUse = game.getHeaders().Black || "Black";
|
|
|
|
if (resigned) {
|
|
game.setHeader("Result", resigned === "w" ? "0-1" : "1-0");
|
|
game.setHeader(
|
|
"Termination",
|
|
`${resigned === "w" ? blackNameToUse : whiteNameToUse} won by resignation`
|
|
);
|
|
}
|
|
|
|
if (!game.isGameOver()) return game;
|
|
|
|
if (game.isCheckmate()) {
|
|
game.setHeader("Result", game.turn() === "w" ? "0-1" : "1-0");
|
|
game.setHeader(
|
|
"Termination",
|
|
`${
|
|
game.turn() === "w" ? blackNameToUse : whiteNameToUse
|
|
} won by checkmate`
|
|
);
|
|
}
|
|
|
|
if (game.isInsufficientMaterial()) {
|
|
game.setHeader("Result", "1/2-1/2");
|
|
game.setHeader("Termination", "Draw by insufficient material");
|
|
}
|
|
|
|
if (game.isStalemate()) {
|
|
game.setHeader("Result", "1/2-1/2");
|
|
game.setHeader("Termination", "Draw by stalemate");
|
|
}
|
|
|
|
if (game.isThreefoldRepetition()) {
|
|
game.setHeader("Result", "1/2-1/2");
|
|
game.setHeader("Termination", "Draw by threefold repetition");
|
|
}
|
|
|
|
return game;
|
|
};
|
|
|
|
export const moveLineUciToSan = (
|
|
fen: string
|
|
): ((moveUci: string) => string) => {
|
|
const game = new Chess(fen);
|
|
|
|
return (moveUci: string): string => {
|
|
try {
|
|
const move = game.move(uciMoveParams(moveUci));
|
|
return move.san;
|
|
} catch {
|
|
return moveUci;
|
|
}
|
|
};
|
|
};
|
|
|
|
export const getEvaluationBarValue = (
|
|
position: PositionEval
|
|
): { whiteBarPercentage: number; label: string } => {
|
|
const whiteBarPercentage = getPositionWinPercentage(position);
|
|
const bestLine = position.lines[0];
|
|
|
|
if (bestLine.mate) {
|
|
return { label: `M${Math.abs(bestLine.mate)}`, whiteBarPercentage };
|
|
}
|
|
|
|
const cp = bestLine.cp;
|
|
if (!cp) return { whiteBarPercentage, label: "0.0" };
|
|
|
|
const pEval = Math.abs(cp) / 100;
|
|
let label = pEval.toFixed(1);
|
|
|
|
if (label.toString().length > 3) {
|
|
label = pEval.toFixed(0);
|
|
}
|
|
|
|
return { whiteBarPercentage, label };
|
|
};
|
|
|
|
export const getIsStalemate = (fen: string): boolean => {
|
|
const game = new Chess(fen);
|
|
return game.isStalemate();
|
|
};
|
|
|
|
export const getWhoIsCheckmated = (fen: string): "w" | "b" | null => {
|
|
const game = new Chess(fen);
|
|
if (!game.isCheckmate()) return null;
|
|
return game.turn();
|
|
};
|
|
|
|
export const uciMoveParams = (
|
|
uciMove: string
|
|
): {
|
|
from: Square;
|
|
to: Square;
|
|
promotion?: string | undefined;
|
|
} => ({
|
|
from: uciMove.slice(0, 2) as Square,
|
|
to: uciMove.slice(2, 4) as Square,
|
|
promotion: uciMove.slice(4, 5) || undefined,
|
|
});
|
|
|
|
export const isSimplePieceRecapture = (
|
|
fen: string,
|
|
uciMoves: [string, string]
|
|
): boolean => {
|
|
const game = new Chess(fen);
|
|
const moves = uciMoves.map((uciMove) => uciMoveParams(uciMove));
|
|
|
|
if (moves[0].to !== moves[1].to) return false;
|
|
|
|
const piece = game.get(moves[0].to);
|
|
if (piece) return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
export const getIsPieceSacrifice = (
|
|
fen: string,
|
|
playedMove: string,
|
|
bestLinePvToPlay: string[]
|
|
): boolean => {
|
|
if (!bestLinePvToPlay.length) return false;
|
|
|
|
const game = new Chess(fen);
|
|
const whiteToPlay = game.turn() === "w";
|
|
const startingMaterialDifference = getMaterialDifference(fen);
|
|
|
|
let moves = [playedMove, ...bestLinePvToPlay];
|
|
if (moves.length % 2 === 1) {
|
|
moves = moves.slice(0, -1);
|
|
}
|
|
let nonCapturingMovesTemp = 1;
|
|
|
|
const capturedPieces: { w: PieceSymbol[]; b: PieceSymbol[] } = {
|
|
w: [],
|
|
b: [],
|
|
};
|
|
for (const move of moves) {
|
|
try {
|
|
const fullMove = game.move(uciMoveParams(move));
|
|
if (fullMove.captured) {
|
|
capturedPieces[fullMove.color].push(fullMove.captured);
|
|
nonCapturingMovesTemp = 1;
|
|
} else {
|
|
nonCapturingMovesTemp--;
|
|
if (nonCapturingMovesTemp < 0) break;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (const p of capturedPieces["w"].slice(0)) {
|
|
if (capturedPieces["b"].includes(p)) {
|
|
capturedPieces["b"].splice(capturedPieces["b"].indexOf(p), 1);
|
|
capturedPieces["w"].splice(capturedPieces["w"].indexOf(p), 1);
|
|
}
|
|
}
|
|
|
|
if (
|
|
Math.abs(capturedPieces["w"].length - capturedPieces["b"].length) <= 1 &&
|
|
capturedPieces["w"].concat(capturedPieces["b"]).every((p) => p === "p")
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const endingMaterialDifference = getMaterialDifference(game.fen());
|
|
|
|
const materialDiff = endingMaterialDifference - startingMaterialDifference;
|
|
const materialDiffPlayerRelative = whiteToPlay ? materialDiff : -materialDiff;
|
|
|
|
return materialDiffPlayerRelative < 0;
|
|
};
|
|
|
|
export const getMaterialDifference = (fen: string): number => {
|
|
const game = new Chess(fen);
|
|
const board = game.board().flat();
|
|
|
|
return board.reduce((acc, square) => {
|
|
if (!square) return acc;
|
|
const piece = square.type;
|
|
|
|
if (square.color === "w") {
|
|
return acc + getPieceValue(piece);
|
|
}
|
|
|
|
return acc - getPieceValue(piece);
|
|
}, 0);
|
|
};
|
|
|
|
const getPieceValue = (piece: PieceSymbol): number => {
|
|
switch (piece) {
|
|
case "p":
|
|
return 1;
|
|
case "n":
|
|
return 3;
|
|
case "b":
|
|
return 3;
|
|
case "r":
|
|
return 5;
|
|
case "q":
|
|
return 9;
|
|
default:
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
export const isCheck = (fen: string): boolean => {
|
|
const game = new Chess(fen);
|
|
return game.inCheck();
|
|
};
|
|
|
|
export const getCapturedPieces = (
|
|
fen: string,
|
|
color: Color
|
|
): Record<string, number | undefined> => {
|
|
const capturedPieces: Record<string, number | undefined> = {};
|
|
if (color === Color.White) {
|
|
capturedPieces.p = 8;
|
|
capturedPieces.r = 2;
|
|
capturedPieces.n = 2;
|
|
capturedPieces.b = 2;
|
|
capturedPieces.q = 1;
|
|
} else {
|
|
capturedPieces.P = 8;
|
|
capturedPieces.R = 2;
|
|
capturedPieces.N = 2;
|
|
capturedPieces.B = 2;
|
|
capturedPieces.Q = 1;
|
|
}
|
|
|
|
const fenPiecePlacement = fen.split(" ")[0];
|
|
for (const piece of Object.keys(capturedPieces)) {
|
|
const count = fenPiecePlacement.match(new RegExp(piece, "g"))?.length;
|
|
if (count) capturedPieces[piece] = (capturedPieces[piece] ?? 0) - count;
|
|
}
|
|
|
|
return capturedPieces;
|
|
};
|
|
|
|
export const getLineEvalLabel = (
|
|
line: Pick<LineEval, "cp" | "mate">
|
|
): string => {
|
|
if (line.cp !== undefined) {
|
|
return `${line.cp > 0 ? "+" : ""}${(line.cp / 100).toFixed(2)}`;
|
|
}
|
|
|
|
if (line.mate) {
|
|
return `${line.mate > 0 ? "+" : "-"}M${Math.abs(line.mate)}`;
|
|
}
|
|
|
|
return "?";
|
|
};
|
|
|
|
export const moveClassificationColors: 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",
|
|
};
|