diff --git a/DroidFish/AndroidManifest.xml b/DroidFish/AndroidManifest.xml index 9645163..8f57774 100644 --- a/DroidFish/AndroidManifest.xml +++ b/DroidFish/AndroidManifest.xml @@ -7,6 +7,7 @@ + Engine error Stockfish CuckooChess + Network Engine Failed to start engine Engine terminated UCI protocol error + Network host:port syntax error + Invalid network port Start New Game? Use the CuckooChess engine for even lower strength. Too few spaces @@ -223,6 +226,8 @@ you are not actively using the program.\ Number of engine threads (CPU cores) to use. Not supported by all engines. Hash Table Hash table size in megabytes + Network engine + Enter host:port to connect to an engine on a computer on the network Time Control Moves Number of moves between time controls diff --git a/DroidFish/res/xml/preferences.xml b/DroidFish/res/xml/preferences.xml index da8bf6e..c9550cc 100644 --- a/DroidFish/res/xml/preferences.xml +++ b/DroidFish/res/xml/preferences.xml @@ -52,6 +52,12 @@ android:entries="@array/engine_hash_texts" android:defaultValue="@string/engine_hash_default"> + + diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java index dee9ccf..0e0f550 100644 --- a/DroidFish/src/org/petero/droidfish/DroidFish.java +++ b/DroidFish/src/org/petero/droidfish/DroidFish.java @@ -150,6 +150,10 @@ public class DroidFish extends Activity implements GUIInterface { // FIXME!!! Handle PGN non-file intents with more than one game. // FIXME!!! File load/save of FEN data + // FIXME!!! Strength setting for external engines + // FIXME!!! Selection dialog for going into variation + // FIXME!!! Use two engines in engine/engine games + private ChessBoard cb; private static DroidChessController ctrl = null; private boolean mShowThinking; @@ -743,6 +747,7 @@ public class DroidFish extends Activity implements GUIInterface { String engine = settings.getString("engine", "stockfish"); int strength = settings.getInt("strength", 1000); + engineOptions.networkEngine = settings.getString("networkEngine", "").trim(); setEngineStrength(engine, strength); mPonderMode = settings.getBoolean("ponderMode", false); @@ -787,8 +792,7 @@ public class DroidFish extends Activity implements GUIInterface { engineOptions.hintsEdit = settings.getBoolean("tbHintsEdit", false); engineOptions.rootProbe = settings.getBoolean("tbRootProbe", true); engineOptions.engineProbe = settings.getBoolean("tbEngineProbe", true); - String gtbPath = settings.getString("gtbPath", ""); - gtbPath = gtbPath.trim(); + String gtbPath = settings.getString("gtbPath", "").trim(); if (gtbPath.length() == 0) { File extDir = Environment.getExternalStorageDirectory(); String sep = File.separator; @@ -897,6 +901,8 @@ public class DroidFish extends Activity implements GUIInterface { int idx = engine.lastIndexOf('/'); String eName = engine.substring(idx + 1); engineTitleText.setText(eName); + } else if (engine.equals("networkEngine")) { + engineTitleText.setText(engineOptions.networkEngine); } else { String eName = getString(engine.equals("cuckoochess") ? R.string.cuckoochess_engine : @@ -1676,7 +1682,8 @@ public class DroidFish extends Activity implements GUIInterface { String[] fileNames = findFilesInDirectory(engineDir, null); final int numFiles = fileNames.length; boolean haveSf = EngineUtil.internalStockFishName() != null; - final int nEngines = numFiles + (haveSf ? 2 : 1); + boolean haveNet = engineOptions.networkEngine.length() > 0; + final int nEngines = numFiles + 1 + (haveSf ? 1 : 0) + (haveNet ? 1 : 0); final String[] items = new String[nEngines]; final String[] ids = new String[nEngines]; int idx = 0; @@ -1684,6 +1691,9 @@ public class DroidFish extends Activity implements GUIInterface { ids[idx] = "stockfish"; items[idx] = getString(R.string.stockfish_engine); idx++; } ids[idx] = "cuckoochess"; items[idx] = getString(R.string.cuckoochess_engine); idx++; + if (haveNet) { + ids[idx] = "networkEngine"; items[idx] = getString(R.string.network_engine); idx++; + } String sep = File.separator; String base = Environment.getExternalStorageDirectory() + sep + engineDir + sep; for (int i = 0; i < numFiles; i++) { diff --git a/DroidFish/src/org/petero/droidfish/EngineOptions.java b/DroidFish/src/org/petero/droidfish/EngineOptions.java index d57d2f6..008f202 100644 --- a/DroidFish/src/org/petero/droidfish/EngineOptions.java +++ b/DroidFish/src/org/petero/droidfish/EngineOptions.java @@ -1,3 +1,21 @@ +/* + DroidFish - An Android chess program. + Copyright (C) 2012 Peter Ă–sterlund, peterosterlund2@gmail.com + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + package org.petero.droidfish; /** Engine options, including endgame tablebase probing options. */ @@ -8,6 +26,7 @@ public final class EngineOptions { public boolean rootProbe; // Only search optimal moves at root public boolean engineProbe; // Let engine use EGTB public String gtbPath; // GTB directory path + public String networkEngine;// Host:port for network engine public EngineOptions() { hashMB = 16; @@ -16,6 +35,7 @@ public final class EngineOptions { rootProbe = false; engineProbe = false; gtbPath = ""; + networkEngine = ""; } public EngineOptions(EngineOptions other) { @@ -25,6 +45,7 @@ public final class EngineOptions { rootProbe = other.rootProbe; engineProbe = other.engineProbe; gtbPath = other.gtbPath; + networkEngine = other.networkEngine; } @Override @@ -38,7 +59,8 @@ public final class EngineOptions { (hintsEdit == other.hintsEdit) && (rootProbe == other.rootProbe) && (engineProbe == other.engineProbe) && - gtbPath.equals(other.gtbPath)); + gtbPath.equals(other.gtbPath) && + networkEngine.equals(other.networkEngine)); } @Override diff --git a/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java b/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java index 7a4ac11..2d2b078 100644 --- a/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java +++ b/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java @@ -602,7 +602,9 @@ public class DroidComputerPlayer { myAssert(searchRequest != null); engineName = "Computer"; - uciEngine = UCIEngineBase.getEngine(context, searchRequest.engine, new UCIEngine.Report() { + uciEngine = UCIEngineBase.getEngine(context, searchRequest.engine, + engineOptions, + new UCIEngine.Report() { @Override public void reportError(String errMsg) { if (errMsg == null) diff --git a/DroidFish/src/org/petero/droidfish/engine/NetworkEngine.java b/DroidFish/src/org/petero/droidfish/engine/NetworkEngine.java new file mode 100644 index 0000000..861102f --- /dev/null +++ b/DroidFish/src/org/petero/droidfish/engine/NetworkEngine.java @@ -0,0 +1,255 @@ +/* + DroidFish - An Android chess program. + Copyright (C) 2012 Peter Ă–sterlund, peterosterlund2@gmail.com + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package org.petero.droidfish.engine; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.UnknownHostException; + +import org.petero.droidfish.EngineOptions; +import org.petero.droidfish.R; +import android.content.Context; + +/** Engine running on a different computer. */ +public class NetworkEngine extends UCIEngineBase { + protected final Context context; + private final Report report; + + private String networkEngine; + private Socket socket; + private Thread startupThread; + private Thread stdInThread; + private Thread stdOutThread; + private LocalPipe guiToEngine; + private LocalPipe engineToGui; + private boolean startedOk; + private boolean isRunning; + private boolean isError; + + public NetworkEngine(Context context, String engine, EngineOptions engineOptions, + Report report) { + this.context = context; + this.report = report; + this.networkEngine = engineOptions.networkEngine; + startupThread = null; + stdInThread = null; + guiToEngine = new LocalPipe(); + engineToGui = new LocalPipe(); + startedOk = false; + isRunning = false; + isError = false; + } + + /** Create socket connection to remote server. */ + private final synchronized void connect() { + if (socket == null) { + int idx = networkEngine.lastIndexOf(':'); + if (idx < 0) { + isError = true; + report.reportError(context.getString(R.string.network_host_syntax_error)); + } else { + try { + String host = networkEngine.substring(0, idx); + String port = networkEngine.substring(idx+1); + int portNr = Integer.parseInt(port); + socket = new Socket(host, portNr); + socket.setTcpNoDelay(true); + } catch (UnknownHostException e) { + isError = true; + report.reportError(e.getMessage()); + } catch (NumberFormatException nfe) { + isError = true; + report.reportError(context.getString(R.string.invalid_network_port)); + } catch (IOException e) { + isError = true; + report.reportError(e.getMessage()); + } + } + if (socket == null) + socket = new Socket(); + } + } + + /** @inheritDoc */ + @Override + protected void startProcess() { + // Start thread to check for startup error + startupThread = new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + return; + } + if (startedOk && isRunning && !isUCI) { + isError = true; + report.reportError(context.getString(R.string.uci_protocol_error)); + } + } + }); + startupThread.start(); + + // Start a thread to read data from engine + stdInThread = new Thread(new Runnable() { + @Override + public void run() { + connect(); + try { + InputStream is = socket.getInputStream(); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr, 8192); + String line; + boolean first = true; + while ((line = br.readLine()) != null) { + if (Thread.currentThread().isInterrupted()) + return; + synchronized (engineToGui) { + engineToGui.addLine(line); + if (first) { + startedOk = true; + isRunning = true; + first = false; + } + } + } + } catch (IOException e) { + } finally { + if (isRunning) { + isError = true; + isRunning = false; + if (!startedOk) + report.reportError(context.getString(R.string.failed_to_start_engine)); + else + report.reportError(context.getString(R.string.engine_terminated)); + } + try { socket.close(); } catch (IOException e) {} + } + engineToGui.close(); + } + }); + stdInThread.start(); + + // Start a thread to write data to engine + stdOutThread = new Thread(new Runnable() { + @Override + public void run() { + try { + connect(); + String line; + while ((line = guiToEngine.readLine()) != null) { + if (Thread.currentThread().isInterrupted()) + return; + line += "\n"; + socket.getOutputStream().write(line.getBytes()); + } + } catch (IOException e) { + if (isRunning) { + isError = true; + report.reportError(e.getMessage()); + } + } finally { + if (isRunning && !isError) { + isError = true; + report.reportError(context.getString(R.string.engine_terminated)); + } + isRunning = false; + try { socket.close(); } catch (IOException ex) {} + } + } + }); + stdOutThread.start(); + } + + private int hashMB = -1; + private String gaviotaTbPath = ""; + private boolean optionsInitialized = false; + + /** @inheritDoc */ + @Override + public void initOptions(EngineOptions engineOptions) { + super.initOptions(engineOptions); + hashMB = engineOptions.hashMB; + setOption("Hash", engineOptions.hashMB); + if (engineOptions.engineProbe) { + gaviotaTbPath = engineOptions.gtbPath; + setOption("GaviotaTbPath", engineOptions.gtbPath); + setOption("GaviotaTbCache", 8); + } + optionsInitialized = true; + } + + /** @inheritDoc */ + @Override + public boolean optionsOk(EngineOptions engineOptions) { + if (isError) + return false; + if (!optionsInitialized) + return true; + if (!networkEngine.equals(engineOptions.networkEngine)) + return false; + if (hashMB != engineOptions.hashMB) + return false; + if (haveOption("gaviotatbpath") && !gaviotaTbPath.equals(engineOptions.gtbPath)) + return false; + return true; + } + + /** @inheritDoc */ + @Override + public void setStrength(int strength) { + } + + /** @inheritDoc */ + @Override + public String readLineFromEngine(int timeoutMillis) { + String ret = engineToGui.readLine(timeoutMillis); + if (ret == null) + return null; + if (ret.length() > 0) { +// System.out.printf("Engine -> GUI: %s\n", ret); + } + return ret; + } + + /** @inheritDoc */ + @Override + public void writeLineToEngine(String data) { +// System.out.printf("GUI -> Engine: %s\n", data); + guiToEngine.addLine(data); + } + + /** @inheritDoc */ + @Override + public void shutDown() { + isRunning = false; + if (startupThread != null) + startupThread.interrupt(); + try { socket.getOutputStream().write("quit\n".getBytes()); } catch (IOException e) {} + try { socket.close(); } catch (IOException e) {} + super.shutDown(); + if (stdOutThread != null) + stdOutThread.interrupt(); + if (stdInThread != null) + stdInThread.interrupt(); + } +} diff --git a/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java b/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java index 0bce0ff..175b7c1 100644 --- a/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java +++ b/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java @@ -33,13 +33,16 @@ public abstract class UCIEngineBase implements UCIEngine { private HashMap currOptions; protected boolean isUCI; - public static UCIEngine getEngine(Context context, String engine, Report report) { + public static UCIEngine getEngine(Context context, String engine, + EngineOptions engineOptions, Report report) { if ("stockfish".equals(engine) && (EngineUtil.internalStockFishName() == null)) engine = "cuckoochess"; if ("cuckoochess".equals(engine)) return new CuckooChessEngine(report); else if ("stockfish".equals(engine)) return new InternalStockFish(context, report); + else if ("networkEngine".equals(engine)) + return new NetworkEngine(context, engine, engineOptions, report); else return new ExternalEngine(context, engine, report); }