Comparing version 1.0.0-alpha.0 to 1.0.0-beta.0
@@ -30,3 +30,4 @@ module.exports = { | ||
], | ||
'multiline-comment-style': ['error', 'starred-block'] | ||
}, | ||
} |
@@ -0,1 +1,27 @@ | ||
/** | ||
* @license | ||
* Copyright (c) 2023, Jeff Hlywa (jhlywa@gmail.com) | ||
* All rights reserved. | ||
* | ||
* Redistribution and use in source and binary forms, with or without | ||
* modification, are permitted provided that the following conditions are met: | ||
* | ||
* 1. Redistributions of source code must retain the above copyright notice, | ||
* this list of conditions and the following disclaimer. | ||
* 2. Redistributions in binary form must reproduce the above copyright notice, | ||
* this list of conditions and the following disclaimer in the documentation | ||
* and/or other materials provided with the distribution. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | ||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
* POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
export declare const WHITE = "w"; | ||
@@ -35,8 +61,11 @@ export declare const BLACK = "b"; | ||
san: string; | ||
lan: string; | ||
}; | ||
export declare const SQUARES: Square[]; | ||
export declare function validateFen(fen: string): { | ||
valid: boolean; | ||
errorNumber: number; | ||
ok: boolean; | ||
error: string; | ||
} | { | ||
ok: boolean; | ||
error?: undefined; | ||
}; | ||
@@ -56,3 +85,3 @@ export declare class Chess { | ||
clear(keepHeaders?: boolean): void; | ||
load(fen: string, keepHeaders?: boolean): boolean; | ||
load(fen: string, keepHeaders?: boolean): void; | ||
fen(): string; | ||
@@ -69,2 +98,3 @@ private _updateSetup; | ||
private _isKingAttacked; | ||
isAttacked(square: Square, attackedBy: Color): boolean; | ||
isCheck(): boolean; | ||
@@ -78,3 +108,15 @@ inCheck(): boolean; | ||
isGameOver(): boolean; | ||
moves({ verbose, square, }?: { | ||
moves(): string[]; | ||
moves({ square }: { | ||
square: Square; | ||
}): string[]; | ||
moves({ verbose, square }: { | ||
verbose: true; | ||
square?: Square; | ||
}): Move[]; | ||
moves({ verbose, square }: { | ||
verbose: false; | ||
square?: Square; | ||
}): string[]; | ||
moves({ verbose, square, }: { | ||
verbose?: boolean; | ||
@@ -92,5 +134,5 @@ square?: Square; | ||
promotion?: string; | ||
}, { sloppy }?: { | ||
sloppy?: boolean; | ||
}): Move | null; | ||
}, { strict }?: { | ||
strict?: boolean; | ||
}): Move; | ||
_push(move: InternalMove): void; | ||
@@ -105,6 +147,6 @@ private _makeMove; | ||
header(...args: string[]): Record<string, string>; | ||
loadPgn(pgn: string, { sloppy, newlineChar, }?: { | ||
sloppy?: boolean; | ||
loadPgn(pgn: string, { strict, newlineChar, }?: { | ||
strict?: boolean; | ||
newlineChar?: string; | ||
}): boolean; | ||
}): void; | ||
private _moveToSan; | ||
@@ -122,5 +164,16 @@ private _moveFromSan; | ||
squareColor(square: Square): "light" | "dark" | null; | ||
history({ verbose }?: { | ||
verbose?: boolean; | ||
}): (string | Move)[]; | ||
history(): string[]; | ||
history({ verbose }: { | ||
verbose: true; | ||
}): (Move & { | ||
fen: string; | ||
})[]; | ||
history({ verbose }: { | ||
verbose: false; | ||
}): string[]; | ||
history({ verbose, }: { | ||
verbose: boolean; | ||
}): string[] | (Move & { | ||
fen: string; | ||
})[]; | ||
private _pruneComments; | ||
@@ -127,0 +180,0 @@ getComment(): string; |
{ | ||
"name": "chess.js", | ||
"version": "1.0.0-alpha.0", | ||
"version": "1.0.0-beta.0", | ||
"license": "BSD-2-Clause", | ||
@@ -10,2 +10,3 @@ "main": "dist/chess.js", | ||
"scripts": { | ||
"postinstall": "npm run build", | ||
"test": "jest", | ||
@@ -12,0 +13,0 @@ "lint": "eslint src/ --ext .ts", |
542
README.md
# chess.js | ||
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/jhlywa/chess.js/Node.js%20CI) | ||
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/jhlywa/chess.js/node.js.yml) | ||
![npm](https://img.shields.io/npm/v/chess.js?color=blue) | ||
![npm](https://img.shields.io/npm/dm/chess.js) | ||
chess.js is a Javascript chess library that is used for chess move | ||
chess.js is a TypeScript chess library used for chess move | ||
generation/validation, piece placement/movement, and check/checkmate/stalemate | ||
@@ -15,16 +15,15 @@ detection - basically everything but the AI. | ||
Run the following command to install the most recent version of chess.js from NPM: | ||
Run the following command to install the most recent version of chess.js from | ||
NPM: | ||
```sh | ||
npm install chess.js@1.0.0-alpha.0 | ||
``` | ||
npm install chess.js | ||
``` | ||
TypeScript type definitions for chess.js are provided by the community-supported | ||
DefinitelyTyped repository and can be installed via: | ||
or install the latest development version straight from GitHub: | ||
```sh | ||
npm install git+https://github.com/jhlywa/chess.js.git | ||
``` | ||
npm install -D @types/chess.js | ||
``` | ||
## Example Code | ||
@@ -34,3 +33,3 @@ | ||
```js | ||
```ts | ||
import { Chess } from 'chess.js' | ||
@@ -41,5 +40,5 @@ | ||
while (!chess.isGameOver()) { | ||
const moves = chess.moves() | ||
const move = moves[Math.floor(Math.random() * moves.length)] | ||
chess.move(move) | ||
const moves = chess.moves() | ||
const move = moves[Math.floor(Math.random() * moves.length)] | ||
chess.move(move) | ||
} | ||
@@ -51,8 +50,18 @@ console.log(chess.pgn()) | ||
By design, chess.js is headless and does not include user interface. Many | ||
developers have had success integrating chess.js with the | ||
By design chess.js is a headless library and does not include user interface | ||
elements. Many developers have successfully integrated chess.js with the | ||
[chessboard.js](http://chessboardjs.com) library. See | ||
[chessboard.js - Random vs Random](http://chessboardjs.com/examples#5002) for | ||
an example. | ||
[chessboard.js - Random vs Random](http://chessboardjs.com/examples#5002) for an | ||
example. | ||
## Move & PGN Parsers | ||
This library includes two parsers (`permissive` and `strict`) which are used to | ||
parse different forms of chess move notation. The `permissive` parser (the | ||
default) is able to handle many derivates of algebraic notation (e.g. `Nf3`, | ||
`g1f3`, `g1-f3`, `Ng1f3`, `Ng1-f3`, `Ng1xf3`). The `strict` parser only accepts | ||
moves in Standard Algebraic Notation and requires that they strictly adhere to | ||
the specification. The `strict` parser runs slightly faster but is much less | ||
forgiving of non-standard notation. | ||
## API | ||
@@ -62,6 +71,8 @@ | ||
The Chess() constructor takes an optional parameter which specifies the board configuration | ||
in [Forsyth-Edwards Notation](http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation). | ||
The Chess() constructor takes an optional parameter which specifies the board | ||
configuration in | ||
[Forsyth-Edwards Notation (FEN)](http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation). | ||
Throws an exception if an invalid FEN string is provided. | ||
```js | ||
```ts | ||
// board defaults to the starting position when called with no parameters | ||
@@ -72,3 +83,3 @@ const chess = new Chess() | ||
const chess = new Chess( | ||
'r1k4r/p2nb1p1/2b4p/1p1n1p2/2PP4/3Q1NB1/1P3PPP/R5K1 b - c3 0 19' | ||
'r1k4r/p2nb1p1/2b4p/1p1n1p2/2PP4/3Q1NB1/1P3PPP/R5K1 b - - 0 19' | ||
) | ||
@@ -81,3 +92,3 @@ ``` | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
@@ -109,3 +120,3 @@ | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
@@ -141,3 +152,3 @@ | ||
```js | ||
```ts | ||
chess.clear() | ||
@@ -152,6 +163,6 @@ chess.fen() | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
chess.loadPgn("1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 {giuoco piano} *") | ||
chess.loadPgn('1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 {giuoco piano} *') | ||
@@ -172,6 +183,8 @@ chess.getComment() | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
chess.loadPgn("1. e4 e5 {king's pawn opening} 2. Nf3 Nc6 3. Bc4 Bc5 {giuoco piano} *") | ||
chess.loadPgn( | ||
"1. e4 e5 {king's pawn opening} 2. Nf3 Nc6 3. Bc4 Bc5 {giuoco piano} *" | ||
) | ||
@@ -181,3 +194,3 @@ chess.deleteComments() | ||
// { | ||
// fen: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2", | ||
// fen: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", | ||
// comment: "king's pawn opening" | ||
@@ -194,7 +207,9 @@ // }, | ||
``` | ||
### .fen() | ||
Returns the FEN string for the current position. | ||
Returns the FEN string for the current position. Note, the en passant square is | ||
only included if the side-to-move can legally capture en passant. | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
@@ -208,25 +223,5 @@ | ||
chess.fen() | ||
// -> 'rnbqkbnr/pppp1ppp/8/4p3/4PP2/8/PPPP2PP/RNBQKBNR b KQkq f3 0 2' | ||
// -> 'rnbqkbnr/pppp1ppp/8/4p3/4PP2/8/PPPP2PP/RNBQKBNR b KQkq - 0 2' | ||
``` | ||
### .isGameOver() | ||
Returns true if the game has ended via checkmate, stalemate, draw, threefold repetition, or insufficient material. Otherwise, returns false. | ||
```js | ||
const chess = new Chess() | ||
chess.isGameOver() | ||
// -> false | ||
// stalemate | ||
chess.load('4k3/4P3/4K3/8/8/8/8/8 b - - 0 78') | ||
chess.isGameOver() | ||
// -> true | ||
// checkmate | ||
chess.load('rnb1kbnr/pppp1ppp/8/4p3/5PPq/8/PPPPP2P/RNBQKBNR w KQkq - 1 3') | ||
chess.isGameOver() | ||
// -> true | ||
``` | ||
### .get(square) | ||
@@ -236,3 +231,3 @@ | ||
```js | ||
```ts | ||
chess.clear() | ||
@@ -251,6 +246,6 @@ chess.put({ type: chess.PAWN, color: chess.BLACK }, 'a5') // put a black pawn on a5 | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
chess.loadPgn("1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 {giuoco piano} *") | ||
chess.loadPgn('1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 {giuoco piano} *') | ||
@@ -265,6 +260,8 @@ chess.getComment() | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
chess.loadPgn("1. e4 e5 {king's pawn opening} 2. Nf3 Nc6 3. Bc4 Bc5 {giuoco piano} *") | ||
chess.loadPgn( | ||
"1. e4 e5 {king's pawn opening} 2. Nf3 Nc6 3. Bc4 Bc5 {giuoco piano} *" | ||
) | ||
@@ -274,3 +271,3 @@ chess.getComments() | ||
// { | ||
// fen: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2", | ||
// fen: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", | ||
// comment: "king's pawn opening" | ||
@@ -290,3 +287,3 @@ // }, | ||
```js | ||
```ts | ||
chess.header('White', 'Robert James Fischer') | ||
@@ -300,5 +297,6 @@ chess.header('Black', 'Mikhail Tal') | ||
Calling .header() without any arguments returns the header information as an object. | ||
Calling .header() without any arguments returns the header information as an | ||
object. | ||
```js | ||
```ts | ||
chess.header() | ||
@@ -311,6 +309,7 @@ // -> { White: 'Morphy', Black: 'Anderssen', Date: '1858-??-??' } | ||
Returns a list containing the moves of the current game. Options is an optional | ||
parameter which may contain a 'verbose' flag. See .moves() for a description of the | ||
verbose move fields. | ||
parameter which may contain a 'verbose' flag. See .moves() for a description of | ||
the verbose move fields. A FEN string of the position _prior_ to the move being | ||
made is added to the verbose history output. | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
@@ -326,6 +325,46 @@ chess.move('e4') | ||
chess.history({ verbose: true }) | ||
// -> [{ color: 'w', from: 'e2', to: 'e4', flags: 'b', piece: 'p', san: 'e4' }, | ||
// { color: 'b', from: 'e7', to: 'e5', flags: 'b', piece: 'p', san: 'e5' }, | ||
// { color: 'w', from: 'f2', to: 'f4', flags: 'b', piece: 'p', san: 'f4' }, | ||
// { color: 'b', from: 'e5', to: 'f4', flags: 'c', piece: 'p', captured: 'p', san: 'exf4' }] | ||
// --> | ||
// [ | ||
// { | ||
// fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', | ||
// color: 'w', | ||
// piece: 'p', | ||
// from: 'e2', | ||
// to: 'e4', | ||
// san: 'e4', | ||
// lan: 'e2e4', | ||
// flags: 'b' | ||
// }, | ||
// { | ||
// fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', | ||
// color: 'b', | ||
// piece: 'p', | ||
// from: 'e7', | ||
// to: 'e5', | ||
// san: 'e5', | ||
// lan: 'e7e5', | ||
// flags: 'b' | ||
// }, | ||
// { | ||
// fen: 'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2', | ||
// color: 'w', | ||
// piece: 'p', | ||
// from: 'f2', | ||
// to: 'f4', | ||
// san: 'f4', | ||
// lan: 'f2f4', | ||
// flags: 'b' | ||
// }, | ||
// { | ||
// fen: 'rnbqkbnr/pppp1ppp/8/4p3/4PP2/8/PPPP2PP/RNBQKBNR b KQkq - 0 2', | ||
// color: 'b', | ||
// piece: 'p', | ||
// from: 'e5', | ||
// to: 'f4', | ||
// san: 'exf4', | ||
// lan: 'e5f4', | ||
// flags: 'c', | ||
// captured: 'p' | ||
// } | ||
// ] | ||
``` | ||
@@ -337,5 +376,5 @@ | ||
```js | ||
```ts | ||
const chess = new Chess( | ||
'rnb1kbnr/pppp1ppp/8/4p3/5PPq/8/PPPPP2P/RNBQKBNR w KQkq - 1 3' | ||
'rnb1kbnr/pppp1ppp/8/4p3/5PPq/8/PPPPP2P/RNBQKBNR w KQkq - 1 3' | ||
) | ||
@@ -346,11 +385,32 @@ chess.inCheck() | ||
### .inCheckmate() | ||
### .isAttacked(square, color) | ||
Returns true if the square is attacked by any piece of the given color. | ||
```ts | ||
const chess = new Chess() | ||
chess.isAttacked('f3', Chess.WHITE) | ||
// -> true (we can attack empty squares) | ||
chess.isAttacked('f6', Chess.BLACK) | ||
// -> true (side to move (e.g. the value returned by .turn) is ignored) | ||
chess.load(Chess.DEFAULT_POSITION) | ||
chess.isAttacked('e2', Chess.WHITE) | ||
// -> true (we can attack our own pieces) | ||
chess.load('4k3/4n3/8/8/8/8/4R3/4K3 w - - 0 1') | ||
chess.isAttacked('c6', Chess.BLACK) | ||
// -> true (pieces still attack a square if even they are pinned) | ||
``` | ||
### .isCheckmate() | ||
Returns true or false if the side to move has been checkmated. | ||
```js | ||
```ts | ||
const chess = new Chess( | ||
'rnb1kbnr/pppp1ppp/8/4p3/5PPq/8/PPPPP2P/RNBQKBNR w KQkq - 1 3' | ||
'rnb1kbnr/pppp1ppp/8/4p3/5PPq/8/PPPPP2P/RNBQKBNR w KQkq - 1 3' | ||
) | ||
chess.inCheckmate() | ||
chess.isCheckmate() | ||
// -> true | ||
@@ -361,5 +421,6 @@ ``` | ||
Returns true or false if the game is drawn (50-move rule or insufficient material). | ||
Returns true or false if the game is drawn (50-move rule or insufficient | ||
material). | ||
```js | ||
```ts | ||
const chess = new Chess('4k3/4P3/4K3/8/8/8/8/8 b - - 0 78') | ||
@@ -370,2 +431,34 @@ chess.isDraw() | ||
### .isInsufficientMaterial() | ||
Returns true if the game is drawn due to insufficient material (K vs. K, K vs. | ||
KB, or K vs. KN) otherwise false. | ||
```ts | ||
const chess = new Chess('k7/8/n7/8/8/8/8/7K b - - 0 1') | ||
chess.isInsufficientMaterial() | ||
// -> true | ||
``` | ||
### .isGameOver() | ||
Returns true if the game has ended via checkmate, stalemate, draw, threefold | ||
repetition, or insufficient material. Otherwise, returns false. | ||
```ts | ||
const chess = new Chess() | ||
chess.isGameOver() | ||
// -> false | ||
// stalemate | ||
chess.load('4k3/4P3/4K3/8/8/8/8/8 b - - 0 78') | ||
chess.isGameOver() | ||
// -> true | ||
// checkmate | ||
chess.load('rnb1kbnr/pppp1ppp/8/4p3/5PPq/8/PPPPP2P/RNBQKBNR w KQkq - 1 3') | ||
chess.isGameOver() | ||
// -> true | ||
``` | ||
### .isStalemate() | ||
@@ -375,3 +468,3 @@ | ||
```js | ||
```ts | ||
const chess = new Chess('4k3/4P3/4K3/8/8/8/8/8 b - - 0 78') | ||
@@ -387,3 +480,3 @@ chess.isStalemate() | ||
```js | ||
```ts | ||
const chess = new Chess('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') | ||
@@ -406,25 +499,18 @@ // -> true | ||
### .isInsufficientMaterial() | ||
Returns true if the game is drawn due to insufficient material (K vs. K, | ||
K vs. KB, or K vs. KN) otherwise false. | ||
```js | ||
const chess = new Chess('k7/8/n7/8/8/8/8/7K b - - 0 1') | ||
chess.isInsufficientMaterial() | ||
// -> true | ||
``` | ||
### .load(fen) | ||
The board is cleared, and the FEN string is loaded. Returns true if the position was | ||
successfully loaded, otherwise false. | ||
Clears the board and loads the provided FEN string. The castling rights, en | ||
passant square and move numbers are defaulted to `- - 0 1` if ommitted. Throws | ||
an exception if the FEN is invalid. | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
chess.load('4r3/8/2p2PPk/1p6/pP2p1R1/P1B5/2P2K2/3r4 w - - 1 45') | ||
// -> true | ||
chess.load('4r3/8/X12XPk/1p6/pP2p1R1/P1B5/2P2K2/3r4 w - - 1 45') | ||
// -> false, bad piece X | ||
try { | ||
chess.load('4r3/8/X12XPk/1p6/pP2p1R1/P1B5/2P2K2/3r4 w - - 1 45') | ||
} catch (e) { | ||
console.log(e) | ||
} | ||
// -> Error: Invalid FEN: piece data is invalid (invalid piece) | ||
``` | ||
@@ -436,48 +522,44 @@ | ||
[Portable Game Notation](http://en.wikipedia.org/wiki/Portable_Game_Notation). | ||
`pgn` should be a string. Options is an optional `object` which may contain | ||
a string `newlineChar` and a boolean `sloppy`. | ||
`pgn` should be a string. Options is an optional object which may contain a | ||
string `newlineChar` and a boolean `strict`. | ||
The `newlineChar` is a string representation of a valid RegExp fragment and is | ||
used to process the PGN. It defaults to `\r?\n`. Special characters | ||
should not be pre-escaped, but any literal special characters should be escaped | ||
as is normal for a RegExp. Keep in mind that backslashes in JavaScript strings | ||
must themselves be escaped (see `sloppyPgn` example below). Avoid using | ||
a `newlineChar` that may occur elsewhere in a PGN, such as `.` or `x`, as this | ||
used to process the PGN. It defaults to `\r?\n`. Special characters should not | ||
be pre-escaped, but any literal special characters should be escaped as is | ||
normal for a RegExp. Keep in mind that backslashes in JavaScript strings must | ||
themselves be escaped (see `sloppyPgn` example below). Avoid using a | ||
`newlineChar` that may occur elsewhere in a PGN, such as `.` or `x`, as this | ||
will result in unexpected behavior. | ||
The `sloppy` flag is a boolean that permits chess.js to parse moves in | ||
non-standard notations. See `.move` documentation for more information about | ||
non-SAN notations. | ||
The `strict` flag is a boolean (default: `false`) that instructs chess.js to | ||
only parse moves in Standard Algebraic Notation form. See `.move` documentation | ||
for more information about non-SAN notations. | ||
The method will return `true` if the PGN was parsed successfully, otherwise `false`. | ||
The method will throw and exception if the PGN fails to parse. | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
const pgn = [ | ||
'[Event "Casual Game"]', | ||
'[Site "Berlin GER"]', | ||
'[Date "1852.??.??"]', | ||
'[EventDate "?"]', | ||
'[Round "?"]', | ||
'[Result "1-0"]', | ||
'[White "Adolf Anderssen"]', | ||
'[Black "Jean Dufresne"]', | ||
'[ECO "C52"]', | ||
'[WhiteElo "?"]', | ||
'[BlackElo "?"]', | ||
'[PlyCount "47"]', | ||
'', | ||
'1.e4 e5 2.Nf3 Nc6 3.Bc4 Bc5 4.b4 Bxb4 5.c3 Ba5 6.d4 exd4 7.O-O', | ||
'd3 8.Qb3 Qf6 9.e5 Qg6 10.Re1 Nge7 11.Ba3 b5 12.Qxb5 Rb8 13.Qa4', | ||
'Bb6 14.Nbd2 Bb7 15.Ne4 Qf5 16.Bxd3 Qh5 17.Nf6+ gxf6 18.exf6', | ||
'Rg8 19.Rad1 Qxf3 20.Rxe7+ Nxe7 21.Qxd7+ Kxd7 22.Bf5+ Ke8', | ||
'23.Bd7+ Kf8 24.Bxe7# 1-0' | ||
'[Event "Casual Game"]', | ||
'[Site "Berlin GER"]', | ||
'[Date "1852.??.??"]', | ||
'[EventDate "?"]', | ||
'[Round "?"]', | ||
'[Result "1-0"]', | ||
'[White "Adolf Anderssen"]', | ||
'[Black "Jean Dufresne"]', | ||
'[ECO "C52"]', | ||
'[WhiteElo "?"]', | ||
'[BlackElo "?"]', | ||
'[PlyCount "47"]', | ||
'', | ||
'1.e4 e5 2.Nf3 Nc6 3.Bc4 Bc5 4.b4 Bxb4 5.c3 Ba5 6.d4 exd4 7.O-O', | ||
'd3 8.Qb3 Qf6 9.e5 Qg6 10.Re1 Nge7 11.Ba3 b5 12.Qxb5 Rb8 13.Qa4', | ||
'Bb6 14.Nbd2 Bb7 15.Ne4 Qf5 16.Bxd3 Qh5 17.Nf6+ gxf6 18.exf6', | ||
'Rg8 19.Rad1 Qxf3 20.Rxe7+ Nxe7 21.Qxd7+ Kxd7 22.Bf5+ Ke8', | ||
'23.Bd7+ Kf8 24.Bxe7# 1-0', | ||
] | ||
chess.loadPgn(pgn.join('\n')) | ||
// -> true | ||
chess.fen() | ||
// -> 1r3kr1/pbpBBp1p/1b3P2/8/8/2P2q2/P4PPP/3R2K1 b - - 0 24 | ||
chess.ascii() | ||
@@ -498,32 +580,24 @@ // -> ' +------------------------+ | ||
const sloppyPgn = [ | ||
'[Event "Wijk aan Zee (Netherlands)"]', | ||
'[Date "1971.01.26"]', | ||
'[Result "1-0"]', | ||
'[White "Tigran Vartanovich Petrosian"]', | ||
'[Black "Hans Ree"]', | ||
'[ECO "A29"]', | ||
'', | ||
'1. Pc2c4 Pe7e5', // non-standard | ||
'2. Nc3 Nf6', | ||
'3. Nf3 Nc6', | ||
'4. g2g3 Bb4', // non-standard | ||
'5. Nd5 Nxd5', | ||
'6. c4xd5 e5-e4', // non-standard | ||
'7. dxc6 exf3', | ||
'8. Qb3 1-0' | ||
].join('|') | ||
'[Event "Wijk aan Zee (Netherlands)"]', | ||
'[Date "1971.01.26"]', | ||
'[Result "1-0"]', | ||
'[White "Tigran Vartanovich Petrosian"]', | ||
'[Black "Hans Ree"]', | ||
'[ECO "A29"]', | ||
'', | ||
'1. Pc2c4 Pe7e5', // non-standard | ||
'2. Nc3 Nf6', | ||
'3. Nf3 Nc6', | ||
'4. g2g3 Bb4', // non-standard | ||
'5. Nd5 Nxd5', | ||
'6. c4xd5 e5-e4', // non-standard | ||
'7. dxc6 exf3', | ||
'8. Qb3 1-0', | ||
].join(':') | ||
const options = { | ||
newlineChar: '\\|', // Literal '|' character escaped | ||
sloppy: true | ||
} | ||
chess.loadPgn(sloppyPgn, { newlineChar: ':' }) | ||
// works by default | ||
chess.loadPgn(sloppyPgn) | ||
// -> false | ||
chess.loadPgn(sloppyPgn, options) | ||
// -> true | ||
chess.fen() | ||
// -> 'r1bqk2r/pppp1ppp/2P5/8/1b6/1Q3pP1/PP1PPP1P/R1B1KB1R b KQkq - 1 8' | ||
chess.loadPgn(sloppyPgn, { newlineChar: ':', strict: true }) | ||
// Error: Invalid move in PGN: Pc2c4 | ||
``` | ||
@@ -533,7 +607,9 @@ | ||
Attempts to make a move on the board, returning a move object if the move was | ||
legal, otherwise null. The .move function can be called two ways, by passing | ||
a string in Standard Algebraic Notation (SAN): | ||
Makes a move on the board and returns a move object if the move was legal. The | ||
move argument can be either a string in Standard Algebraic Notation (SAN) or a | ||
move object. Throws an 'Illegal move' exception if the move was illegal. | ||
```js | ||
#### .move() - Standard Algebraic Notation (SAN) | ||
```ts | ||
const chess = new Chess() | ||
@@ -545,3 +621,3 @@ | ||
chess.move('nf6') // SAN is case sensitive!! | ||
// -> null | ||
// Error: Invalid move: nf6 | ||
@@ -552,6 +628,8 @@ chess.move('Nf6') | ||
Or by passing .move() a move object (only the 'to', 'from', and when necessary | ||
'promotion', fields are needed): | ||
#### .move() - Move Object | ||
```js | ||
A move object contains `to`, `from` and, `promotion` (only when necessary) | ||
fields. | ||
```ts | ||
const chess = new Chess() | ||
@@ -563,28 +641,28 @@ | ||
An optional sloppy flag can be used to parse a variety of non-standard move | ||
notations: | ||
#### .move() - Permissive Parser | ||
```js | ||
The permissive (default) move parser can be used to parse a variety of | ||
non-standard move notations. Users may specify an `{ strict: true }` flag to | ||
verify that all supplied moves adhere to the Standard Algebraic Notation | ||
specification. | ||
```ts | ||
const chess = new Chess() | ||
// various forms of Long Algebraic Notation | ||
chess.move('e2e4', { sloppy: true }) | ||
// -> { color: 'w', from: 'e2', to: 'e4', flags: 'b', piece: 'p', san: 'e4' } | ||
chess.move('e7-e5', { sloppy: true }) | ||
// -> { color: 'b', from: 'e7', to: 'e5', flags: 'b', piece: 'p', san: 'e5' } | ||
chess.move('Pf2f4', { sloppy: true }) | ||
// -> { color: 'w', from: 'f2', to: 'f4', flags: 'b', piece: 'p', san: 'f4' } | ||
chess.move('Pe5xf4', { sloppy: true }) | ||
// -> { color: 'b', from: 'e5', to: 'f4', flags: 'c', piece: 'p', captured: 'p', san: 'exf4' } | ||
// permissive parser accepts various forms of algebraic notation | ||
chess.move('e2e4') | ||
chess.move('e7-e5') | ||
chess.move('Pf2-f4') | ||
chess.move('ef4') // missing 'x' in capture | ||
chess.move('Ng1-f3') | ||
chess.move('d7xd6') // ignore 'x' when not a capture | ||
chess.move('d4') | ||
// correctly parses incorrectly disambiguated moves | ||
chess = new Chess( | ||
'r2qkbnr/ppp2ppp/2n5/1B2pQ2/4P3/8/PPP2PPP/RNB1K2R b KQkq - 3 7' | ||
) | ||
chess.load('r2qkbnr/ppp2ppp/2n5/1B2pQ2/4P3/8/PPP2PPP/RNB1K2R b KQkq - 3 7') | ||
chess.move('Nge7') // Ne7 is unambiguous because the knight on c6 is pinned | ||
// -> null | ||
chess.move('Nge7', { sloppy: true }) | ||
// -> { color: 'b', from: 'g8', to: 'e7', flags: 'n', piece: 'n', san: 'Ne7' } | ||
chess.undo() | ||
chess.move('Nge7', { strict: true }) // strict SAN requires Ne7 | ||
// Error: Invalid move: Nge7 | ||
``` | ||
@@ -594,5 +672,7 @@ | ||
Returns a list of legal moves from the current position. The function takes an optional parameter which controls the single-square move generation and verbosity. | ||
Returns a list of legal moves from the current position. This function takes an | ||
optional parameter which can be used to generate detailed move objects or to | ||
restrict the move generator to specific squares or pieces. | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
@@ -603,13 +683,13 @@ chess.moves() | ||
chess.moves({ square: 'e2' }) | ||
chess.moves({ square: 'e2' }) // single square move generation | ||
// -> ['e3', 'e4'] | ||
chess.moves({ square: 'e9' }) // invalid square | ||
// -> [] | ||
chess.moves({ piece: 'n' }) // generate moves for piece type | ||
// ['Na3', 'Nc3', 'Nf3', 'Nh3'] | ||
chess.moves({ verbose: true }) | ||
chess.moves({ verbose: true }) // return verbose moves | ||
// -> [{ color: 'w', from: 'a2', to: 'a3', | ||
// flags: 'n', piece: 'p', san 'a3' | ||
// # a captured: key is included when the move is a capture | ||
// # a promotion: key is included when the move is a promotion | ||
// # a `captured` field is included when the move is a capture | ||
// # a `promotion` field is included when the move is a promotion | ||
// }, | ||
@@ -620,23 +700,34 @@ // ... | ||
The _piece_, _captured_, and _promotion_ fields contain the lowercase | ||
representation of the applicable piece. | ||
#### Move Objects (e.g. { verbose: true }) | ||
The _flags_ field in verbose mode may contain one or more of the following values: | ||
The `color` field indicates the color of the moving piece (`w` or `b`). | ||
- 'n' - a non-capture | ||
- 'b' - a pawn push of two squares | ||
- 'e' - an en passant capture | ||
- 'c' - a standard capture | ||
- 'p' - a promotion | ||
- 'k' - kingside castling | ||
- 'q' - queenside castling | ||
The `from` and `to` fields are from and to squares in algebraic notation. | ||
A flag of 'pc' would mean that a pawn captured a piece on the 8th rank and promoted. | ||
The `piece`, `captured`, and `promotion` fields contain the lowercase | ||
representation of the applicable piece (`pnbrqk`). The `captured` and | ||
`promotion` fields are only present when the move is a valid capture or | ||
promotion. | ||
The `san` field is the move in Standard Algebraic Notation (SAN). | ||
The `flags` field contains one or more of the string values: | ||
- `n` - a non-capture | ||
- `b` - a pawn push of two squares | ||
- `e` - an en passant capture | ||
- `c` - a standard capture | ||
- `p` - a promotion | ||
- `k` - kingside castling | ||
- `q` - queenside castling | ||
A `flags` value of `pc` would mean that a pawn captured a piece on the 8th rank | ||
and promoted. | ||
### .pgn([ options ]) | ||
Returns the game in PGN format. Options is an optional parameter which may include | ||
max width and/or a newline character settings. | ||
Returns the game in PGN format. Options is an optional parameter which may | ||
include max width and/or a newline character settings. | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
@@ -655,9 +746,8 @@ chess.header('White', 'Plunky', 'Black', 'Plinkie') | ||
Place a piece on the square where piece is an object with the form | ||
{ type: ..., color: ... }. Returns true if the piece was successfully placed, | ||
otherwise, the board remains unchanged and false is returned. `put()` will fail | ||
when passed an invalid piece or square, or when two or more kings of the | ||
same color are placed. | ||
Place a piece on the square where piece is an object with the form { type: ..., | ||
color: ... }. Returns true if the piece was successfully placed, otherwise, the | ||
board remains unchanged and false is returned. `put()` will fail when passed an | ||
invalid piece or square, or when two or more kings of the same color are placed. | ||
```js | ||
```ts | ||
chess.clear() | ||
@@ -689,3 +779,3 @@ | ||
```js | ||
```ts | ||
chess.clear() | ||
@@ -711,6 +801,6 @@ chess.put({ type: chess.PAWN, color: chess.BLACK }, 'a5') // put a black pawn on a5 | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
chess.move("e4") | ||
chess.move('e4') | ||
chess.setComment("king's pawn opening") | ||
@@ -726,3 +816,3 @@ | ||
```js | ||
```ts | ||
const chess = Chess() | ||
@@ -741,3 +831,3 @@ chess.squareColor('h1') | ||
```js | ||
```ts | ||
chess.load('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1') | ||
@@ -750,5 +840,6 @@ chess.turn() | ||
Takeback the last half-move, returning a move object if successful, otherwise null. | ||
Takeback the last half-move, returning a move object if successful, otherwise | ||
null. | ||
```js | ||
```ts | ||
const chess = new Chess() | ||
@@ -760,3 +851,3 @@ | ||
chess.fen() | ||
// -> 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1' | ||
// -> 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1' | ||
@@ -776,8 +867,8 @@ chess.undo() | ||
```js | ||
```ts | ||
chess.validateFen('2n1r3/p1k2pp1/B1p3b1/P7/5bP1/2N1B3/1P2KP2/2R5 b - - 4 25') | ||
// -> { valid: true, error_number: 0, error: 'No errors.' } | ||
// -> { ok: true } | ||
chess.validateFen('4r3/8/X12XPk/1p6/pP2p1R1/P1B5/2P2K2/3r4 w - - 1 45') | ||
// -> { valid: false, error_number: 9, | ||
// -> { ok: false, | ||
// error: '1st field (piece positions) is invalid [invalid piece].' } | ||
@@ -788,2 +879,3 @@ ``` | ||
- The en passant square and castling flags aren't adjusted when using the put/remove functions (workaround: use .load() instead) | ||
- The en passant square and castling flags aren't adjusted when using the | ||
put/remove functions (workaround: use .load() instead) |
764
src/chess.ts
@@ -1,3 +0,4 @@ | ||
/* | ||
* Copyright (c) 2022, Jeff Hlywa (jhlywa@gmail.com) | ||
/** | ||
* @license | ||
* Copyright (c) 2023, Jeff Hlywa (jhlywa@gmail.com) | ||
* All rights reserved. | ||
@@ -25,4 +26,4 @@ * | ||
* POSSIBILITY OF SUCH DAMAGE. | ||
* | ||
*----------------------------------------------------------------------------*/ | ||
*/ | ||
export const WHITE = 'w' | ||
@@ -89,2 +90,3 @@ export const BLACK = 'b' | ||
san: string | ||
lan: string | ||
} | ||
@@ -126,40 +128,43 @@ | ||
// NOTES ABOUT 0x88 MOVE GENERATION ALGORITHM | ||
// ---------------------------------------------------------------------------- | ||
// From https://github.com/jhlywa/chess.js/issues/230 | ||
// | ||
// A lot of people are confused when they first see the internal representation | ||
// of chess.js. It uses the 0x88 Move Generation Algorithm which internally | ||
// stores the board as an 8x16 array. This is purely for efficiency but has a | ||
// couple of interesting benefits: | ||
// | ||
// 1. 0x88 offers a very inexpensive "off the board" check. Bitwise AND (&) any | ||
// square with 0x88, if the result is non-zero then the square is off the | ||
// board. For example, assuming a knight square A8 (0 in 0x88 notation), | ||
// there are 8 possible directions in which the knight can move. These | ||
// directions are relative to the 8x16 board and are stored in the | ||
// PIECE_OFFSETS map. One possible move is A8 - 18 (up one square, and two | ||
// squares to the left - which is off the board). 0 - 18 = -18 & 0x88 = 0x88 | ||
// (because of two-complement representation of -18). The non-zero result | ||
// means the square is off the board and the move is illegal. Take the | ||
// opposite move (from A8 to C7), 0 + 18 = 18 & 0x88 = 0. A result of zero | ||
// means the square is on the board. | ||
// | ||
// 2. The relative distance (or difference) between two squares on a 8x16 board | ||
// is unique and can be used to inexpensively determine if a piece on a | ||
// square can attack any other arbitrary square. For example, let's see if a | ||
// pawn on E7 can attack E2. The difference between E7 (20) - E2 (100) is | ||
// -80. We add 119 to make the ATTACKS array index non-negative (because the | ||
// worst case difference is A8 - H1 = -119). The ATTACKS array contains a | ||
// bitmask of pieces that can attack from that distance and direction. | ||
// ATTACKS[-80 + 119=39] gives us 24 or 0b11000 in binary. Look at the | ||
// PIECE_MASKS map to determine the mask for a given piece type. In our pawn | ||
// example, we would check to see if 24 & 0x1 is non-zero, which it is | ||
// not. So, naturally, a pawn on E7 can't attack a piece on E2. However, a | ||
// rook can since 24 & 0x8 is non-zero. The only thing left to check is that | ||
// there are no blocking pieces between E7 and E2. That's where the RAYS | ||
// array comes in. It provides an offset (in this case 16) to add to E7 (20) | ||
// to check for blocking pieces. E7 (20) + 16 = E6 (36) + 16 = E5 (52) etc. | ||
/* | ||
* NOTES ABOUT 0x88 MOVE GENERATION ALGORITHM | ||
* ---------------------------------------------------------------------------- | ||
* From https://github.com/jhlywa/chess.js/issues/230 | ||
* | ||
* A lot of people are confused when they first see the internal representation | ||
* of chess.js. It uses the 0x88 Move Generation Algorithm which internally | ||
* stores the board as an 8x16 array. This is purely for efficiency but has a | ||
* couple of interesting benefits: | ||
* | ||
* 1. 0x88 offers a very inexpensive "off the board" check. Bitwise AND (&) any | ||
* square with 0x88, if the result is non-zero then the square is off the | ||
* board. For example, assuming a knight square A8 (0 in 0x88 notation), | ||
* there are 8 possible directions in which the knight can move. These | ||
* directions are relative to the 8x16 board and are stored in the | ||
* PIECE_OFFSETS map. One possible move is A8 - 18 (up one square, and two | ||
* squares to the left - which is off the board). 0 - 18 = -18 & 0x88 = 0x88 | ||
* (because of two-complement representation of -18). The non-zero result | ||
* means the square is off the board and the move is illegal. Take the | ||
* opposite move (from A8 to C7), 0 + 18 = 18 & 0x88 = 0. A result of zero | ||
* means the square is on the board. | ||
* | ||
* 2. The relative distance (or difference) between two squares on a 8x16 board | ||
* is unique and can be used to inexpensively determine if a piece on a | ||
* square can attack any other arbitrary square. For example, let's see if a | ||
* pawn on E7 can attack E2. The difference between E7 (20) - E2 (100) is | ||
* -80. We add 119 to make the ATTACKS array index non-negative (because the | ||
* worst case difference is A8 - H1 = -119). The ATTACKS array contains a | ||
* bitmask of pieces that can attack from that distance and direction. | ||
* ATTACKS[-80 + 119=39] gives us 24 or 0b11000 in binary. Look at the | ||
* PIECE_MASKS map to determine the mask for a given piece type. In our pawn | ||
* example, we would check to see if 24 & 0x1 is non-zero, which it is | ||
* not. So, naturally, a pawn on E7 can't attack a piece on E2. However, a | ||
* rook can since 24 & 0x8 is non-zero. The only thing left to check is that | ||
* there are no blocking pieces between E7 and E2. That's where the RAYS | ||
* array comes in. It provides an offset (in this case 16) to add to E7 (20) | ||
* to check for blocking pieces. E7 (20) + 16 = E6 (36) + 16 = E5 (52) etc. | ||
*/ | ||
// prettier-ignore | ||
// eslint-disable-next-line | ||
const Ox88: Record<Square, number> = { | ||
@@ -235,6 +240,8 @@ a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7, | ||
const RANK_2 = 6 | ||
// const RANK_3 = 5 | ||
// const RANK_4 = 4 | ||
// const RANK_5 = 3 | ||
// const RANK_6 = 2 | ||
/* | ||
* const RANK_3 = 5 | ||
* const RANK_4 = 4 | ||
* const RANK_5 = 3 | ||
* const RANK_6 = 2 | ||
*/ | ||
const RANK_7 = 1 | ||
@@ -258,5 +265,3 @@ const RANK_8 = 0 | ||
/** | ||
* Extracts the zero-based rank of an 0x88 square. | ||
*/ | ||
// Extracts the zero-based rank of an 0x88 square. | ||
function rank(square: number): number { | ||
@@ -266,5 +271,3 @@ return square >> 4 | ||
/** | ||
* Extracts the zero-based file of an 0x88 square. | ||
*/ | ||
// Extracts the zero-based file of an 0x88 square. | ||
function file(square: number): number { | ||
@@ -278,5 +281,3 @@ return square & 0xf | ||
/** | ||
* Converts a 0x88 square to algebraic notation. | ||
*/ | ||
// Converts a 0x88 square to algebraic notation. | ||
function algebraic(square: number): Square { | ||
@@ -293,63 +294,58 @@ const f = file(square) | ||
/* TODO: this needs a bit of work - it validates structure but completely | ||
* ignores content (e.g. doesn't verify that each side has a king) ... we should | ||
* rewrite this, and ditch the silly error_number field while we're at it */ | ||
export function validateFen(fen: string) { | ||
const errors = [] | ||
errors[0] = 'No errors.' | ||
errors[1] = 'FEN string must contain six space-delimited fields.' | ||
errors[2] = '6th field (move number) must be a positive integer.' | ||
errors[3] = '5th field (half move counter) must be a non-negative integer.' | ||
errors[4] = '4th field (en-passant square) is invalid.' | ||
errors[5] = '3rd field (castling availability) is invalid.' | ||
errors[6] = '2nd field (side to move) is invalid.' | ||
errors[7] = | ||
"1st field (piece positions) does not contain 8 '/'-delimited rows." | ||
errors[8] = '1st field (piece positions) is invalid [consecutive numbers].' | ||
errors[9] = '1st field (piece positions) is invalid [invalid piece].' | ||
errors[10] = '1st field (piece positions) is invalid [row too large].' | ||
errors[11] = 'Illegal en-passant square' | ||
/* 1st criterion: 6 space-seperated fields? */ | ||
// 1st criterion: 6 space-seperated fields? | ||
const tokens = fen.split(/\s+/) | ||
if (tokens.length !== 6) { | ||
return { valid: false, errorNumber: 1, error: errors[1] } | ||
return { | ||
ok: false, | ||
error: 'Invalid FEN: must contain six space-delimited fields', | ||
} | ||
} | ||
/* 2nd criterion: move number field is a integer value > 0? */ | ||
// 2nd criterion: move number field is a integer value > 0? | ||
const moveNumber = parseInt(tokens[5], 10) | ||
if (isNaN(moveNumber) || moveNumber <= 0) { | ||
return { valid: false, errorNumber: 2, error: errors[2] } | ||
return { | ||
ok: false, | ||
error: 'Invalid FEN: move number must be a positive integer', | ||
} | ||
} | ||
/* 3rd criterion: half move counter is an integer >= 0? */ | ||
// 3rd criterion: half move counter is an integer >= 0? | ||
const halfMoves = parseInt(tokens[4], 10) | ||
if (isNaN(halfMoves) || halfMoves < 0) { | ||
return { valid: false, errorNumber: 3, error: errors[3] } | ||
return { | ||
ok: false, | ||
error: | ||
'Invalid FEN: half move counter number must be a non-negative integer', | ||
} | ||
} | ||
/* 4th criterion: 4th field is a valid e.p.-string? */ | ||
// 4th criterion: 4th field is a valid e.p.-string? | ||
if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) { | ||
return { valid: false, errorNumber: 4, error: errors[4] } | ||
return { ok: false, error: 'Invalid FEN: en-passant square is invalid' } | ||
} | ||
/* 5th criterion: 3th field is a valid castle-string? */ | ||
if (!/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(tokens[2])) { | ||
return { valid: false, errorNumber: 5, error: errors[5] } | ||
// 5th criterion: 3th field is a valid castle-string? | ||
if (/[^kKqQ-]/.test(tokens[2])) { | ||
return { ok: false, error: 'Invalid FEN: castling availability is invalid' } | ||
} | ||
/* 6th criterion: 2nd field is "w" (white) or "b" (black)? */ | ||
// 6th criterion: 2nd field is "w" (white) or "b" (black)? | ||
if (!/^(w|b)$/.test(tokens[1])) { | ||
return { valid: false, errorNumber: 6, error: errors[6] } | ||
return { ok: false, error: 'Invalid FEN: side-to-move is invalid' } | ||
} | ||
/* 7th criterion: 1st field contains 8 rows? */ | ||
// 7th criterion: 1st field contains 8 rows? | ||
const rows = tokens[0].split('/') | ||
if (rows.length !== 8) { | ||
return { valid: false, errorNumber: 7, error: errors[7] } | ||
return { | ||
ok: false, | ||
error: "Invalid FEN: piece data does not contain 8 '/'-delimited rows", | ||
} | ||
} | ||
/* 8th criterion: every row is valid? */ | ||
// 8th criterion: every row is valid? | ||
for (let i = 0; i < rows.length; i++) { | ||
/* check for right sum of fields AND not two numbers in succession */ | ||
// check for right sum of fields AND not two numbers in succession | ||
let sumFields = 0 | ||
@@ -361,3 +357,6 @@ let previousWasNumber = false | ||
if (previousWasNumber) { | ||
return { valid: false, errorNumber: 8, error: errors[8] } | ||
return { | ||
ok: false, | ||
error: 'Invalid FEN: piece data is invalid (consecutive number)', | ||
} | ||
} | ||
@@ -368,3 +367,6 @@ sumFields += parseInt(rows[i][k], 10) | ||
if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) { | ||
return { valid: false, errorNumber: 9, error: errors[9] } | ||
return { | ||
ok: false, | ||
error: 'Invalid FEN: piece data is invalid (invalid piece)', | ||
} | ||
} | ||
@@ -376,3 +378,6 @@ sumFields += 1 | ||
if (sumFields !== 8) { | ||
return { valid: false, errorNumber: 10, error: errors[10] } | ||
return { | ||
ok: false, | ||
error: 'Invalid FEN: piece data is invalid (too many squares in rank)', | ||
} | ||
} | ||
@@ -385,10 +390,24 @@ } | ||
) { | ||
return { valid: false, errorNumber: 11, error: errors[11] } | ||
return { ok: false, error: 'Invalid FEN: illegal en-passant square' } | ||
} | ||
/* everything's okay! */ | ||
return { valid: true, errorNumber: 0, error: errors[0] } | ||
const kings = [ | ||
{ color: 'white', regex: /K/g }, | ||
{ color: 'black', regex: /k/g }, | ||
] | ||
for (const { color, regex } of kings) { | ||
if (!regex.test(tokens[0])) { | ||
return { ok: false, error: `Invalid FEN: missing ${color} king` } | ||
} | ||
if ((tokens[0].match(regex) || []).length > 1) { | ||
return { ok: false, error: `Invalid FEN: too many ${color} kings` } | ||
} | ||
} | ||
return { ok: true } | ||
} | ||
/* this function is used to uniquely identify ambiguous moves */ | ||
// this function is used to uniquely identify ambiguous moves | ||
function getDisambiguator(move: InternalMove, moves: InternalMove[]) { | ||
@@ -408,4 +427,5 @@ const from = move.from | ||
/* if a move of the same piece type ends on the same to square, we'll | ||
* need to add a disambiguator to the algebraic notation | ||
/* | ||
* if a move of the same piece type ends on the same to square, we'll need | ||
* to add a disambiguator to the algebraic notation | ||
*/ | ||
@@ -426,15 +446,16 @@ if (piece === ambigPiece && from !== ambigFrom && to === ambigTo) { | ||
if (ambiguities > 0) { | ||
/* if there exists a similar moving piece on the same rank and file | ||
as the move in question, use the square as the disambiguator | ||
*/ | ||
if (sameRank > 0 && sameFile > 0) { | ||
/* | ||
* if there exists a similar moving piece on the same rank and file as | ||
* the move in question, use the square as the disambiguator | ||
*/ | ||
return algebraic(from) | ||
} else if (sameFile > 0) { | ||
/* if the moving piece rests on the same file, use the rank symbol | ||
as the disambiguator | ||
/* | ||
* if the moving piece rests on the same file, use the rank symbol as the | ||
* disambiguator | ||
*/ | ||
return algebraic(from).charAt(1) | ||
} else { | ||
/* else use the file symbol */ | ||
// else use the file symbol | ||
return algebraic(from).charAt(0) | ||
@@ -478,3 +499,2 @@ } | ||
captured, | ||
promotion: undefined, | ||
flags, | ||
@@ -537,10 +557,20 @@ }) | ||
load(fen: string, keepHeaders = false) { | ||
const tokens = fen.split(/\s+/) | ||
let tokens = fen.split(/\s+/) | ||
// append commonly omitted fen tokens | ||
if (tokens.length >= 2 && tokens.length < 6) { | ||
const adjustments = ['-', '-', '0', '1'] | ||
fen = tokens.concat(adjustments.slice(-(6 - tokens.length))).join(' ') | ||
} | ||
tokens = fen.split(/\s+/) | ||
const { ok, error } = validateFen(fen) | ||
if (!ok) { | ||
throw new Error(error) | ||
} | ||
const position = tokens[0] | ||
let square = 0 | ||
if (!validateFen(fen).valid) { | ||
return false | ||
} | ||
this.clear(keepHeaders) | ||
@@ -585,4 +615,2 @@ | ||
this._updateSetup(this.fen()) | ||
return true | ||
} | ||
@@ -621,26 +649,67 @@ | ||
let cflags = '' | ||
let castling = '' | ||
if (this._castling[WHITE] & BITS.KSIDE_CASTLE) { | ||
cflags += 'K' | ||
castling += 'K' | ||
} | ||
if (this._castling[WHITE] & BITS.QSIDE_CASTLE) { | ||
cflags += 'Q' | ||
castling += 'Q' | ||
} | ||
if (this._castling[BLACK] & BITS.KSIDE_CASTLE) { | ||
cflags += 'k' | ||
castling += 'k' | ||
} | ||
if (this._castling[BLACK] & BITS.QSIDE_CASTLE) { | ||
cflags += 'q' | ||
castling += 'q' | ||
} | ||
/* do we have an empty castling flag? */ | ||
cflags = cflags || '-' | ||
// do we have an empty castling flag? | ||
castling = castling || '-' | ||
const epflags = this._epSquare === EMPTY ? '-' : algebraic(this._epSquare) | ||
let epSquare = '-' | ||
/* | ||
* only print the ep square if en passant is a valid move (pawn is present | ||
* and ep capture is not pinned) | ||
*/ | ||
if (this._epSquare !== EMPTY) { | ||
const bigPawnSquare = this._epSquare + (this._turn === WHITE ? 16 : -16) | ||
const squares = [bigPawnSquare + 1, bigPawnSquare - 1] | ||
for (const square of squares) { | ||
// is the square off the board? | ||
if (square & 0x88) { | ||
continue | ||
} | ||
const color = this._turn | ||
// is there a pawn that can capture the epSquare? | ||
if ( | ||
this._board[square]?.color === color && | ||
this._board[square]?.type === PAWN | ||
) { | ||
// if the pawn makes an ep capture, does it leave it's king in check? | ||
this._makeMove({ | ||
color, | ||
from: square, | ||
to: this._epSquare, | ||
piece: PAWN, | ||
captured: PAWN, | ||
flags: BITS.EP_CAPTURE, | ||
}) | ||
const isLegal = !this._isKingAttacked(color) | ||
this._undoMove() | ||
// if ep is legal, break and set the ep square in the FEN output | ||
if (isLegal) { | ||
epSquare = algebraic(this._epSquare) | ||
break | ||
} | ||
} | ||
} | ||
} | ||
return [ | ||
fen, | ||
this._turn, | ||
cflags, | ||
epflags, | ||
castling, | ||
epSquare, | ||
this._halfMoves, | ||
@@ -651,6 +720,7 @@ this._moveNumber, | ||
/* Called when the initial board setup is changed with put() or remove(). | ||
* modifies the SetUp and FEN properties of the header object. if the FEN | ||
/* | ||
* Called when the initial board setup is changed with put() or remove(). | ||
* modifies the SetUp and FEN properties of the header object. If the FEN | ||
* is equal to the default position, the SetUp and FEN are deleted the setup | ||
* is only updated if history.length is zero, ie moves haven't been made. | ||
* is only updated if history.length is zero, ie moves haven't been made. | ||
*/ | ||
@@ -678,3 +748,3 @@ private _updateSetup(fen: string) { | ||
put({ type, color }: { type: PieceSymbol; color: Color }, square: Square) { | ||
/* check for piece */ | ||
// check for piece | ||
if (SYMBOLS.indexOf(type.toLowerCase()) === -1) { | ||
@@ -684,3 +754,3 @@ return false | ||
/* check for valid square */ | ||
// check for valid square | ||
if (!(square in Ox88)) { | ||
@@ -692,3 +762,3 @@ return false | ||
/* don't let the user place more than one king */ | ||
// don't let the user place more than one king | ||
if ( | ||
@@ -726,3 +796,3 @@ type == KING && | ||
for (let i = Ox88.a8; i <= Ox88.h1; i++) { | ||
/* did we run off the end of the board */ | ||
// did we run off the end of the board | ||
if (i & 0x88) { | ||
@@ -733,3 +803,3 @@ i += 7 | ||
/* if empty square or wrong color */ | ||
// if empty square or wrong color | ||
if (this._board[i] === undefined || this._board[i].color !== color) { | ||
@@ -741,2 +811,8 @@ continue | ||
const difference = i - square | ||
// skip - to/from square are the same | ||
if (difference === 0) { | ||
continue | ||
} | ||
const index = difference + 119 | ||
@@ -754,3 +830,3 @@ | ||
/* if the piece is a knight or a king */ | ||
// if the piece is a knight or a king | ||
if (piece.type === 'n' || piece.type === 'k') return true | ||
@@ -781,2 +857,6 @@ | ||
isAttacked(square: Square, attackedBy: Color) { | ||
return this._attacked(attackedBy, Ox88[square]) | ||
} | ||
isCheck() { | ||
@@ -799,7 +879,9 @@ return this._isKingAttacked(this._turn) | ||
isInsufficientMaterial() { | ||
// k.b. vs k.b. (of opposite colors) with mate in 1: | ||
// 8/8/8/8/1b6/8/B1k5/K7 b - - 0 1 | ||
// | ||
// k.b. vs k.n. with mate in 1: | ||
// 8/8/8/8/1n6/8/B7/K1k5 b - - 2 1 | ||
/* | ||
* k.b. vs k.b. (of opposite colors) with mate in 1: | ||
* 8/8/8/8/1b6/8/B1k5/K7 b - - 0 1 | ||
* | ||
* k.b. vs k.n. with mate in 1: | ||
* 8/8/8/8/1n6/8/B7/K1k5 b - - 2 1 | ||
*/ | ||
const pieces: Record<PieceSymbol, number> = { | ||
@@ -859,8 +941,2 @@ b: 0, | ||
isThreefoldRepetition() { | ||
/* TODO: while this function is fine for casual use, a better | ||
* implementation would use a Zobrist key (instead of FEN). the | ||
* Zobrist key would be maintained in the make_move/undo_move | ||
functions, | ||
* avoiding the costly that we do below. | ||
*/ | ||
const moves = [] | ||
@@ -877,7 +953,9 @@ const positions: Record<string, number> = {} | ||
while (true) { | ||
/* remove the last two fields in the FEN string, they're not needed | ||
* when checking for draw by rep */ | ||
/* | ||
* remove the last two fields in the FEN string, they're not needed when | ||
* checking for draw by rep | ||
*/ | ||
const fen = this.fen().split(' ').slice(0, 4).join(' ') | ||
/* has the position occurred three or move times */ | ||
// has the position occurred three or move times | ||
positions[fen] = fen in positions ? positions[fen] + 1 : 1 | ||
@@ -913,3 +991,14 @@ if (positions[fen] >= 3) { | ||
moves(): string[] | ||
moves({ square }: { square: Square }): string[] | ||
moves({ verbose, square }: { verbose: true; square?: Square }): Move[] | ||
moves({ verbose, square }: { verbose: false; square?: Square }): string[] | ||
moves({ | ||
verbose, | ||
square, | ||
}: { | ||
verbose?: boolean | ||
square?: Square | ||
}): string[] | Move[] | ||
moves({ | ||
verbose = false, | ||
@@ -947,3 +1036,3 @@ square = undefined, | ||
/* are we generating moves for a single square? */ | ||
// are we generating moves for a single square? | ||
if (forSquare) { | ||
@@ -960,3 +1049,3 @@ // illegal square, return empty moves | ||
for (let from = firstSquare; from <= lastSquare; from++) { | ||
/* did we run off the end of the board */ | ||
// did we run off the end of the board | ||
if (from & 0x88) { | ||
@@ -977,3 +1066,3 @@ from += 7 | ||
/* single square, non-capturing */ | ||
// single square, non-capturing | ||
to = from + PAWN_OFFSETS[us][0] | ||
@@ -983,3 +1072,3 @@ if (!this._board[to]) { | ||
/* double square */ | ||
// double square | ||
to = from + PAWN_OFFSETS[us][1] | ||
@@ -991,3 +1080,3 @@ if (SECOND_RANK[us] === rank(from) && !this._board[to]) { | ||
/* pawn captures */ | ||
// pawn captures | ||
for (let j = 2; j < 4; j++) { | ||
@@ -1047,9 +1136,11 @@ to = from + PAWN_OFFSETS[us][j] | ||
/* check for castling if: | ||
* a) we're generating all moves, or | ||
* b) we're doing single square move generation on the king's square | ||
/* | ||
* check for castling if we're: | ||
* a) generating all moves, or | ||
* b) doing single square move generation on the king's square | ||
*/ | ||
if (forPiece === undefined || forPiece === KING) { | ||
if (!singleSquare || lastSquare === this._kings[us]) { | ||
/* king-side castling */ | ||
// king-side castling | ||
if (this._castling[us] & BITS.KSIDE_CASTLE) { | ||
@@ -1077,3 +1168,4 @@ const castlingFrom = this._kings[us] | ||
} | ||
/* queen-side castling */ | ||
// queen-side castling | ||
if (this._castling[us] & BITS.QSIDE_CASTLE) { | ||
@@ -1105,4 +1197,6 @@ const castlingFrom = this._kings[us] | ||
/* return all pseudo-legal moves (this includes moves that allow the king | ||
* to be captured) */ | ||
/* | ||
* return all pseudo-legal moves (this includes moves that allow the king | ||
* to be captured) | ||
*/ | ||
if (!legal) { | ||
@@ -1112,3 +1206,3 @@ return moves | ||
/* filter out illegal moves */ | ||
// filter out illegal moves | ||
const legalMoves = [] | ||
@@ -1129,26 +1223,26 @@ | ||
move: string | { from: string; to: string; promotion?: string }, | ||
{ sloppy = false }: { sloppy?: boolean } = {} | ||
{ strict = false }: { strict?: boolean } = {} | ||
) { | ||
/* The move function can be called with in the following parameters: | ||
* | ||
* .move('Nxb7') <- where 'move' is a case-sensitive SAN string | ||
* | ||
* .move({ from: 'h7', <- where the 'move' is a move object | ||
(additional | ||
* to :'h8', fields are ignored) | ||
* promotion: 'q', | ||
* }) | ||
*/ | ||
/* | ||
* The move function can be called with in the following parameters: | ||
* | ||
* .move('Nxb7') <- argument is a case-sensitive SAN string | ||
* | ||
* .move({ from: 'h7', <- argument is a move object | ||
* to :'h8', | ||
* promotion: 'q' }) | ||
* | ||
* | ||
* An optional strict argument may be supplied to tell chess.js to | ||
* strictly follow the SAN specification. | ||
*/ | ||
// sloppy parser allows the move parser to work around over disambiguation | ||
// bugs in Fritz and Chessbase | ||
let moveObj = null | ||
if (typeof move === 'string') { | ||
moveObj = this._moveFromSan(move, sloppy) | ||
moveObj = this._moveFromSan(move, strict) | ||
} else if (typeof move === 'object') { | ||
const moves = this._moves() | ||
/* convert the pretty move object to an ugly move object */ | ||
// convert the pretty move object to an ugly move object | ||
for (let i = 0, len = moves.length; i < len; i++) { | ||
@@ -1166,9 +1260,15 @@ if ( | ||
/* failed to find move */ | ||
// failed to find move | ||
if (!moveObj) { | ||
return null | ||
if (typeof move === 'string') { | ||
throw new Error(`Invalid move: ${move}`) | ||
} else { | ||
throw new Error(`Invalid move: ${JSON.stringify(move)}`) | ||
} | ||
} | ||
/* need to make a copy of move because we can't generate SAN after | ||
the move is made */ | ||
/* | ||
* need to make a copy of move because we can't generate SAN after the move | ||
* is made | ||
*/ | ||
const prettyMove = this._makePretty(moveObj) | ||
@@ -1201,3 +1301,3 @@ | ||
/* if ep capture, remove the captured pawn */ | ||
// if ep capture, remove the captured pawn | ||
if (move.flags & BITS.EP_CAPTURE) { | ||
@@ -1211,3 +1311,3 @@ if (this._turn === BLACK) { | ||
/* if pawn promotion, replace with new piece */ | ||
// if pawn promotion, replace with new piece | ||
if (move.promotion) { | ||
@@ -1217,7 +1317,7 @@ this._board[move.to] = { type: move.promotion, color: us } | ||
/* if we moved the king */ | ||
// if we moved the king | ||
if (this._board[move.to].type === KING) { | ||
this._kings[us] = move.to | ||
/* if we castled, move the rook next to the king */ | ||
// if we castled, move the rook next to the king | ||
if (move.flags & BITS.KSIDE_CASTLE) { | ||
@@ -1235,7 +1335,7 @@ const castlingTo = move.to - 1 | ||
/* turn off castling */ | ||
// turn off castling | ||
this._castling[us] = 0 | ||
} | ||
/* turn off castling if we move a rook */ | ||
// turn off castling if we move a rook | ||
if (this._castling[us]) { | ||
@@ -1253,3 +1353,3 @@ for (let i = 0, len = ROOKS[us].length; i < len; i++) { | ||
/* turn off castling if we capture a rook */ | ||
// turn off castling if we capture a rook | ||
if (this._castling[them]) { | ||
@@ -1267,3 +1367,3 @@ for (let i = 0, len = ROOKS[them].length; i < len; i++) { | ||
/* if big pawn move, update the en passant square */ | ||
// if big pawn move, update the en passant square | ||
if (move.flags & BITS.BIG_PAWN) { | ||
@@ -1279,3 +1379,3 @@ if (us === BLACK) { | ||
/* reset the 50 move counter if a pawn is moved or a piece is captured */ | ||
// reset the 50 move counter if a pawn is moved or a piece is captured | ||
if (move.piece === PAWN) { | ||
@@ -1360,5 +1460,7 @@ this._halfMoves = 0 | ||
}: { newline?: string; maxWidth?: number } = {}) { | ||
/* using the specification from http://www.chessclub.com/help/PGN-spec | ||
/* | ||
* using the specification from http://www.chessclub.com/help/PGN-spec | ||
* example for html usage: .pgn({ max_width: 72, newline_char: "<br />" }) | ||
*/ | ||
const result: string[] = [] | ||
@@ -1369,3 +1471,4 @@ let headerExists = false | ||
for (const i in this._header) { | ||
/* TODO: order of enumerated properties in header object is not | ||
/* | ||
* TODO: order of enumerated properties in header object is not | ||
* guaranteed, see ECMA-262 spec (section 12.6.4) | ||
@@ -1390,3 +1493,3 @@ */ | ||
/* pop all of history onto reversed_history */ | ||
// pop all of history onto reversed_history | ||
const reversedHistory = [] | ||
@@ -1400,3 +1503,3 @@ while (this._history.length > 0) { | ||
/* special case of a commented starting position with no moves */ | ||
// special case of a commented starting position with no moves | ||
if (reversedHistory.length === 0) { | ||
@@ -1406,3 +1509,3 @@ moves.push(appendComment('')) | ||
/* build the list of moves. a move_string looks like: "3. e3 e6" */ | ||
// build the list of moves. a move_string looks like: "3. e3 e6" | ||
while (reversedHistory.length > 0) { | ||
@@ -1417,9 +1520,9 @@ moveString = appendComment(moveString) | ||
/* if the position started with black to move, start PGN with #. ... */ | ||
// if the position started with black to move, start PGN with #. ... | ||
if (!this._history.length && move.color === 'b') { | ||
const prefix = `${this._moveNumber}. ...` | ||
/* is there a comment preceding the first move? */ | ||
// is there a comment preceding the first move? | ||
moveString = moveString ? `${moveString} ${prefix}` : prefix | ||
} else if (move.color === 'w') { | ||
/* store the previous generated move_string if we have one */ | ||
// store the previous generated move_string if we have one | ||
if (moveString.length) { | ||
@@ -1436,3 +1539,3 @@ moves.push(moveString) | ||
/* are there any other leftover moves? */ | ||
// are there any other leftover moves? | ||
if (moveString.length) { | ||
@@ -1442,3 +1545,3 @@ moves.push(appendComment(moveString)) | ||
/* is there a result? */ | ||
// is there a result? | ||
if (typeof this._header.Result !== 'undefined') { | ||
@@ -1448,3 +1551,4 @@ moves.push(this._header.Result) | ||
/* history should be back to what it was before we started generating PGN, | ||
/* | ||
* history should be back to what it was before we started generating PGN, | ||
* so join together moves | ||
@@ -1456,3 +1560,3 @@ */ | ||
// JAH: huh? | ||
// TODO (jah): huh? | ||
const strip = function () { | ||
@@ -1466,3 +1570,3 @@ if (result.length > 0 && result[result.length - 1] === ' ') { | ||
/* NB: this does not preserve comment whitespace. */ | ||
// NB: this does not preserve comment whitespace. | ||
const wrapComment = function (width: number, move: string) { | ||
@@ -1491,3 +1595,3 @@ for (const token of move.split(' ')) { | ||
/* wrap the PGN output at max_width */ | ||
// wrap the PGN output at max_width | ||
let currentWidth = 0 | ||
@@ -1501,5 +1605,5 @@ for (let i = 0; i < moves.length; i++) { | ||
} | ||
/* if the current move will push past max_width */ | ||
// if the current move will push past max_width | ||
if (currentWidth + moves[i].length > maxWidth && i !== 0) { | ||
/* don't end the line with whitespace */ | ||
// don't end the line with whitespace | ||
if (result[result.length - 1] === ' ') { | ||
@@ -1534,10 +1638,6 @@ result.pop() | ||
{ | ||
sloppy = false, | ||
strict = false, | ||
newlineChar = '\r?\n', | ||
}: { sloppy?: boolean; newlineChar?: string } = {} | ||
}: { strict?: boolean; newlineChar?: string } = {} | ||
) { | ||
// option sloppy=true | ||
// allow the user to specify the sloppy move parser to work around over | ||
// disambiguation bugs in Fritz and Chessbase | ||
function mask(str: string): string { | ||
@@ -1554,3 +1654,3 @@ return str.replace(/\\/g, '\\') | ||
for (let i = 0; i < headers.length; i++) { | ||
const regex = /^\s*\[([A-Za-z]+)\s*"(.*)"\s*\]\s*$/ | ||
const regex = /^\s*\[\s*([A-Za-z]+)\s*"(.*)"\s*\]\s*$/ | ||
key = headers[i].replace(regex, '$1') | ||
@@ -1569,5 +1669,11 @@ value = headers[i].replace(regex, '$2') | ||
// RegExp to split header. Takes advantage of the fact that header and movetext | ||
// will always have a blank line between them (ie, two newline_char's). | ||
// With default newline_char, will equal: /^(\[((?:\r?\n)|.)*\])(?:\s*\r?\n){2}/ | ||
/* | ||
* RegExp to split header. Takes advantage of the fact that header and movetext | ||
* will always have a blank line between them (ie, two newline_char's). Handles | ||
* case where movetext is empty by matching newlineChar until end of string is | ||
* matched - effectively trimming from the end extra newlineChar. | ||
* | ||
* With default newline_char, will equal: | ||
* /^(\[((?:\r?\n)|.)*\])((?:\s*\r?\n){2}|(?:\s*\r?\n)*$)/ | ||
*/ | ||
const headerRegex = new RegExp( | ||
@@ -1577,5 +1683,7 @@ '^(\\[((?:' + | ||
')|.)*\\])' + | ||
'(?:\\s*' + | ||
'((?:\\s*' + | ||
mask(newlineChar) + | ||
'){2}' | ||
'){2}|(?:\\s*' + | ||
mask(newlineChar) + | ||
')*$)' | ||
) | ||
@@ -1594,3 +1702,3 @@ | ||
/* parse PGN header */ | ||
// parse PGN header | ||
const headers = parsePgnHeader(headerString) | ||
@@ -1608,30 +1716,36 @@ let fen = '' | ||
/* sloppy parser should attempt to load a fen tag, even if it's | ||
* the wrong case and doesn't include a corresponding [SetUp "1"] tag */ | ||
if (sloppy) { | ||
/* | ||
* the permissive parser should attempt to load a fen tag, even if it's the | ||
* wrong case and doesn't include a corresponding [SetUp "1"] tag | ||
*/ | ||
if (!strict) { | ||
if (fen) { | ||
if (!this.load(fen, true)) { | ||
return false | ||
} | ||
this.load(fen, true) | ||
} | ||
} else { | ||
/* strict parser - load the starting position indicated by [Setup '1'] | ||
* and [FEN position] */ | ||
/* | ||
* strict parser - load the starting position indicated by [Setup '1'] | ||
* and [FEN position] | ||
*/ | ||
if (headers['SetUp'] === '1') { | ||
if (!('FEN' in headers && this.load(headers['FEN'], true))) { | ||
// second argument to load: don't clear the headers | ||
return false | ||
if (!('FEN' in headers)) { | ||
throw new Error( | ||
'Invalid PGN: FEN tag must be supplied with SetUp tag' | ||
) | ||
} | ||
// second argument to load: don't clear the headers | ||
this.load(headers['FEN'], true) | ||
} | ||
} | ||
/* NB: the regexes below that delete move numbers, recursive | ||
* annotations, and numeric annotation glyphs may also match | ||
* text in comments. To prevent this, we transform comments | ||
* by hex-encoding them in place and decoding them again after | ||
* the other tokens have been deleted. | ||
/* | ||
* NB: the regexes below that delete move numbers, recursive annotations, | ||
* and numeric annotation glyphs may also match text in comments. To | ||
* prevent this, we transform comments by hex-encoding them in place and | ||
* decoding them again after the other tokens have been deleted. | ||
* | ||
* While the spec states that PGN files should be ASCII encoded, | ||
* we use {en,de}codeURIComponent here to support arbitrary UTF8 | ||
* as a convenience for modern users */ | ||
* While the spec states that PGN files should be ASCII encoded, we use | ||
* {en,de}codeURIComponent here to support arbitrary UTF8 as a convenience | ||
* for modern users | ||
*/ | ||
@@ -1641,4 +1755,6 @@ function toHex(s: string): string { | ||
.map(function (c) { | ||
/* encodeURI doesn't transform most ASCII characters, | ||
* so we handle these ourselves */ | ||
/* | ||
* encodeURI doesn't transform most ASCII characters, so we handle | ||
* these ourselves | ||
*/ | ||
return c.charCodeAt(0) < 128 | ||
@@ -1668,7 +1784,7 @@ ? c.charCodeAt(0).toString(16) | ||
/* delete header to get the moves */ | ||
// delete header to get the moves | ||
let ms = pgn | ||
.replace(headerString, '') | ||
.replace( | ||
/* encode comments so they don't get deleted below */ | ||
// encode comments so they don't get deleted below | ||
new RegExp(`({[^}]*})+?|;([^${mask(newlineChar)}]*)`, 'g'), | ||
@@ -1683,3 +1799,3 @@ function (_match, bracket, semicolon) { | ||
/* delete recursive annotation variations */ | ||
// delete recursive annotation variations | ||
const ravRegex = /(\([^()]+\))+?/g | ||
@@ -1690,6 +1806,6 @@ while (ravRegex.test(ms)) { | ||
/* delete move numbers */ | ||
// delete move numbers | ||
ms = ms.replace(/\d+\.(\.\.)?/g, '') | ||
/* delete ... indicating black to move */ | ||
// delete ... indicating black to move | ||
ms = ms.replace(/\.\.\./g, '') | ||
@@ -1700,7 +1816,7 @@ | ||
/* trim and get array of moves */ | ||
// trim and get array of moves | ||
let moves = ms.trim().split(new RegExp(/\s+/)) | ||
/* delete empty entries */ | ||
moves = moves.join(',').replace(/,,+/g, ',').split(',') | ||
// delete empty entries | ||
moves = moves.filter((move) => move !== '') | ||
@@ -1716,14 +1832,14 @@ let result = '' | ||
const move = this._moveFromSan(moves[halfMove], sloppy) | ||
const move = this._moveFromSan(moves[halfMove], strict) | ||
/* invalid move */ | ||
// invalid move | ||
if (move == null) { | ||
/* was the move an end of game marker */ | ||
// was the move an end of game marker | ||
if (TERMINATION_MARKERS.indexOf(moves[halfMove]) > -1) { | ||
result = moves[halfMove] | ||
} else { | ||
return false | ||
throw new Error(`Invalid move in PGN: ${moves[halfMove]}`) | ||
} | ||
} else { | ||
/* reset the end of game marker if making a valid move */ | ||
// reset the end of game marker if making a valid move | ||
result = '' | ||
@@ -1734,25 +1850,25 @@ this._makeMove(move) | ||
/* Per section 8.2.6 of the PGN spec, the Result tag pair must match | ||
* match the termination marker. Only do this when headers are | ||
present, | ||
* but the result tag is missing | ||
*/ | ||
/* | ||
* Per section 8.2.6 of the PGN spec, the Result tag pair must match match | ||
* the termination marker. Only do this when headers are present, but the | ||
* result tag is missing | ||
*/ | ||
if (result && Object.keys(this._header).length && !this._header['Result']) { | ||
this.header('Result', result) | ||
} | ||
return true | ||
} | ||
/* convert a move from 0x88 coordinates to Standard Algebraic Notation | ||
* (SAN) | ||
* | ||
* @param {boolean} sloppy Use the sloppy SAN generator to work around | ||
over | ||
* disambiguation bugs in Fritz and Chessbase. See below: | ||
* | ||
* r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4 | ||
* 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned | ||
* 4. ... Ne7 is technically the valid SAN | ||
*/ | ||
/* | ||
* Convert a move from 0x88 coordinates to Standard Algebraic Notation | ||
* (SAN) | ||
* | ||
* @param {boolean} strict Use the strict SAN parser. It will throw errors | ||
* on overly disambiguated moves (see below): | ||
* | ||
* r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4 | ||
* 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned | ||
* 4. ... Ne7 is technically the valid SAN | ||
*/ | ||
private _moveToSan(move: InternalMove, moves: InternalMove[]) { | ||
@@ -1798,5 +1914,4 @@ let output = '' | ||
// convert a move from Standard Algebraic Notation (SAN) to 0x88 | ||
// coordinates | ||
private _moveFromSan(move: string, sloppy = false): InternalMove | null { | ||
// convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates | ||
private _moveFromSan(move: string, strict = false): InternalMove | null { | ||
// strip off any move decorations: e.g Nf3+?! becomes Nf3 | ||
@@ -1815,4 +1930,4 @@ const cleanMove = strippedSan(move) | ||
// strict parser failed and the sloppy parser wasn't used, return null | ||
if (!sloppy) { | ||
// the strict parser failed | ||
if (strict) { | ||
return null | ||
@@ -1827,22 +1942,19 @@ } | ||
// The sloppy parser allows the user to parse non-standard chess | ||
// notations. This parser is opt-in (by specifying the | ||
// '{ sloppy: true }' setting) and is only run after the Standard | ||
// Algebraic Notation (SAN) parser has failed. | ||
// | ||
// When running the sloppy parser, we'll run a regex to grab the piece, | ||
// the to/from square, and an optional promotion piece. This regex will | ||
// parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7, | ||
// f7f8q, b1c3 | ||
/* | ||
* The default permissive (non-strict) parser allows the user to parse | ||
* non-standard chess notations. This parser is only run after the strict | ||
* Standard Algebraic Notation (SAN) parser has failed. | ||
* | ||
* When running the permissive parser, we'll run a regex to grab the piece, the | ||
* to/from square, and an optional promotion piece. This regex will | ||
* parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7, | ||
* f7f8q, b1c3 | ||
* | ||
* NOTE: Some positions and moves may be ambiguous when using the permissive | ||
* parser. For example, in this position: 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1, | ||
* the move b1c3 may be interpreted as Nc3 or B1c3 (a disambiguated bishop | ||
* move). In these cases, the permissive parser will default to the most | ||
* basic interpretation (which is b1c3 parsing to Nc3). | ||
*/ | ||
// NOTE: Some positions and moves may be ambiguous when using the | ||
// sloppy parser. For example, in this position: | ||
// 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1, the move b1c3 may be interpreted | ||
// as Nc3 or B1c3 (a disambiguated bishop move). In these cases, the | ||
// sloppy parser will default to the most most basic interpretation | ||
// (which is b1c3 parsing to Nc3). | ||
// FIXME: these var's are hoisted into function scope, this will need | ||
// to change when switching to const/let | ||
let overlyDisambiguated = false | ||
@@ -1865,6 +1977,9 @@ | ||
} else { | ||
// The [a-h]?[1-8]? portion of the regex below handles moves that may | ||
// be overly disambiguated (e.g. Nge7 is unnecessary and non-standard | ||
// when there is one legal knight move to e7). In this case, the value | ||
// of 'from' variable will be a rank or file, not a square. | ||
/* | ||
* The [a-h]?[1-8]? portion of the regex below handles moves that may be | ||
* overly disambiguated (e.g. Nge7 is unnecessary and non-standard when | ||
* there is one legal knight move to e7). In this case, the value of | ||
* 'from' variable will be a rank or file, not a square. | ||
*/ | ||
matches = cleanMove.match( | ||
@@ -1894,4 +2009,3 @@ /([pnbrqkPNBRQK])?([a-h]?[1-8]?)x?-?([a-h][1-8])([qrbnQRBN])?/ | ||
if (from && to) { | ||
// hand-compare move properties with the results from our sloppy | ||
// regex | ||
// hand-compare move properties with the results from our permissive regex | ||
if ( | ||
@@ -1905,5 +2019,6 @@ (!piece || piece.toLowerCase() == moves[i].piece) && | ||
} else if (overlyDisambiguated) { | ||
// SPECIAL CASE: we parsed a move string that may have an | ||
// unneeded rank/file disambiguator (e.g. Nge7). The 'from' | ||
// variable will | ||
/* | ||
* SPECIAL CASE: we parsed a move string that may have an unneeded | ||
* rank/file disambiguator (e.g. Nge7). The 'from' variable will | ||
*/ | ||
@@ -1929,3 +2044,3 @@ const square = algebraic(moves[i].from) | ||
for (let i = Ox88.a8; i <= Ox88.h1; i++) { | ||
/* display the rank */ | ||
// display the rank | ||
if (file(i) === 0) { | ||
@@ -1976,3 +2091,3 @@ s += ' ' + '87654321'[rank(i)] + ' |' | ||
/* pretty = external move object */ | ||
// pretty = external move object | ||
private _makePretty(uglyMove: InternalMove): Move { | ||
@@ -1989,9 +2104,13 @@ const { color, piece, from, to, flags, captured, promotion } = uglyMove | ||
const fromAlgebraic = algebraic(from) | ||
const toAlgebraic = algebraic(to) | ||
const move: Move = { | ||
color, | ||
piece, | ||
from: algebraic(from), | ||
to: algebraic(to), | ||
from: fromAlgebraic, | ||
to: toAlgebraic, | ||
san: this._moveToSan(uglyMove, this._moves({ legal: true })), | ||
flags: prettyFlags, | ||
lan: fromAlgebraic + toAlgebraic, | ||
} | ||
@@ -2004,2 +2123,3 @@ | ||
move.promotion = promotion | ||
move.lan += promotion | ||
} | ||
@@ -2047,2 +2167,10 @@ | ||
history(): string[] | ||
history({ verbose }: { verbose: true }): (Move & { fen: string })[] | ||
history({ verbose }: { verbose: false }): string[] | ||
history({ | ||
verbose, | ||
}: { | ||
verbose: boolean | ||
}): string[] | (Move & { fen: string })[] | ||
history({ verbose = false }: { verbose?: boolean } = {}) { | ||
@@ -2063,3 +2191,3 @@ const reversedHistory = [] | ||
if (verbose) { | ||
moveHistory.push(this._makePretty(move)) | ||
moveHistory.push({ fen: this.fen(), ...this._makePretty(move) }) | ||
} else { | ||
@@ -2131,25 +2259,1 @@ moveHistory.push(this._moveToSan(move, this._moves())) | ||
} | ||
// return { | ||
// /*************************************************************************** | ||
// * PUBLIC CONSTANTS (is there a better way to do this?) | ||
// **************************************************************************/ | ||
// SQUARES: (function () { | ||
// /* from the ECMA-262 spec (section 12.6.4): | ||
// * "The mechanics of enumerating the properties ... is | ||
// * implementation dependent" | ||
// * so: for (var sq in SQUARES) { keys.push(sq); } might not be | ||
// * ordered correctly | ||
// */ | ||
// var keys = [] | ||
// for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { | ||
// if (i & 0x88) { | ||
// i += 7 | ||
// continue | ||
// } | ||
// keys.push(algebraic(i)) | ||
// } | ||
// return keys | ||
// })(), | ||
// FLAGS: FLAGS, | ||
// } |
@@ -9,4 +9,3 @@ { | ||
}, | ||
"include": ["src/**/*"], | ||
"files": ["__tests__/global.d.ts"] | ||
"include": ["src/**/*"] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
155344
3868
835
0
1