package minechess.common.ai; import java.util.Arrays; /** * MineChess * @author MineMaarten * www.minemaarten.com * @license Lesser GNU Public License v3 (http://www.gnu.org/licenses/lgpl.html) * * This class came from pate's ChessMate, found at https://github.com/pate/chessmate. Many thanks to him for allowing * the usage of his code by others. These classes are a bit modified to be able to have more AI's running at the same time. */ public class Chess{ /** * A circular reference to the parent Main class. */ public AIMain main; final static public boolean WHITE = true; final static public boolean BLACK = false; public boolean HUMAN = WHITE; public boolean PROGRAM = BLACK; public boolean bWhoseTurn = WHITE; public boolean bIterativeDeepening = true; /** * An integer that stores the number of nodes traversed. */ public int nodeCount = 0; // number of many alpha-beta nodes being searched /** * An integer that holds the maximum recursive depth. This is controlled by the * difficulty slider in the Main class. * @see difficultySlider */ public int maxDepth; public static int maxDepthSetting = 5; // default AI setting. /** * True while the AICaller thread is working. If it is falsed, the thread will stop ASAP. */ public boolean bThinking = false; /** * Piece indexes used to index the piece movement table when generating moves. * @see pieceMovementTable */ private final static int[] index = {0, 12, 15, 10, 1, 6, 6}; /** * The current state of the board, used for drawing by Main Class */ public ChessPosition pos = new ChessPosition(); public ChessPosition workPos = new ChessPosition(); /** * The all-important movement table for generating possible moves for a given piece. * This is indexed via index. * @see index */ private final static int[] pieceMovementTable = {0, -1, 1, 10, -10, 0, // Rook -1, 1, 10, -10, -9, -11, 9, 11, 0, // Queen / Bishop / King 8, -8, 12, -12, 19, -19, 21, -21, 0, // Knight 10, 20, 0}; /** * Arbitrary piece values. Note how the king is indispensable - important for checks. */ private final static float[] value = {0.0f, 1.0f, 3.0f, 3.2f, 5.0f, 9.0f, 500.0f}; /** * A temporary list of all possible moves for a given position. */ private final ChessMove[] possibleCaptures = new ChessMove[256]; private final ChessMove[] possibleMoveList = new ChessMove[256]; /** * A temporary list of possible moves for a given piece. Contains a maximum of 32 moves. */ private final int[] piece_moves = new int[32]; /** * Test for a stalemate. */ public boolean drawnPosition(ChessPosition p){ return false; } /** * Tests whether a side has won the game, i.e. check for checkmates. */ public boolean wonPosition(ChessPosition p, boolean player){ return false; } /** * Called by the alphaBetaHelper function to decide whether to cut-off at a certain depth. * This function has been deprecated and is now in-lined with the alphaBetaHelper function for speed. */ /*public boolean reachedMaxDepth( ChessPosition p, int depth ) { if ( depth <= maxDepth && bThinking ) return false; return true; }*/ /** * Initiates the alphaBetaHelper function and handles iterative deepening. */ protected ChessMove alphaBeta(int depth, ChessPosition p, boolean player){ nodeCount = 0; reachedDepth = 0; // Find out if anyone is currently in check (this is kinda skipped by 0-level a-b) //calcPossibleMoves( p, player ); if(maxDepth > 3 && bIterativeDeepening) { int prevDepth = maxDepth; maxDepth = 3; float beta = 100000.0f; for(int md = 3; md < prevDepth + 1; md++) { maxDepth = md; beta = alphaBetaHelper(depth, p, player, 100000.0f, -beta); if(!bThinking) break; } // int nodeCount maxDepth = prevDepth; } return bestMove; } /** * An integer to display the maximum depth reached while searching for a move. */ public int reachedDepth = 0; public ChessMove bestMove = new ChessMove(); // Used to reference the ACTUAL move int localMaxima = 0; /** * This function is the core of Chessmate. It does all the node searching by * recursively calling itself and storing the best move. */ public ChessMove localBestMove = null; public boolean bCheck = false; public int lastDepth = -1; public ChessMove[] principalVariation = new ChessMove[16]; // there will under no circumstances be more than 16 moves in there :) protected float alphaBetaHelper(int depth, ChessPosition p, boolean player, float alpha, float beta){ //System.out.println("depth: " + depth); /** * This used to decrease speed by a magnitude of 5, * but we're optimizing it by only searching for checks if we're already in check, * thus finding only mates. Checks will register as 'king takes' and will count as such. */ if(lastDepth != depth) { lastDepth = depth; // main.aiCaller.entityPiece.sendChatToNearbyPlayers("Current Depth: " + depth, null); } int num = 0; /** * First, check whether the other player is in check due to previous move */ if(player ? p.bBlackChecked : p.bWhiteChecked) { num = calcPossibleMoves(p, player); /** * If the player making the move is still checked - he is effectively mated, * which must be avoided at all costs. This is worse if it was his turn. * If he is not checked any more, that is OK. If */ if(player ? p.bBlackChecked : p.bWhiteChecked)// && !(player ? p.bWhiteChecked : p.bBlackChecked) ) { /** * This forces the side to move out of check, despite seeing a riposting check(mate). */ if(depth == 2) { return (maxDepth - depth + localMaxima + 1) * 2000; } //System.out.println("Detected checkmate."); return (maxDepth - depth + localMaxima + 1) * 1000; // +1 so it's never 0; }// else // return (maxDepth-depth+localMaxima+1) * -2000; // +1 so it's never 0; } if(depth > reachedDepth) reachedDepth = depth; // Have we reached our maximum depths? if(depth - localMaxima >= maxDepth || !bThinking) // Test if we have reached our maximum depth and cut-off here { return positionEvaluation(p, player); } if(num == 0) // we're hoping this isn't a stalemate :) num = calcPossibleMoves(p, player); float value = 0; ChessMove[] chessMove = new ChessMove[num];// = new ChessMove[num]; int nMoves = num - nPossibleCaptures; int i; for(i = 0; i < nPossibleCaptures; i++) { chessMove[i] = new ChessMove(possibleCaptures[i]); } for(i = 0; i < nMoves; i++) { chessMove[nPossibleCaptures + i] = new ChessMove(possibleMoveList[i]); } int prevToVal = 0; // the previous value of the to block int prevFromVal = 0; boolean bWhiteKingMoved;// = false; boolean bBlackKingMoved;// = false; for(i = 0; i < num; i++) // Iterate through moves and recursively call self { // If we are taking a piece, make sure to search ahead so that it's not bogus! if(p.board[chessMove[i].to] != 0 || p.board[chessMove[i].from] == (player ? ChessPosition.PAWN : -ChessPosition.PAWN)) { localMaxima = 2; } /** * Make the move */ bWhiteKingMoved = p.bWhiteKingMoved; bBlackKingMoved = p.bBlackKingMoved; prevFromVal = p.board[chessMove[i].from]; prevToVal = p.board[chessMove[i].to]; p.makeMove(chessMove[i]); ++nodeCount; // A diagnostic, counting the positions we have searched. value = -alphaBetaHelper(depth + 1, p, !player, -beta, -alpha); /** * Unmake the move */ p.board[chessMove[i].from] = prevFromVal; p.board[chessMove[i].to] = prevToVal; p.bWhiteKingMoved = bWhiteKingMoved; p.bBlackKingMoved = bBlackKingMoved; if(localMaxima != 0) localMaxima = 0; if(value >= alpha) return alpha; if(value > beta) { beta = value; principalVariation[depth].from = chessMove[i].from; principalVariation[depth].to = chessMove[i].to; if(depth == 0) bestMove = chessMove[i]; } } return beta; } /** * Invokes AI Move Computation. Calculates the best move and makes it. */ public ChessMove playGame(ChessPosition startingPosition, boolean bAsPlayer){ // Let's clear the principal variation for(int i = 0; i < 16; i++) { principalVariation[i].from = 0; principalVariation[i].to = 0; } workPos = new ChessPosition(startingPosition); bestMove = null; // First, build a vector of "good" possible positions // we're using iterative deepening to first a get a feel for a good move bestMove = alphaBeta(0, workPos, bAsPlayer); if(!bThinking) return null; if(bestMove == null) { main.alert("It's a draw", "Stalemate."); } while(true) // a dummy to avoid silly returns { // Do some tests about the current position if(wonPosition(startingPosition, PROGRAM)) { System.out.println("Program won"); break; } if(wonPosition(startingPosition, HUMAN)) { System.out.println("Human won"); break; } if(drawnPosition(startingPosition)) { System.out.println("Drawn game"); break; } break; } return bestMove; } /** * Some variables to hold check information */ int sideChecked = 0; // if 0, no side, if 1, white, if -1, black /** * A grand part of Chessmate. This function analyses a given position and assigns * a floating-point value to it, guessing as to the chances of a win arising from that position for a given side. * It takes into account material values (the pieces on the board) and calls a special setControlData * function to determine board control. This control value is adjusted and added to the evaluation value, * before it is returned to the caller, alphaBetaHelper. */ public float positionEvaluation(ChessPosition p, boolean player){ ChessPosition pos = p; int[] b = pos.board; float ret = 0.0f; int pieceType = 0; float val = 0; int i; float control = 0; // adjust for material: for(int y = 0; y < 8; y++) { for(int x = 0; x < 8; x++) { i = y * 10 + x; if(b[i] == 0) continue; // Calculate Positional Advantage control += humanControl[i]; control -= computerControl[i]; if(b[i] < 0) { if(humanControl[i] > computerControl[i]) { control += value[-b[i]]; } } else { if(humanControl[i] < computerControl[i]) { control -= value[b[i]]; } } // Calculate Material Advantage pieceType = b[i]; if(pieceType < 0) pieceType = -pieceType; val = value[pieceType]; val *= 5; // quadruple material value if(b[i] < 0) val = -val; ret += val; } } // Count center squares extra: control += humanControl[33] - computerControl[33]; control += humanControl[34] - computerControl[34]; control += humanControl[43] - computerControl[43]; control += humanControl[44] - computerControl[44]; control *= 0.333;///= 30;//20; ret += control; // adjust if computer side to move: if(!player) ret = -ret; return ret; } /** * Generates all possible moves for a given piece (square_index) on a given position (pos), * storing the moves in piece_moves and returning the number of available moves. * This function is extensively called indirectly by the alphaBetaHelper function. */ private int calcPieceMoves(ChessPosition pos, int square_index){ int[] b = pos.board; int piece = b[square_index]; int piece_type = piece; if(piece_type < 0) piece_type = -piece_type; int count = 0, side_index, move_offset, temp, next_square; int piece_index = index[piece_type]; int move_index = pieceMovementTable[piece_index]; int target = 0; if(piece < 0) side_index = -1; else side_index = 1; int[] control = piece < 0 ? computerControl : humanControl; switch(piece_type){ case 0: case 7: break; case ChessPosition.PAWN: { // Check if pawn can take left move_offset = square_index + side_index * 10 + 1; if(move_offset >= 0 && move_offset < 80) { target = b[move_offset]; if(piece > 0 && target < 0 && target != 7 || piece < 0 && target > 0 && target != 7) { piece_moves[count++] = move_offset; control[move_offset] += 12; /** * Is the pawn checking the player? */ if(target == pos.KING) pos.bWhiteChecked = true; else if(target == -pos.KING) pos.bBlackChecked = true; } } // Can pawn take to the right? move_offset = square_index + side_index * 10 - 1; if(move_offset >= 0 && move_offset < 80) { target = b[move_offset]; if(piece > 0 && target < 0 && target != 7 || piece < 0 && target > 0 && target != 7) { piece_moves[count++] = move_offset; control[move_offset] += 12; /** * Is the pawn checking the player? */ if(target == pos.KING) pos.bWhiteChecked = true; else if(target == -pos.KING) pos.bBlackChecked = true; } } // check for initial pawn move of 2 squares forward: move_offset = square_index + side_index * 20; if(move_offset >= 0 && move_offset < 80) { temp = piece > 0 ? 1 : 6; if(b[move_offset] == 0 && square_index / 10 == temp && (piece < 0 && b[square_index - 10] == 0 || piece > 0 && b[square_index + 10] == 0)) { piece_moves[count++] = move_offset; } } // try to move forward 1 square: move_offset = square_index + side_index * 10; if(move_offset >= 0 && move_offset < 80) if(b[move_offset] == 0) piece_moves[count++] = move_offset; } break; default: { move_index = piece; if(move_index < 0) move_index = -move_index; move_index = index[move_index]; next_square = square_index + pieceMovementTable[move_index]; outer: while(true) { inner: while(true) { if(next_square >= 80 || next_square < 0 || b[next_square] == 7) // can't I just % 10 ? break inner; target = b[next_square]; // check for piece on the same side: if(side_index < 0 && target < 0 || side_index > 0 && target > 0) break inner; control[next_square] += 1; piece_moves[count++] = next_square; /** * Is the opponent in check? */ if(target == pos.KING) pos.bWhiteChecked = true; else if(target == -pos.KING) pos.bBlackChecked = true; if(target != 0) { //System.out.println(square_index + " can attack " + next_square + "(" + b[next_square] + ")"); break inner; } // Kings and Knights don't re-iterate their moves because they can only take a single "step" if(piece_type == ChessPosition.KNIGHT || piece_type == ChessPosition.KING) break inner; // Important to advance the move table to cover all directions next_square += pieceMovementTable[move_index]; } ++move_index; if(pieceMovementTable[move_index] == 0) break outer; next_square = square_index + pieceMovementTable[move_index]; } } } return count; } /** * The following routine cycles through all the pieces on the board, calculating moves for each piece */ public int nPossibleCaptures = 0; public int calcPossibleMoves(ChessPosition pos, boolean player){ // Clear our control data, so it can be cleanly manipulated by the calcPiece function Arrays.fill(humanControl, 0); Arrays.fill(computerControl, 0); int[] b = pos.board; pos.bWhiteChecked = false; // we will shortly determine this pos.bBlackChecked = false; // we will shortly determine this int count = 0; nPossibleCaptures = 0; int i = 0; for(int y = 0; y < 8; y++) for(int x = 0; x < 8; x++) { i = y * 10 + x; if(b[i] == 0) continue; int board_val = b[i]; int num = calcPieceMoves(pos, i); if(board_val < 0 && !player || board_val > 0 && player) { for(int j = 0; j < num; j++) { // if it's a capture add it to the capture list, to be evaluated first. if(b[piece_moves[j]] != 0 || b[i] == (player ? ChessPosition.PAWN : -ChessPosition.PAWN) || piece_moves[j] == 33 || piece_moves[j] == 34 || piece_moves[j] == 43 || piece_moves[j] == 44) { possibleCaptures[nPossibleCaptures].from = i; possibleCaptures[nPossibleCaptures].to = piece_moves[j]; ++nPossibleCaptures; } else // if ( b[ piece_moves[j] ] == 0 && (b[i] != PAWN && b[i] != -PAWN) ) { possibleMoveList[count].from = i; possibleMoveList[count].to = piece_moves[j]; ++count; } } } } /** * Now that we have generated all possible moves, sort the moves. * We will put captures first. */ //if ( count > 0 ) // Arrays.sort( possibleMoveList, possibleMoveList[0] ); return count + nPossibleCaptures; } /** * Calles NewGame() and initialises the temoporary piece move lists. */ public Chess(AIMain main){ // Allocate temporary possibleMoveList for AI iteration for(int i = 0; i < 256; i++) possibleMoveList[i] = new ChessMove(); // Allocate temporary captureList for AI iteration - better Alpha-Beta move-sorting for(int i = 0; i < 256; i++) possibleCaptures[i] = new ChessMove(); // Allocate space for a principal variation list, existing mainly for visual display for(int i = 0; i < 16; i++) principalVariation[i] = new ChessMove(); this.main = main; maxDepth = maxDepthSetting; // NewGame(); } /** * Data to store board control for each player, used when evaluating board position * This data is static that can be re-used (assume single threading!) * Used by setControlData */ private final int[] computerControl = new int[80]; /** * Data to store board control for each player, used when evaluating board position * This data is static that can be re-used (assume single threading!) * Used by setControlData */ private final int[] humanControl = new int[80]; }