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);
 }