UNPKG

xchess

Version:

Chess Engine

467 lines (380 loc) 7.2 kB
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();