This is a chess implementation in javascript and a fork of https://github.com/kbjorklu/chess
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

400 lines
12 KiB

"use strict";
/**
* AI (artificial intelligence) is a computer player for chess.
* The implementation is an alpha-beta pruned minimax with a simple evaluation function.
* AI takes a chess position, evaluates possible moves up to a certain depth, and returns the move it considers best (or null if the game is lost).
* @constructor
* TODO: add some sort of randomness; per side so that two computers playing against each other act differently (and don't know how the other is acting).
* TODO: static exchange evaluation (see)
* TODO: transposition table
* TODO: iterative deepening
* TODO: negamax formulation
*/
Chess.AI = function() {
};
/**
* @const
* @type {!Array.<number>}
* @see http://goo.gl/zxAE9 (Chess piece relative value)
*/
Chess.AI.PIECE_VALUES = [100, 300, 300, 500, 900, 20000];
/**
* @const
* @type {!Array.<!Array.<number>>}
* @see http://goo.gl/X326e (Simplified evaluation function)
*/
Chess.AI.PIECE_SQUARE_TABLES = [
// pawn
[
0, 0, 0, 0, 0, 0, 0, 0,
50, 50, 50, 50, 50, 50, 50, 50,
10, 10, 20, 30, 30, 20, 10, 10,
5, 5, 10, 25, 25, 10, 5, 5,
0, 0, 0, 20, 20, 0, 0, 0,
5, -5, -10, 0, 0, -10, -5, 5,
5, 10, 10, -20, -20, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0
],
// knight
[
-50, -40, -30, -30, -30, -30, -40, -50,
-40, -20, 0, 0, 0, 0, -20, -40,
-30, 0, 10, 15, 15, 10, 0, -30,
-30, 5, 15, 20, 20, 15, 5, -30,
-30, 0, 15, 20, 20, 15, 0, -30,
-30, 5, 10, 15, 15, 10, 5, -30,
-40, -20, 0, 5, 5, 0, -20, -40,
-50, -40, -30, -30, -30, -30, -40, -50
],
// bishop
[
-20, -10, -10, -10, -10, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 5, 10, 10, 5, 0, -10,
-10, 5, 5, 10, 10, 5, 5, -10,
-10, 0, 10, 10, 10, 10, 0, -10,
-10, 10, 10, 10, 10, 10, 10, -10,
-10, 5, 0, 0, 0, 0, 5, -10,
-20, -10, -10, -10, -10, -10, -10, -20
],
// rook
[
0, 0, 0, 0, 0, 0, 0, 0,
5, 10, 10, 10, 10, 10, 10, 5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
0, 0, 0, 5, 5, 0, 0, 0
],
// queen
[
-20, -10, -10, -5, -5, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 5, 5, 5, 5, 0, -10,
-5, 0, 5, 5, 5, 5, 0, -5,
0, 0, 5, 5, 5, 5, 0, -5,
-10, 5, 5, 5, 5, 5, 0, -10,
-10, 0, 5, 0, 0, 0, 0, -10,
-20, -10, -10, -5, -5, -10, -10, -20
],
// king middle game
[
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-20, -30, -30, -40, -40, -30, -30, -20,
-10, -20, -20, -20, -20, -20, -20, -10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20
]/*,
// king end game
[
-50, -40, -30, -20, -20, -30, -40, -50,
-30, -20, -10, 0, 0, -10, -20, -30,
-30, -10, 20, 30, 30, 20, -10, -30,
-30, -10, 30, 40, 40, 30, -10, -30,
-30, -10, 30, 40, 40, 30, -10, -30,
-30, -10, 20, 30, 30, 20, -10, -30,
-30, -30, 0, 0, 0, 0, -30, -30,
-50, -30, -30, -30, -30, -30, -30, -50
]*/
];
/**
* @const
* @type {number}
* @see http://goo.gl/adkwe (Bishop pair)
*/
Chess.AI.BISHOP_PAIR_VALUE = Chess.AI.PIECE_VALUES[Chess.Piece.PAWN] / 2;
/**
* @param {!Chess.Position} chessPosition
* @param {!Chess.PieceColor} color
* @return {number}
*/
Chess.AI.getMaterialValue = function(chessPosition, color) {
var value = 0;
for (var piece = Chess.Piece.PAWN; piece < Chess.Piece.KING; ++piece) {
value += chessPosition.getPieceColorBitboard(piece, color).popcnt() * Chess.AI.PIECE_VALUES[piece];
}
if (chessPosition.getPieceColorBitboard(Chess.Piece.BISHOP, color).popcnt() > 1) {
value += Chess.AI.BISHOP_PAIR_VALUE;
}
return value;
};
/**
* @param {!Chess.Position} chessPosition
* @return {number}
*/
Chess.AI.evaluateMaterial = function(chessPosition) {
return Chess.AI.getMaterialValue(chessPosition, Chess.PieceColor.WHITE) - Chess.AI.getMaterialValue(chessPosition, Chess.PieceColor.BLACK);
};
/**
* @param {!Chess.Position} chessPosition
* @param {!Chess.PieceColor} color
* @return {number}
* TODO: game phase
*/
Chess.AI.getPieceSquareValue = function(chessPosition, color) {
var value = 0;
for (var piece = Chess.Piece.PAWN; piece <= Chess.Piece.KING; ++piece) {
var pieces = chessPosition.getPieceColorBitboard(piece, color).dup();
while (!pieces.isEmpty()) {
var index = pieces.extractLowestBitPosition();
value += Chess.AI.PIECE_SQUARE_TABLES[piece][color ? index : (56 ^ index)];
}
}
return value;
};
/**
* @param {!Chess.Position} chessPosition
* @return {number}
*/
Chess.AI.evaluateLocations = function(chessPosition) {
return Chess.AI.getPieceSquareValue(chessPosition, Chess.PieceColor.WHITE) - Chess.AI.getPieceSquareValue(chessPosition, Chess.PieceColor.BLACK);
};
/**
* @param {!Chess.PieceColor} color white = attacks by white pieces
* @param {!Chess.Bitboard} pawns
* @param {!Chess.Bitboard} empty
* @return {!Chess.Bitboard}
*/
Chess.AI.makePawnPositionalMask = function(color, pawns, empty) {
var white = (color === Chess.PieceColor.WHITE);
var positional = pawns.dup().shiftLeft(white ? 8 : -8).and(empty);
var doublePush = pawns.dup().and(Chess.Bitboard.RANKS[white ? 1 : 6]).shiftLeft(white ? 16 : -16).and(empty).and(empty.dup().shiftLeft(white ? 8 : -8));
return positional.or(doublePush);
};
/**
* @param {!Chess.Position} chessPosition
* @param {!Chess.PieceColor} color
* @return {number}
* TODO: it's easy to give bonuses for attack and defend here by and(us) or and(opponent)
* TODO: legality
* TODO: does not count all moves; e.g. two pawns can capture the same square, ditto two rooks, two queens
*/
Chess.AI.getMobilityValue = function(chessPosition, color) {
var us = chessPosition.getColorBitboard(color);
var opponent = chessPosition.getColorBitboard(Chess.getOtherPieceColor(color));
var occupied = chessPosition.getOccupiedBitboard();
var empty = chessPosition.getEmptyBitboard();
var mobility = 0;
mobility += Chess.AI.makePawnPositionalMask(color, chessPosition.getPieceColorBitboard(Chess.Piece.PAWN, color), empty).popcnt();
mobility += Chess.Position.makePawnAttackMask(color, chessPosition.getPieceColorBitboard(Chess.Piece.PAWN, color)).and(opponent).popcnt();
var knights = chessPosition.getPieceColorBitboard(Chess.Piece.KNIGHT, color).dup();
while (!knights.isEmpty()) {
mobility += Chess.Bitboard.KNIGHT_MOVEMENTS[knights.extractLowestBitPosition()].dup().and_not(us).popcnt();
}
mobility += Chess.Bitboard.KING_MOVEMENTS[chessPosition.getKingPosition(color)].dup().and_not(us).popcnt();
var queens = chessPosition.getPieceColorBitboard(Chess.Piece.QUEEN, color);
var bq = chessPosition.getPieceColorBitboard(Chess.Piece.BISHOP, color).dup().or(queens);
mobility += Chess.Position.makeBishopAttackMask(bq, occupied).and_not(us).popcnt();
var rq = chessPosition.getPieceColorBitboard(Chess.Piece.ROOK, color).dup().or(queens);
mobility += Chess.Position.makeRookAttackMask(rq, occupied).and_not(us).popcnt();
return mobility * Chess.AI.PIECE_VALUES[Chess.Piece.PAWN] / 100;
};
/**
* @param {!Chess.Position} chessPosition
* @return {number}
*/
Chess.AI.evaluate = function(chessPosition) {
return Chess.AI.evaluateMaterial(chessPosition) + Chess.AI.evaluateLocations(chessPosition);
};
/**
* @param {!Chess.Position} chessPosition
* @return {?Chess.Move}
*/
Chess.AI.prototype.search = function(chessPosition) {
/**
* @param {!Array.<!Chess.Move>} moves
* @return {!Array.<!Chess.Move>}
*/
function sortMoves(moves) {
/**
* @param {!Chess.Move} move
* @return {number}
* TODO: killer heuristic, history, etc
*/
function scoreMove(move) {
var score = move.isCapture() ? ((1 + move.getCapturedPiece()) / (1 + move.getPiece())) : 0;
score = 6 * score + move.getPiece();
score = 16 * score + move.getKind();
score = 64 * score + move.getTo();
score = 64 * score + move.getFrom();
return score;
}
/**
* @param {!Chess.Move} a
* @param {!Chess.Move} b
* @return {number}
*/
function compareMoves(a, b) {
return scoreMove(b) - scoreMove(a);
}
moves.sort(compareMoves);
return moves;
}
var evaluations = 0;
/**
* @param {!Chess.Position} chessPosition
* @param {number} alpha
* @param {number} beta
* @return {number}
*/
function quiescenceSearch(chessPosition, alpha, beta) {
if (chessPosition.isDraw()) {
// always assume the draw will be claimed
return 0;
}
var standPatValue = Chess.AI.evaluate(chessPosition);
++evaluations;
var white = (chessPosition.getTurnColor() === Chess.PieceColor.WHITE);
if (white) {
if (standPatValue >= beta) {
return beta;
}
alpha = (standPatValue > alpha) ? standPatValue : alpha;
} else {
if (standPatValue <= alpha) {
return alpha;
}
beta = (standPatValue < beta) ? standPatValue : beta;
}
var moves = sortMoves(chessPosition.getMoves(true, !chessPosition.isKingInCheck()));
for (var i = 0; i < moves.length; ++i) {
if (chessPosition.makeMove(moves[i])) {
var value = quiescenceSearch(chessPosition, alpha, beta);
chessPosition.unmakeMove();
if (white) {
if (value >= beta) {
return beta;
}
alpha = (value > alpha) ? value : alpha; // max player (white)
} else {
if (value <= alpha) {
return alpha;
}
beta = (value < beta) ? value : beta; // min player (black)
}
}
}
return /** @type {number} */(white ? alpha : beta);
}
/**
* @param {!Chess.Position} chessPosition
* @param {number} depth
* @param {number} alpha
* @param {number} beta
* @return {number}
*/
function alphaBeta(chessPosition, depth, alpha, beta) {
if (depth < 1) {
return quiescenceSearch(chessPosition, alpha, beta);
}
var moves = sortMoves(chessPosition.getMoves(true, false));
var white = (chessPosition.getTurnColor() === Chess.PieceColor.WHITE);
var legal = false;
for (var i = 0; i < moves.length; ++i) {
if (chessPosition.makeMove(moves[i])) {
legal = true;
var value = alphaBeta(chessPosition, depth - 1, alpha, beta);
chessPosition.unmakeMove();
if (white) {
alpha = (value > alpha) ? value : alpha; // max player (white)
} else {
beta = (value < beta) ? value : beta; // min player (black)
}
if (beta <= alpha) {
break;
}
}
}
if (!legal) {
// no legal moves
if (!chessPosition.isKingInCheck()) {
// stalemate, draw
return 0;
}
// checkmate, the player in turn loses
var mate = Chess.AI.PIECE_VALUES[Chess.Piece.KING];// - chessPosition.getMadeMoveCount(); TODO: punish longer games
return white ? -mate : mate;
}
// TODO: avoid the search above before checking this, just check for checkmate
if (chessPosition.isDraw()) {
// always assume the draw will be claimed
return 0;
}
return /** @type {number} */(white ? alpha : beta);
}
var bestMove = null;
var alpha = -Infinity;
var beta = Infinity;
var moves = sortMoves(chessPosition.getMoves(true));
for (var i = 0; i < moves.length; ++i) {
if (chessPosition.makeMove(moves[i])) {
var value = alphaBeta(chessPosition, 3, alpha, beta);
chessPosition.unmakeMove();
if (chessPosition.getTurnColor() === Chess.PieceColor.WHITE) {
// max player (white)
if (value > alpha) {
alpha = value;
bestMove = moves[i];
}
} else {
// min player (black)
if (value < beta) {
beta = value;
bestMove = moves[i];
}
}
// Notice that alpha is always smaller than beta here, because we only update one one them
// at the main level, the other stays infinite (+ or -)
}
}
window.console.log("Evaluations: " + evaluations + ", result move: " + bestMove.getString() + ", alpha: " + alpha + ", beta: " + beta);
return bestMove;
};