From c1ab57b4f955e8433c124338dfbe9b609b27e19e Mon Sep 17 00:00:00 2001 From: Peter Osterlund <peterosterlund2@gmail.com> Date: Sat, 31 Dec 2011 02:30:18 +0000 Subject: [PATCH] DroidFish: Rewrote the engine communication so that the GUI never waits for the engine. --- DroidFish/jni/jni.cpp | 20 +- .../src/org/petero/droidfish/DroidFish.java | 117 ++- .../petero/droidfish/SeekBarPreference.java | 8 +- .../droidfish/activities/EditBoard.java | 16 +- .../petero/droidfish/engine/DroidBook.java | 4 +- .../droidfish/engine/DroidComputerPlayer.java | 955 ++++++++++++------ .../petero/droidfish/engine/UCIEngine.java | 1 + .../engine/cuckoochess/CuckooChessEngine.java | 8 +- .../gamelogic/DroidChessController.java | 512 ++++------ .../droidfish/gamelogic/SearchListener.java | 33 +- 10 files changed, 959 insertions(+), 715 deletions(-) diff --git a/DroidFish/jni/jni.cpp b/DroidFish/jni/jni.cpp index 0ad5e7c..36a985f 100644 --- a/DroidFish/jni/jni.cpp +++ b/DroidFish/jni/jni.cpp @@ -73,20 +73,26 @@ static std::deque<char> inBuf; static bool getNextChar(int& c, int timeoutMillis) { if (inBuf.empty()) { - fd_set readfds, writefds; + fd_set readfds, exceptfds; FD_ZERO(&readfds); FD_SET(fdFromChild, &readfds); + FD_ZERO(&exceptfds); + FD_SET(fdFromChild, &exceptfds); struct timeval tv; tv.tv_sec = timeoutMillis / 1000; tv.tv_usec = (timeoutMillis % 1000) * 1000; - int ret = select(fdFromChild + 1, &readfds, NULL, NULL, &tv); - if (ret < 0) + int ret = select(fdFromChild + 1, &readfds, NULL, &exceptfds, &tv); + if ((ret < 0) || FD_ISSET(fdFromChild, &exceptfds)) return false; - static char buf[4096]; - int len = read(fdFromChild, &buf[0], sizeof(buf)); - for (int i = 0; i < len; i++) - inBuf.push_back(buf[i]); + if (FD_ISSET(fdFromChild, &readfds)) { + static char buf[4096]; + int len = read(fdFromChild, &buf[0], sizeof(buf)); + if (len == 0) + return false; // EOF + for (int i = 0; i < len; i++) + inBuf.push_back(buf[i]); + } } if (inBuf.empty()) { c = -1; diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java index fde12ef..4427b73 100644 --- a/DroidFish/src/org/petero/droidfish/DroidFish.java +++ b/DroidFish/src/org/petero/droidfish/DroidFish.java @@ -108,6 +108,7 @@ public class DroidFish extends Activity implements GUIInterface { // FIXME!!! Remove invalid playerActions in PGN import (should be done in verifyChildren) // FIXME!!! Implement bookmark mechanism for positions in pgn files // FIXME!!! Display chess notation in local language + // FIXME!!! Add support for "Chess Leipzig" font // FIXME!!! Computer clock should stop if phone turned off (computer stops thinking if unplugged) // FIXME!!! Add support for all time controls defined by the PGN standard @@ -118,8 +119,6 @@ public class DroidFish extends Activity implements GUIInterface { // FIXME!!! Add chess960 support // FIXME!!! Implement "hint" feature - // FIXME!!! Don't send "stop" command when engine is already stopped - // FIXME!!! Show extended book info. (Win percent, number of games, performance rating, etc.) // FIXME!!! Green color for "main move". Red color for "don't play in tournaments" moves. @@ -542,7 +541,7 @@ public class DroidFish extends Activity implements GUIInterface { mEngineThreads = getIntSetting("threads", 0); - String engine = settings.getString("engine", ""); + String engine = settings.getString("engine", "stockfish"); int strength = settings.getInt("strength", 1000); setEngineStrength(engine, strength); @@ -1142,7 +1141,7 @@ public class DroidFish extends Activity implements GUIInterface { return alert; } case SELECT_MOVE_DIALOG: { - View content = View.inflate(this, R.layout.select_move_number, null); + View content = View.inflate(this, R.layout.select_move_number, null); final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setView(content); builder.setTitle(R.string.goto_move); @@ -1159,14 +1158,14 @@ public class DroidFish extends Activity implements GUIInterface { } }; builder.setPositiveButton(R.string.ok, new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - gotoMove.run(); - } + public void onClick(DialogInterface dialog, int which) { + gotoMove.run(); + } }); builder.setNegativeButton(R.string.cancel, null); - + final AlertDialog dialog = builder.create(); - + moveNrView.setOnKeyListener(new OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { @@ -1297,7 +1296,7 @@ public class DroidFish extends Activity implements GUIInterface { return alert; } case SELECT_PGN_SAVE_NEWFILE_DIALOG: { - View content = View.inflate(this, R.layout.create_pgn_file, null); + View content = View.inflate(this, R.layout.create_pgn_file, null); final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setView(content); builder.setTitle(R.string.select_pgn_file_save); @@ -1312,12 +1311,12 @@ public class DroidFish extends Activity implements GUIInterface { } }; builder.setPositiveButton(R.string.ok, new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - savePGN.run(); - } + public void onClick(DialogInterface dialog, int which) { + savePGN.run(); + } }); builder.setNegativeButton(R.string.cancel, null); - + final Dialog dialog = builder.create(); fileNameView.setOnKeyListener(new OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { @@ -1419,57 +1418,57 @@ public class DroidFish extends Activity implements GUIInterface { final TreeMap<String,String> headers = new TreeMap<String,String>(); ctrl.getHeaders(headers); - AlertDialog.Builder builder = new AlertDialog.Builder(DroidFish.this); - builder.setTitle(R.string.edit_headers); - View content = View.inflate(DroidFish.this, R.layout.edit_headers, null); - builder.setView(content); - - final TextView event, site, date, round, white, black; - - event = (TextView)content.findViewById(R.id.ed_header_event); + AlertDialog.Builder builder = new AlertDialog.Builder(DroidFish.this); + builder.setTitle(R.string.edit_headers); + View content = View.inflate(DroidFish.this, R.layout.edit_headers, null); + builder.setView(content); + + final TextView event, site, date, round, white, black; + + event = (TextView)content.findViewById(R.id.ed_header_event); site = (TextView)content.findViewById(R.id.ed_header_site); date = (TextView)content.findViewById(R.id.ed_header_date); round = (TextView)content.findViewById(R.id.ed_header_round); white = (TextView)content.findViewById(R.id.ed_header_white); black = (TextView)content.findViewById(R.id.ed_header_black); - + event.setText(headers.get("Event")); site .setText(headers.get("Site")); date .setText(headers.get("Date")); round.setText(headers.get("Round")); white.setText(headers.get("White")); black.setText(headers.get("Black")); - - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ok, new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - headers.put("Event", event.getText().toString().trim()); - headers.put("Site", site .getText().toString().trim()); - headers.put("Date", date .getText().toString().trim()); - headers.put("Round", round.getText().toString().trim()); - headers.put("White", white.getText().toString().trim()); - headers.put("Black", black.getText().toString().trim()); - ctrl.setHeaders(headers); - } - }); - builder.show(); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + headers.put("Event", event.getText().toString().trim()); + headers.put("Site", site .getText().toString().trim()); + headers.put("Date", date .getText().toString().trim()); + headers.put("Round", round.getText().toString().trim()); + headers.put("White", white.getText().toString().trim()); + headers.put("Black", black.getText().toString().trim()); + ctrl.setHeaders(headers); + } + }); + + builder.show(); break; } case EDIT_COMMENTS: { AlertDialog.Builder builder = new AlertDialog.Builder(DroidFish.this); - builder.setTitle(R.string.edit_comments); - View content = View.inflate(DroidFish.this, R.layout.edit_comments, null); - builder.setView(content); - - DroidChessController.CommentInfo commInfo = ctrl.getComments(); - - final TextView preComment, moveView, nag, postComment; - preComment = (TextView)content.findViewById(R.id.ed_comments_pre); + builder.setTitle(R.string.edit_comments); + View content = View.inflate(DroidFish.this, R.layout.edit_comments, null); + builder.setView(content); + + DroidChessController.CommentInfo commInfo = ctrl.getComments(); + + final TextView preComment, moveView, nag, postComment; + preComment = (TextView)content.findViewById(R.id.ed_comments_pre); moveView = (TextView)content.findViewById(R.id.ed_comments_move); nag = (TextView)content.findViewById(R.id.ed_comments_nag); postComment = (TextView)content.findViewById(R.id.ed_comments_post); - + preComment.setText(commInfo.preComment); postComment.setText(commInfo.postComment); moveView.setText(commInfo.move); @@ -1477,22 +1476,22 @@ public class DroidFish extends Activity implements GUIInterface { if ((nagStr.length() == 0) && (commInfo.nag > 0)) nagStr = String.format("%d", commInfo.nag); nag.setText(nagStr); - + builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.ok, new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - String pre = preComment.getText().toString().trim(); - String post = postComment.getText().toString().trim(); - int nagVal = Node.strToNag(nag.getText().toString()); - - DroidChessController.CommentInfo commInfo = new DroidChessController.CommentInfo(); - commInfo.preComment = pre; - commInfo.postComment = post; - commInfo.nag = nagVal; - ctrl.setComments(commInfo); - } + public void onClick(DialogInterface dialog, int which) { + String pre = preComment.getText().toString().trim(); + String post = postComment.getText().toString().trim(); + int nagVal = Node.strToNag(nag.getText().toString()); + + DroidChessController.CommentInfo commInfo = new DroidChessController.CommentInfo(); + commInfo.preComment = pre; + commInfo.postComment = post; + commInfo.nag = nagVal; + ctrl.setComments(commInfo); + } }); - + builder.show(); break; } diff --git a/DroidFish/src/org/petero/droidfish/SeekBarPreference.java b/DroidFish/src/org/petero/droidfish/SeekBarPreference.java index f3f4b6a..7ee3478 100644 --- a/DroidFish/src/org/petero/droidfish/SeekBarPreference.java +++ b/DroidFish/src/org/petero/droidfish/SeekBarPreference.java @@ -86,7 +86,7 @@ public class SeekBarPreference extends Preference currValBox.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - View content = View.inflate(SeekBarPreference.this.getContext(), R.layout.select_percentage, null); + View content = View.inflate(SeekBarPreference.this.getContext(), R.layout.select_percentage, null); final AlertDialog.Builder builder = new AlertDialog.Builder(SeekBarPreference.this.getContext()); builder.setView(content); String title = ""; @@ -121,9 +121,9 @@ public class SeekBarPreference extends Preference } }); builder.setPositiveButton("Ok", new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - selectValue.run(); - } + public void onClick(DialogInterface dialog, int which) { + selectValue.run(); + } }); builder.setNegativeButton("Cancel", null); diff --git a/DroidFish/src/org/petero/droidfish/activities/EditBoard.java b/DroidFish/src/org/petero/droidfish/activities/EditBoard.java index 83d3c1b..c6850c4 100644 --- a/DroidFish/src/org/petero/droidfish/activities/EditBoard.java +++ b/DroidFish/src/org/petero/droidfish/activities/EditBoard.java @@ -347,7 +347,7 @@ public class EditBoard extends Activity { checkValid(); dialog.cancel(); } - } + } }); AlertDialog alert = builder.create(); return alert; @@ -406,9 +406,9 @@ public class EditBoard extends Activity { return alert; } case MOVCNT_DIALOG: { - View content = View.inflate(this, R.layout.edit_move_counters, null); + View content = View.inflate(this, R.layout.edit_move_counters, null); final AlertDialog.Builder builder = new AlertDialog.Builder(this); - + builder.setView(content); builder.setTitle(R.string.edit_move_counters); final EditText halfMoveClock = (EditText)content.findViewById(R.id.ed_cnt_halfmove); @@ -428,14 +428,14 @@ public class EditBoard extends Activity { } }; builder.setPositiveButton("Ok", new Dialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - setCounters.run(); - } + public void onClick(DialogInterface dialog, int which) { + setCounters.run(); + } }); builder.setNegativeButton("Cancel", null); - + final Dialog dialog = builder.create(); - + fullMoveCounter.setOnKeyListener(new OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { diff --git a/DroidFish/src/org/petero/droidfish/engine/DroidBook.java b/DroidFish/src/org/petero/droidfish/engine/DroidBook.java index 539b5bb..41a1861 100644 --- a/DroidFish/src/org/petero/droidfish/engine/DroidBook.java +++ b/DroidFish/src/org/petero/droidfish/engine/DroidBook.java @@ -118,7 +118,7 @@ public final class DroidBook { StringBuilder ret = new StringBuilder(); ArrayList<Move> bookMoveList = new ArrayList<Move>(); List<BookEntry> bookMoves = getBook().getBookEntries(pos); - + // Check legality if (bookMoves != null) { ArrayList<Move> legalMoves = new MoveGen().pseudoLegalMoves(pos); @@ -131,7 +131,7 @@ public final class DroidBook { } } } - + if (bookMoves != null) { Collections.sort(bookMoves, new Comparator<BookEntry>() { public int compare(BookEntry arg0, BookEntry arg1) { diff --git a/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java b/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java index 238f405..b25efa6 100644 --- a/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java +++ b/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java @@ -25,7 +25,6 @@ import java.util.ArrayList; import org.petero.droidfish.BookOptions; import org.petero.droidfish.engine.cuckoochess.CuckooChessEngine; -import org.petero.droidfish.gamelogic.Game; import org.petero.droidfish.gamelogic.Move; import org.petero.droidfish.gamelogic.MoveGen; import org.petero.droidfish.gamelogic.Pair; @@ -40,28 +39,209 @@ import org.petero.droidfish.gamelogic.SearchListener.PvInfo; * @author petero */ public class DroidComputerPlayer { - private String engineName = ""; - private UCIEngine uciEngine = null; private final SearchListener listener; private final DroidBook book; + /** Set when "ucinewgame" needs to be sent. */ private boolean newGame = false; - /** Engine identifier, "cuckoochess" or "stockfish". */ - private String engine = ""; + /** >1 if multiPV mode is supported. */ private int maxPV = 1; private int numCPUs = 1; - private int strength = 1000; + private String engineName = "Computer"; - private boolean havePonderHit = false; + /** Engine state. */ + private static enum MainState { + READ_OPTIONS, // "uci" command sent, waiting for "option" and "uciok" response. + WAIT_READY, // "isready" sent, waiting for "readyok". + IDLE, // engine not searching. + SEARCH, // "go" sent, waiting for "bestmove" + PONDER, // "go" sent, waiting for "bestmove" + ANALYZE, // "go" sent, waiting for "bestmove" (which will be ignored) + STOP_SEARCH, // "stop" sent, waiting for "bestmove" + DEAD, // engine process has terminated + } + + /** Engine state details. */ + private static final class EngineState { + String engine; + + /** Current engine state. */ + MainState state; + + /** ID of current search job. */ + int searchId; + + /** Default constructor. */ + EngineState() { + engine = ""; + state = MainState.DEAD; + searchId = -1; + } + } + + /** Information about current/next engine search task. */ + public static final class SearchRequest { + int searchId; // Unique identifier for this search request + long startTime; // System time (milliseconds) when search request was created + + Position prevPos; // Position at last irreversible move + ArrayList<Move> mList; // Moves after prevPos, including ponderMove + Position currPos; // currPos = prevPos + mList - ponderMove + boolean drawOffer; // True if other side made draw offer + + boolean isSearch; // True if regular search or ponder search + boolean isAnalyze; // True if analysis search + int wTime; // White remaining time, milliseconds + int bTime; // Black remaining time, milliseconds + int inc; // Time increment per move, milliseconds + int movesToGo; // Number of moves to next time control + + String engine; // Engine name (identifier) + int engineThreads; // Number of engine threads to use + int strength; // Engine strength setting (0 - 1000) + int numPV; // Number of PV lines to compute + + boolean ponderEnabled; // True if pondering enabled, for engine time management + Move ponderMove; // Ponder move, or null if not a ponder search + + long[] posHashList; // For draw decision after completed search + int posHashListSize; // For draw decision after completed search + + /** + * Create a request to start an engine. + * @param id Search ID. + * @param engine Chess engine to use for searching. + */ + public static SearchRequest startRequest(int id, String engine) { + SearchRequest sr = new SearchRequest(); + sr.searchId = id; + sr.isSearch = false; + sr.isAnalyze = false; + sr.engine = engine; + sr.posHashList = null; + sr.posHashListSize = 0; + return sr; + } + + /** + * Create a search request object. + * @param id Search ID. + * @param now Current system time. + * @param pos An earlier position from the game. + * @param mList List of moves to go from the earlier position to the current position. + * This list makes it possible for the computer to correctly handle draw + * by repetition/50 moves. + * @param ponderEnabled True if pondering is enabled in the GUI. Can affect time management. + * @param ponderMove Move to ponder, or null for non-ponder search. + * @param engine Chess engine to use for searching. + * @param engineThreads Number of engine threads to use, if supported by engine. + * @param strength Engine strength setting. + */ + public static SearchRequest searchRequest(int id, long now, + Position prevPos, ArrayList<Move> mList, + Position currPos, boolean drawOffer, + int wTime, int bTime, int inc, int movesToGo, + boolean ponderEnabled, Move ponderMove, + String engine, int engineThreads, + int strength) { + SearchRequest sr = new SearchRequest(); + sr.searchId = id; + sr.startTime = now; + sr.prevPos = prevPos; + sr.mList = mList; + sr.currPos = currPos; + sr.drawOffer = drawOffer; + sr.isSearch = true; + sr.isAnalyze = false; + sr.wTime = wTime; + sr.bTime = bTime; + sr.inc = inc; + sr.movesToGo = movesToGo; + sr.engine = engine; + sr.engineThreads = engineThreads; + sr.strength = strength; + sr.numPV = 1; + sr.ponderEnabled = ponderEnabled; + sr.ponderMove = ponderMove; + sr.posHashList = null; + sr.posHashListSize = 0; + return sr; + } + + /** + * Create an analysis request object. + * @param id Search ID. + * @param prevPos Position corresponding to last irreversible move. + * @param mList List of moves from prevPos to currPos. + * @param currPos Position to analyze. + * @param drawOffer True if other side have offered draw. + * @param engine Chess engine to use for searching + * @param engineThreads Number of threads to use, or 0 for default value. + * @param numPV Multi-PV mode. + */ + public static SearchRequest analyzeRequest(int id, Position prevPos, + ArrayList<Move> mList, + Position currPos, + boolean drawOffer, + String engine, + int engineThreads, + int numPV) { + SearchRequest sr = new SearchRequest(); + sr.searchId = id; + sr.startTime = System.currentTimeMillis(); + sr.prevPos = prevPos; + sr.mList = mList; + sr.currPos = currPos; + sr.drawOffer = drawOffer; + sr.isSearch = false; + sr.isAnalyze = true; + sr.wTime = sr.bTime = sr.inc = sr.movesToGo = 0; + sr.engine = engine; + sr.engineThreads = engineThreads; + sr.strength = 1000; + sr.numPV = numPV; + sr.ponderEnabled = false; + sr.ponderMove = null; + sr.posHashList = null; + sr.posHashListSize = 0; + return sr; + } + + /** Update data for ponder hit. */ + final void ponderHit() { + if (ponderMove == null) + return; + UndoInfo ui = new UndoInfo(); + currPos.makeMove(ponderMove, ui); + ponderMove = null; + } + } + + private EngineState engineState; + private SearchRequest searchRequest; + private Thread engineMonitor; /** Constructor. Starts engine process if not already started. */ - public DroidComputerPlayer(String engine, SearchListener listener) { - this.engine = engine; - startEngine(); + public DroidComputerPlayer(SearchListener listener) { this.listener = listener; book = DroidBook.getInstance(); + engineState = new EngineState(); + searchRequest = null; + } + + /** Return true if computer player is consuming CPU time. */ + public final synchronized boolean computerBusy() { + switch (engineState.state) { + case SEARCH: + case PONDER: + case ANALYZE: + case STOP_SEARCH: + return true; + default: + return false; + } } /** Return maximum number of PVs supported by engine. */ @@ -79,12 +259,9 @@ public class DroidComputerPlayer { return book.getAllBookMoves(pos); } - /** Get engine reported name, including strength setting. */ + /** Get engine reported name. */ public final synchronized String getEngineName() { - String ret = engineName; - if (strength < 1000) - ret += String.format(" (%.1f%%)", strength * 0.1); - return ret; + return engineName; } /** Clear transposition table. Takes effect when next search started. */ @@ -93,11 +270,22 @@ public class DroidComputerPlayer { } /** Sends "ponderhit" command to engine. */ - public final synchronized void ponderHit(Position pos, Move ponderMove) { - havePonderHit = true; - uciEngine.writeLineToEngine("ponderhit"); - pvModified = true; - notifyGUI(pos, ponderMove); + public final synchronized void ponderHit(int id) { + if ((searchRequest == null) || + (searchRequest.ponderMove == null) || + (searchRequest.searchId != id)) + return; + + searchRequest.ponderHit(); + if (engineState.state != MainState.PONDER) + searchRequest.startTime = System.currentTimeMillis(); + + if (engineState.state == MainState.PONDER) { + uciEngine.writeLineToEngine("ponderhit"); + engineState.state = MainState.SEARCH; + pvModified = true; + notifyGUI(); + } } /** Stop the engine process. */ @@ -105,281 +293,395 @@ public class DroidComputerPlayer { if (uciEngine != null) { uciEngine.shutDown(); uciEngine = null; + engineMonitor.interrupt(); + engineMonitor = null; } + engineState.state = MainState.DEAD; + } + + /** Start an engine, if not already started. + * Will shut down old engine first, if needed. */ + public final synchronized void queueStartEngine(int id, String engine) { + stopSearch(); + searchRequest = SearchRequest.startRequest(id, engine); + handleQueue(); } /** - * Do a search and return a command from the computer player. - * The command can be a valid move string, in which case the move is played - * and the turn goes over to the other player. The command can also be a special + * Start a search. Search result is returned to the search listener object. + * The result can be a valid move string, in which case the move is played + * and the turn goes over to the other player. The result can also be a special * command, such as "draw" and "resign". - * @param pos An earlier position from the game - * @param mList List of moves to go from the earlier position to the current position. - * This list makes it possible for the computer to correctly handle draw - * by repetition/50 moves. - * @param ponderEnabled True if pondering is enabled in the GUI. Can affect time management. - * @param ponderMove Move to ponder, or null for non-ponder search. - * @param engineThreads Number of engine threads to use, if supported by engine. - * @param engine Chess engine to use for searching. - * @param strength Engine strength setting. */ - public final void doSearch(Position prevPos, ArrayList<Move> mList, - Position currPos, boolean drawOffer, - int wTime, int bTime, int inc, int movesToGo, - boolean ponderEnabled, Move ponderMove, - int engineThreads, - String engine, int strength, Game g) { - setEngineStrength(engine, strength); - setNumPV(1); - listener.notifyBookInfo("", null); - - if (ponderMove != null) - mList.add(ponderMove); - - havePonderHit = false; - + public final synchronized void queueSearchRequest(SearchRequest sr) { + stopSearch(); + + if (sr.ponderMove != null) + sr.mList.add(sr.ponderMove); + // Set up for draw detection - long[] posHashList = new long[mList.size()+1]; + long[] posHashList = new long[sr.mList.size()+1]; int posHashListSize = 0; - Position p = new Position(prevPos); + Position p = new Position(sr.prevPos); UndoInfo ui = new UndoInfo(); - for (int i = 0; i < mList.size(); i++) { + for (int i = 0; i < sr.mList.size(); i++) { posHashList[posHashListSize++] = p.zobristHash(); - p.makeMove(mList.get(i), ui); + p.makeMove(sr.mList.get(i), ui); } - - if (ponderMove == null) { + + if (sr.ponderMove == null) { // If we have a book move, play it. - Move bookMove = book.getBookMove(currPos); + Move bookMove = book.getBookMove(sr.currPos); if (bookMove != null) { - if (canClaimDraw(currPos, posHashList, posHashListSize, bookMove) == "") { - listener.notifySearchResult(g, TextIO.moveToString(currPos, bookMove, false), null); + if (canClaimDraw(sr.currPos, posHashList, posHashListSize, bookMove) == "") { + listener.notifySearchResult(sr.searchId, TextIO.moveToString(sr.currPos, bookMove, false), null); return; } } - + // If only one legal move, play it without searching - ArrayList<Move> moves = new MoveGen().pseudoLegalMoves(currPos); - moves = MoveGen.removeIllegal(currPos, moves); + ArrayList<Move> moves = new MoveGen().pseudoLegalMoves(sr.currPos); + moves = MoveGen.removeIllegal(sr.currPos, moves); if (moves.size() == 0) { - listener.notifySearchResult(g, "", null); // User set up a position where computer has no valid moves. + listener.notifySearchResult(sr.searchId, "", null); // User set up a position where computer has no valid moves. return; } if (moves.size() == 1) { Move bestMove = moves.get(0); - if (canClaimDraw(currPos, posHashList, posHashListSize, bestMove) == "") { - listener.notifySearchResult(g, TextIO.moveToUCIString(bestMove), null); + if (canClaimDraw(sr.currPos, posHashList, posHashListSize, bestMove) == "") { + listener.notifySearchResult(sr.searchId, TextIO.moveToUCIString(bestMove), null); return; } } } - - StringBuilder posStr = new StringBuilder(); - posStr.append("position fen "); - posStr.append(TextIO.toFEN(prevPos)); - int nMoves = mList.size(); - if (nMoves > 0) { - posStr.append(" moves"); - for (int i = 0; i < nMoves; i++) { - posStr.append(" "); - posStr.append(TextIO.moveToUCIString(mList.get(i))); - } + + sr.posHashList = posHashList; + sr.posHashListSize = posHashListSize; + + searchRequest = sr; + handleQueue(); + } + + /** Start analyzing a position. */ + public final synchronized void queueAnalyzeRequest(SearchRequest sr) { + stopSearch(); + + // If no legal moves, there is nothing to analyze + ArrayList<Move> moves = new MoveGen().pseudoLegalMoves(sr.currPos); + moves = MoveGen.removeIllegal(sr.currPos, moves); + if (moves.size() == 0) + return; + + searchRequest = sr; + handleQueue(); + } + + private final void handleQueue() { + if (engineState.state == MainState.DEAD) { + engineState.engine = ""; + engineState.state = MainState.IDLE; } - maybeNewGame(); - uciEngine.setOption("Ponder", ponderEnabled); - uciEngine.setOption("UCI_AnalyseMode", false); - uciEngine.setOption("Threads", engineThreads > 0 ? engineThreads : numCPUs); - uciEngine.writeLineToEngine(posStr.toString()); - if (wTime < 1) wTime = 1; - if (bTime < 1) bTime = 1; - StringBuilder goStr = new StringBuilder(96); - goStr.append(String.format("go wtime %d btime %d", wTime, bTime)); - if (inc > 0) - goStr.append(String.format(" winc %d binc %d", inc, inc)); - if (movesToGo > 0) - goStr.append(String.format(" movestogo %d", movesToGo)); - if (ponderMove != null) - goStr.append(" ponder"); - uciEngine.writeLineToEngine(goStr.toString()); - - Pair<String,String> pair = monitorEngine(currPos, ponderMove); - String bestMove = pair.first; - Move nextPonderMove = TextIO.UCIstringToMove(pair.second); - + if (engineState.state == MainState.IDLE) + handleIdleState(); + } + + /** Tell engine to stop searching. */ + public final synchronized boolean stopSearch() { + searchRequest = null; + switch (engineState.state) { + case SEARCH: + case PONDER: + case ANALYZE: + uciEngine.writeLineToEngine("stop"); + engineState.state = MainState.STOP_SEARCH; + return true; + default: + return false; + } + } + + /** Return true if current search job is equal to id. */ + public final synchronized boolean sameSearchId(int id) { + return (searchRequest != null) && (searchRequest.searchId == id); + } + + /** Type of search the engine is currently requested to perform. */ + public static enum SearchType { + NONE, + SEARCH, + PONDER, + ANALYZE + } + + /** Return type of search the engine is currently requested to perform. */ + public final synchronized SearchType getSearchType() { + if (searchRequest == null) + return SearchType.NONE; + if (searchRequest.isAnalyze) + return SearchType.ANALYZE; + if (!searchRequest.isSearch) + return SearchType.NONE; + if (searchRequest.ponderMove == null) + return SearchType.SEARCH; + else + return SearchType.PONDER; + } + + /** Determine what to do next when in idle state. */ + private final void handleIdleState() { + SearchRequest sr = searchRequest; + if (sr == null) + return; + + // Make sure correct engine is running + if ((uciEngine == null) || !engineState.engine.equals(sr.engine)) { + shutdownEngine(); + startEngine(); + return; + } + + // Send "ucinewgame" (clear hash table) if needed + if (newGame) { + uciEngine.writeLineToEngine("ucinewgame"); + uciEngine.writeLineToEngine("isready"); + engineState.state = MainState.WAIT_READY; + newGame = false; + return; + } + + // Check if only engine start was requested + boolean isSearch = sr.isSearch; + boolean isAnalyze = sr.isAnalyze; + if (!isSearch && !isAnalyze) { + searchRequest = null; + return; + } + + engineState.searchId = searchRequest.searchId; + + // Reduce remaining time there was an engine delay + if (isSearch) { + long now = System.currentTimeMillis(); + int delay = (int)(now - searchRequest.startTime); + boolean wtm = searchRequest.currPos.whiteMove ^ (searchRequest.ponderMove != null); + if (wtm) + searchRequest.wTime = Math.max(1, searchRequest.wTime - delay); + else + searchRequest.bTime = Math.max(1, searchRequest.bTime - delay); + } + + // Set strength and MultiPV parameters + clearInfo(); + uciEngine.setStrength(searchRequest.strength); + if (maxPV > 1) { + int num = Math.min(maxPV, searchRequest.numPV); + uciEngine.setOption("MultiPV", num); + } + + if (isSearch) { // Search or ponder search + StringBuilder posStr = new StringBuilder(); + posStr.append("position fen "); + posStr.append(TextIO.toFEN(sr.prevPos)); + int nMoves = sr.mList.size(); + if (nMoves > 0) { + posStr.append(" moves"); + for (int i = 0; i < nMoves; i++) { + posStr.append(" "); + posStr.append(TextIO.moveToUCIString(sr.mList.get(i))); + } + } + uciEngine.setOption("Ponder", sr.ponderEnabled); + uciEngine.setOption("UCI_AnalyseMode", false); + uciEngine.setOption("Threads", sr.engineThreads > 0 ? sr.engineThreads : numCPUs); + uciEngine.writeLineToEngine(posStr.toString()); + if (sr.wTime < 1) sr.wTime = 1; + if (sr.bTime < 1) sr.bTime = 1; + StringBuilder goStr = new StringBuilder(96); + goStr.append(String.format("go wtime %d btime %d", sr.wTime, sr.bTime)); + if (sr.inc > 0) + goStr.append(String.format(" winc %d binc %d", sr.inc, sr.inc)); + if (sr.movesToGo > 0) + goStr.append(String.format(" movestogo %d", sr.movesToGo)); + if (sr.ponderMove != null) + goStr.append(" ponder"); + uciEngine.writeLineToEngine(goStr.toString()); + engineState.state = (sr.ponderMove == null) ? MainState.SEARCH : MainState.PONDER; + } else { // Analyze + StringBuilder posStr = new StringBuilder(); + posStr.append("position fen "); + posStr.append(TextIO.toFEN(sr.prevPos)); + int nMoves = sr.mList.size(); + if (nMoves > 0) { + posStr.append(" moves"); + for (int i = 0; i < nMoves; i++) { + posStr.append(" "); + posStr.append(TextIO.moveToUCIString(sr.mList.get(i))); + } + } + uciEngine.writeLineToEngine(posStr.toString()); + uciEngine.setOption("UCI_AnalyseMode", true); + uciEngine.setOption("Threads", sr.engineThreads > 0 ? sr.engineThreads : numCPUs); + uciEngine.writeLineToEngine("go infinite"); + engineState.state = MainState.ANALYZE; + } + } + + private final void startEngine() { + myAssert(uciEngine == null); + myAssert(engineMonitor == null); + myAssert(engineState.state == MainState.DEAD); + myAssert(searchRequest != null); + + engineName = "Computer"; + if ("cuckoochess".equals(searchRequest.engine)) + uciEngine = new CuckooChessEngine(); + else + uciEngine = new NativePipedProcess(); + uciEngine.initialize(); + + engineMonitor = new Thread(new Runnable() { + public void run() { + monitorLoop(); + } + }); + engineMonitor.start(); + + uciEngine.writeLineToEngine("uci"); + int nThreads = getNumCPUs(); + if (nThreads > 8) nThreads = 8; + numCPUs = nThreads; + maxPV = 1; + engineState.engine = searchRequest.engine; + engineState.state = MainState.READ_OPTIONS; + } + + + private final static long guiUpdateInterval = 100; + private long lastGUIUpdate = 0; + + private final void monitorLoop() { + while (true) { + int timeout = getReadTimeout(); + String s = uciEngine.readLineFromEngine(timeout); + if (Thread.currentThread().isInterrupted()) + return; + processEngineOutput(s); + notifyGUI(); + if (Thread.currentThread().isInterrupted()) + return; + } + } + + /** Process one line of data from the engine. */ + private final synchronized void processEngineOutput(String s) { + if (s == null) { + shutdownEngine(); + return; + } + + if (s.length() == 0) + return; + + UCIEngine uci = uciEngine; + if (uci == null) + return; + switch (engineState.state) { + case READ_OPTIONS: { + if (readUCIOption(s)) { + if (!"cuckoochess".equals(engineState.engine)) + uci.setOption("Hash", 16); + uci.setOption("Ponder", false); + uci.writeLineToEngine("ucinewgame"); + uciEngine.writeLineToEngine("isready"); + engineState.state = MainState.WAIT_READY; + } + break; + } + case WAIT_READY: { + if ("readyok".equals(s)) { + engineState.state = MainState.IDLE; + handleIdleState(); + } + break; + } + case SEARCH: + case PONDER: + case ANALYZE: { + String[] tokens = tokenize(s); + if (tokens[0].equals("info")) { + parseInfoCmd(tokens); + } else if (tokens[0].equals("bestmove")) { + String bestMove = tokens[1]; + String nextPonderMoveStr = ""; + if ((tokens.length >= 4) && (tokens[2].equals("ponder"))) + nextPonderMoveStr = tokens[3]; + Move nextPonderMove = TextIO.UCIstringToMove(nextPonderMoveStr); + + if (engineState.state == MainState.SEARCH) + reportMove(bestMove, nextPonderMove); + + engineState.state = MainState.IDLE; + searchRequest = null; + handleIdleState(); + } + break; + } + case STOP_SEARCH: { + String[] tokens = tokenize(s); + if (tokens[0].equals("bestmove")) { + uciEngine.writeLineToEngine("isready"); + engineState.state = MainState.WAIT_READY; + } + break; + } + default: + } + } + + /** Handle reading of UCI options. Return true when finished. */ + private final boolean readUCIOption(String s) { + String[] tokens = tokenize(s); + if (tokens[0].equals("uciok")) + return true; + + if (tokens[0].equals("id")) { + if (tokens[1].equals("name")) { + engineName = ""; + for (int i = 2; i < tokens.length; i++) { + if (engineName.length() > 0) + engineName += " "; + engineName += tokens[i]; + } + listener.notifyEngineName(engineName); + } + } else if ((tokens.length > 2) && tokens[2].toLowerCase().equals("multipv")) { + try { + for (int i = 3; i < tokens.length; i++) { + if (tokens[i].equals("max") && (i+1 < tokens.length)) { + maxPV = Math.max(maxPV, Integer.parseInt(tokens[i+1])); + break; + } + } + } catch (NumberFormatException nfe) { } + } + return false; + } + + private final void reportMove(String bestMove, Move nextPonderMove) { + SearchRequest sr = searchRequest; + // Claim draw if appropriate if (statScore <= 0) { - String drawClaim = canClaimDraw(currPos, posHashList, posHashListSize, TextIO.UCIstringToMove(bestMove)); + String drawClaim = canClaimDraw(sr.currPos, sr.posHashList, sr.posHashListSize, + TextIO.UCIstringToMove(bestMove)); if (drawClaim != "") bestMove = drawClaim; } // Accept draw offer if engine is losing - if (drawOffer && !statIsMate && (statScore <= -300)) { + if (sr.drawOffer && !statIsMate && (statScore <= -300)) { bestMove = "draw accept"; } - listener.notifySearchResult(g, bestMove, nextPonderMove); - } - - public boolean shouldStop = false; - - /** Tell engine to stop searching. */ - public final synchronized void stopSearch() { - shouldStop = true; - if (uciEngine != null) - uciEngine.writeLineToEngine("stop"); - havePonderHit = false; - } - - /** Start analyzing a position. - * @param prevPos Position corresponding to last irreversible move. - * @param mList List of moves from prevPos to currPos. - * @param currPos Position to analyze. - * @param drawOffer True if other side have offered draw. - * @param engineThreads Number of threads to use, or 0 for default value. - * @param engine Chess engine to use for searching - * @param numPV Multi-PV mode. - */ - public final void analyze(Position prevPos, ArrayList<Move> mList, Position currPos, - boolean drawOffer, int engineThreads, - String engine, int numPV) { - setEngineStrength(engine, 1000); - setNumPV(numPV); - if (shouldStop) - return; - { - Pair<String, ArrayList<Move>> bi = getBookHints(currPos); - listener.notifyBookInfo(bi.first, bi.second); - } - - // If no legal moves, there is nothing to analyze - ArrayList<Move> moves = new MoveGen().pseudoLegalMoves(currPos); - moves = MoveGen.removeIllegal(currPos, moves); - if (moves.size() == 0) - return; - - StringBuilder posStr = new StringBuilder(); - posStr.append("position fen "); - posStr.append(TextIO.toFEN(prevPos)); - int nMoves = mList.size(); - if (nMoves > 0) { - posStr.append(" moves"); - for (int i = 0; i < nMoves; i++) { - posStr.append(" "); - posStr.append(TextIO.moveToUCIString(mList.get(i))); - } - } - maybeNewGame(); - uciEngine.writeLineToEngine(posStr.toString()); - uciEngine.setOption("UCI_AnalyseMode", true); - uciEngine.setOption("Threads", engineThreads > 0 ? engineThreads : numCPUs); - String goStr = String.format("go infinite"); - uciEngine.writeLineToEngine(goStr); - - monitorEngine(currPos, null); - } - - /** Set engine and engine strength. - * @param engine Name of engine. - * @param strength Engine strength, 0 - 1000. */ - private final synchronized void setEngineStrength(String engine, int strength) { - if (!engine.equals(this.engine)) { - shutdownEngine(); - this.engine = engine; - startEngine(); - } - this.strength = strength; - if (uciEngine != null) - uciEngine.setStrength(strength); - } - - /** Set engine multi-PV mode. */ - private final synchronized void setNumPV(int numPV) { - if ((uciEngine != null) && (maxPV > 1)) { - int num = Math.min(maxPV, numPV); - uciEngine.setOption("MultiPV", num); - } - } - - private final synchronized void startEngine() { - boolean useCuckoo = engine.equals("cuckoochess"); - if (uciEngine == null) { - if (useCuckoo) { - uciEngine = new CuckooChessEngine(); - } else { - uciEngine = new NativePipedProcess(); - } - uciEngine.initialize(); - uciEngine.writeLineToEngine("uci"); - readUCIOptions(); - int nThreads = getNumCPUs(); - if (nThreads > 8) nThreads = 8; - numCPUs = nThreads; - if (!useCuckoo) - uciEngine.setOption("Hash", 16); - uciEngine.setOption("Ponder", false); - uciEngine.writeLineToEngine("ucinewgame"); - syncReady(); - } - } - - /** Sends "ucinewgame" to engine if clearTT() has previously been called. */ - private final void maybeNewGame() { - if (newGame) { - newGame = false; - if (uciEngine != null) { - uciEngine.writeLineToEngine("ucinewgame"); - syncReady(); - } - } - } - - private static final int getNumCPUs() { - int nCPUsFromProc = 1; - try { - FileReader fr = new FileReader("/proc/stat"); - BufferedReader inBuf = new BufferedReader(fr, 8192); - String line; - int nCPUs = 0; - while ((line = inBuf.readLine()) != null) { - if ((line.length() >= 4) && line.startsWith("cpu") && Character.isDigit(line.charAt(3))) - nCPUs++; - } - inBuf.close(); - if (nCPUs < 1) nCPUs = 1; - nCPUsFromProc = nCPUs; - } catch (IOException e) { - } - int nCPUsFromOS = NativePipedProcess.getNPhysicalProcessors(); - return Math.max(nCPUsFromProc, nCPUsFromOS); - } - - private final void readUCIOptions() { - int timeout = 1000; - maxPV = 1; - while (true) { - String s = uciEngine.readLineFromEngine(timeout); - String[] tokens = tokenize(s); - if (tokens[0].equals("uciok")) - break; - else if (tokens[0].equals("id")) { - if (tokens[1].equals("name")) { - engineName = ""; - for (int i = 2; i < tokens.length; i++) { - if (engineName.length() > 0) - engineName += " "; - engineName += tokens[i]; - } - } - } else if ((tokens.length > 2) && tokens[2].toLowerCase().equals("multipv")) { - try { - for (int i = 3; i < tokens.length; i++) { - if (tokens[i].equals("max") && (i+1 < tokens.length)) { - maxPV = Math.max(maxPV, Integer.parseInt(tokens[i+1])); - break; - } - } - } catch (NumberFormatException nfe) { } - } - } + listener.notifySearchResult(sr.searchId, bestMove, nextPonderMove); } /** Convert a string to tokens by splitting at whitespace characters. */ @@ -388,59 +690,11 @@ public class DroidComputerPlayer { return cmdLine.split("\\s+"); } - private final void syncReady() { - uciEngine.writeLineToEngine("isready"); - while (true) { - String s = uciEngine.readLineFromEngine(1000); - if (s.equals("readyok")) - break; - } - } - - /** Wait for engine to respond with bestMove and ponderMove. - * While waiting, monitor and report search info. */ - private final Pair<String,String> monitorEngine(Position pos, Move ponderMove) { - // Monitor engine response - clearInfo(); - boolean stopSent = false; - while (true) { - int timeout = 2000; - while (true) { - UCIEngine uci = uciEngine; - if (uci == null) - break; - if (shouldStop && !stopSent) { - uci.writeLineToEngine("stop"); - stopSent = true; - } - String s = uci.readLineFromEngine(timeout); - if (s.length() == 0) - break; - String[] tokens = tokenize(s); - if (tokens[0].equals("info")) { - parseInfoCmd(tokens, ponderMove); - } else if (tokens[0].equals("bestmove")) { - String bestMove = tokens[1]; - String nextPonderMove = ""; - if ((tokens.length >= 4) && (tokens[2].equals("ponder"))) - nextPonderMove = tokens[3]; - return new Pair<String,String>(bestMove, nextPonderMove); - } - timeout = 0; - } - notifyGUI(pos, ponderMove); - try { - Thread.sleep(100); // 10 GUI updates per second is enough - } catch (InterruptedException e) { - } - } - } - /** Check if a draw claim is allowed, possibly after playing "move". * @param move The move that may have to be made before claiming draw. * @return The draw string that claims the draw, or empty string if draw claim not valid. */ - private final String canClaimDraw(Position pos, long[] posHashList, int posHashListSize, Move move) { + private final static String canClaimDraw(Position pos, long[] posHashList, int posHashListSize, Move move) { String drawStr = ""; if (canClaimDraw50(pos)) { drawStr = "draw 50"; @@ -509,7 +763,18 @@ public class DroidComputerPlayer { statPvInfo.clear(); } - private final void parseInfoCmd(String[] tokens, Move ponderMove) { + private final synchronized int getReadTimeout() { + boolean needGuiUpdate = depthModified || currMoveModified || pvModified || statsModified; + int timeout = 1000; + if (needGuiUpdate) { + long now = System.currentTimeMillis(); + timeout = (int)(lastGUIUpdate + guiUpdateInterval - now + 1); + timeout = Math.max(1, Math.min(1000, timeout)); + } + return timeout; + } + + private final void parseInfoCmd(String[] tokens) { try { boolean havePvData = false; int nTokens = tokens.length; @@ -567,8 +832,6 @@ public class DroidComputerPlayer { while (statPvInfo.size() <= pvNum) statPvInfo.add(null); ArrayList<Move> moves = new ArrayList<Move>(); - if (ponderMove != null) - moves.add(ponderMove); int nMoves = statPV.size(); for (i = 0; i < nMoves; i++) moves.add(TextIO.UCIstringToMove(statPV.get(i))); @@ -583,40 +846,64 @@ public class DroidComputerPlayer { } /** Notify GUI about search statistics. */ - private final synchronized void notifyGUI(Position pos, Move ponderMove) { + private final synchronized void notifyGUI() { + long now = System.currentTimeMillis(); + if (now < lastGUIUpdate + guiUpdateInterval) + return; + lastGUIUpdate = now; + + if (searchRequest == null) + return; + + int id = engineState.searchId; if (depthModified) { - listener.notifyDepth(statCurrDepth); + listener.notifyDepth(id, statCurrDepth); depthModified = false; } if (currMoveModified) { Move m = TextIO.UCIstringToMove(statCurrMove); - listener.notifyCurrMove(pos, m, statCurrMoveNr); + Position pos = searchRequest.currPos; + if ((searchRequest.ponderMove != null) && (m != null)) { + pos = new Position(pos); + UndoInfo ui = new UndoInfo(); + pos.makeMove(searchRequest.ponderMove, ui); + } + listener.notifyCurrMove(id, pos, m, statCurrMoveNr); currMoveModified = false; } if (pvModified) { - Position notifyPos = pos; - ArrayList<PvInfo> pvInfo = statPvInfo; - boolean isPonder = ponderMove != null; - if (isPonder && havePonderHit) { - isPonder = false; - - UndoInfo ui = new UndoInfo(); - notifyPos = new Position(pos); - notifyPos.makeMove(ponderMove, ui); - - pvInfo = new ArrayList<PvInfo>(statPvInfo.size()); - for (int i = 0; i < statPvInfo.size(); i++) { - PvInfo pvi = new PvInfo(statPvInfo.get(i)); - pvi.removeFirstMove(); - pvInfo.add(pvi); - } - } - listener.notifyPV(notifyPos, pvInfo, isPonder); + listener.notifyPV(id, searchRequest.currPos, statPvInfo, + searchRequest.ponderMove); pvModified = false; } if (statsModified) { - listener.notifyStats(statNodes, statNps, statTime); + listener.notifyStats(id, statNodes, statNps, statTime); statsModified = false; } } + + private static final int getNumCPUs() { + int nCPUsFromProc = 1; + try { + FileReader fr = new FileReader("/proc/stat"); + BufferedReader inBuf = new BufferedReader(fr, 8192); + String line; + int nCPUs = 0; + while ((line = inBuf.readLine()) != null) { + if ((line.length() >= 4) && line.startsWith("cpu") && Character.isDigit(line.charAt(3))) + nCPUs++; + } + inBuf.close(); + if (nCPUs < 1) nCPUs = 1; + nCPUsFromProc = nCPUs; + } catch (IOException e) { + } + int nCPUsFromOS = NativePipedProcess.getNPhysicalProcessors(); + return Math.max(nCPUsFromProc, nCPUsFromOS); + } + + private final static void myAssert(boolean b) { + if (!b) + throw new RuntimeException(); + } } diff --git a/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java b/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java index 03e44f8..f965505 100644 --- a/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java +++ b/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java @@ -35,6 +35,7 @@ public interface UCIEngine { */ public String readLineFromEngine(int timeoutMillis); + // FIXME!! Writes should be handled by separate thread. /** Write a line to the engine. \n will be added automatically. */ public void writeLineToEngine(String data); diff --git a/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java b/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java index 8bf5889..0cf8aca 100644 --- a/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java +++ b/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java @@ -49,6 +49,7 @@ public class CuckooChessEngine extends UCIEngineBase { private Pipe guiToEngine; private Pipe engineToGui; private NioInputStream inFromEngine; + private Thread engineThread; public CuckooChessEngine() { try { @@ -72,13 +73,14 @@ public class CuckooChessEngine extends UCIEngineBase { } protected final void startProcess() { - new Thread(new Runnable() { + engineThread = new Thread(new Runnable() { public void run() { NioInputStream in = new NioInputStream(guiToEngine); NioPrintStream out = new NioPrintStream(engineToGui); mainLoop(in, out); } - }).start(); + }); + engineThread.start(); } private final void mainLoop(NioInputStream is, NioPrintStream os) { @@ -93,6 +95,8 @@ public class CuckooChessEngine extends UCIEngineBase { @Override public final String readLineFromEngine(int timeoutMillis) { + if ((engineThread != null) && !engineThread.isAlive()) + return null; String ret = inFromEngine.readLine(timeoutMillis); if (ret == null) return null; diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java index b25b2e4..a6058fa 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java @@ -27,6 +27,8 @@ import org.petero.droidfish.GUIInterface; import org.petero.droidfish.GameMode; import org.petero.droidfish.PGNOptions; import org.petero.droidfish.engine.DroidComputerPlayer; +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; @@ -43,8 +45,6 @@ public class DroidChessController { private GUIInterface gui; private GameMode gameMode; private PGNOptions pgnOptions; - private Thread computerThread; - private Thread analysisThread; private String engine = ""; private int strength = 1000; @@ -60,30 +60,30 @@ public class DroidChessController { /** Partial move that needs promotion choice to be completed. */ private Move promoteMove; - private Object shutdownEngineLock = new Object(); - + private int searchId; /** 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) { - ss.searchResultWanted = false; - boolean updateGui = stopComputerThinking(); - updateGui |= stopAnalysis(); + boolean updateGui = abortSearch(); if (updateGui) updateGUI(); this.gameMode = gameMode; - ponderMove = null; if (computerPlayer == null) { - computerPlayer = new DroidComputerPlayer(engine, listener); + computerPlayer = new DroidComputerPlayer(listener); computerPlayer.setBookOptions(bookOptions); } + computerPlayer.queueStartEngine(searchId, engine); + searchId++; game = new Game(gameTextListener, timeControl, movesPerSession, timeIncrement); computerPlayer.clearTT(); setPlayerNames(game); @@ -92,7 +92,7 @@ public class DroidChessController { /** Start playing a new game. Should be called after newGame(). */ public final synchronized void startGame() { - updateComputeThreads(true); + updateComputeThreads(); setSelection(); updateGUI(); updateGameMode(); @@ -117,15 +117,13 @@ public class DroidChessController { public final synchronized void setGameMode(GameMode newMode) { if (!gameMode.equals(newMode)) { if (newMode.humansTurn(game.currPos().whiteMove)) - ss.searchResultWanted = false; + searchId++; gameMode = newMode; if (!gameMode.playerWhite() || !gameMode.playerBlack()) setPlayerNames(game); // If computer player involved, set player names updateGameMode(); - ponderMove = null; - ss.searchResultWanted = false; - stopComputerThinking(); - updateComputeThreads(true); + abortSearch(); + updateComputeThreads(); updateGUI(); } } @@ -136,12 +134,6 @@ public class DroidChessController { bookOptions = options; if (computerPlayer != null) { computerPlayer.setBookOptions(bookOptions); - if (analysisThread != null) { - boolean updateGui = stopAnalysis(); - updateGui |= startAnalysis(); - if (updateGui) - updateGUI(); - } updateBookHints(); } } @@ -157,9 +149,9 @@ public class DroidChessController { if (newEngine || (strength != this.strength)) { this.engine = engine; this.strength = strength; - if (newEngine && ((analysisThread != null) || (computerThread != null))) { - stopAnalysis(); - updateComputeThreads(true); + if (game != null) { + abortSearch(); + updateComputeThreads(); updateGUI(); } } @@ -169,7 +161,7 @@ public class DroidChessController { public final synchronized void prefsChanged() { updateBookHints(); updateMoveList(); - listener.prefsChanged(); + listener.prefsChanged(searchId); } /** De-serialize from byte array. */ @@ -204,29 +196,27 @@ public class DroidChessController { if (!newGame.readPGN(fenPgn, pgnOptions)) throw e; } - ss.searchResultWanted = false; + searchId++; game = newGame; - if (computerPlayer != null) - computerPlayer.clearTT(); gameTextListener.clear(); updateGameMode(); - stopAnalysis(); - stopComputerThinking(); + abortSearch(); computerPlayer.clearTT(); - ponderMove = null; - updateComputeThreads(true); + 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 (computerThread != null) || (analysisThread != null); + return (computerPlayer != null) && computerPlayer.computerBusy(); } /** Make a move for a human player. */ @@ -235,13 +225,12 @@ public class DroidChessController { Position oldPos = new Position(game.currPos()); if (doMove(m)) { if (m.equals(ponderMove) && !gameMode.analysisMode() && - (analysisThread == null) && (computerThread != null)) { - computerPlayer.ponderHit(oldPos, ponderMove); + (computerPlayer.getSearchType() == SearchType.PONDER)) { + computerPlayer.ponderHit(searchId); + ponderMove = null; } else { - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); - updateComputeThreads(true); + abortSearch(); + updateComputeThreads(); } setAnimMove(oldPos, m, true); updateGUI(); @@ -251,7 +240,7 @@ public class DroidChessController { } } - /** Report promotion choice for incomplete move. + /** 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) @@ -283,11 +272,8 @@ public class DroidChessController { if (humansTurn()) { int varNo = game.tree.addMove("--", "", 0, "", ""); game.tree.goForward(varNo); - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); - ponderMove = null; - updateComputeThreads(true); + abortSearch(); + updateComputeThreads(); updateGUI(); gui.setSelection(-1); } @@ -312,11 +298,9 @@ public class DroidChessController { /** Undo last move. Does not truncate game tree. */ public final synchronized void undoMove() { if (game.getLastMove() != null) { - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); + abortSearch(); boolean didUndo = undoMoveNoUpdate(); - updateComputeThreads(true); + updateComputeThreads(); setSelection(); if (didUndo) setAnimMove(game.currPos(), game.getNextMove(), false); @@ -327,11 +311,9 @@ public class DroidChessController { /** Redo last move. Follows default variation. */ public final synchronized void redoMove() { if (game.canRedoMove()) { - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); + abortSearch(); redoMoveNoUpdate(); - updateComputeThreads(true); + updateComputeThreads(); setSelection(); setAnimMove(game.prevPos(), game.getLastMove(), true); updateGUI(); @@ -359,11 +341,8 @@ public class DroidChessController { needUpdate = true; } if (needUpdate) { - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); - ponderMove = null; - updateComputeThreads(true); + abortSearch(); + updateComputeThreads(); setSelection(); updateGUI(); } @@ -380,11 +359,8 @@ public class DroidChessController { break; } if (needUpdate) { - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); - ponderMove = null; - updateComputeThreads(true); + abortSearch(); + updateComputeThreads(); setSelection(); updateGUI(); } @@ -396,7 +372,6 @@ public class DroidChessController { return; if (!game.goNode(node)) return; - ponderMove = null; if (!humansTurn()) { if (game.getLastMove() != null) { game.undoMove(); @@ -404,11 +379,8 @@ public class DroidChessController { game.redoMove(); } } - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); - ponderMove = null; - updateComputeThreads(true); + abortSearch(); + updateComputeThreads(); setSelection(); updateGUI(); } @@ -426,12 +398,9 @@ public class DroidChessController { /** Go to a new variation in the game tree. */ public final synchronized void changeVariation(int delta) { if (game.numVariations() > 1) { - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); + abortSearch(); game.changeVariation(delta); - ponderMove = null; - updateComputeThreads(true); + updateComputeThreads(); setSelection(); updateGUI(); } @@ -439,12 +408,9 @@ public class DroidChessController { /** Delete whole game sub-tree rooted at current position. */ public final synchronized void removeSubTree() { - ss.searchResultWanted = false; - stopAnalysis(); - stopComputerThinking(); + abortSearch(); game.removeSubTree(); - ponderMove = null; - updateComputeThreads(true); + updateComputeThreads(); setSelection(); updateGUI(); } @@ -459,7 +425,7 @@ public class DroidChessController { /** Add a variation to the game tree. * @param preComment Comment to add before first move. - * @param pvMoves List of moves in variation. + * @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++) { @@ -508,43 +474,34 @@ public class DroidChessController { if (numPV > maxPV()) numPV = maxPV(); if (numPV != this.numPV) { this.numPV = numPV; - if (analysisThread != null) { - stopAnalysis(); - ponderMove = null; - updateComputeThreads(true); - updateGUI(); - } + abortSearch(); + updateComputeThreads(); + updateGUI(); } } /** Request computer player to make a move immediately. */ public final synchronized void stopSearch() { - if (computerThread != null) { + if (!humansTurn() && (computerPlayer != null)) computerPlayer.stopSearch(); - } } /** Stop ponder search. */ public final synchronized void stopPonder() { - if ((computerThread != null) && humansTurn()) { - boolean updateGui = stopComputerThinking(); - if (updateGui) - updateGUI(); + 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() { - synchronized (shutdownEngineLock) { - gameMode = new GameMode(GameMode.TWO_PLAYERS); - ss.searchResultWanted = false; - boolean updateGui = stopComputerThinking(); - updateGui |= stopAnalysis(); - if (updateGui) - updateGUI(); - ponderMove = null; - computerPlayer.shutdownEngine(); - } + gameMode = new GameMode(GameMode.TWO_PLAYERS); + abortSearch(); + computerPlayer.shutdownEngine(); } /** Get PGN header tags and values. */ @@ -600,17 +557,19 @@ public class DroidChessController { private String bookInfo = ""; private List<Move> bookMoves = null; + private Move ponderMove = null; private ArrayList<PvInfo> pvInfoV = new ArrayList<PvInfo>(); - public final void clearSearchInfo() { + public final void clearSearchInfo(int id) { + ponderMove = null; pvInfoV.clear(); currDepth = 0; bookInfo = ""; bookMoves = null; - setSearchInfo(); + setSearchInfo(id); } - private final void setSearchInfo() { + private final void setSearchInfo(final int id) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < pvInfoV.size(); i++) { PvInfo pvi = pvInfoV.get(i); @@ -639,35 +598,42 @@ public class DroidChessController { : ""; final String newPV = buf.toString(); final String newBookInfo = bookInfo; - final SearchStatus localSS = ss; + 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); + } + } gui.runOnUIThread(new Runnable() { public void run() { - if (!localSS.searchResultWanted && (bookMoves != null)) - return; - ArrayList<ArrayList<Move>> pvMoves = new ArrayList<ArrayList<Move>>(); - for (int i = 0; i < pvInfoV.size(); i++) - pvMoves.add(pvInfoV.get(i).pv); - gui.setThinkingInfo(newPV, statStr, newBookInfo, pvMoves, bookMoves); + setThinkingInfo(id, pvMoves, newPV, statStr, newBookInfo, bookMoves); } }); } @Override - public void notifyDepth(int depth) { + public void notifyDepth(int id, int depth) { currDepth = depth; - setSearchInfo(); + setSearchInfo(id); } @Override - public void notifyCurrMove(Position pos, Move m, int moveNr) { + public void notifyCurrMove(int id, Position pos, Move m, int moveNr) { currMove = TextIO.moveToString(pos, m, false); currMoveNr = moveNr; - setSearchInfo(); + setSearchInfo(id); } @SuppressWarnings("unchecked") @Override - public void notifyPV(Position pos, ArrayList<PvInfo> pvInfo, boolean isPonder) { + public void notifyPV(int id, Position pos, ArrayList<PvInfo> pvInfo, Move ponderMove) { + this.ponderMove = ponderMove; pvInfoV = (ArrayList<PvInfo>) pvInfo.clone(); for (PvInfo pv : pvInfo) { currTime = pv.time; @@ -677,65 +643,85 @@ public class DroidChessController { StringBuilder buf = new StringBuilder(); Position tmpPos = new Position(pos); UndoInfo ui = new UndoInfo(); - boolean first = true; + if (ponderMove != null) { + String moveStr = TextIO.moveToString(tmpPos, ponderMove, false); + buf.append(String.format(" [%s]", moveStr)); + tmpPos.makeMove(ponderMove, ui); + } for (Move m : pv.pv) { String moveStr = TextIO.moveToString(tmpPos, m, false); - if (first && isPonder) { - buf.append(String.format(" [%s]", moveStr)); - first = false; - } else { - buf.append(String.format(" %s", moveStr)); - } + buf.append(String.format(" %s", moveStr)); tmpPos.makeMove(m, ui); } pv.pvStr = buf.toString(); } - whiteMove = pos.whiteMove ^ isPonder; + whiteMove = pos.whiteMove ^ (ponderMove != null); - setSearchInfo(); + setSearchInfo(id); } @Override - public void notifyStats(int nodes, int nps, int time) { + public void notifyStats(int id, int nodes, int nps, int time) { currNodes = nodes; currNps = nps; currTime = time; - setSearchInfo(); + setSearchInfo(id); } @Override - public void notifyBookInfo(String bookInfo, List<Move> moveList) { + public void notifyBookInfo(int id, String bookInfo, List<Move> moveList) { this.bookInfo = bookInfo; bookMoves = moveList; - setSearchInfo(); + setSearchInfo(id); } - public void prefsChanged() { - setSearchInfo(); + public void prefsChanged(int id) { + setSearchInfo(id); } @Override - public void notifySearchResult(Game g, String cmd, Move ponder) { - makeComputerMove(g, cmd, ponder); + 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); + } + }); + } + } + + /** 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 (gameMode != null) { - boolean analysis = gameMode.analysisMode(); - if (!analysis && humansTurn()) { - ss = new SearchStatus(); - Pair<String, ArrayList<Move>> bi = computerPlayer.getBookHints(game.currPos()); - listener.notifyBookInfo(bi.first, bi.second); - } + if (humansTurn()) { + Pair<String, ArrayList<Move>> bi = computerPlayer.getBookHints(game.currPos()); + listener.notifyBookInfo(searchId, bi.first, bi.second); } } - private final static class SearchStatus { - boolean searchResultWanted = true; - } - private SearchStatus ss = new SearchStatus(); - private final void updateGameMode() { if (game != null) { boolean gamePaused = !gameMode.clocksActive() || (humansTurn() && guiPaused); @@ -747,40 +733,98 @@ public class DroidChessController { } /** Start/stop computer thinking/analysis as appropriate. */ - private final void updateComputeThreads(boolean clearPV) { - boolean analysis = gameMode.analysisMode(); - boolean computersTurn = !humansTurn(); - boolean ponder = gui.ponderMode() && !analysis && !computersTurn && (ponderMove != null); - if (!analysis) - stopAnalysis(); - if (!(computersTurn || ponder)) - stopComputerThinking(); - if (clearPV) { - listener.clearSearchInfo(); - updateBookHints(); + 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, + gui.engineThreads(), numPV); + computerPlayer.queueAnalyzeRequest(sr); + } else if (computersTurn || ponder) { + listener.clearSearchInfo(searchId); + listener.notifyBookInfo(searchId, "", null); + final Pair<Position, ArrayList<Move>> ph = game.getUCIHistory(); + Position currPos = new Position(game.currPos()); + long now = System.currentTimeMillis(); + int wTime = game.timeController.getRemainingTime(true, now); + int bTime = game.timeController.getRemainingTime(false, now); + int inc = game.timeController.getIncrement(); + int movesToGo = game.timeController.getMovesToTC(); + if (ponder && !currPos.whiteMove && (movesToGo > 0)) { + movesToGo--; + if (movesToGo <= 0) + movesToGo += game.timeController.getMovesPerSession(); + } + final Move fPonderMove = ponder ? ponderMove : null; + SearchRequest sr = DroidComputerPlayer.SearchRequest.searchRequest( + searchId, now, ph.first, ph.second, currPos, + game.haveDrawOffer(), + wTime, bTime, inc, movesToGo, + gui.ponderMode(), fPonderMove, + engine, gui.engineThreads(), + strength); + computerPlayer.queueSearchRequest(sr); + } } - if (analysis) - startAnalysis(); - if (computersTurn || ponder) - startComputerThinking(ponder); + } + + private final synchronized void makeComputerMove(int id, final String cmd, final Move ponder) { + if (searchId != id) + return; + searchId++; + Position oldPos = new Position(game.currPos()); + game.processString(cmd); + ponderMove = ponder; + updateGameMode(); + gui.computerMoveMade(); + listener.clearSearchInfo(searchId); + updateComputeThreads(); + setSelection(); + setAnimMove(oldPos, game.getLastMove(), true); + updateGUI(); } private final void setPlayerNames(Game game) { if (game != null) { - String engine = (computerPlayer != null) ? computerPlayer.getEngineName() : - "Computer"; + String engine = "Computer"; + if (computerPlayer != null) { + engine = computerPlayer.getEngineName(); + if (strength < 1000) + engine += String.format(" (%.1f%%)", strength * 0.1); + } 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(" (%.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; - ss.searchResultWanted = false; + searchId++; game.undoMove(); - ponderMove = null; if (!humansTurn()) { if (game.getLastMove() != null) { game.undoMove(); @@ -802,9 +846,8 @@ public class DroidChessController { private final void redoMoveNoUpdate() { if (game.canRedoMove()) { - ss.searchResultWanted = false; + searchId++; game.redoMove(); - ponderMove = null; if (!humansTurn() && game.canRedoMove()) { game.redoMove(); if (!humansTurn()) @@ -846,13 +889,14 @@ public class DroidChessController { if (s.state == Game.GameState.ALIVE) { s.moveNr = game.currPos().fullMoveCounter; s.white = game.currPos().whiteMove; - if (computerThread != null) - if (humansTurn()) - s.ponder = true; - else - s.thinking = true; - if (analysisThread != null) - s.analyzing = true; + 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; + } } else { if ((s.state == GameState.DRAW_REP) || (s.state == GameState.DRAW_50)) s.drawInfo = game.getDrawInfo(); @@ -880,6 +924,12 @@ public class DroidChessController { updateRemainingTime(); } + private final synchronized void setThinkingInfo(int id, ArrayList<ArrayList<Move>> pvMoves, String pvStr, + String statStr, String bookInfo, List<Move> bookMoves) { + if (id == searchId) + gui.setThinkingInfo(pvStr, statStr, bookInfo, pvMoves, bookMoves); + } + private final void updateMoveList() { if (game == null) return; @@ -909,120 +959,6 @@ public class DroidChessController { gui.setAnimMove(sourcePos, move, forward); } - private final synchronized void startComputerThinking(boolean ponder) { - if (analysisThread != null) return; - if (game.getGameState() != GameState.ALIVE) return; - if (computerThread == null) { - ss = new SearchStatus(); - final Pair<Position, ArrayList<Move>> ph = game.getUCIHistory(); - final Game g = game; - final boolean haveDrawOffer = g.haveDrawOffer(); - final Position currPos = new Position(g.currPos()); - long now = System.currentTimeMillis(); - final int wTime = game.timeController.getRemainingTime(true, now); - final int bTime = game.timeController.getRemainingTime(false, now); - final int inc = game.timeController.getIncrement(); - int movesToGo = game.timeController.getMovesToTC(); - if (ponder && !currPos.whiteMove && (movesToGo > 0)) { - movesToGo--; - if (movesToGo <= 0) - movesToGo += game.timeController.getMovesPerSession(); - } - final int fMovesToGo = movesToGo; - final Move fPonderMove = ponder ? ponderMove : null; - computerThread = new Thread(new Runnable() { - public void run() { - computerPlayer.doSearch(ph.first, ph.second, currPos, haveDrawOffer, - wTime, bTime, inc, fMovesToGo, - gui.ponderMode(), fPonderMove, - gui.engineThreads(), - engine, strength, g); - } - }); - listener.clearSearchInfo(); - computerPlayer.shouldStop = false; - computerThread.start(); - } - } - - private final void makeComputerMove(final Game g, final String cmd, final Move ponder) { - final SearchStatus localSS = ss; - gui.runOnUIThread(new Runnable() { - public void run() { - synchronized (shutdownEngineLock) { - if (!localSS.searchResultWanted) - return; - Position oldPos = new Position(g.currPos()); - g.processString(cmd); - ponderMove = ponder; - updateGameMode(); - gui.computerMoveMade(); - listener.clearSearchInfo(); - stopComputerThinking(); - stopAnalysis(); // To force analysis to restart for new position - updateComputeThreads(true); - setSelection(); - setAnimMove(oldPos, g.getLastMove(), true); - updateGUI(); - } - } - }); - } - - private final synchronized boolean stopComputerThinking() { - if (computerThread != null) { - computerPlayer.stopSearch(); - try { - computerThread.join(); - } catch (InterruptedException ex) { - System.out.printf("Could not stop computer thread%n"); - } - computerThread = null; - return true; - } - return false; - } - - private final synchronized boolean startAnalysis() { - if (gameMode.analysisMode()) { - if (computerThread != null) return false; - if (analysisThread == null) { - ss = new SearchStatus(); - final Pair<Position, ArrayList<Move>> ph = game.getUCIHistory(); - final boolean haveDrawOffer = game.haveDrawOffer(); - final Position currPos = new Position(game.currPos()); - final boolean alive = game.tree.getGameState() == GameState.ALIVE; - analysisThread = new Thread(new Runnable() { - public void run() { - if (alive) - computerPlayer.analyze(ph.first, ph.second, currPos, haveDrawOffer, - gui.engineThreads(), engine, numPV); - } - }); - listener.clearSearchInfo(); - computerPlayer.shouldStop = false; - analysisThread.start(); - return true; - } - } - return false; - } - - private final synchronized boolean stopAnalysis() { - if (analysisThread != null) { - computerPlayer.stopSearch(); - try { - analysisThread.join(); - } catch (InterruptedException ex) { - System.out.printf("Could not stop analysis thread%n"); - } - analysisThread = null; - listener.clearSearchInfo(); - return true; - } - return false; - } - private final boolean findValidDrawClaim() { if (game.getGameState() != GameState.ALIVE) return true; game.processString("draw accept"); diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java b/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java index 7ee14bd..c30b786 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java @@ -65,18 +65,29 @@ public interface SearchListener { this.lowerBound = lowerBound; this.pv = pv; } - - public final void removeFirstMove() { - if (!pv.isEmpty()) - pv.remove(0); - } } - public void notifyDepth(int depth); - public void notifyCurrMove(Position pos, Move m, int moveNr); - public void notifyPV(Position pos, ArrayList<PvInfo> pvInfo, boolean isPonder); - public void notifyStats(int nodes, int nps, int time); - public void notifyBookInfo(String bookInfo, List<Move> moveList); + /** Report current engine search depth. */ + public void notifyDepth(int id, int depth); - public void notifySearchResult(Game g, String cmd, Move ponder); + /** Report the move, valid in position pos, that the engine is currently searching. */ + public void notifyCurrMove(int id, Position pos, Move m, int moveNr); + + /** + * Report PV information. If ponderMove is non-null, ponderMove is the first move + * to play from position pos. + */ + public void notifyPV(int id, Position pos, ArrayList<PvInfo> pvInfo, Move ponderMove); + + /** Report search statistics. */ + public void notifyStats(int id, int nodes, int nps, int time); + + /** Report opening book information. */ + public void notifyBookInfo(int id, String bookInfo, List<Move> moveList); + + /** Report move (or command, such as "resign") played by the engine. */ + public void notifySearchResult(int id, String cmd, Move ponder); + + /** Report engine name. */ + public void notifyEngineName(String engineName); }