xchess
Version:
Chess Engine
467 lines (380 loc) • 7.2 kB
JavaScript
export {fenToState, stateToFEN, fenToBoard, boardToFEN}
import {Color} from './color.js'
import {Piece} from './piece.js'
import {Square} from './square.js'
import {ranks} from './rank.js'
function stateToFEN(state){
return writer.stateToFEN(state);
}
function boardToFEN(board){
return writer.boardToFEN(board);
}
function fenToBoard(fen, board){
return parser.fenToBoard(fen);
}
function fenToState(fen, state){
return parser.fenToState(fen);
}
function EnPassantPawnToCapture(color, square){
if(Color.isWhite(color))
return square.to(0, -1);
if(Color.isBlack(color))
return square.to(0, 1);
}
function EnPassantCaptureToPawn(color, square){
if(Color.isWhite(color))
return square.to(0, 1);
if(Color.isBlack(color))
return square.to(0, -1);
}
class FenSyntaxError extends Error {}
class Writer {
#fen = [];
#castling = [];
#board = [];
#rank = [];
#dx = 0;
get fen(){
return this.#fen.join(' ');
}
get castling(){
if(this.#castling.length > 0)
return this.#castling.join('');
return '-';
}
get board(){
return this.#board.join('/');
}
get rank(){
return this.#rank.join('');
}
get dx(){
return this.#dx;
}
push(value){
this.#fen.push(value);
}
release(){
const fen = this.fen;
this.#fen = [];
return fen;
}
releaseCastling(){
this.push(this.castling);
this.#castling = [];
}
releaseBoard(){
this.push(this.board);
this.#board = [];
}
releaseRank(){
this.#board.push(this.rank);
this.#rank = [];
}
releaseDX(){
if(this.dx > 0){
this.#rank.push(this.dx);
this.#dx = 0;
}
}
fenSquare(board, square){
const piece = board.get(square);
if(piece){
this.releaseDX();
this.#rank.push(piece.fen);
} else this.#dx ++;
}
fenRank(board, squares){
for(const square of squares)
this.fenSquare(board, square);
this.releaseDX();
this.releaseRank();
}
fenBoard(board){
for(const {squares} of ranks)
this.fenRank(board, squares);
this.releaseBoard();
}
cPush(value){
this.#castling.push(value);
}
fenCastling({wk, wq, bk, bq}){
if(wk) this.cPush('K');
if(wq) this.cPush('Q');
if(bk) this.cPush('k');
if(bq) this.cPush('q');
this.releaseCastling();
}
fenDMP(state){
const square = state.enPassantTarget;
if(square){
const target = EnPassantPawnToCapture(state.color, state.enPassantTarget);
this.push(target);
} else this.push('-');
}
fenState(state){
this.fenBoard(state.board);
this.push(state.color.fen);
this.fenCastling(state);
this.fenDMP(state);
this.push(state.halfmoveClock);
this.push(state.fullmoveNumber);
}
stateToFEN(state){
this.fenState(state);
return this.release();
}
boardToFEN(board){
this.fenBoard(board);
return this.release();
}
}
class Parser {
#fen = '';
#offset = 0;
#x = 0;
#y = 0;
#board = new Map();
#state = Object.create(null);
get fen(){
return this.#fen;
}
set fen(fen){
this.#fen = String(fen);
this.#offset = 0;
}
get offset(){
return this.#offset;
}
get length(){
return this.#fen.length;
}
get x(){
return this.#x;
}
get y(){
return this.#y;
}
get board(){
return this.#board;
}
get state(){
return this.#state;
}
get isEnd(){
return this.offset >= this.length;
}
get hasNext(){
return this.offset < this.length;
}
get char(){
return this.#fen[this.#offset];
}
next(){
this.#offset ++;
}
releaseBoard(){
const board = this.board;
this.#board = new Map();
return board;
}
releaseState(){
const state = this.state;
this.#state = Object.create(null);
return state;
}
// Errors
error(message){
throw new FenSyntaxError(message);
}
unexpected(){
this.error(`unexpected symbol '${this.char}'`);
}
fail(){
if(this.isEnd)
this.error('unexpected ending');
this.unexpected();
}
rankOverflow(){
this.error('overflow rank count');
}
fileOverflow(){
this.error('overflow file count');
}
// Parsing
get isWS(){
return /\s/.test(this.char);
}
trim(){
while(this.isWS) this.next();
}
end(){
this.trim();
if(this.hasNext)
this.unexpected();
}
// Board
fenToBoard(fen){
this.fen = fen;
this.trim();
this.parseBoard();
this.end();
return this.releaseBoard();
}
parseBoard(){
this.#y = 0;
this.#x = 0;
this.#board = new Map();
while(this.parseSquare())
this.next();
}
get isP(){
return 'kqrbnpKQRBNP'.includes(this.char);
}
get isDX(){
return '12345678'.includes(this.char);
}
get isDY(){
return this.char === '/';
}
get xCount(){
return + this.char;
}
checkX(){
if(this.x > 8)
this.fileOverflow();
}
checkY(){
if(this.y > 7)
this.rankOverflow();
}
at(){
return Square.at(this.x, this.y);
}
piece(){
const square = this.at();
const piece = Piece.from(this.char);
this.board.set(square, piece);
}
parseSquare(){
if(this.isP){
this.piece();
this.#x ++;
this.checkX();
return true;
}
if(this.isDX){
this.#x += this.xCount;
this.checkX();
return true;
}
if(this.isDY){
this.#x = 0;
this.#y ++;
this.checkY();
return true;
}
return false;
}
// State
fenToState(fen){
this.fen = fen;
this.trim();
const state = this.parseState();
this.end();
return state;
}
parseState(){
this.parseBoard();
const board = this.releaseBoard();
const color = this.color();
const castling = this.castling();
const square = this.enPassantTarget(color);
const halfmoveClock = this.count();
const fullmoveNumber = this.count();
const doubleMovePawn = board.get(square) ?? null;
return {board, color, doubleMovePawn, castling, halfmoveClock, fullmoveNumber};
}
get isColor(){
return 'WwBb'.includes(this.char);
}
get isCastling(){
return 'KQkq'.includes(this.char);
}
get isSkip(){
return this.char === '-';
}
get isFile(){
return /[A-H]/i.test(this.char);
}
get isRank(){
return /[1-8]/.test(this.char);
}
get isZero(){
return this.char == '0';
}
get isDigit(){
return /[0-9]/.test(this.char);
}
skip(){
if(this.isSkip){
this.next();
return true;
} return false;
}
square(){
if(this.isFile){
const file = this.char;
this.next();
if(this.isRank){
const rank = this.char;
this.next();
return Square.from(file + rank);
}
} return null;
}
slice(from, to = this.offset){
return this.#fen.substring(from, to);
}
color(){
this.trim();
if(this.isColor){
const color = Color.from(this.char);
this.next();
return color;
} this.fail();
}
castling(){
this.trim();
if(this.skip()) return new Set();
const offset = this.offset;
while(this.isCastling)
this.next();
if(this.offset > offset)
return new Set(this.slice(offset));
this.fail();
}
enPassantTarget(color){
this.trim();
if(this.skip()) return null;
const square = this.square();
if(square)
return EnPassantCaptureToPawn(color, square);
this.fail();
}
count(){
this.trim();
if(this.isZero){
this.next();
return 0;
}
const offset = this.offset;
while(this.isDigit)
this.next();
if(this.offset > offset)
return + this.slice(offset);
this.fail();
}
}
const parser = new Parser();
const writer = new Writer();