DroidFish: Added support for running the chess engine on a different computer, communicating with it using a network socket.

This commit is contained in:
Peter Osterlund 2012-08-05 18:58:28 +00:00
parent 04f1225d3e
commit a7e21224b8
8 changed files with 310 additions and 6 deletions

View File

@ -7,6 +7,7 @@
<supports-screens android:largeScreens="true"
android:anyDensity="true" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-sdk android:minSdkVersion="3"

View File

@ -177,9 +177,12 @@ you are not actively using the program.\
<string name="engine_error">Engine error</string>
<string name="stockfish_engine">Stockfish</string>
<string name="cuckoochess_engine">CuckooChess</string>
<string name="network_engine">Network Engine</string>
<string name="failed_to_start_engine">Failed to start engine</string>
<string name="engine_terminated">Engine terminated</string>
<string name="uci_protocol_error">UCI protocol error</string>
<string name="network_host_syntax_error">Network host:port syntax error</string>
<string name="invalid_network_port">Invalid network port</string>
<string name="start_new_game">Start New Game?</string>
<string name="strength_cuckoo_hint">Use the CuckooChess engine for even lower strength.</string>
<string name="err_too_few_spaces">Too few spaces</string>
@ -223,6 +226,8 @@ you are not actively using the program.\
<string name="prefs_threads_summary">Number of engine threads (CPU cores) to use. Not supported by all engines.</string>
<string name="prefs_hash_title">Hash Table</string>
<string name="prefs_hash_summary">Hash table size in megabytes</string>
<string name="prefs_networkEngine_title">Network engine</string>
<string name="prefs_networkEngine_summary">Enter host:port to connect to an engine on a computer on the network</string>
<string name="prefs_time_control">Time Control</string>
<string name="prefs_movesPerSession_title">Moves</string>
<string name="prefs_movesPerSession_summary">Number of moves between time controls</string>

View File

@ -52,6 +52,12 @@
android:entries="@array/engine_hash_texts"
android:defaultValue="@string/engine_hash_default">
</ListPreference>
<EditTextPreference
android:key="networkEngine"
android:title="@string/prefs_networkEngine_title"
android:summary="@string/prefs_networkEngine_summary"
android:defaultValue="">
</EditTextPreference>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/prefs_time_control">

View File

@ -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++) {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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

View File

@ -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)

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -33,13 +33,16 @@ public abstract class UCIEngineBase implements UCIEngine {
private HashMap<String, String> 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);
}