import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.FutureTask; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.Future; import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.stream.Stream; import java.util.ArrayList; import java.util.HashMap; import java.util.Arrays; import java.util.List; import java.util.Map; /** * Implementation of the Minimax algorithm with alpha-beta pruning. *

*

* Minimax algorithm overview: https://en.wikipedia.org/wiki/Minimax */ public class LiteMinimax implements Serializable { private static final long serialVersionUID = 7249069248361182397L; private ExecutorService executorForMainSearch = null; private ExecutorService pool = null; private Object processedLock = null; public Future currentSearch; private FutureTask[] threadStack; private double acceptableRiskLevel; private Consumer callback; private String serDeserFilename; private long searchTimeLimit; private long movesProcessed; private long gameDuration; private boolean maximize; private long maxSeconds; private int maxThreads; private int numThreads; private int startDepth; private long gameTime; private int throttle; public BestMove best; // Board state and move maps // CachedMoveMap cachedMoves; /** * Create a LiteMinimax object * * @param filename the filename to deserialize the move map from * @param depth The ply depth this minimax engine will search to * @param maxSeconds The maximum number of seconds allowed to find * a best move. If it times out it will return * the best move found so far. */ public LiteMinimax(String filename, int depth, int maxSeconds) { this.acceptableRiskLevel = Main.riskLevel; this.best = new BestMove(false); this.cachedMoves = new CachedMoveMap(); this.gameTime = System.nanoTime(); this.serDeserFilename = filename; this.maxSeconds = maxSeconds; this.currentSearch = null; this.startDepth = depth; this.movesProcessed = 0L; this.threadStack = null; this.maximize = false; this.callback = null; this.maxThreads = 0; this.numThreads = 0; } private void initThreadSupport() { if (executorForMainSearch == null) { executorForMainSearch = Executors.newSingleThreadExecutor(); pool = Executors.newFixedThreadPool(100); processedLock = new Object(); currentSearch = null; threadStack = null; maxThreads = 0; numThreads = 0; } } /** * See if a previous search launched in the background has completed or not * * @return true if the last background search initiated has completed */ public boolean moveSearchIsDone() { if (!Main.useThreads) return true; if (currentSearch == null) return true; if (!currentSearch.isDone()) { return false; } else { Move move = null; try { move = currentSearch.get(); if (move != null) { best.move = move; best.value = move.getValue(); } else { best = null; } currentSearch = null; } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } finally { currentSearch = null; } currentSearch = null; } return true; } /** * Get the best move found by the last search * * @return the best move found by the last search performed or null if no move was found */ public Move getBestMove() { return best.move; } /** * Get the maximum number of threads launched during a search * * @return */ public int getMaxThreads() { return maxThreads; } /** * Get the number of moves examined so far by the last search that was started. * If the search was launched in a background thread then repeated calls to * this method will return higher values indicating the progress of the * move examinations. * * @return the number of moves examined for the last search started */ public long getNumMovesExamined() { if (processedLock != null) { synchronized (processedLock) { return movesProcessed; } } else { return movesProcessed; } } /** * Add to the number of moves examined so far during this search * * @param num the number of moves examined */ public void addNumMovesExamined(int num) { if (processedLock != null) { synchronized (processedLock) { movesProcessed += num; } } else { movesProcessed += num; } } /** * Register a String message consumer that can receive * any asynchronous messages this object needs to send. * * @param cb an instance of a functional object to send * our messages to if needed. */ public void registerDisplayCallback(Consumer cb) { callback = cb; } /** * Ask the currently associated board evaluator object * to give a score for this board state. *

* * Scores above 0 indicate an advantage for the white * player. Scores below 0 indicate a more favorable * board for the black player. * * @param board the board state to examine * @return the score for the evaluation of this board */ public int evaluate(final LiteBoard board) { return LiteEval.evaluate(board); } /** * Set a throttle on the worker threads used in the search. The threads * will all sleep the specified number of nanoseconds (billionths of a second) * after each move is examined in order to reduce CPU consumption by this * process if needed. * * @param nanos the number of nanoseconds to sleep after each move is examined */ public void setThrottle(int nanos) { throttle = nanos; } private int fileNum = 1; public void saveMoveMap(String filename) throws IOException { Main.logWriter.close(); Main.logWriter = null; Main.logFile = "chess" + (fileNum++) + ".log"; Main.log("Saving moves to disk.."); log(null); Main.log(""); Main.logWriter.close(); Main.logWriter = null; int outerSize = cachedMoves.size(); int outerCount = 0; int innerCount = 0; int innerSize = 0; CachedMoveMap temp = new CachedMoveMap(); for (String key : cachedMoves.keySet()) { if (!temp.containsKey(key)) { innerSize = cachedMoves.get(key).size(); temp.put(key, new ConcurrentHashMap<>(innerSize, 0.1f, 1)); } ConcurrentMap bestMap = cachedMoves.get(key); System.out.print('\r' + Ansi.clearEOL); for (Boolean b : bestMap.keySet()) { int num = (++innerCount + outerCount * innerSize); if ((num % 100_000) == 0) { System.out.printf("\rCopying move %,d of %,d%s", num, (outerSize * innerSize), Ansi.clearEOL); } BestMove best = bestMap.get(b); if (!temp.get(key).containsKey(b)) { temp.get(key).put(b, best); } } innerCount = 0; outerCount++; } System.out.print('\r' + Ansi.clearEOL); System.out.print("\rSerializing to disk.." + Ansi.clearEOL); ObjectOutputStream objectOutputStream = null; try { FileOutputStream fos = new FileOutputStream(filename); BufferedOutputStream bos = new BufferedOutputStream(fos, 1024 * 1024 * 128); objectOutputStream = new ObjectOutputStream(bos); objectOutputStream.writeObject(temp); objectOutputStream.flush(); } finally { if (objectOutputStream != null) { objectOutputStream.close(); } } System.out.print('\r' + Ansi.clearEOL); System.out.flush(); temp = null; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.gc(); } public void readMoveMap(String filename) throws IOException, ClassNotFoundException { FileInputStream fileInputStream = new FileInputStream(filename); BufferedInputStream bis = new BufferedInputStream(fileInputStream, 1024 * 1024 * 128); ObjectInputStream objectInputStream = new ObjectInputStream(bis); cachedMoves = (CachedMoveMap) objectInputStream.readObject(); objectInputStream.close(); System.out.print("Loaded move map!!"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } public void log(LiteBoard board) { gameDuration = (System.nanoTime() - gameTime) / 1_000_000_000L; int hours = 0; int minutes = 0; int seconds; while (gameDuration > 360) { hours++; gameDuration /= 360; } while (gameDuration > 60) { minutes++; gameDuration /= 60; } seconds = (int) gameDuration; int totalCaches = 2 * cachedMoves.size(); long totalMapChecks = cachedMoves.numCacheHits + cachedMoves.numCacheMisses; Main.setLogLevel(Main.LogLevel.DEBUG); List logLines = Arrays.asList( "Minimax Statistics = ", "", String.format("Ply Depth for game: %d", Main.maxDepth), String.format("Game Duration: %02d:%02d:%02d", hours, minutes, seconds), (board == null) ? "" : String.format("Turn: %d", board.turns), "", String.format("Total Boards stored in Cache: %,14d", cachedMoves.size()), String.format("Max Boards stored in Cache: %,14d", cachedMoves.maxMovesCached), String.format("Total Moves in Cache: %,14d", totalCaches), "", String.format("Number Cached Moves Offered: %,14d", cachedMoves.numMovesOffered), String.format("Number Cached Moves Added: %,14d", cachedMoves.numMovesCached), String.format("Number of times Replaced Existing: %,14d", cachedMoves.numMovesUpdated), "", String.format("Number of Total Cache Checks: %,14d", totalMapChecks), String.format("Number of Cache hits: %,14d", cachedMoves.numCacheHits), String.format("Number Cache misses: %,14d", cachedMoves.numCacheMisses), String.format("Number of Saved Move examinations: %,14d", cachedMoves.numExaminationsSaved), "", String.format("Number time Ran Anyway: %,14d", cachedMoves.numMovesTested), String.format("Number Ran Anyway Better: %,14d", cachedMoves.numMovesImproved), "", "Minimax Statistics = "); Main.log(Main.LogLevel.DEBUG, logLines); } /** * Private helper function to send a message to a * message consumer (observer) that has registered with us. * * @param msg the message to send */ private void printMsg(String msg) { if (callback != null) { callback.accept(msg); } } class PieceList extends ArrayList { } class SidePieceMap extends HashMap { public PieceList pawns; public PieceList knights; public PieceList bishops; public PieceList queen; public PieceList king; SidePieceMap() { super(); for (int type = LiteBoard.Pawn; type <= LiteBoard.King; type++) put(type, new PieceList()); pawns = get(LiteBoard.Pawn); knights = get(LiteBoard.Knight); bishops = get(LiteBoard.Bishop); queen = get(LiteBoard.Queen); king = get(LiteBoard.King); } } class PieceMap extends HashMap { } /** * Search the specified board for the best move for the player who moves next. * * @param board The board state to find the best move for the current player for * @param returnImmediate if set to true then this call will start the search in the a background * and return immediately. * @return the best move for the current player or null if there are no moves found so far * (If 'returnImmediate' is false and this method returns null then no move could * be found. */ public Move bestMove(final LiteBoard board, boolean returnImmediate) { final int side = board.turn; maximize = (side == Side.White); movesProcessed = 0L; // Clear the best move we have for this search and set the time limit for them to finish. // If maxSeconds == 0 then the threads ignore the time limit and run to completion. searchTimeLimit = (maxSeconds == 0) ? 0 : System.nanoTime() + (maxSeconds * 1_000_000_000L); best = new BestMove(maximize); if (board.numMoves1 == 1) { // We have only one move so nothing the other side can do in response will change // what move we make so just return it now and save the recursive depth cost. addNumMovesExamined(1); best.move = board.moves1[0]; best.value = board.moves1[0].getValue(); return board.moves1[0]; } if (board.numMoves1 <= 0) { String playerName = (side == Side.Black) ? Main.config.player2 : Main.config.player1; Main.log(Main.LogLevel.DEBUG, "bestMove(...) called and no moves are available for ", playerName); return null; } // create a map of the pieces on the board by side, and then by type, and then their locations SidePieceMap piecesOurs = new SidePieceMap(); SidePieceMap piecesTheirs = new SidePieceMap(); PieceMap pieceMap = new PieceMap(); pieceMap.put(side, piecesOurs); pieceMap.put((side + 1) % 2, piecesTheirs); for (int ndx = 0; ndx < LiteBoard.BOARD_SIZE; ndx++) { if (board.isEmpty(ndx)) continue; pieceMap.get((int) LiteUtil.getSide(board.board[ndx])).get((int) LiteUtil.getType(board.board[ndx])).add(ndx); } // If threads are not enabled then we search here and now on the current thread: if (!Main.useThreads) { return searchWithNoThreads(board, pieceMap); } initThreadSupport(); // Cancel any existing parent search thread (and its child threads) before we start a new one cancelSearchAndWait(); // Start the search threads, one for each one of our moves: launchMoveThreads(board); // Now that we have started threads exploring all moves available for the current // player, see if the caller wants us to wait for them to finish or put the current // thread into its own background thread and return immediately. if (returnImmediate) { currentSearch = executorForMainSearch.submit(() -> finishCurrentSearch(board, pieceMap)); return best.move; } else { // Otherwise wait here for the search to complete and return the move it suggests: currentSearch = null; return finishCurrentSearch(board, pieceMap); } } /** * Iterate over all available moves for the current player and decide which move is the best. * This executes on the current thread and is a blocking call. * * @param board the board state to examine each move on * @param pieceMap board pieces mapped by type and side * @return the best move for this board or null if no legal move is available */ private Move searchWithNoThreads(final LiteBoard board, PieceMap pieceMap) { // We are not using threads. Walk through all moves and find the best and return it in this calling thread. for (int index = 0; index < board.numMoves1; index++) { Move move = board.moves1[index]; LiteBoard currentBoard = new LiteBoard(board); currentBoard.executeMove(move); currentBoard.advanceTurn(); // See if we have a best move already stored away for this board arrangement: if (currentBoard.numPieces1 > 5) { // we force moves to be manually evaluated via minmax when we // get down to the end game BestMove check = cachedMoves.lookupBestMove(currentBoard.board, maximize); if (check != null && check.move != null) { double confidence = cachedMoves.getMoveRisk(currentBoard.board); if (confidence <= acceptableRiskLevel) { if ((!maximize && check.value < best.value) || (maximize && check.value > best.value)) { best = check; continue; } } } } int lookAheadVal = minmax(currentBoard, LiteUtil.MIN_VALUE, LiteUtil.MAX_VALUE, startDepth, !maximize); if ((maximize && lookAheadVal > best.value) || (!maximize && lookAheadVal < best.value)) { best.value = lookAheadVal; best.move = move; best.move.setValue(best.value); cachedMoves.addMoveValue(board.board, maximize, best.move, best.value, best.movesExamined); } // Check for specific corner cases when we might want to make // specific kinds of moves for Move check = checkEndGameCornerCases(board, pieceMap, best.move); if (check != null) { best.move = check; best.move.setValue(best.value); } if (best.move != null) { cachedMoves.addMoveValue(board.board, maximize, best.move, best.value, best.movesExamined); } } return best.move; } /** * Launch a separate thread for each move available to the current player to move. * All threads are stored in the threadStack[] array with numThreads threads in it. * * @param board the board to find the best move on. */ private void launchMoveThreads(final LiteBoard board) { // Loop through all of the moves available to the current player and launch a // thread for each one so each can go explore what good board valuations we // have in the future of this move and keep track of the best one // threadStack = new FutureTask[board.numMoves1]; numThreads = 0; for (int i = 0; i < board.numMoves1; i++) { Move move = board.moves1[i]; // See if we have a best move already stored away for this board arrangement: if (board.numPieces1 > 5) { // we force moves to be manually evaluated via minmax when we get down to the end game BestMove check = cachedMoves.lookupBestMove(board.board, maximize); if (check != null && check.move != null) { double confidence = cachedMoves.getMoveRisk(board.board); if (confidence <= acceptableRiskLevel) { if ((!maximize && check.value < best.value) || (maximize && check.value > best.value)) { best = check; continue; } } } } // Create a Futures object to represent the eventual result of the move we give it to explore LiteMoveThread lookAheadThread = new LiteMoveThread(board, this, maximize, move, startDepth); // Create a background thread for this move search and start it FutureTask task = new FutureTask<>(lookAheadThread); threadStack[numThreads++] = task; maxThreads = Integer.max(maxThreads, numThreads); Thread t = new Thread(task); t.setName(String.format("Turn-%03d:Move-%02d", board.turns, numThreads)); t.start(); } } /** * Call the get() method on the executor of the threads of an existing background search and gather their * results as they complete, keeping track of which move was the best one returned. * * @param board the board state the search is for * @return the best move found for this board state or null if no legal move was found */ private Move finishCurrentSearch(final LiteBoard board, PieceMap pieceMap) { // Now we wait on all of the threads to finish so we can see which has the best score for (int index=0; index < numThreads; index++) { BestMove threadResult; try { threadResult = (BestMove) threadStack[index].get(); } catch (Exception e) { Main.log(Main.LogLevel.DEBUG, "We're having problems waiting for move threads to finish"); StackTraceElement[] stack = e.getStackTrace(); for (StackTraceElement elem : stack) { Main.log(Main.LogLevel.DEBUG, elem.toString()); } continue; } if (threadResult == null) { // Not sure what to do here. The thread completed but did not return // any useful move suggestions. // // I guess we just loop back and continue gathering other threads as // they complete until we're done with all of them continue; } // Continue on to the next thread if this one found no moves at all if (threadResult.move == null) { continue; } // save the number of moves it examined (even if it's not the best // move we keep, it still examined those moves and their results): addNumMovesExamined(threadResult.movesExamined); // See if the results of this thread's search are a better move than // we have so far and keep it if so: // if ((maximize && threadResult.value >= best.value) || (!maximize && threadResult.value <= best.value)) { best = threadResult; cachedMoves.addMoveValue(board.board, maximize, best.move, best.value, best.movesExamined); } // play nice with the other processes on this cpu Thread.yield(); // Check for specific corner cases when we might want to make // specific kinds of moves for Move check = checkEndGameCornerCases(board, pieceMap, best.move); if (check != null) { best.move = check; best.value = check.getValue(); cachedMoves.addMoveValue(board.board, maximize, best.move, best.value, best.movesExamined); } } // We are finished making all available moves for the current player for // this board setup and gathered the result of each. If we have a best move // to suggest for this board it should have been set in the loop above as it // examined each search thread's results // Return the best move found for this board setup: return best.move; } /** * Cancel any background search threads that may be still * running from any last search we may have started */ public void cancelThreadStack(int wait) { if (threadStack == null) return; searchTimeLimit = System.nanoTime(); try { Thread.sleep(wait); } catch (InterruptedException e) { e.printStackTrace(); } } private void cancelSearchAndWait() { if (currentSearch != null) { searchTimeLimit = System.nanoTime(); boolean completed = moveSearchIsDone(); while (!completed) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } completed = moveSearchIsDone(); } currentSearch = null; threadStack = null; numThreads = 0; } } /** * See if this board state contains any specific 'corner-cases' * that we can direct special attention to * * @param board the board state to examine * @param bestMove the best move so far for this board * @return a new best move if necessary, or the one already passed to us * if we found no reason to change it. */ private Move checkEndGameCornerCases(final LiteBoard board, PieceMap pieceMap, Move bestMove) { // Follow the best move if we are in check.. if (board.kingInCheck(board.turn)) { return bestMove; } // // Map our moves by type // Map> ourMoveMap = mapMovesByType(board, board.moves1, board.numMoves1); BiPredicate> pawnIsBlocked = (location, moves) -> { for (Move move : moves) { if ((move.getFrom() == location)) return false; } if (board.turn == Side.Black) { if (location / 8 < 7) return true; } else { if (location / 8 > 0) return true; } return false; }; SidePieceMap ourPieces = pieceMap.get(board.turn); SidePieceMap theirPieces = pieceMap.get((board.turn + 1) % 2); Stream ourPieceStream = Stream.of(ourPieces); int totalPieces = ourPieceStream.flatMap(s -> s.values().stream()) .reduce(0, (sum, list) -> sum + list.size(), Integer::sum); boolean tryAPawn = false; if (totalPieces - ourPieces.get(LiteBoard.Pawn).size() < 4) { if (!board.kingInCheck(board.turn)) { tryAPawn = true; } } boolean endRepetition = false; if (bestMove != null && board.checkDrawByRepetition(bestMove, 2)) { endRepetition = true; } // If we are getting repetitive and possibly stuck at a local maxima then // see if we have any possible pawn advances available and pick one // of those instead if (tryAPawn || endRepetition) { if (endRepetition) { printMsg("Attempting to end repetition.."); } if (tryAPawn) { printMsg("looking at pawn strategy.."); } // see if we have any pawn moves if (ourMoveMap.get(LiteBoard.Pawn).size() > 0) { return ourMoveMap.get(LiteBoard.Pawn).get(0); } // see if we have any pawns at all PieceList ourPawns = ourPieces.get(LiteBoard.Pawn); if (!ourPawns.isEmpty()) { // See which of them are blocked for (int ndx = 0; ndx < ourPawns.size(); ndx++) { int location = ourPawns.get(ndx); if (pawnIsBlocked.test(location, ourMoveMap.get(LiteBoard.Pawn))) { // This pawn has no moves but it isn't on the last row, so it is // blocked by another piece. See if that piece is ours int forward = (board.turn == Side.Black) ? 1 : -1; int spotInFront = ((location % 8) + ((location / 8) + forward) * 8); if (board.getSide(spotInFront) == board.turn) { // The piece in the way is ours. See if it can move int typeOfPieceInTheWay = board.getType(spotInFront); if (typeOfPieceInTheWay == LiteBoard.Empty) { // FixMe! break; } for (Move move : ourMoveMap.get(typeOfPieceInTheWay)) { if (move.getFrom() == spotInFront) { // the piece in the way of our pawn has moves. Choose one // to be our next move return move; } } } } } } Move endRepMove = null; if (ourMoveMap.get(LiteBoard.Queen).size() > 0) { endRepMove = ourMoveMap.get(LiteBoard.Queen).get(0); } else if (ourMoveMap.get(LiteBoard.Rook).size() > 0) { endRepMove = ourMoveMap.get(LiteBoard.Rook).get(0); } else if (ourMoveMap.get(LiteBoard.Bishop).size() > 0) { endRepMove = ourMoveMap.get(LiteBoard.Bishop).get(0); } else if (ourMoveMap.get(LiteBoard.Knight).size() > 0) { endRepMove = ourMoveMap.get(LiteBoard.Knight).get(0); } else if (ourMoveMap.get(LiteBoard.King).size() > 0) { endRepMove = ourMoveMap.get(LiteBoard.King).get(0); } if (endRepMove != null) { return endRepMove; } } if (bestMove == null) { return null; } int bestNdx = bestMove.getFrom(); // Additional corner-case: // If the best move we think we have is the king and we are not in check: // Check to see if we have any pawn moves left and use one of those // to see if it can get promoted before we move this king // Only do this if we are not in check: if (!board.kingInCheck(board.turn)) { if (board.getType(bestNdx) == LiteBoard.King) { // See if all we have left are pawn and king moves if (ourMoveMap.get(LiteBoard.Rook).size() == 0 && ourMoveMap.get(LiteBoard.Queen).size() == 0) { if (ourMoveMap.get(LiteBoard.Pawn).size() > 0) { return ourMoveMap.get(LiteBoard.Pawn).get(0); } // see if we have any pawns at all PieceList ourPawns = ourPieces.get(LiteBoard.Pawn); if (!ourPawns.isEmpty()) { // See which of them are blocked for (int ndx = 0; ndx < ourPawns.size(); ndx++) { int location = ourPawns.get(ndx); if (pawnIsBlocked.test(location, ourMoveMap.get(LiteBoard.Pawn))) { // This pawn has no moves but it isn't on the last row, so it is // blocked by another piece. See if that piece is ours int forward = (board.turn == Side.Black) ? 1 : -1; int spotInFront = ((location % 8) + ((location / 8) + forward) * 8); if (board.getSide(spotInFront) == board.turn) { // The piece in the way is ours. See if it can move int typeOfPieceInTheWay = board.getType(spotInFront); if (typeOfPieceInTheWay == LiteBoard.Empty) { // FixMe! break; } for (Move move : ourMoveMap.get(typeOfPieceInTheWay)) { if (move.getFrom() == spotInFront) { // the piece in the way of our pawn has moves. Choose one // to be our next move return move; } } } } } } } } } // See if our opponent only has their king left and get more aggressive if so: if (!board.kingInCheck(board.turn)) { Map> theirMoveMap = mapMovesByType(board, board.moves2, board.numMoves2); // If all they have left is their king.. if (theirMoveMap.get(LiteBoard.Pawn).size() == 0 && theirMoveMap.get(LiteBoard.Knight).size() == 0 && theirMoveMap.get(LiteBoard.Bishop).size() == 0 && theirMoveMap.get(LiteBoard.Rook).size() == 0 && theirMoveMap.get(LiteBoard.Queen).size() == 0 && theirMoveMap.get(LiteBoard.King).size() > 0) { // if we have pawns march them towards the end row if (ourMoveMap.get(LiteBoard.Pawn).size() > 0) { bestMove = ourMoveMap.get(LiteBoard.Pawn).get(0); } else { // If we have at least one queen and one rook then look further to find a game ending move if (ourMoveMap.get(LiteBoard.Queen).size() > 0 && ourMoveMap.get(LiteBoard.Rook).size() > 0 && ourMoveMap.get(LiteBoard.King).size() > 0 && startDepth < 3) { // Extend the depth and search again for a game ending move startDepth = 3; printMsg("Looking for game-ending moves..."); } else if ((ourMoveMap.get(LiteBoard.Queen).size() > 0) || (ourMoveMap.get(LiteBoard.Rook).size() > 0) && ourMoveMap.get(LiteBoard.King).size() > 0 && startDepth < 4) { // We have (at minimum) a queen or a rook along with our king so we should // be able to back them into a corner if we search even further ahead.. startDepth = 4; printMsg("Looking even further for a game ending move.."); } } } } return bestMove; } /** * Private helper function to create a map of available * moves, indexed by their pieces types. * * @param board the board state the moves are for * @param moves an array of moves to add to the map * @param numMoves the number of moves to add * @return the new Map> containing * the moves mapped to the types of pieces that * can make them. */ private static Map> mapMovesByType(LiteBoard board, Move[] moves, int numMoves) { Map> movesByPiece = new HashMap<>(6); movesByPiece.put(LiteBoard.Pawn, new ArrayList<>()); movesByPiece.put(LiteBoard.Rook, new ArrayList<>()); movesByPiece.put(LiteBoard.Knight, new ArrayList<>()); movesByPiece.put(LiteBoard.Bishop, new ArrayList<>()); movesByPiece.put(LiteBoard.Queen, new ArrayList<>()); movesByPiece.put(LiteBoard.King, new ArrayList<>()); for (int ndx = 0; ndx < numMoves; ndx++) { int fi = moves[ndx].getFrom(); byte b = board.board[fi]; int pieceType = LiteUtil.getType(b); if (pieceType == LiteBoard.Empty) { List logLines = new ArrayList<>(Arrays.asList( "", "Yet another attempt to map a move that seems to point to an empty spot:", String.format("fi = %d:", fi), String.format("b = %d:", b), String.format("pieceType = %d:", pieceType), String.format("board.board[fi] = %d:", board.board[fi]), "")); StackTraceElement[] stack = Thread.currentThread().getStackTrace(); for (StackTraceElement s : stack) { logLines.add(s.toString()); } logLines.add(""); Main.log(Main.LogLevel.ERROR, logLines); continue; } movesByPiece.get(pieceType).add(moves[ndx]); } return movesByPiece; } /** * The awesome, one and only, minimax algorithm method which recursively searches * for the best moves up to a certain number of moves ahead (plies) or until a * move timeout occurs (if any timeouts are in effect). * * @param origBoard the board state to examine all moves for * @param alpha the lower bounds of the best move and score found so far * @param beta the upper bounds of the best move and score found so far * @param depth the number of turns to search ahead. ply is a "half-turn' * where one player has moved but the responding move has not * been made. A full turn for fair evaluation usually requires * a balanced number of exchanges. * @param maximize true if we are looking for a board state with the maximum score (white player's turn) * false if we are looking for a board state with the lowest score (black player's turn) * @return the best score this move (and all consequential response/exchanges up to the allowed * look-ahead depth or time limit for searching). */ int minmax(final LiteBoard origBoard, int alpha, int beta, int depth, boolean maximize) { BestMove mmBest = new BestMove(maximize); int lookAheadValue = 0; int cachedValue = 0; boolean gotCacheHit; BestMove check; if (throttle > 0) { try { Thread.sleep(0, throttle); } catch (InterruptedException e) { e.printStackTrace(); } } for (int i = 0; i < origBoard.numMoves1; i++) { // Get the next move available for this board and this player Move move = origBoard.moves1[i]; /////////////////////////////////////////////////////////////////// // See if we are at the end of our allowed depth to search and if so, // continue search if we still have made capture moves that we want to // see the responses for. That way we don't take a pawn with our queen // at the end of a ply, and not know that in response we lose our queen! // // This is known as quiescent searching. if (depth <= 0) { if ((move.getValue() == 0) || depth < -2) { addNumMovesExamined(mmBest.movesExamined); return LiteEval.evaluate(origBoard); } } /////////////////////////////////////////////////////////////////// // Before we try to find our own best move for this board state, see // if one is already cached: gotCacheHit = false; check = null; if (origBoard.numPieces1 > 5) { // we force moves to be manually evaluated via minmax when we get down to the end game check = cachedMoves.lookupBestMove(origBoard.board, maximize); } if (check != null) { if (check.move != null) { gotCacheHit = true; cachedValue = check.value; lookAheadValue = check.value; } else { check = null; } } if (check != null) { double moveRisk = cachedMoves.getMoveRisk(origBoard.board); if (moveRisk > acceptableRiskLevel) { // The risk is too high so we will do this manually and increase the count // of how many times we have rechecked this move for this board cachedMoves.increaseMoveUsedCount(origBoard.board); check = null; } } if (check == null) { LiteBoard currentBoard = new LiteBoard(origBoard); currentBoard.executeMove(move); currentBoard.advanceTurn(); mmBest.movesExamined++; // See if the move we just made leaves the other player with no moves // and if so, return it as the best value we'll ever see on this search: if (currentBoard.numMoves1 == 0) { mmBest.move = move; mmBest.value = maximize ? LiteUtil.MAX_VALUE - (100 - depth) : LiteUtil.MIN_VALUE + (100 - depth); break; } Thread.yield(); // The recursive minimax step // While we have the depth keep looking ahead to see what this move accomplishes lookAheadValue = minmax(currentBoard, alpha, beta, depth - 1, !maximize); // See if this move is better than any we've seen for this board: // if ((!maximize && lookAheadValue < mmBest.value) || (maximize && lookAheadValue > mmBest.value)) { mmBest.value = lookAheadValue; mmBest.move = move; cachedMoves.addMoveValue(origBoard.board, maximize, move, lookAheadValue, mmBest.movesExamined); } // See if we had a cache hit but ran it anyway, and whether this improved the existing move if (((maximize && lookAheadValue > cachedValue) || (!maximize && lookAheadValue < cachedValue)) && gotCacheHit) { cachedMoves.increaseMoveImprovedCount(origBoard.board); } } else { mmBest.movesExamined += check.movesExamined; } // The alpha-beta pruning step // Continually tighten the low and high range of what we see this accomplishes // in the future and stop now if all future results are worse than the best move // we already have. if (maximize) alpha = Integer.max(alpha, lookAheadValue); else beta = Integer.min(beta, lookAheadValue); if (alpha >= beta) { break; } // If we are out of time then return the best outcome we've seen this move and thread accomplish so far if (searchTimeLimit > 0 && System.nanoTime() >= searchTimeLimit) { break; } if (Thread.currentThread().isInterrupted()) { break; } } addNumMovesExamined(mmBest.movesExamined); return mmBest.value; } /** * Called when our object is being torn down. * Originally written to ensure all threads were collected * and finalised if the program was exiting. It would seem * now that the program does fine on cleaning them all up * without this method needing to be specifically called. *

* May be deprecated in the future. * * @throws IOException */ public void close() throws IOException { // Disable new tasks from being submitted pool.shutdown(); executorForMainSearch.shutdown(); try { // Wait a while for existing tasks to terminate if (!pool.awaitTermination(5, TimeUnit.SECONDS)) { // Cancel currently executing tasks pool.shutdownNow(); // Wait a while for tasks to respond to being cancelled if (!pool.awaitTermination(5, TimeUnit.SECONDS)) System.err.println("some look-ahead threads did not terminate"); } // same for the other executor: // Wait a while for existing tasks to terminate if (!executorForMainSearch.awaitTermination(5, TimeUnit.SECONDS)) { // Cancel currently executing tasks executorForMainSearch.shutdownNow(); // Wait a while for tasks to respond to being cancelled if (!executorForMainSearch.awaitTermination(5, TimeUnit.SECONDS)) System.err.println("some look-ahead threads did not terminate"); } } catch (InterruptedException ie) { // (Re-)Cancel if current thread also interrupted pool.shutdownNow(); executorForMainSearch.shutdown(); // Preserve interrupt status // Thread.currentThread().interrupt(); } } }