/* DroidFish - An Android chess program. Copyright (C) 2011-2012 Peter Ă–sterlund, peterosterlund2@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.petero.droidfish.gamelogic; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; import org.petero.droidfish.EngineOptions; import org.petero.droidfish.GUIInterface; import org.petero.droidfish.GUIInterface.ThinkingInfo; import org.petero.droidfish.GameMode; import org.petero.droidfish.PGNOptions; import org.petero.droidfish.Util; import org.petero.droidfish.book.BookOptions; import org.petero.droidfish.book.EcoDb; import org.petero.droidfish.engine.DroidComputerPlayer; import org.petero.droidfish.engine.UCIOptions; import org.petero.droidfish.engine.DroidComputerPlayer.SearchRequest; import org.petero.droidfish.engine.DroidComputerPlayer.SearchType; import org.petero.droidfish.gamelogic.Game.GameState; import org.petero.droidfish.gamelogic.GameTree.Node; /** The glue between the chess engine and the GUI. */ public class DroidChessController { private DroidComputerPlayer computerPlayer = null; private PgnToken.PgnTokenReceiver gameTextListener = null; private BookOptions bookOptions = new BookOptions(); private EngineOptions engineOptions = new EngineOptions(); private Game game = null; private Move ponderMove = null; private GUIInterface gui; private GameMode gameMode; private PGNOptions pgnOptions; private String engine = ""; private int strength = 1000; private int numPV = 1; private SearchListener listener; private boolean guiPaused = false; /** Partial move that needs promotion choice to be completed. */ private Move promoteMove; private int searchId; private volatile ThinkingInfo latestThinkingInfo = null; /** Constructor. */ public DroidChessController(GUIInterface gui, PgnToken.PgnTokenReceiver gameTextListener, PGNOptions options) { this.gui = gui; this.gameTextListener = gameTextListener; gameMode = new GameMode(GameMode.TWO_PLAYERS); pgnOptions = options; listener = new SearchListener(); searchId = 0; } /** Start a new game. */ public final synchronized void newGame(GameMode gameMode, TimeControlData tcData) { boolean updateGui = abortSearch(); if (updateGui) updateGUI(); this.gameMode = gameMode; if (computerPlayer == null) { computerPlayer = new DroidComputerPlayer(listener); computerPlayer.setBookOptions(bookOptions); computerPlayer.setEngineOptions(engineOptions); } computerPlayer.queueStartEngine(searchId, engine); searchId++; game = new Game(gameTextListener, tcData); computerPlayer.uciNewGame(); setPlayerNames(game); updateGameMode(); } /** Start playing a new game. Should be called after newGame(). */ public final synchronized void startGame() { updateComputeThreads(); setSelection(); updateGUI(); updateGameMode(); } /** @return Array containing time control, moves per session and time increment. */ public final int[] getTimeLimit() { if (game != null) return game.timeController.getTimeLimit(game.currPos().whiteMove); return new int[]{5*60*1000, 60, 0}; } /** The chess clocks are stopped when the GUI is paused. */ public final synchronized void setGuiPaused(boolean paused) { guiPaused = paused; updateGameMode(); } /** Set game mode. */ public final synchronized void setGameMode(GameMode newMode) { if (!gameMode.equals(newMode)) { if (newMode.humansTurn(game.currPos().whiteMove)) searchId++; gameMode = newMode; if (!gameMode.playerWhite() || !gameMode.playerBlack()) setPlayerNames(game); // If computer player involved, set player names updateGameMode(); abortSearch(); updateComputeThreads(); gui.updateEngineTitle(); updateGUI(); } } public GameMode getGameMode() { return gameMode; } /** Return true if game mode is analysis. */ public final boolean analysisMode() { return gameMode.analysisMode(); } /** Set engine book options. */ public final synchronized void setBookOptions(BookOptions options) { if (!bookOptions.equals(options)) { bookOptions = options; if (computerPlayer != null) { computerPlayer.setBookOptions(bookOptions); updateBookHints(); } } } /** Set engine options. */ public final synchronized void setEngineOptions(EngineOptions options, boolean restart) { if (!engineOptions.equals(options)) { engineOptions = options; if (computerPlayer != null) computerPlayer.setEngineOptions(engineOptions); if (restart && (game != null)) { abortSearch(); updateComputeThreads(); updateGUI(); } } } /** Set engine and engine strength. Restart computer thinking if appropriate. * @param engine Name of engine. * @param strength Engine strength, 0 - 1000. */ public final synchronized void setEngineStrength(String engine, int strength) { boolean newEngine = !engine.equals(this.engine); if (newEngine || (strength != this.strength)) { this.engine = engine; this.strength = strength; if (game != null) { abortSearch(); updateComputeThreads(); updateGUI(); } } } /** Set engine UCI options. */ public final synchronized void setEngineUCIOptions(Map<String,String> uciOptions) { if (computerPlayer != null) computerPlayer.setEngineUCIOptions(uciOptions); } /** Return current engine identifier. */ public final synchronized String getEngine() { return engine; } /** Notify controller that preferences has changed. */ public final synchronized void prefsChanged(boolean translateMoves) { if (game == null) translateMoves = false; if (translateMoves) game.tree.translateMoves(); updateBookHints(); updateMoveList(); listener.prefsChanged(searchId, translateMoves); if (translateMoves) updateGUI(); } /** De-serialize from byte array. */ public final synchronized void fromByteArray(byte[] data, int version) { ByteArrayInputStream bais = null; DataInputStream dis = null; try { bais = new ByteArrayInputStream(data); dis = new DataInputStream(bais); game.readFromStream(dis, version); game.tree.translateMoves(); } catch (IOException e) { } catch (ChessParseError e) { } finally { if (dis != null) try { dis.close(); } catch (IOException ex) {} if (bais != null) try { bais.close(); } catch (IOException ex) {} } } /** Serialize to byte array. */ public final synchronized byte[] toByteArray() { ByteArrayOutputStream baos = null; DataOutputStream dos = null; try { baos = new ByteArrayOutputStream(32768); dos = new DataOutputStream(baos); game.writeToStream(dos); dos.flush(); return baos.toByteArray(); } catch (IOException e) { return null; } finally { if (dos != null) try { dos.close(); } catch (IOException ex) {} if (baos != null) try { baos.close(); } catch (IOException ex) {} } } /** Return FEN string corresponding to a current position. */ public final synchronized String getFEN() { return TextIO.toFEN(game.tree.currentPos); } /** Convert current game to PGN format. */ public final synchronized String getPGN() { return game.tree.toPGN(pgnOptions); } /** Parse a string as FEN or PGN data. */ public final synchronized void setFENOrPGN(String fenPgn) throws ChessParseError { if (!fenPgn.isEmpty() && fenPgn.charAt(0) == '\ufeff') fenPgn = fenPgn.substring(1); // Remove BOM Game newGame = new Game(gameTextListener, game.timeController.tcData); try { Position pos = TextIO.readFEN(fenPgn); newGame.setPos(pos); setPlayerNames(newGame); } catch (ChessParseError e) { // Try read as PGN instead if (!newGame.readPGN(fenPgn, pgnOptions)) throw e; newGame.tree.translateMoves(); } searchId++; game = newGame; gameTextListener.clear(); updateGameMode(); abortSearch(); computerPlayer.uciNewGame(); updateComputeThreads(); gui.setSelection(-1); updateGUI(); } /** True if human's turn to make a move. (True in analysis mode.) */ public final synchronized boolean humansTurn() { if (game == null) return false; return gameMode.humansTurn(game.currPos().whiteMove); } /** Return true if computer player is using CPU power. */ public final synchronized boolean computerBusy() { return (computerPlayer != null) && computerPlayer.computerBusy(); } /** Return engine UCI options if an engine has been loaded and has * reported its UCI options. */ public final synchronized UCIOptions getUCIOptions() { if (computerPlayer == null || !computerPlayer.computerLoaded()) return null; return computerPlayer.getUCIOptions(); } /** Make a move for a human player. */ public final synchronized void makeHumanMove(Move m) { if (!humansTurn()) return; Position oldPos = new Position(game.currPos()); if (game.pendingDrawOffer) { ArrayList<Move> moves = new MoveGen().legalMoves(oldPos); for (Move m2 : moves) { if (m2.equals(m)) { if (findValidDrawClaim(TextIO.moveToUCIString(m))) { stopPonder(); updateGUI(); gui.setSelection(-1); return; } break; } } } if (doMove(m)) { if (m.equals(ponderMove) && !gameMode.analysisMode() && (computerPlayer.getSearchType() == SearchType.PONDER)) { computerPlayer.ponderHit(searchId); ponderMove = null; } else { abortSearch(); updateComputeThreads(); } setAnimMove(oldPos, m, true); updateGUI(); } else { gui.setSelection(-1); } } /** Report promotion choice for incomplete move. * @param choice 0=queen, 1=rook, 2=bishop, 3=knight. */ public final synchronized void reportPromotePiece(int choice) { if (promoteMove == null) return; final boolean white = game.currPos().whiteMove; int promoteTo; switch (choice) { case 1: promoteTo = white ? Piece.WROOK : Piece.BROOK; break; case 2: promoteTo = white ? Piece.WBISHOP : Piece.BBISHOP; break; case 3: promoteTo = white ? Piece.WKNIGHT : Piece.BKNIGHT; break; default: promoteTo = white ? Piece.WQUEEN : Piece.BQUEEN; break; } promoteMove.promoteTo = promoteTo; Move m = promoteMove; promoteMove = null; makeHumanMove(m); } /** Add a null-move to the game tree. */ public final synchronized void makeHumanNullMove() { if (humansTurn()) { int varNo = game.tree.addMove("--", "", 0, "", ""); game.tree.goForward(varNo); abortSearch(); updateComputeThreads(); updateGUI(); gui.setSelection(-1); } } /** Help human to claim a draw by trying to find and execute a valid draw claim. */ public final synchronized boolean claimDrawIfPossible() { if (!findValidDrawClaim("")) return false; updateGUI(); return true; } /** Resign game for current player. */ public final synchronized void resignGame() { if (game.getGameState() == GameState.ALIVE) { game.processString("resign"); updateGUI(); } } /** Return true if the player to move in the current position is in check. */ public final synchronized boolean inCheck() { return MoveGen.inCheck(game.tree.currentPos); } /** Undo last move. Does not truncate game tree. */ public final synchronized void undoMove() { if (game.getLastMove() != null) { abortSearch(); boolean didUndo = undoMoveNoUpdate(); updateComputeThreads(); setSelection(); if (didUndo) setAnimMove(game.currPos(), game.getNextMove(), false); updateGUI(); } } /** Redo last move. Follows default variation. */ public final synchronized void redoMove() { if (game.canRedoMove()) { abortSearch(); redoMoveNoUpdate(); updateComputeThreads(); setSelection(); setAnimMove(game.prevPos(), game.getLastMove(), true); updateGUI(); } } /** Go back/forward to a given move number. * Follows default variations when going forward. */ public final synchronized void gotoMove(int moveNr) { boolean needUpdate = false; while (game.currPos().fullMoveCounter > moveNr) { // Go backward int before = game.currPos().fullMoveCounter * 2 + (game.currPos().whiteMove ? 0 : 1); undoMoveNoUpdate(); int after = game.currPos().fullMoveCounter * 2 + (game.currPos().whiteMove ? 0 : 1); if (after >= before) break; needUpdate = true; } while (game.currPos().fullMoveCounter < moveNr) { // Go forward int before = game.currPos().fullMoveCounter * 2 + (game.currPos().whiteMove ? 0 : 1); redoMoveNoUpdate(); int after = game.currPos().fullMoveCounter * 2 + (game.currPos().whiteMove ? 0 : 1); if (after <= before) break; needUpdate = true; } if (needUpdate) { abortSearch(); updateComputeThreads(); setSelection(); updateGUI(); } } /** Go to start of the current variation. */ public final synchronized void gotoStartOfVariation() { boolean needUpdate = false; while (true) { if (!undoMoveNoUpdate()) break; needUpdate = true; if (game.numVariations() > 1) break; } if (needUpdate) { abortSearch(); updateComputeThreads(); setSelection(); updateGUI(); } } /** Go to given node in game tree. */ public final synchronized void goNode(Node node) { if (node == null) return; if (!game.goNode(node)) return; if (!humansTurn()) { if (game.getLastMove() != null) { game.undoMove(); if (!humansTurn()) game.redoMove(); } } abortSearch(); updateComputeThreads(); setSelection(); updateGUI(); } /** Get number of variations in current game position. */ public final synchronized int numVariations() { return game.numVariations(); } /** Return true if the current variation can be moved closer to the main-line. */ public final synchronized boolean canMoveVariationUp() { return game.canMoveVariation(-1); } /** Return true if the current variation can be moved farther away from the main-line. */ public final synchronized boolean canMoveVariationDown() { return game.canMoveVariation(1); } /** Get current variation in current position. */ public final synchronized int currVariation() { return game.currVariation(); } /** Go to a new variation in the game tree. */ public final synchronized void changeVariation(int delta) { if (game.numVariations() > 1) { abortSearch(); game.changeVariation(delta); updateComputeThreads(); setSelection(); updateGUI(); } } /** Delete whole game sub-tree rooted at current position. */ public final synchronized void removeSubTree() { abortSearch(); game.removeSubTree(); updateComputeThreads(); setSelection(); updateGUI(); } /** Move current variation up/down in the game tree. */ public final synchronized void moveVariation(int delta) { if (((delta > 0) && canMoveVariationDown()) || ((delta < 0) && canMoveVariationUp())) { game.moveVariation(delta); updateGUI(); } } /** Add a variation to the game tree. * @param preComment Comment to add before first move. * @param pvMoves List of moves in variation. * @param updateDefault If true, make this the default variation. */ public final synchronized void addVariation(String preComment, List<Move> pvMoves, boolean updateDefault) { for (int i = 0; i < pvMoves.size(); i++) { Move m = pvMoves.get(i); String moveStr = TextIO.moveToUCIString(m); String pre = (i == 0) ? preComment : ""; int varNo = game.tree.addMove(moveStr, "", 0, pre, ""); game.tree.goForward(varNo, updateDefault); } for (int i = 0; i < pvMoves.size(); i++) game.tree.goBack(); gameTextListener.clear(); updateGUI(); } /** Update remaining time and trigger GUI update of clocks. */ public final synchronized void updateRemainingTime() { long now = System.currentTimeMillis(); int wTime = game.timeController.getRemainingTime(true, now); int bTime = game.timeController.getRemainingTime(false, now); int nextUpdate = 0; if (game.timeController.clockRunning()) { int t = game.currPos().whiteMove ? wTime : bTime; nextUpdate = t % 1000; if (nextUpdate < 0) nextUpdate += 1000; nextUpdate += 1; } gui.setRemainingTime(wTime, bTime, nextUpdate); } /** Return maximum number of PVs supported by engine. */ public final synchronized int maxPV() { if (computerPlayer == null) return 1; return computerPlayer.getMaxPV(); } /** Set multi-PV mode. */ public final synchronized void setMultiPVMode(int numPV) { int clampedNumPV = Math.min(numPV, maxPV()); clampedNumPV = Math.max(clampedNumPV, 1); boolean modified = clampedNumPV != this.numPV; this.numPV = numPV; if (modified) { abortSearch(); updateComputeThreads(); updateGUI(); } } /** Request computer player to make a move immediately. */ public final synchronized void stopSearch() { if (!humansTurn() && (computerPlayer != null)) computerPlayer.moveNow(); } /** Stop ponder search. */ public final synchronized void stopPonder() { if (humansTurn() && (computerPlayer != null)) { if (computerPlayer.getSearchType() == SearchType.PONDER) { boolean updateGui = abortSearch(); if (updateGui) updateGUI(); } } } /** Shut down chess engine process. */ public final synchronized void shutdownEngine() { gameMode = new GameMode(GameMode.TWO_PLAYERS); abortSearch(); computerPlayer.shutdownEngine(); } /** Get PGN header tags and values. */ public final synchronized void getHeaders(Map<String,String> headers) { if (game != null) game.tree.getHeaders(headers); } /** Set PGN header tags and values. */ public final synchronized void setHeaders(Map<String,String> headers) { boolean resultChanged = game.tree.setHeaders(headers); gameTextListener.clear(); if (resultChanged) { abortSearch(); updateComputeThreads(); setSelection(); } updateGUI(); } /** Add ECO classification headers. */ public final synchronized void addECO() { EcoDb.Result r = game.tree.getGameECO(); Map<String,String> headers = new TreeMap<String,String>(); headers.put("ECO", r.eco.isEmpty() ? null : r.eco); headers.put("Opening", r.opn.isEmpty() ? null : r.opn); headers.put("Variation", r.var.isEmpty() ? null : r.var); game.tree.setHeaders(headers); gameTextListener.clear(); updateGUI(); } /** Comments associated with a move. */ public static final class CommentInfo { public String move; public String preComment, postComment; public int nag; } /** Get comments associated with current position. */ public final synchronized CommentInfo getComments() { Node cur = game.tree.currentNode; CommentInfo ret = new CommentInfo(); ret.move = cur.moveStrLocal; ret.preComment = cur.preComment; ret.postComment = cur.postComment; ret.nag = cur.nag; return ret; } /** Set comments associated with current position. */ public final synchronized void setComments(CommentInfo commInfo) { Node cur = game.tree.currentNode; cur.preComment = commInfo.preComment; cur.postComment = commInfo.postComment; cur.nag = commInfo.nag; gameTextListener.clear(); updateGUI(); } /** Return true if localized piece names should be used. */ private final boolean localPt() { switch (pgnOptions.view.pieceType) { case PGNOptions.PT_ENGLISH: return false; case PGNOptions.PT_LOCAL: case PGNOptions.PT_FIGURINE: default: return true; } } /** Engine search information receiver. */ private final class SearchListener implements org.petero.droidfish.gamelogic.SearchListener { private int currDepth = 0; private int currMoveNr = 0; private Move currMove = null; private String currMoveStr = ""; private long currNodes = 0; private int currNps = 0; private long currTBHits = 0; private int currHash = 0; private int currTime = 0; private boolean whiteMove = true; private String bookInfo = ""; private ArrayList<Move> bookMoves = null; private String eco = ""; // ECO classification private int distToEcoTree = 0; // Number of plies since game was in the "ECO tree". private Move ponderMove = null; private ArrayList<PvInfo> pvInfoV = new ArrayList<PvInfo>(); private int pvInfoSearchId = -1; // Search ID corresponding to pvInfoV public final void clearSearchInfo(int id) { pvInfoSearchId = -1; ponderMove = null; pvInfoV.clear(); currDepth = 0; bookInfo = ""; bookMoves = null; eco = ""; distToEcoTree = 0; setSearchInfo(id); } private final void setSearchInfo(final int id) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < pvInfoV.size(); i++) { PvInfo pvi = pvInfoV.get(i); if (pvi.depth <= 0) continue; if (i > 0) buf.append('\n'); buf.append(String.format(Locale.US, "[%d] ", pvi.depth)); boolean negateScore = !whiteMove && gui.whiteBasedScores(); if (pvi.upperBound || pvi.lowerBound) { boolean upper = pvi.upperBound ^ negateScore; buf.append(upper ? "<=" : ">="); } int score = negateScore ? -pvi.score : pvi.score; if (pvi.isMate) { buf.append(String.format(Locale.US, "m%d", score)); } else { buf.append(String.format(Locale.US, "%.2f", score / 100.0)); } buf.append(pvi.pvStr); } StringBuilder statStrTmp = new StringBuilder(); if (currDepth > 0) { long nodes = currNodes; String nodesPrefix = ""; if (nodes > 100000000) { nodes /= 1000000; nodesPrefix = "M"; } else if (nodes > 100000) { nodes /= 1000; nodesPrefix = "k"; } int nps = currNps; String npsPrefix = ""; if (nps > 100000000) { nps /= 1000000; npsPrefix = "M"; } else if (nps > 100000) { nps /= 1000; npsPrefix = "k"; } statStrTmp.append(String.format(Locale.US, "d:%d", currDepth)); if (currMoveNr > 0) statStrTmp.append(String.format(Locale.US, " %d:%s", currMoveNr, currMoveStr)); if (currTime < 99995) { statStrTmp.append(String.format(Locale.US, " t:%.2f", currTime / 1000.0)); } else if (currTime < 999950) { statStrTmp.append(String.format(Locale.US, " t:%.1f", currTime / 1000.0)); } else { statStrTmp.append(String.format(Locale.US, " t:%d", (int)((currTime + 500) / 1000))); } statStrTmp.append(String.format(Locale.US, " n:%d%s nps:%d%s", nodes, nodesPrefix, nps, npsPrefix)); if (currTBHits > 0) { long tbHits = currTBHits; String tbHitsPrefix = ""; if (tbHits > 100000000) { tbHits /= 1000000; tbHitsPrefix = "M"; } else if (tbHits > 100000) { tbHits /= 1000; tbHitsPrefix = "k"; } statStrTmp.append(String.format(Locale.US, " tb:%d%s", tbHits, tbHitsPrefix)); } if (currHash > 0) statStrTmp.append(String.format(Locale.US, " h:%d", currHash / 10)); } final String statStr = statStrTmp.toString(); final String newPV = buf.toString(); final ArrayList<ArrayList<Move>> pvMoves = new ArrayList<ArrayList<Move>>(); for (int i = 0; i < pvInfoV.size(); i++) { if (ponderMove != null) { ArrayList<Move> tmp = new ArrayList<Move>(); tmp.add(ponderMove); for (Move m : pvInfoV.get(i).pv) tmp.add(m); pvMoves.add(tmp); } else { pvMoves.add(pvInfoV.get(i).pv); } } final ThinkingInfo ti = new ThinkingInfo(); ti.id = id; ti.pvStr = newPV; ti.statStr = statStr; ti.bookInfo = bookInfo; ti.eco = eco; ti.distToEcoTree = distToEcoTree; ti.pvMoves = pvMoves; ti.bookMoves = bookMoves; latestThinkingInfo = ti; gui.runOnUIThread(new Runnable() { public void run() { setThinkingInfo(ti); } }); } @Override public void notifyDepth(int id, int depth) { currDepth = depth; setSearchInfo(id); } @Override public void notifyCurrMove(int id, Position pos, Move m, int moveNr) { currMove = m; currMoveStr = TextIO.moveToString(pos, m, false, localPt()); currMoveNr = moveNr; setSearchInfo(id); } @SuppressWarnings("unchecked") @Override public void notifyPV(int id, Position pos, ArrayList<PvInfo> pvInfo, Move ponderMove) { this.ponderMove = ponderMove; pvInfoSearchId = id; pvInfoV = (ArrayList<PvInfo>) pvInfo.clone(); for (PvInfo pv : pvInfo) { currTime = pv.time; currNodes = pv.nodes; currNps = pv.nps; currTBHits = pv.tbHits; currHash = pv.hash; StringBuilder buf = new StringBuilder(); Position tmpPos = new Position(pos); UndoInfo ui = new UndoInfo(); if (ponderMove != null) { String moveStr = TextIO.moveToString(tmpPos, ponderMove, false, localPt()); buf.append(String.format(Locale.US, " [%s]", moveStr)); tmpPos.makeMove(ponderMove, ui); } for (Move m : pv.pv) { if (m == null) break; if (!TextIO.isValid(tmpPos, m)) break; String moveStr = TextIO.moveToString(tmpPos, m, false, localPt()); buf.append(String.format(Locale.US, " %s", moveStr)); tmpPos.makeMove(m, ui); } pv.pvStr = buf.toString(); } whiteMove = pos.whiteMove ^ (ponderMove != null); setSearchInfo(id); } @Override public void notifyStats(int id, long nodes, int nps, long tbHits, int hash, int time) { currNodes = nodes; currNps = nps; currTBHits = tbHits; currHash = hash; currTime = time; setSearchInfo(id); } @Override public void notifyBookInfo(int id, String bookInfo, ArrayList<Move> moveList, String eco, int distToEcoTree) { this.bookInfo = bookInfo; bookMoves = moveList; this.eco = eco; this.distToEcoTree = distToEcoTree; setSearchInfo(id); } public void prefsChanged(int id, boolean translateMoves) { if (translateMoves && (id == pvInfoSearchId)) { Position pos = game.currPos(); if (currMove != null) notifyCurrMove(id, pos, currMove, currMoveNr); notifyPV(id, pos, pvInfoV, ponderMove); } else { setSearchInfo(id); } } @Override public void notifySearchResult(final int id, final String cmd, final Move ponder) { new Thread(new Runnable() { public void run() { gui.runOnUIThread(new Runnable() { public void run() { makeComputerMove(id, cmd, ponder); } }); } }).start(); } @Override public void notifyEngineName(final String engineName) { gui.runOnUIThread(new Runnable() { public void run() { updatePlayerNames(engineName); gui.reportEngineName(engineName); } }); } @Override public void reportEngineError(final String errMsg) { gui.runOnUIThread(new Runnable() { public void run() { gui.reportEngineError(errMsg); } }); } } /** Discard current search. Return true if GUI update needed. */ private final boolean abortSearch() { ponderMove = null; searchId++; if (computerPlayer == null) return false; if (computerPlayer.stopSearch()) { listener.clearSearchInfo(searchId); return true; } return false; } private final void updateBookHints() { if (game != null) { Pair<String, ArrayList<Move>> bi = computerPlayer.getBookHints(game.currPos(), localPt()); EcoDb.Result ecoData = EcoDb.getInstance().getEco(game.tree); String eco = ecoData.getName(); listener.notifyBookInfo(searchId, bi.first, bi.second, eco, ecoData.distToEcoTree); } } private final void updateGameMode() { if (game != null) { boolean gamePaused = !gameMode.clocksActive() || (humansTurn() && guiPaused); game.setGamePaused(gamePaused); updateRemainingTime(); Game.AddMoveBehavior amb; if (gui.discardVariations()) amb = Game.AddMoveBehavior.REPLACE; else if (gameMode.clocksActive()) amb = Game.AddMoveBehavior.ADD_FIRST; else amb = Game.AddMoveBehavior.ADD_LAST; game.setAddFirst(amb); } } /** Start/stop computer thinking/analysis as appropriate. */ private final void updateComputeThreads() { boolean alive = game.tree.getGameState() == GameState.ALIVE; boolean analysis = gameMode.analysisMode() && alive; boolean computersTurn = !humansTurn() && alive; boolean ponder = gui.ponderMode() && !analysis && !computersTurn && (ponderMove != null) && alive; if (!analysis && !(computersTurn || ponder)) computerPlayer.stopSearch(); listener.clearSearchInfo(searchId); updateBookHints(); if (!computerPlayer.sameSearchId(searchId)) { if (analysis) { Pair<Position, ArrayList<Move>> ph = game.getUCIHistory(); SearchRequest sr = DroidComputerPlayer.SearchRequest.analyzeRequest( searchId, ph.first, ph.second, new Position(game.currPos()), game.haveDrawOffer(), engine, numPV); computerPlayer.queueAnalyzeRequest(sr); } else if (computersTurn || ponder) { listener.clearSearchInfo(searchId); EcoDb.Result ecoData = EcoDb.getInstance().getEco(game.tree); String eco = ecoData.getName(); listener.notifyBookInfo(searchId, "", null, eco, ecoData.distToEcoTree); final Pair<Position, ArrayList<Move>> ph = game.getUCIHistory(); Position currPos = new Position(game.currPos()); long now = System.currentTimeMillis(); if (ponder) game.timeController.advanceMove(1); int wTime = game.timeController.getRemainingTime(true, now); int bTime = game.timeController.getRemainingTime(false, now); int wInc = game.timeController.getIncrement(true); int bInc = game.timeController.getIncrement(false); boolean wtm = currPos.whiteMove; int movesToGo = game.timeController.getMovesToTC(wtm ^ ponder); if (ponder) game.timeController.advanceMove(-1); final Move fPonderMove = ponder ? ponderMove : null; SearchRequest sr = DroidComputerPlayer.SearchRequest.searchRequest( searchId, now, ph.first, ph.second, currPos, game.haveDrawOffer(), wTime, bTime, wInc, bInc, movesToGo, gui.ponderMode(), fPonderMove, engine, strength); computerPlayer.queueSearchRequest(sr); } else { computerPlayer.queueStartEngine(searchId, engine); } } } private final synchronized void makeComputerMove(int id, final String cmd, final Move ponder) { if (searchId != id) return; searchId++; Position oldPos = new Position(game.currPos()); Pair<Boolean,Move> res = game.processString(cmd); ponderMove = ponder; updateGameMode(); gui.movePlayed(game.prevPos(), res.second, true); listener.clearSearchInfo(searchId); updateComputeThreads(); setSelection(); setAnimMove(oldPos, game.getLastMove(), true); updateGUI(); } public final void repeatLastMove() { gui.movePlayed(game.prevPos(), game.tree.currentNode.move, true); } private final void setPlayerNames(Game game) { if (game != null) { String engine = "Computer"; if (computerPlayer != null) { engine = computerPlayer.getEngineName(); if (strength < 1000) engine += String.format(Locale.US, " (%.1f%%)", strength * 0.1); } String player = gui.playerName(); String white = gameMode.playerWhite() ? player : engine; String black = gameMode.playerBlack() ? player : engine; game.tree.setPlayerNames(white, black); } } private final synchronized void updatePlayerNames(String engineName) { if (game != null) { if (strength < 1000) engineName += String.format(Locale.US, " (%.1f%%)", strength * 0.1); String white = gameMode.playerWhite() ? game.tree.white : engineName; String black = gameMode.playerBlack() ? game.tree.black : engineName; game.tree.setPlayerNames(white, black); updateMoveList(); } } private final boolean undoMoveNoUpdate() { if (game.getLastMove() == null) return false; searchId++; game.undoMove(); if (!humansTurn()) { if (game.getLastMove() != null) { game.undoMove(); if (!humansTurn()) { game.redoMove(); } } else { // Don't undo first white move if playing black vs computer, // because that would cause computer to immediately make // a new move. if (gameMode.playerWhite() || gameMode.playerBlack()) { game.redoMove(); return false; } } } return true; } private final void redoMoveNoUpdate() { if (game.canRedoMove()) { searchId++; game.redoMove(); if (!humansTurn() && game.canRedoMove()) { game.redoMove(); if (!humansTurn()) game.undoMove(); } } } /** * Move a piece from one square to another. * @return True if the move was legal, false otherwise. */ private final boolean doMove(Move move) { Position pos = game.currPos(); ArrayList<Move> moves = new MoveGen().legalMoves(pos); int promoteTo = move.promoteTo; for (Move m : moves) { if ((m.from == move.from) && (m.to == move.to)) { if ((m.promoteTo != Piece.EMPTY) && (promoteTo == Piece.EMPTY)) { promoteMove = m; gui.requestPromotePiece(); return false; } if (m.promoteTo == promoteTo) { String strMove = TextIO.moveToString(pos, m, false, false, moves); Pair<Boolean,Move> res = game.processString(strMove); gui.movePlayed(game.prevPos(), res.second, false); return true; } } } gui.reportInvalidMove(move); return false; } private final void updateGUI() { GUIInterface.GameStatus s = new GUIInterface.GameStatus(); s.state = game.getGameState(); if (s.state == Game.GameState.ALIVE) { s.moveNr = game.currPos().fullMoveCounter; s.white = game.currPos().whiteMove; DroidComputerPlayer.SearchType st = SearchType.NONE; if (computerPlayer != null) st = computerPlayer.getSearchType(); switch (st) { case SEARCH: s.thinking = true; break; case PONDER: s.ponder = true; break; case ANALYZE: s.analyzing = true; break; case NONE: break; } } else { if ((s.state == GameState.DRAW_REP) || (s.state == GameState.DRAW_50)) s.drawInfo = game.getDrawInfo(localPt()); } gui.setStatus(s); updateMoveList(); StringBuilder sb = new StringBuilder(); if (game.tree.currentNode != game.tree.rootNode) { game.tree.goBack(); Position pos = game.currPos(); List<Move> prevVarList = game.tree.variations(); for (int i = 0; i < prevVarList.size(); i++) { if (i > 0) sb.append(' '); if (i == game.tree.currentNode.defaultChild) sb.append(Util.boldStart); sb.append(TextIO.moveToString(pos, prevVarList.get(i), false, localPt())); if (i == game.tree.currentNode.defaultChild) sb.append(Util.boldStop); } game.tree.goForward(-1); } gui.setPosition(game.currPos(), sb.toString(), game.tree.variations()); updateRemainingTime(); updateMaterialDiffList(); gui.updateTimeControlTitle(); } public final void updateMaterialDiffList() { gui.updateMaterialDifferenceTitle(Util.getMaterialDiff(game.currPos())); } private final synchronized void setThinkingInfo(ThinkingInfo ti) { if ((ti.id == searchId) && (ti == latestThinkingInfo)) gui.setThinkingInfo(ti); } private final void updateMoveList() { if (game == null) return; if (!gameTextListener.isUpToDate()) { PGNOptions tmpOptions = new PGNOptions(); tmpOptions.exp.variations = pgnOptions.view.variations; tmpOptions.exp.comments = pgnOptions.view.comments; tmpOptions.exp.nag = pgnOptions.view.nag; tmpOptions.exp.playerAction = false; tmpOptions.exp.clockInfo = false; tmpOptions.exp.moveNrAfterNag = false; tmpOptions.exp.pieceType = pgnOptions.view.pieceType; gameTextListener.clear(); game.tree.pgnTreeWalker(tmpOptions, gameTextListener); } gameTextListener.setCurrent(game.tree.currentNode); gui.moveListUpdated(); } /** Mark last played move in the GUI. */ private final void setSelection() { Move m = game.getLastMove(); int sq = ((m != null) && (m.from != m.to)) ? m.to : -1; gui.setSelection(sq); } private void setAnimMove(Position sourcePos, Move move, boolean forward) { gui.setAnimMove(sourcePos, move, forward); } private final boolean findValidDrawClaim(String ms) { if (!ms.isEmpty()) ms = " " + ms; if (game.getGameState() != GameState.ALIVE) return true; game.tryClaimDraw("draw accept"); if (game.getGameState() != GameState.ALIVE) return true; game.tryClaimDraw("draw rep" + ms); if (game.getGameState() != GameState.ALIVE) return true; game.tryClaimDraw("draw 50" + ms); if (game.getGameState() != GameState.ALIVE) return true; return false; } }