Files
chesskit/src/lib/chess.ts
2024-04-17 02:02:22 +02:00

298 lines
7.4 KiB
TypeScript

import { EvaluateGameParams, PositionEval } from "@/types/eval";
import { Game } from "@/types/game";
import { Chess, PieceSymbol, Square } from "chess.js";
import { getPositionWinPercentage } from "./engine/helpers/winPercentage";
import { Color } 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.header();
return {
pgn: game.pgn(),
event: headers.Event,
site: headers.Site,
date: headers.Date,
round: headers.Round ?? "?",
white: {
name: headers.White,
rating: headers.WhiteElo ? Number(headers.WhiteElo) : undefined,
},
black: {
name: headers.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: { whiteName?: string; blackName?: string; resigned?: Color } = {}
): Chess => {
game.header("Event", "Freechess Game");
game.header("Site", "Freechess");
game.header(
"Date",
new Date().toISOString().split("T")[0].replaceAll("-", ".")
);
const { whiteName, blackName, resigned } = params;
if (whiteName) game.header("White", whiteName);
if (blackName) game.header("Black", blackName);
const whiteNameToUse = game.header().White || "White";
const blackNameToUse = game.header().Black || "Black";
if (resigned) {
game.header("Result", resigned === "w" ? "0-1" : "1-0");
game.header(
"Termination",
`${resigned === "w" ? blackNameToUse : whiteNameToUse} won by resignation`
);
}
if (!game.isGameOver()) return game;
if (game.isCheckmate()) {
game.header("Result", game.turn() === "w" ? "0-1" : "1-0");
game.header(
"Termination",
`${
game.turn() === "w" ? blackNameToUse : whiteNameToUse
} won by checkmate`
);
}
if (game.isInsufficientMaterial()) {
game.header("Result", "1/2-1/2");
game.header("Termination", "Draw by insufficient material");
}
if (game.isStalemate()) {
game.header("Result", "1/2-1/2");
game.header("Termination", "Draw by stalemate");
}
if (game.isThreefoldRepetition()) {
game.header("Result", "1/2-1/2");
game.header("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 (e) {
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 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 ||
bestLinePvToPlay[0].slice(2, 4) !== playedMove.slice(2, 4)
)
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;
for (const move of moves) {
const fullMove = game.move(uciMoveParams(move));
if (fullMove.captured) {
nonCapturingMovesTemp = 1;
} else {
nonCapturingMovesTemp--;
if (nonCapturingMovesTemp < 0) break;
}
}
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 getStartingFen = (
params: { pgn: string } | { game: Chess }
): string => {
const game = "game" in params ? params.game : getGameFromPgn(params.pgn);
const history = game.history({ verbose: true });
if (!history.length) return game.fen();
return history[0].before;
};
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;
};