@connectorvol/chessops 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +674 -0
- package/README.md +61 -0
- package/dist/cjs/attacks.js +152 -0
- package/dist/cjs/attacks.js.map +1 -0
- package/dist/cjs/board.js +143 -0
- package/dist/cjs/board.js.map +1 -0
- package/dist/cjs/chess.js +638 -0
- package/dist/cjs/chess.js.map +1 -0
- package/dist/cjs/compat.js +89 -0
- package/dist/cjs/compat.js.map +1 -0
- package/dist/cjs/debug.js +103 -0
- package/dist/cjs/debug.js.map +1 -0
- package/dist/cjs/fen.js +325 -0
- package/dist/cjs/fen.js.map +1 -0
- package/dist/cjs/index.js +94 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/pgn.js +796 -0
- package/dist/cjs/pgn.js.map +1 -0
- package/dist/cjs/san.js +174 -0
- package/dist/cjs/san.js.map +1 -0
- package/dist/cjs/setup.js +167 -0
- package/dist/cjs/setup.js.map +1 -0
- package/dist/cjs/squareSet.js +206 -0
- package/dist/cjs/squareSet.js.map +1 -0
- package/dist/cjs/transform.js +57 -0
- package/dist/cjs/transform.js.map +1 -0
- package/dist/cjs/types.js +24 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/util.js +104 -0
- package/dist/cjs/util.js.map +1 -0
- package/dist/cjs/variant.js +833 -0
- package/dist/cjs/variant.js.map +1 -0
- package/dist/esm/attacks.js +140 -0
- package/dist/esm/attacks.js.map +1 -0
- package/dist/esm/board.js +138 -0
- package/dist/esm/board.js.map +1 -0
- package/dist/esm/chess.js +624 -0
- package/dist/esm/chess.js.map +1 -0
- package/dist/esm/compat.js +81 -0
- package/dist/esm/compat.js.map +1 -0
- package/dist/esm/debug.js +94 -0
- package/dist/esm/debug.js.map +1 -0
- package/dist/esm/fen.js +308 -0
- package/dist/esm/fen.js.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/pgn.js +769 -0
- package/dist/esm/pgn.js.map +1 -0
- package/dist/esm/san.js +167 -0
- package/dist/esm/san.js.map +1 -0
- package/dist/esm/setup.js +157 -0
- package/dist/esm/setup.js.map +1 -0
- package/dist/esm/squareSet.js +202 -0
- package/dist/esm/squareSet.js.map +1 -0
- package/dist/esm/transform.js +48 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.js +19 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/util.js +87 -0
- package/dist/esm/util.js.map +1 -0
- package/dist/esm/variant.js +812 -0
- package/dist/esm/variant.js.map +1 -0
- package/dist/types/attacks.d.ts +58 -0
- package/dist/types/board.d.ts +62 -0
- package/dist/types/chess.d.ts +82 -0
- package/dist/types/compat.d.ts +26 -0
- package/dist/types/debug.d.ts +10 -0
- package/dist/types/fen.d.ts +40 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/pgn.d.ts +203 -0
- package/dist/types/san.d.ts +6 -0
- package/dist/types/setup.d.ts +65 -0
- package/dist/types/squareSet.d.ts +50 -0
- package/dist/types/transform.d.ts +9 -0
- package/dist/types/types.d.ts +58 -0
- package/dist/types/util.d.ts +21 -0
- package/dist/types/variant.d.ts +92 -0
- package/package.json +86 -0
- package/src/attacks.ts +160 -0
- package/src/board.ts +168 -0
- package/src/chess.ts +687 -0
- package/src/compat.ts +120 -0
- package/src/debug.ts +100 -0
- package/src/fen.ts +328 -0
- package/src/index.ts +85 -0
- package/src/pgn.ts +876 -0
- package/src/san.ts +190 -0
- package/src/setup.ts +203 -0
- package/src/squareSet.ts +243 -0
- package/src/transform.ts +49 -0
- package/src/types.ts +93 -0
- package/src/util.ts +116 -0
- package/src/variant.ts +939 -0
package/src/compat.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility with other libraries.
|
|
3
|
+
*
|
|
4
|
+
* Convert between the formats used by chessops,
|
|
5
|
+
* [chessground](https://github.com/lichess-org/chessground),
|
|
6
|
+
* and [scalachess](https://github.com/lichess-org/scalachess).
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Position } from "./chess.js";
|
|
12
|
+
import { isDrop, Move, Rules, SquareName } from "./types.js";
|
|
13
|
+
import { makeSquare, squareFile } from "./util.js";
|
|
14
|
+
|
|
15
|
+
export interface ChessgroundDestsOpts {
|
|
16
|
+
chess960?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Computes the legal move destinations in the format used by chessground.
|
|
21
|
+
*
|
|
22
|
+
* Includes both possible representations of castling moves (unless
|
|
23
|
+
* `chess960` mode is enabled), so that the `rookCastles` option will work
|
|
24
|
+
* correctly.
|
|
25
|
+
*/
|
|
26
|
+
export const chessgroundDests = (
|
|
27
|
+
pos: Position,
|
|
28
|
+
opts?: ChessgroundDestsOpts,
|
|
29
|
+
): Map<SquareName, SquareName[]> => {
|
|
30
|
+
const result = new Map();
|
|
31
|
+
const ctx = pos.ctx();
|
|
32
|
+
for (const [from, squares] of pos.allDests(ctx)) {
|
|
33
|
+
if (squares.nonEmpty()) {
|
|
34
|
+
const d = Array.from(squares, makeSquare);
|
|
35
|
+
if (!opts?.chess960 && from === ctx.king && squareFile(from) === 4) {
|
|
36
|
+
// Chessground needs both types of castling dests and filters based on
|
|
37
|
+
// a rookCastles setting.
|
|
38
|
+
if (squares.has(0)) d.push("c1");
|
|
39
|
+
else if (squares.has(56)) d.push("c8");
|
|
40
|
+
if (squares.has(7)) d.push("g1");
|
|
41
|
+
else if (squares.has(63)) d.push("g8");
|
|
42
|
+
}
|
|
43
|
+
result.set(makeSquare(from), d);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const chessgroundMove = (move: Move): SquareName[] =>
|
|
50
|
+
isDrop(move) ? [makeSquare(move.to)] : [makeSquare(move.from), makeSquare(move.to)];
|
|
51
|
+
|
|
52
|
+
export const scalachessCharPair = (move: Move): string =>
|
|
53
|
+
isDrop(move)
|
|
54
|
+
? String.fromCharCode(
|
|
55
|
+
35 + move.to,
|
|
56
|
+
35 + 64 + 8 * 5 + ["queen", "rook", "bishop", "knight", "pawn"].indexOf(move.role),
|
|
57
|
+
)
|
|
58
|
+
: String.fromCharCode(
|
|
59
|
+
35 + move.from,
|
|
60
|
+
move.promotion
|
|
61
|
+
? 35 +
|
|
62
|
+
64 +
|
|
63
|
+
8 * ["queen", "rook", "bishop", "knight", "king"].indexOf(move.promotion) +
|
|
64
|
+
squareFile(move.to)
|
|
65
|
+
: 35 + move.to,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export const lichessRules = (
|
|
69
|
+
variant:
|
|
70
|
+
| "standard"
|
|
71
|
+
| "chess960"
|
|
72
|
+
| "antichess"
|
|
73
|
+
| "fromPosition"
|
|
74
|
+
| "kingOfTheHill"
|
|
75
|
+
| "threeCheck"
|
|
76
|
+
| "atomic"
|
|
77
|
+
| "horde"
|
|
78
|
+
| "racingKings"
|
|
79
|
+
| "crazyhouse",
|
|
80
|
+
): Rules => {
|
|
81
|
+
switch (variant) {
|
|
82
|
+
case "standard":
|
|
83
|
+
case "chess960":
|
|
84
|
+
case "fromPosition":
|
|
85
|
+
return "chess";
|
|
86
|
+
case "threeCheck":
|
|
87
|
+
return "3check";
|
|
88
|
+
case "kingOfTheHill":
|
|
89
|
+
return "kingofthehill";
|
|
90
|
+
case "racingKings":
|
|
91
|
+
return "racingkings";
|
|
92
|
+
default:
|
|
93
|
+
return variant;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const lichessVariant = (
|
|
98
|
+
rules: Rules,
|
|
99
|
+
):
|
|
100
|
+
| "standard"
|
|
101
|
+
| "antichess"
|
|
102
|
+
| "kingOfTheHill"
|
|
103
|
+
| "threeCheck"
|
|
104
|
+
| "atomic"
|
|
105
|
+
| "horde"
|
|
106
|
+
| "racingKings"
|
|
107
|
+
| "crazyhouse" => {
|
|
108
|
+
switch (rules) {
|
|
109
|
+
case "chess":
|
|
110
|
+
return "standard";
|
|
111
|
+
case "3check":
|
|
112
|
+
return "threeCheck";
|
|
113
|
+
case "kingofthehill":
|
|
114
|
+
return "kingOfTheHill";
|
|
115
|
+
case "racingkings":
|
|
116
|
+
return "racingKings";
|
|
117
|
+
default:
|
|
118
|
+
return rules;
|
|
119
|
+
}
|
|
120
|
+
};
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Board } from "./board.js";
|
|
2
|
+
import { Position } from "./chess.js";
|
|
3
|
+
import { makePiece } from "./fen.js";
|
|
4
|
+
import { SquareSet } from "./squareSet.js";
|
|
5
|
+
import { Piece, Role, ROLES, Square } from "./types.js";
|
|
6
|
+
import { makeSquare, makeUci, opposite, squareRank } from "./util.js";
|
|
7
|
+
|
|
8
|
+
export const squareSet = (squares: SquareSet): string => {
|
|
9
|
+
const r = [];
|
|
10
|
+
for (let y = 7; y >= 0; y--) {
|
|
11
|
+
for (let x = 0; x < 8; x++) {
|
|
12
|
+
const square = x + y * 8;
|
|
13
|
+
r.push(squares.has(square) ? "1" : ".");
|
|
14
|
+
r.push(x < 7 ? " " : "\n");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return r.join("");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const piece = (piece: Piece): string => makePiece(piece);
|
|
21
|
+
|
|
22
|
+
export const board = (board: Board): string => {
|
|
23
|
+
const r = [];
|
|
24
|
+
for (let y = 7; y >= 0; y--) {
|
|
25
|
+
for (let x = 0; x < 8; x++) {
|
|
26
|
+
const square = x + y * 8;
|
|
27
|
+
const p = board.get(square);
|
|
28
|
+
const col = p ? piece(p) : ".";
|
|
29
|
+
r.push(col);
|
|
30
|
+
r.push(x < 7 ? (col.length < 2 ? " " : "") : "\n");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return r.join("");
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const square = (sq: Square): string => makeSquare(sq);
|
|
37
|
+
|
|
38
|
+
export const dests = (dests: Map<Square, SquareSet>): string => {
|
|
39
|
+
const lines = [];
|
|
40
|
+
for (const [from, to] of dests) {
|
|
41
|
+
lines.push(`${makeSquare(from)}: ${Array.from(to, square).join(" ")}`);
|
|
42
|
+
}
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const perft = (pos: Position, depth: number, log = false): number => {
|
|
47
|
+
if (depth < 1) return 1;
|
|
48
|
+
|
|
49
|
+
const promotionRoles: Role[] = ["queen", "knight", "rook", "bishop"];
|
|
50
|
+
if (pos.rules === "antichess") promotionRoles.push("king");
|
|
51
|
+
|
|
52
|
+
const ctx = pos.ctx();
|
|
53
|
+
const dropDests = pos.dropDests(ctx);
|
|
54
|
+
|
|
55
|
+
if (!log && depth === 1 && dropDests.isEmpty()) {
|
|
56
|
+
// Optimization for leaf nodes.
|
|
57
|
+
let nodes = 0;
|
|
58
|
+
for (const [from, to] of pos.allDests(ctx)) {
|
|
59
|
+
nodes += to.size();
|
|
60
|
+
if (pos.board.pawn.has(from)) {
|
|
61
|
+
const backrank = SquareSet.backrank(opposite(pos.turn));
|
|
62
|
+
nodes += to.intersect(backrank).size() * (promotionRoles.length - 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return nodes;
|
|
66
|
+
} else {
|
|
67
|
+
let nodes = 0;
|
|
68
|
+
for (const [from, dests] of pos.allDests(ctx)) {
|
|
69
|
+
const promotions: Array<Role | undefined> =
|
|
70
|
+
squareRank(from) === (pos.turn === "white" ? 6 : 1) && pos.board.pawn.has(from)
|
|
71
|
+
? promotionRoles
|
|
72
|
+
: [undefined];
|
|
73
|
+
for (const to of dests) {
|
|
74
|
+
for (const promotion of promotions) {
|
|
75
|
+
const child = pos.clone();
|
|
76
|
+
const move = { from, to, promotion };
|
|
77
|
+
child.play(move);
|
|
78
|
+
const children = perft(child, depth - 1, false);
|
|
79
|
+
if (log) console.log(makeUci(move), children);
|
|
80
|
+
nodes += children;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (pos.pockets) {
|
|
85
|
+
for (const role of ROLES) {
|
|
86
|
+
if (pos.pockets[pos.turn][role] > 0) {
|
|
87
|
+
for (const to of role === "pawn" ? dropDests.diff(SquareSet.backranks()) : dropDests) {
|
|
88
|
+
const child = pos.clone();
|
|
89
|
+
const move = { role, to };
|
|
90
|
+
child.play(move);
|
|
91
|
+
const children = perft(child, depth - 1, false);
|
|
92
|
+
if (log) console.log(makeUci(move), children);
|
|
93
|
+
nodes += children;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return nodes;
|
|
99
|
+
}
|
|
100
|
+
};
|
package/src/fen.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { Result } from "@badrap/result";
|
|
2
|
+
import { Board } from "./board.js";
|
|
3
|
+
import { Material, MaterialSide, RemainingChecks, Setup } from "./setup.js";
|
|
4
|
+
import { SquareSet } from "./squareSet.js";
|
|
5
|
+
import { Color, COLORS, FILE_NAMES, Piece, ROLES, Square } from "./types.js";
|
|
6
|
+
import {
|
|
7
|
+
charToRole,
|
|
8
|
+
defined,
|
|
9
|
+
makeSquare,
|
|
10
|
+
parseSquare,
|
|
11
|
+
roleToChar,
|
|
12
|
+
squareFile,
|
|
13
|
+
squareFromCoords,
|
|
14
|
+
} from "./util.js";
|
|
15
|
+
|
|
16
|
+
export const INITIAL_BOARD_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
|
|
17
|
+
export const INITIAL_EPD = INITIAL_BOARD_FEN + " w KQkq -";
|
|
18
|
+
export const INITIAL_FEN = INITIAL_EPD + " 0 1";
|
|
19
|
+
export const EMPTY_BOARD_FEN = "8/8/8/8/8/8/8/8";
|
|
20
|
+
export const EMPTY_EPD = EMPTY_BOARD_FEN + " w - -";
|
|
21
|
+
export const EMPTY_FEN = EMPTY_EPD + " 0 1";
|
|
22
|
+
|
|
23
|
+
export enum InvalidFen {
|
|
24
|
+
Fen = "ERR_FEN",
|
|
25
|
+
Board = "ERR_BOARD",
|
|
26
|
+
Pockets = "ERR_POCKETS",
|
|
27
|
+
Turn = "ERR_TURN",
|
|
28
|
+
Castling = "ERR_CASTLING",
|
|
29
|
+
EpSquare = "ERR_EP_SQUARE",
|
|
30
|
+
RemainingChecks = "ERR_REMAINING_CHECKS",
|
|
31
|
+
Halfmoves = "ERR_HALFMOVES",
|
|
32
|
+
Fullmoves = "ERR_FULLMOVES",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class FenError extends Error {}
|
|
36
|
+
|
|
37
|
+
const nthIndexOf = (haystack: string, needle: string, n: number): number => {
|
|
38
|
+
let index = haystack.indexOf(needle);
|
|
39
|
+
while (n-- > 0) {
|
|
40
|
+
if (index === -1) break;
|
|
41
|
+
index = haystack.indexOf(needle, index + needle.length);
|
|
42
|
+
}
|
|
43
|
+
return index;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const parseSmallUint = (str: string): number | undefined =>
|
|
47
|
+
/^\d{1,4}$/.test(str) ? parseInt(str, 10) : undefined;
|
|
48
|
+
|
|
49
|
+
const charToPiece = (ch: string): Piece | undefined => {
|
|
50
|
+
const role = charToRole(ch);
|
|
51
|
+
return role && { role, color: ch.toLowerCase() === ch ? "black" : "white" };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const parseBoardFen = (boardPart: string): Result<Board, FenError> => {
|
|
55
|
+
const board = Board.empty();
|
|
56
|
+
let rank = 7;
|
|
57
|
+
let file = 0;
|
|
58
|
+
for (let i = 0; i < boardPart.length; i++) {
|
|
59
|
+
const c = boardPart[i];
|
|
60
|
+
if (c === "/" && file === 8) {
|
|
61
|
+
file = 0;
|
|
62
|
+
rank--;
|
|
63
|
+
} else {
|
|
64
|
+
const step = parseInt(c, 10);
|
|
65
|
+
if (step > 0) file += step;
|
|
66
|
+
else {
|
|
67
|
+
if (file >= 8 || rank < 0) return Result.err(new FenError(InvalidFen.Board));
|
|
68
|
+
const square = file + rank * 8;
|
|
69
|
+
const piece = charToPiece(c);
|
|
70
|
+
if (!piece) return Result.err(new FenError(InvalidFen.Board));
|
|
71
|
+
if (boardPart[i + 1] === "~") {
|
|
72
|
+
piece.promoted = true;
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
board.set(square, piece);
|
|
76
|
+
file++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (rank !== 0 || file !== 8) return Result.err(new FenError(InvalidFen.Board));
|
|
81
|
+
return Result.ok(board);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const parsePockets = (pocketPart: string): Result<Material, FenError> => {
|
|
85
|
+
if (pocketPart.length > 64) return Result.err(new FenError(InvalidFen.Pockets));
|
|
86
|
+
const pockets = Material.empty();
|
|
87
|
+
for (const c of pocketPart) {
|
|
88
|
+
const piece = charToPiece(c);
|
|
89
|
+
if (!piece) return Result.err(new FenError(InvalidFen.Pockets));
|
|
90
|
+
pockets[piece.color][piece.role]++;
|
|
91
|
+
}
|
|
92
|
+
return Result.ok(pockets);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const parseCastlingFen = (
|
|
96
|
+
board: Board,
|
|
97
|
+
castlingPart: string,
|
|
98
|
+
): Result<SquareSet, FenError> => {
|
|
99
|
+
let castlingRights = SquareSet.empty();
|
|
100
|
+
if (castlingPart === "-") return Result.ok(castlingRights);
|
|
101
|
+
|
|
102
|
+
for (const c of castlingPart) {
|
|
103
|
+
const lower = c.toLowerCase();
|
|
104
|
+
const color = c === lower ? "black" : "white";
|
|
105
|
+
const rank = color === "white" ? 0 : 7;
|
|
106
|
+
if ("a" <= lower && lower <= "h") {
|
|
107
|
+
castlingRights = castlingRights.with(
|
|
108
|
+
squareFromCoords(lower.charCodeAt(0) - "a".charCodeAt(0), rank)!,
|
|
109
|
+
);
|
|
110
|
+
} else if (lower === "k" || lower === "q") {
|
|
111
|
+
const rooksAndKings = board[color]
|
|
112
|
+
.intersect(SquareSet.backrank(color))
|
|
113
|
+
.intersect(board.rook.union(board.king));
|
|
114
|
+
const candidate = lower === "k" ? rooksAndKings.last() : rooksAndKings.first();
|
|
115
|
+
castlingRights = castlingRights.with(
|
|
116
|
+
defined(candidate) && board.rook.has(candidate)
|
|
117
|
+
? candidate
|
|
118
|
+
: squareFromCoords(lower === "k" ? 7 : 0, rank)!,
|
|
119
|
+
);
|
|
120
|
+
} else return Result.err(new FenError(InvalidFen.Castling));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (COLORS.some((color) => SquareSet.backrank(color).intersect(castlingRights).size() > 2)) {
|
|
124
|
+
return Result.err(new FenError(InvalidFen.Castling));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Result.ok(castlingRights);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const parseRemainingChecks = (part: string): Result<RemainingChecks, FenError> => {
|
|
131
|
+
const parts = part.split("+");
|
|
132
|
+
if (parts.length === 3 && parts[0] === "") {
|
|
133
|
+
const white = parseSmallUint(parts[1]);
|
|
134
|
+
const black = parseSmallUint(parts[2]);
|
|
135
|
+
if (!defined(white) || white > 3 || !defined(black) || black > 3) {
|
|
136
|
+
return Result.err(new FenError(InvalidFen.RemainingChecks));
|
|
137
|
+
}
|
|
138
|
+
return Result.ok(new RemainingChecks(3 - white, 3 - black));
|
|
139
|
+
} else if (parts.length === 2) {
|
|
140
|
+
const white = parseSmallUint(parts[0]);
|
|
141
|
+
const black = parseSmallUint(parts[1]);
|
|
142
|
+
if (!defined(white) || white > 3 || !defined(black) || black > 3) {
|
|
143
|
+
return Result.err(new FenError(InvalidFen.RemainingChecks));
|
|
144
|
+
}
|
|
145
|
+
return Result.ok(new RemainingChecks(white, black));
|
|
146
|
+
} else return Result.err(new FenError(InvalidFen.RemainingChecks));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const parseFen = (fen: string): Result<Setup, FenError> => {
|
|
150
|
+
const parts = fen.split(/[\s_]+/);
|
|
151
|
+
const boardPart = parts.shift()!;
|
|
152
|
+
|
|
153
|
+
// Board and pockets
|
|
154
|
+
let board: Result<Board, FenError>;
|
|
155
|
+
let pockets = Result.ok<Material | undefined, FenError>(undefined);
|
|
156
|
+
if (boardPart.endsWith("]")) {
|
|
157
|
+
const pocketStart = boardPart.indexOf("[");
|
|
158
|
+
if (pocketStart === -1) return Result.err(new FenError(InvalidFen.Fen));
|
|
159
|
+
board = parseBoardFen(boardPart.slice(0, pocketStart));
|
|
160
|
+
pockets = parsePockets(boardPart.slice(pocketStart + 1, -1));
|
|
161
|
+
} else {
|
|
162
|
+
const pocketStart = nthIndexOf(boardPart, "/", 7);
|
|
163
|
+
if (pocketStart === -1) board = parseBoardFen(boardPart);
|
|
164
|
+
else {
|
|
165
|
+
board = parseBoardFen(boardPart.slice(0, pocketStart));
|
|
166
|
+
pockets = parsePockets(boardPart.slice(pocketStart + 1));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Turn
|
|
171
|
+
let turn: Color;
|
|
172
|
+
const turnPart = parts.shift();
|
|
173
|
+
if (!defined(turnPart) || turnPart === "w") turn = "white";
|
|
174
|
+
else if (turnPart === "b") turn = "black";
|
|
175
|
+
else return Result.err(new FenError(InvalidFen.Turn));
|
|
176
|
+
|
|
177
|
+
return board.chain((board) => {
|
|
178
|
+
// Castling
|
|
179
|
+
const castlingPart = parts.shift();
|
|
180
|
+
const castlingRights = defined(castlingPart)
|
|
181
|
+
? parseCastlingFen(board, castlingPart)
|
|
182
|
+
: Result.ok(SquareSet.empty());
|
|
183
|
+
|
|
184
|
+
// En passant square
|
|
185
|
+
const epPart = parts.shift();
|
|
186
|
+
let epSquare: Square | undefined;
|
|
187
|
+
if (defined(epPart) && epPart !== "-") {
|
|
188
|
+
epSquare = parseSquare(epPart);
|
|
189
|
+
if (!defined(epSquare)) return Result.err(new FenError(InvalidFen.EpSquare));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Halfmoves or remaining checks
|
|
193
|
+
let halfmovePart = parts.shift();
|
|
194
|
+
let earlyRemainingChecks: Result<RemainingChecks, FenError> | undefined;
|
|
195
|
+
if (defined(halfmovePart) && halfmovePart.includes("+")) {
|
|
196
|
+
earlyRemainingChecks = parseRemainingChecks(halfmovePart);
|
|
197
|
+
halfmovePart = parts.shift();
|
|
198
|
+
}
|
|
199
|
+
const halfmoves = defined(halfmovePart) ? parseSmallUint(halfmovePart) : 0;
|
|
200
|
+
if (!defined(halfmoves)) return Result.err(new FenError(InvalidFen.Halfmoves));
|
|
201
|
+
|
|
202
|
+
const fullmovesPart = parts.shift();
|
|
203
|
+
const fullmoves = defined(fullmovesPart) ? parseSmallUint(fullmovesPart) : 1;
|
|
204
|
+
if (!defined(fullmoves)) return Result.err(new FenError(InvalidFen.Fullmoves));
|
|
205
|
+
|
|
206
|
+
const remainingChecksPart = parts.shift();
|
|
207
|
+
let remainingChecks: Result<RemainingChecks | undefined, FenError> = Result.ok(undefined);
|
|
208
|
+
if (defined(remainingChecksPart)) {
|
|
209
|
+
if (defined(earlyRemainingChecks))
|
|
210
|
+
return Result.err(new FenError(InvalidFen.RemainingChecks));
|
|
211
|
+
remainingChecks = parseRemainingChecks(remainingChecksPart);
|
|
212
|
+
} else if (defined(earlyRemainingChecks)) {
|
|
213
|
+
remainingChecks = earlyRemainingChecks;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (parts.length > 0) return Result.err(new FenError(InvalidFen.Fen));
|
|
217
|
+
|
|
218
|
+
return pockets.chain((pockets) =>
|
|
219
|
+
castlingRights.chain((castlingRights) =>
|
|
220
|
+
remainingChecks.map((remainingChecks) => {
|
|
221
|
+
return {
|
|
222
|
+
board,
|
|
223
|
+
pockets,
|
|
224
|
+
turn,
|
|
225
|
+
castlingRights,
|
|
226
|
+
remainingChecks,
|
|
227
|
+
epSquare,
|
|
228
|
+
halfmoves,
|
|
229
|
+
fullmoves: Math.max(1, fullmoves),
|
|
230
|
+
};
|
|
231
|
+
}),
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export interface FenOpts {
|
|
238
|
+
epd?: boolean;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const parsePiece = (str: string): Piece | undefined => {
|
|
242
|
+
if (!str) return;
|
|
243
|
+
const piece = charToPiece(str[0]);
|
|
244
|
+
if (!piece) return;
|
|
245
|
+
if (str.length === 2 && str[1] === "~") piece.promoted = true;
|
|
246
|
+
else if (str.length > 1) return;
|
|
247
|
+
return piece;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const makePiece = (piece: Piece): string => {
|
|
251
|
+
let r: string = roleToChar(piece.role);
|
|
252
|
+
if (piece.color === "white") r = r.toUpperCase();
|
|
253
|
+
if (piece.promoted) r += "~";
|
|
254
|
+
return r;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export const makeBoardFen = (board: Board): string => {
|
|
258
|
+
let fen = "";
|
|
259
|
+
let empty = 0;
|
|
260
|
+
for (let rank = 7; rank >= 0; rank--) {
|
|
261
|
+
for (let file = 0; file < 8; file++) {
|
|
262
|
+
const square = file + rank * 8;
|
|
263
|
+
const piece = board.get(square);
|
|
264
|
+
if (!piece) empty++;
|
|
265
|
+
else {
|
|
266
|
+
if (empty > 0) {
|
|
267
|
+
fen += empty;
|
|
268
|
+
empty = 0;
|
|
269
|
+
}
|
|
270
|
+
fen += makePiece(piece);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (file === 7) {
|
|
274
|
+
if (empty > 0) {
|
|
275
|
+
fen += empty;
|
|
276
|
+
empty = 0;
|
|
277
|
+
}
|
|
278
|
+
if (rank !== 0) fen += "/";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return fen;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const makePocket = (material: MaterialSide): string =>
|
|
286
|
+
ROLES.map((role) => roleToChar(role).repeat(material[role])).join("");
|
|
287
|
+
|
|
288
|
+
export const makePockets = (pocket: Material): string =>
|
|
289
|
+
makePocket(pocket.white).toUpperCase() + makePocket(pocket.black);
|
|
290
|
+
|
|
291
|
+
export const makeCastlingFen = (board: Board, castlingRights: SquareSet): string => {
|
|
292
|
+
let fen = "";
|
|
293
|
+
for (const color of COLORS) {
|
|
294
|
+
const backrank = SquareSet.backrank(color);
|
|
295
|
+
let king = board.kingOf(color);
|
|
296
|
+
if (defined(king) && !backrank.has(king)) king = undefined;
|
|
297
|
+
const candidates = board.pieces(color, "rook").intersect(backrank);
|
|
298
|
+
for (const rook of castlingRights.intersect(backrank).reversed()) {
|
|
299
|
+
if (rook === candidates.first() && defined(king) && rook < king) {
|
|
300
|
+
fen += color === "white" ? "Q" : "q";
|
|
301
|
+
} else if (rook === candidates.last() && defined(king) && king < rook) {
|
|
302
|
+
fen += color === "white" ? "K" : "k";
|
|
303
|
+
} else {
|
|
304
|
+
const file = FILE_NAMES[squareFile(rook)];
|
|
305
|
+
fen += color === "white" ? file.toUpperCase() : file;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return fen || "-";
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const makeRemainingChecks = (checks: RemainingChecks): string =>
|
|
313
|
+
`${checks.white}+${checks.black}`;
|
|
314
|
+
|
|
315
|
+
export const makeFen = (setup: Setup, opts?: FenOpts): string =>
|
|
316
|
+
[
|
|
317
|
+
makeBoardFen(setup.board) + (setup.pockets ? `[${makePockets(setup.pockets)}]` : ""),
|
|
318
|
+
setup.turn[0],
|
|
319
|
+
makeCastlingFen(setup.board, setup.castlingRights),
|
|
320
|
+
defined(setup.epSquare) ? makeSquare(setup.epSquare) : "-",
|
|
321
|
+
...(setup.remainingChecks ? [makeRemainingChecks(setup.remainingChecks)] : []),
|
|
322
|
+
...(opts?.epd
|
|
323
|
+
? []
|
|
324
|
+
: [
|
|
325
|
+
Math.max(0, Math.min(setup.halfmoves, 9999)),
|
|
326
|
+
Math.max(1, Math.min(setup.fullmoves, 9999)),
|
|
327
|
+
]),
|
|
328
|
+
].join(" ");
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ByCastlingSide,
|
|
3
|
+
ByColor,
|
|
4
|
+
ByRole,
|
|
5
|
+
BySquare,
|
|
6
|
+
CASTLING_SIDES,
|
|
7
|
+
CastlingSide,
|
|
8
|
+
Color,
|
|
9
|
+
COLORS,
|
|
10
|
+
DropMove,
|
|
11
|
+
FILE_NAMES,
|
|
12
|
+
FileName,
|
|
13
|
+
isDrop,
|
|
14
|
+
isNormal,
|
|
15
|
+
Move,
|
|
16
|
+
NormalMove,
|
|
17
|
+
Outcome,
|
|
18
|
+
Piece,
|
|
19
|
+
RANK_NAMES,
|
|
20
|
+
RankName,
|
|
21
|
+
Role,
|
|
22
|
+
ROLE_CHARS,
|
|
23
|
+
RoleChar,
|
|
24
|
+
ROLES,
|
|
25
|
+
Rules,
|
|
26
|
+
Square,
|
|
27
|
+
SquareName,
|
|
28
|
+
} from "./types.js";
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
charToRole,
|
|
32
|
+
defined,
|
|
33
|
+
kingCastlesTo,
|
|
34
|
+
makeSquare,
|
|
35
|
+
makeUci,
|
|
36
|
+
opposite,
|
|
37
|
+
parseSquare,
|
|
38
|
+
parseUci,
|
|
39
|
+
roleToChar,
|
|
40
|
+
squareFile,
|
|
41
|
+
squareRank,
|
|
42
|
+
} from "./util.js";
|
|
43
|
+
|
|
44
|
+
export { SquareSet } from "./squareSet.js";
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
attacks,
|
|
48
|
+
between,
|
|
49
|
+
bishopAttacks,
|
|
50
|
+
kingAttacks,
|
|
51
|
+
knightAttacks,
|
|
52
|
+
pawnAttacks,
|
|
53
|
+
queenAttacks,
|
|
54
|
+
ray,
|
|
55
|
+
rookAttacks,
|
|
56
|
+
} from "./attacks.js";
|
|
57
|
+
|
|
58
|
+
export { Board, boardEquals } from "./board.js";
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
defaultSetup,
|
|
62
|
+
emptySetup,
|
|
63
|
+
Material,
|
|
64
|
+
MaterialSide,
|
|
65
|
+
RemainingChecks,
|
|
66
|
+
Setup,
|
|
67
|
+
setupClone,
|
|
68
|
+
setupEquals,
|
|
69
|
+
} from "./setup.js";
|
|
70
|
+
|
|
71
|
+
export { Castles, Chess, Context, IllegalSetup, Position, PositionError } from "./chess.js";
|
|
72
|
+
|
|
73
|
+
export * as compat from "./compat.js";
|
|
74
|
+
|
|
75
|
+
export * as debug from "./debug.js";
|
|
76
|
+
|
|
77
|
+
export * as fen from "./fen.js";
|
|
78
|
+
|
|
79
|
+
export * as san from "./san.js";
|
|
80
|
+
|
|
81
|
+
export * as transform from "./transform.js";
|
|
82
|
+
|
|
83
|
+
export * as variant from "./variant.js";
|
|
84
|
+
|
|
85
|
+
export * as pgn from "./pgn.js";
|