black-horse
Version:
Chess engine built in Javascript using the alpha-beta algorithm
591 lines (493 loc) • 17 kB
JavaScript
const PieceService = require('./piece-service.js');
const MoveService = require('./move-service.js');
const HashService = require('./hash-service.js');
const NotationService = require('./notation-service.js');
const BoardModel = require('../models/board-model-8x8.js');
const configs = require('../configurations');
class BoardService {
constructor() {
this.PieceService = PieceService;
this.MoveService = MoveService;
this.initBoard();
}
initBoard() {
this.boardModel = new BoardModel();
HashService.initHashKeys();
this.parseFEN(configs.fen.startingString);
this.MoveService.init();
}
updateBoardHash(hash) {
this.boardModel.setHash(hash || HashService.generateBoardHash(this.boardModel));
}
getBoard() {
return this.boardModel;
}
/*
http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
//rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
*/
parseFEN(fen) {
let fenCnt;
let piece = configs.pieces.empty;
let column = 1;
let row = 8;
let count;
let index;
let castleFlags;
let fenLength;
this.boardModel.resetBoard();
for (fenCnt = 0, fenLength = fen.length; fenCnt < fenLength; fenCnt += 1) {
count = 1;
//check is a piece
if (configs.fen.validPieces.indexOf(fen[fenCnt]) !== -1) {
piece = configs.fen.piecesConversion[fen[fenCnt]];
}
//check if is a number
else if (parseInt(fen[fenCnt]) >= 1 && parseInt(fen[fenCnt]) <= 8) {
count = parseInt(fen[fenCnt]);
piece = configs.pieces.empty;
}
//check if a row ending
else if (fen[fenCnt] === '/') {
row--;
column = 1;
continue;
}
//check if a empty space
else if (fen[fenCnt] === ' ') {
fenCnt += 1;
break;
}
//add pieces to board
for (index = 0; index < count; index += 1) {
//save the kings positions for check validations
if (piece === configs.pieces.wK) {
this.boardModel.setKingPosition(configs.colors.white, [row, column]);
} else if (piece === configs.pieces.bK) {
this.boardModel.setKingPosition(configs.colors.black, [row, column]);
}
this.boardModel.setPiece(row, column, piece);
if (piece !== configs.pieces.empty) {
this.boardModel.incrementPieceCounter(piece);
this._updateMaterial(piece, true);
this._addToPieceList(piece, row, column);
if (this.boardModel.isPawn(piece)) {
this._updatePawnList(piece, row, column);
}
}
column += 1;
}
}
//rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
//set the play color (black or white)
this.boardModel.setColor(fen[fenCnt] === 'w' ? configs.colors.white : configs.colors.black);
fenCnt += 2;
//set the castling configuration
castleFlags = this.boardModel.getCastleFlags();
for (index = 0; index < 4; index += 1) {
if (fen[fenCnt] === ' ') {
break;
}
switch (fen[fenCnt]) {
case 'K':
castleFlags[configs.colors.white].kingSide = true;
break;
case 'Q':
castleFlags[configs.colors.white].queenSide = true;
break;
case 'k':
castleFlags[configs.colors.black].kingSide = true;
break;
case 'q':
castleFlags[configs.colors.black].queenSide = true;
break;
default:
break;
}
fenCnt += 1;
}
fenCnt += 1;
//en passant
if (fen[fenCnt] !== '-') {
this.boardModel.setEnPassantPosition(parseInt(fen[fenCnt + 1]), configs.columnChar.indexOf(fen[fenCnt]) + 1);
fenCnt += 3;
} else {
fenCnt += 2;
}
this.updateBoardHash();
if (!isNaN(parseInt(fen[fenCnt + 1], 10))) {
this.boardModel.setFiftyMoveCounter(parseInt(fen[fenCnt] + fen[fenCnt + 1], 10));
fenCnt += 3;
} else {
this.boardModel.setFiftyMoveCounter(parseInt(fen[fenCnt], 10));
fenCnt += 2;
}
if (!isNaN(parseInt(fen[fenCnt + 1], 10))) {
this.boardModel.setFullMoveCounter(parseInt(fen[fenCnt] + fen[fenCnt + 1], 10));
} else {
this.boardModel.setFullMoveCounter(parseInt(fen[fenCnt], 10));
}
}
convertToFEN(board) {
board = board || this.boardModel;
return NotationService.convertToFEN(board);
}
getPieceMoves(row, column) {
return this.MoveService.getPieceMoves(this.boardModel, row, column);
}
getPieceValidMoves(row, column) {
const moves = this.getPieceMoves(row, column);
const validMoves = [];
for (let move of moves) {
if (this.makeMove(move)) {
validMoves.push(move);
this.rollbackMove();
}
}
return validMoves;
}
isPieceAttacked(row, column) {
return this.MoveService.isPieceAttacked(this.boardModel, row, column);
}
_updateMaterial(piece, up = true) {
const color = this.boardModel.getPieceColor(piece);
let value;
const sign = up ? 1 : -1;
if (piece !== configs.pieces.empty) {
value = this.PieceService.getStartingPieceValue(piece);
this.boardModel.setPieceMaterial(color, this.boardModel.getPieceMaterial(color) + value * sign);
if (this.boardModel.isPawn(piece)) {
this.boardModel.setPawnMaterial(color, this.boardModel.getPawnMaterial(color) + value * sign);
}
}
}
_updatePawnList(piece, row, column, remove = false) {
if (!this.boardModel.isPawn(piece)) {
return;
}
const color = this.boardModel.getPieceColor(piece);
const pos = row + '' + column;
let index;
const pawnList = this.boardModel.getPawnList(color);
index = pawnList.search(pos);
if (!remove && !index) {
pawnList.insert(pos, { row, column, piece });
} else if (remove && index) {
pawnList.remove(pos);
}
}
_addToPieceList(piece, row, column) {
const value = { row, column, piece };
this.boardModel.getPieceList(this.boardModel.getPieceColor(piece)).insert(row + '' + column, value);
}
_updatePieceList(pieceOrig, pieceDest, rowOrig, columnOrig, rowDest, columnDest) {
const colorOrig = this.boardModel.getPieceColor(pieceOrig);
const colorDest = this.boardModel.getPieceColor(pieceDest);
const posOrig = rowOrig + '' + columnOrig;
const posDest = rowDest + '' + columnDest;
if (pieceDest !== configs.pieces.empty && colorDest !== -1 && colorOrig !== colorDest) {
this.boardModel.getPieceList(colorDest).remove(posDest);
}
const pieceList = this.boardModel.getPieceList(colorOrig);
pieceList.remove(posOrig);
pieceList.insert(posDest, { row: rowDest, column: columnDest, piece: pieceOrig });
}
_removeFromPieceList(row, column) {
const piece = this.boardModel.getPiece(row, column);
const color = this.boardModel.getPieceColor(row, column);
if (piece !== configs.pieces.empty) {
this.boardModel.getPieceList(color).remove(row + '' + column);
}
}
_castleMove(pieceOrig, rowOrigin, columnOrigin, rowDest, columnDest) {
let rook;
const flags = [];
let castleFlags;
flags[configs.colors.white] = {
kingSide: false,
queenSide: false,
};
flags[configs.colors.black] = {
kingSide: false,
queenSide: false,
};
castleFlags = this.boardModel.getCastleFlags();
switch (pieceOrig) {
case configs.pieces.wR:
if (rowOrigin === 1 && columnOrigin === 1) {
castleFlags[configs.colors.white].queenSide = false;
} else if (rowOrigin === 1 && columnOrigin === 8) {
castleFlags[configs.colors.white].kingSide = false;
}
break;
case configs.pieces.bR:
if (rowOrigin === 8 && columnOrigin === 1) {
castleFlags[configs.colors.black].queenSide = false;
} else if (rowOrigin === 8 && columnOrigin === 8) {
castleFlags[configs.colors.black].kingSide = false;
}
break;
case configs.pieces.wK:
if (rowOrigin === 1 && columnOrigin === 5) {
//move the rook
if (
castleFlags[configs.colors.white].kingSide &&
rowDest === 1 &&
columnDest === 7 &&
this.boardModel.getPiece(1, 8) === configs.pieces.wR
) {
rook = this.boardModel.getPiece(1, 8);
this._hashPiece(rook, 1, 8);
this._hashPiece(rook, 1, 6);
this._updatePieceList(rook, this.boardModel.getPiece(1, 6), 1, 8, 1, 6);
this.boardModel.setPiece(1, 6, rook);
this.boardModel.setPiece(1, 8, configs.pieces.empty);
flags[configs.colors.white].kingSide = true;
} else if (
castleFlags[configs.colors.white].queenSide &&
rowDest === 1 &&
columnDest === 3 &&
this.boardModel.getPiece(1, 1) === configs.pieces.wR
) {
rook = this.boardModel.getPiece(1, 1);
this._hashPiece(rook, 1, 1);
this._hashPiece(rook, 1, 4);
this._updatePieceList(rook, this.boardModel.getPiece(1, 4), 1, 1, 1, 4);
this.boardModel.setPiece(1, 4, rook);
this.boardModel.setPiece(1, 1, configs.pieces.empty);
flags[configs.colors.white].queenSide = true;
}
castleFlags[configs.colors.white].queenSide = false;
castleFlags[configs.colors.white].kingSide = false;
}
break;
case configs.pieces.bK:
if (rowOrigin === 8 && columnOrigin === 5) {
//move the rook
if (
castleFlags[configs.colors.black].kingSide &&
rowDest === 8 &&
columnDest === 7 &&
this.boardModel.getPiece(8, 8) === configs.pieces.bR
) {
rook = this.boardModel.getPiece(8, 8);
this._hashPiece(rook, 8, 8);
this._hashPiece(rook, 8, 6);
this._updatePieceList(rook, this.boardModel.getPiece(8, 6), 8, 8, 8, 6);
this.boardModel.setPiece(8, 6, rook);
this.boardModel.setPiece(8, 8, configs.pieces.empty);
flags[configs.colors.black].kingSide = true;
} else if (
castleFlags[configs.colors.black].queenSide &&
rowDest === 8 &&
columnDest === 3 &&
this.boardModel.getPiece(8, 1) === configs.pieces.bR
) {
rook = this.boardModel.getPiece(8, 1);
this._hashPiece(rook, 8, 1);
this._hashPiece(rook, 8, 4);
this._updatePieceList(rook, this.boardModel.getPiece(8, 4), 8, 1, 8, 4);
this.boardModel.setPiece(8, 4, rook);
this.boardModel.setPiece(8, 1, configs.pieces.empty);
flags[configs.colors.black].queenSide = true;
}
castleFlags[configs.colors.black].queenSide = false;
castleFlags[configs.colors.black].kingSide = false;
}
break;
}
return flags;
}
_hashCastle() {
this.boardModel.setHash(HashService.hashCastle(this.boardModel.getHash(), this.boardModel.getCastleFlags()));
}
_hashEnPassant() {
this.boardModel.setHash(HashService.hashEnPassant(this.boardModel.getHash(), this.boardModel.getEnPassantPosition()));
}
_hashPiece(piece, row, column) {
this.boardModel.setHash(HashService.hashPiece(this.boardModel.getHash(), piece, row, column));
}
_hashColor() {
this.boardModel.setHash(HashService.hashColor(this.boardModel.getHash(), this.boardModel.getColor()));
}
makeMove(move) {
let pieceOrig;
let pieceDest;
let pieceColor;
let kingPosition;
let enPassantPiece;
let enPassantPosition;
let dir;
const flagsObj = {
enPassant: false,
castle: false,
};
if (!move || !move.rowOrig || !move.columnOrig || !move.rowDest || !move.columnDest) {
return false;
}
pieceOrig = move.piece;
pieceDest = move.pieceDest;
pieceColor = move.color;
dir = pieceColor === configs.colors.white ? 1 : -1;
if (
pieceOrig === configs.pieces.empty ||
pieceOrig === configs.pieces.offBoard ||
this.boardModel.getPieceColor(move.rowDest, move.columnDest) === pieceColor
) {
return false;
}
//add move to history
this.MoveService.addToHistory(this.boardModel, move);
this._updatePieceList(pieceOrig, pieceDest, move.rowOrig, move.columnOrig, move.rowDest, move.columnDest);
//add en passant pawn to capture list
enPassantPosition = this.boardModel.getEnPassantPosition();
if (move.flag === configs.flags.enPassant && enPassantPosition) {
const enPassantPiecePos = [enPassantPosition[0] - dir, enPassantPosition[1]];
enPassantPiece = this.boardModel.getPiece(enPassantPiecePos[0], enPassantPiecePos[1]);
this.boardModel.decrementPieceCounter(enPassantPiece);
this.boardModel.addLostPiece(enPassantPiecePos[0], enPassantPiecePos[1]);
this._updateMaterial(enPassantPiece, false);
this._updatePawnList(enPassantPiece, enPassantPiecePos[0], enPassantPiecePos[1], true);
this._hashPiece(enPassantPiece, enPassantPiecePos[0], enPassantPiecePos[1]);
this._removeFromPieceList(enPassantPiecePos[0], enPassantPiecePos[1]);
this.boardModel.setPiece(enPassantPiecePos[0], enPassantPiecePos[1], configs.pieces.empty);
flagsObj.enPassant = true;
}
//add piece to captured list
if (pieceDest !== configs.pieces.empty) {
this.boardModel.decrementPieceCounter(pieceDest);
this.boardModel.addLostPiece(pieceDest);
this._updateMaterial(pieceDest, false);
this._updatePawnList(pieceDest, move.rowDest, move.columnDest, true);
//remove the captured piece from the hash
this._hashPiece(pieceDest, move.rowDest, move.columnDest);
}
//remove the en passant flag
this._hashEnPassant();
//detect en passant move
if (
(pieceOrig === configs.pieces.bP && move.rowDest === 5 && move.rowOrig === 7) ||
(pieceOrig === configs.pieces.wP && move.rowDest === 4 && move.rowOrig === 2)
) {
this.boardModel.setEnPassantPosition(move.rowDest - dir, move.columnDest);
} else {
this.boardModel.setEnPassantPosition();
}
//add the en passant flag
this._hashEnPassant();
//remove the castle flags from the hash
this._hashCastle();
//set the castle flags
flagsObj.castle = this._castleMove(pieceOrig, move.rowOrig, move.columnOrig, move.rowDest, move.columnDest);
//add the castle flags
this._hashCastle();
//remove the piece at the origin location from the hash
this._hashPiece(pieceOrig, move.rowOrig, move.columnOrig);
//pawns promotions
if (move.flag === configs.flags.promotion) {
this._updatePawnList(pieceOrig, move.rowOrig, move.columnOrig, true);
this._updateMaterial(pieceOrig, false);
this._updateMaterial(move.promotedPiece, true);
this.boardModel.setPiece(move.rowOrig, move.columnOrig, configs.pieces.empty);
this.boardModel.setPiece(move.rowDest, move.columnDest, move.promotedPiece);
//update the hash
this._hashPiece(move.promotedPiece, move.rowDest, move.columnDest);
flagsObj.promotion = true;
} else {
//move the piece on the board
this.boardModel.setPiece(move.rowDest, move.columnDest, pieceOrig);
this.boardModel.setPiece(move.rowOrig, move.columnOrig, configs.pieces.empty);
//remove the piece at the destiny location from the hash
this._hashPiece(pieceOrig, move.rowDest, move.columnDest);
}
//update the king position
kingPosition = this.boardModel.getKingPosition(pieceColor);
if (kingPosition[0] === move.rowOrig && kingPosition[1] === move.columnOrig) {
kingPosition = [move.rowDest, move.columnDest];
this.boardModel.setKingPosition(pieceColor, kingPosition);
}
this.boardModel.incrementFullMoveCounter();
//check if the move is valid
if (this.isPieceAttacked(kingPosition[0], kingPosition[1])) {
this.rollbackMove();
return false;
}
return flagsObj;
}
makeMoveXY(rowOrig, columnOrig, rowDest, columnDest) {
const pieceMoves = this.getPieceMoves(rowOrig, columnOrig);
let move;
for (let pieceMove of pieceMoves) {
if (pieceMove.rowDest === rowDest && pieceMove.columnDest === columnDest) {
move = pieceMove;
break;
}
}
return this.makeMove(move);
}
moveAndSwitch(rowOrig, columnOrig, rowDest, columnDest) {
const flags = this.makeMoveXY(rowOrig, columnOrig, rowDest, columnDest);
this.switchColor();
return flags;
}
rollbackMove(cb) {
const move = this.MoveService.rollback();
cb = cb || function () {};
if (move) {
this.boardModel = new BoardModel();
this.boardModel.rebuild(move.board);
cb();
}
}
getLastMove() {
return this.MoveService.getLastMove();
}
isCheckMate(color) {
const board = this.getBoard();
const kingPosition = board.getKingPosition(color);
let isKingInCheck = this.isPieceAttacked(kingPosition[0], kingPosition[1]);
if (isKingInCheck === false) {
return false;
}
const moves = this.generateAllMoves(color);
let legalMoves = 0;
for (let move of moves) {
if (!this.makeMove(move)) {
continue;
}
legalMoves += 1;
isKingInCheck = this.isPieceAttacked(kingPosition[0], kingPosition[1]);
this.rollbackMove();
if (isKingInCheck === false) {
return false;
}
}
return legalMoves === 0;
}
switchColor() {
this.boardModel.switchColor();
this._hashColor();
}
generateAllMoves(color) {
return this.MoveService.generateAllMoves(this.boardModel, color);
}
generateAllCaptureMoves(color) {
return this.MoveService.generateAllCaptureMoves(this.boardModel, color);
}
generateAllValidMoves(color) {
const moves = this.generateAllMoves(color);
const validMoves = [];
for (let move of moves) {
if (this.makeMove(move)) {
validMoves.push(move);
this.rollbackMove();
}
}
return validMoves;
}
printBoard(board) {
board = board || this.boardModel.get64Board();
NotationService.printBoard(board);
}
}
module.exports = new BoardService();