DroidFish: Implemented support for external UCI engines.

This commit is contained in:
Peter Osterlund 2012-01-01 22:36:25 +00:00
parent a1fdce1942
commit c653ef8ec2
14 changed files with 364 additions and 83 deletions

View File

@ -44,6 +44,10 @@
android:title="@string/option_select_book">
</item>
<item
android:id="@+id/select_engine"
android:title="@string/option_select_engine">
</item>
<item
android:id="@+id/set_color_theme"
android:title="@string/option_color_theme">
</item>

View File

@ -1,14 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DroidFish</string>
<string-array name="engine_texts">
<item>Stockfish</item>
<item>CuckooChess</item>
</string-array>
<string-array name="engine_values">
<item>stockfish</item>
<item>cuckoochess</item>
</string-array>
<string-array name="engine_threads_texts">
<item>Automatisch</item>
<item>1</item>
@ -414,8 +407,6 @@ wenn Sie es nicht aktiv nutzen.\
<string name="prefs_autoSwapSides_title">Automatischer Seitenwechsel</string>
<string name="prefs_autoSwapSides_summary">Seiten beim Start einer neuen Partie automatisch wechseln (Einstellung <i>Ansicht drehen</i> ignorieren)</string>
<string name="prefs_engine_settings">Engine-Einstellungen</string>
<string name="prefs_engine_title">Schach-Engine</string>
<string name="prefs_engine_summary">Auswahl der Schach-Engine</string>
<string name="prefs_strength_title">Spielstärke</string>
<string name="prefs_ponderMode_title">Vorausberechnung</string>
<string name="prefs_ponderMode_summary">Vorausberechnung von Zügen durch die Engine, wenn der Spieler am Zug ist</string>

View File

@ -1,15 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DroidFish</string>
<string-array name="engine_texts">
<item>Stockfish</item>
<item>CuckooChess</item>
</string-array>
<string-array name="engine_values">
<item>stockfish</item>
<item>cuckoochess</item>
</string-array>
<string name="engine_default">stockfish</string>
<string-array name="engine_threads_texts">
<item>Automatic</item>
@ -305,6 +296,7 @@ you are not actively using the program.\
<string name="edit_move_counters">Edit Move Counters</string>
<string name="internal_book">&lt;Internal Book&gt;</string>
<string name="select_opening_book_file">Select opening book file</string>
<string name="select_chess_engine">Select Chess Engine</string>
<string name="select_pgn_file">Select PGN file to open</string>
<string name="select_pgn_file_save">Save to PGN file</string>
<string name="select_scid_file">Select Scid file to open</string>
@ -401,6 +393,9 @@ you are not actively using the program.\
<string name="after_selected">After Selected</string>
<string name="replace_selected">Replace Selected</string>
<string name="engine">Engine</string>
<string name="engine_error">Engine error</string>
<string name="stockfish_engine">Stockfish</string>
<string name="cuckoochess_engine">CuckooChess</string>
<string name="err_too_few_spaces">Too few spaces</string>
<string name="err_invalid_piece">Invalid piece</string>
@ -425,6 +420,7 @@ you are not actively using the program.\
<string name="option_force_computer_move">Force Computer Move</string>
<string name="option_draw">Claim/Offer/Accept Draw</string>
<string name="option_select_book">Select Opening Book</string>
<string name="option_select_engine">Select Chess Engine</string>
<string name="option_color_theme">Set Color Theme</string>
<string name="option_about">About / Help</string>
@ -434,8 +430,6 @@ you are not actively using the program.\
<string name="prefs_autoSwapSides_title">Auto Swap Sides</string>
<string name="prefs_autoSwapSides_summary">Automatically swap sides when new game started. Also overrides Flip Board setting</string>
<string name="prefs_engine_settings">Engine Settings</string>
<string name="prefs_engine_title">Engine</string>
<string name="prefs_engine_summary">Chess Engine</string>
<string name="prefs_strength_title">Strength</string>
<string name="prefs_ponderMode_title">Pondering</string>
<string name="prefs_ponderMode_summary">Let engine think while waiting for opponent\'s move</string>

View File

@ -18,14 +18,6 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/prefs_engine_settings">
<ListPreference
android:key="engine"
android:title="@string/prefs_engine_title"
android:summary="@string/prefs_engine_summary"
android:entryValues="@array/engine_values"
android:entries="@array/engine_texts"
android:defaultValue="@string/engine_default">
</ListPreference>
<org.petero.droidfish.SeekBarPreference
android:key="strength"
android:defaultValue="1000"

View File

@ -160,6 +160,7 @@ public class DroidFish extends Activity implements GUIInterface {
private final static String bookDir = "DroidFish";
private final static String pgnDir = "DroidFish" + File.separator + "pgn";
private final static String engineDir = "DroidFish" + File.separator + "uci";
private BookOptions bookOptions = new BookOptions();
private PGNOptions pgnOptions = new PGNOptions();
@ -732,6 +733,10 @@ public class DroidFish extends Activity implements GUIInterface {
removeDialog(SELECT_BOOK_DIALOG);
showDialog(SELECT_BOOK_DIALOG);
return true;
case R.id.select_engine:
removeDialog(SELECT_ENGINE_DIALOG);
showDialog(SELECT_ENGINE_DIALOG);
return true;
case R.id.set_color_theme:
showDialog(SET_COLOR_THEME_DIALOG);
return true;
@ -1004,16 +1009,17 @@ public class DroidFish extends Activity implements GUIInterface {
static private final int ABOUT_DIALOG = 2;
static private final int SELECT_MOVE_DIALOG = 3;
static private final int SELECT_BOOK_DIALOG = 4;
static private final int SELECT_PGN_FILE_DIALOG = 5;
static private final int SELECT_PGN_FILE_SAVE_DIALOG = 6;
static private final int SET_COLOR_THEME_DIALOG = 7;
static private final int GAME_MODE_DIALOG = 8;
static private final int SELECT_PGN_SAVE_NEWFILE_DIALOG = 9;
static private final int MOVELIST_MENU_DIALOG = 10;
static private final int THINKING_MENU_DIALOG = 11;
static private final int GO_BACK_MENU_DIALOG = 12;
static private final int GO_FORWARD_MENU_DIALOG = 13;
static private final int FILE_MENU_DIALOG = 14;
static private final int SELECT_ENGINE_DIALOG = 5;
static private final int SELECT_PGN_FILE_DIALOG = 6;
static private final int SELECT_PGN_FILE_SAVE_DIALOG = 7;
static private final int SET_COLOR_THEME_DIALOG = 8;
static private final int GAME_MODE_DIALOG = 9;
static private final int SELECT_PGN_SAVE_NEWFILE_DIALOG = 10;
static private final int MOVELIST_MENU_DIALOG = 11;
static private final int THINKING_MENU_DIALOG = 12;
static private final int GO_BACK_MENU_DIALOG = 13;
static private final int GO_FORWARD_MENU_DIALOG = 14;
static private final int FILE_MENU_DIALOG = 15;
@Override
protected Dialog onCreateDialog(int id) {
@ -1225,6 +1231,45 @@ public class DroidFish extends Activity implements GUIInterface {
AlertDialog alert = builder.create();
return alert;
}
case SELECT_ENGINE_DIALOG: {
String[] fileNames = findFilesInDirectory(engineDir, null);
final int numFiles = fileNames.length;
final String[] items = new String[numFiles + 2];
final String[] ids = new String[numFiles + 2];
ids[0] = "stockfish"; items[0] = getString(R.string.stockfish_engine);
ids[1] = "cuckoochess"; items[1] = getString(R.string.cuckoochess_engine);
String sep = File.separator;
String base = Environment.getExternalStorageDirectory() + sep + engineDir + sep;
for (int i = 0; i < numFiles; i++) {
ids[i+2] = base + fileNames[i];
items[i+2] = fileNames[i];
}
String currEngine = ctrl.getEngine();
int defaultItem = 0;
for (int i = 0; i < ids.length; i++) {
if (ids[i].equals(currEngine)) {
defaultItem = i;
break;
}
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.select_chess_engine);
builder.setSingleChoiceItems(items, defaultItem, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
if ((item < 0) || (item >= ids.length))
return;
Editor editor = settings.edit();
String engine = ids[item];
editor.putString("engine", engine);
editor.commit();
dialog.dismiss();
int strength = settings.getInt("strength", 1000);
setEngineStrength(engine, strength);
}
});
AlertDialog alert = builder.create();
return alert;
}
case SELECT_PGN_FILE_DIALOG: {
final String[] fileNames = findFilesInDirectory(pgnDir, null);
final int numFiles = fileNames.length;
@ -1773,6 +1818,13 @@ public class DroidFish extends Activity implements GUIInterface {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
@Override
public void reportEngineError(String errMsg) {
String msg = String.format("%s: %s",
getString(R.string.engine_error), errMsg);
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
}
@Override
public void computerMoveMade() {
if (soundEnabled) {

View File

@ -67,6 +67,9 @@ public interface GUIInterface {
/** Report UCI engine name. */
public void reportEngineName(String engine);
/** Report UCI engine error message. */
public void reportEngineError(String errMsg);
/** Called when computer made a move. GUI can notify user, for example by playing a sound. */
public void computerMoveMade();

View File

@ -25,7 +25,6 @@ import java.util.ArrayList;
import org.petero.droidfish.book.BookOptions;
import org.petero.droidfish.book.DroidBook;
import org.petero.droidfish.engine.cuckoochess.CuckooChessEngine;
import org.petero.droidfish.gamelogic.Move;
import org.petero.droidfish.gamelogic.MoveGen;
import org.petero.droidfish.gamelogic.Pair;
@ -292,10 +291,10 @@ public class DroidComputerPlayer {
/** Stop the engine process. */
public final synchronized void shutdownEngine() {
if (uciEngine != null) {
uciEngine.shutDown();
uciEngine = null;
engineMonitor.interrupt();
engineMonitor = null;
uciEngine.shutDown();
uciEngine = null;
}
engineState.state = MainState.DEAD;
}
@ -543,15 +542,20 @@ public class DroidComputerPlayer {
myAssert(searchRequest != null);
engineName = "Computer";
if ("cuckoochess".equals(searchRequest.engine))
uciEngine = new CuckooChessEngine();
else
uciEngine = new StockFishJNI();
uciEngine = UCIEngineBase.getEngine(searchRequest.engine, new UCIEngine.Report() {
@Override
public void reportError(String errMsg) {
if (errMsg != null) {
listener.reportEngineError(errMsg);
}
}
});
uciEngine.initialize();
final UCIEngine uci = uciEngine;
engineMonitor = new Thread(new Runnable() {
public void run() {
monitorLoop();
monitorLoop(uci);
}
});
engineMonitor.start();
@ -569,16 +573,17 @@ public class DroidComputerPlayer {
private final static long guiUpdateInterval = 100;
private long lastGUIUpdate = 0;
private final void monitorLoop() {
private final void monitorLoop(UCIEngine uci) {
while (true) {
int timeout = getReadTimeout();
UCIEngine uci = uciEngine;
if (uci == null)
return;
String s = uci.readLineFromEngine(timeout);
if (Thread.currentThread().isInterrupted())
return;
processEngineOutput(s);
String s = uci.readLineFromEngine(timeout);
if ((s == null) || Thread.currentThread().isInterrupted())
return;
processEngineOutput(uci, s);
if (Thread.currentThread().isInterrupted())
return;
notifyGUI();
if (Thread.currentThread().isInterrupted())
return;
@ -586,7 +591,10 @@ public class DroidComputerPlayer {
}
/** Process one line of data from the engine. */
private final synchronized void processEngineOutput(String s) {
private final synchronized void processEngineOutput(UCIEngine uci, String s) {
if (Thread.currentThread().isInterrupted())
return;
if (s == null) {
shutdownEngine();
return;
@ -595,17 +603,13 @@ public class DroidComputerPlayer {
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.initOptions();
uci.setOption("Ponder", false);
uci.writeLineToEngine("ucinewgame");
uciEngine.writeLineToEngine("isready");
uci.writeLineToEngine("isready");
engineState.state = MainState.WAIT_READY;
}
break;
@ -642,7 +646,7 @@ public class DroidComputerPlayer {
case STOP_SEARCH: {
String[] tokens = tokenize(s);
if (tokens[0].equals("bestmove")) {
uciEngine.writeLineToEngine("isready");
uci.writeLineToEngine("isready");
engineState.state = MainState.WAIT_READY;
}
break;
@ -860,6 +864,9 @@ public class DroidComputerPlayer {
/** Notify GUI about search statistics. */
private final synchronized void notifyGUI() {
if (Thread.currentThread().isInterrupted())
return;
long now = System.currentTimeMillis();
if (now < lastGUIUpdate + guiUpdateInterval)
return;

View File

@ -0,0 +1,183 @@
package org.petero.droidfish.engine;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.channels.FileChannel;
import java.util.LinkedList;
import java.util.List;
public class ExternalEngine extends UCIEngineBase {
private File engineFileName;
private static final String exePath = "/data/data/org.petero.droidfish/engine.exe";
private final Report report;
private Process engineProc;
private Thread stdInThread;
private Thread stdErrThread;
private List<String> inLines;
public ExternalEngine(String engine, Report report) {
this.report = report;
engineFileName = new File(engine);
engineProc = null;
stdInThread = null;
stdErrThread = null;
inLines = new LinkedList<String>();
}
/** @inheritDoc */
@Override
protected void startProcess() {
try {
copyFile(engineFileName, new File(exePath));
chmod(exePath);
ProcessBuilder pb = new ProcessBuilder(exePath);
engineProc = pb.start();
// Start a thread to read stdin
stdInThread = new Thread(new Runnable() {
@Override
public void run() {
Process ep = engineProc;
if (ep == null)
return;
InputStream is = ep.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
try {
while ((line = br.readLine()) != null) {
if ((ep == null) || Thread.currentThread().isInterrupted())
return;
synchronized (inLines) {
inLines.add(line);
inLines.notify();
}
}
} catch (IOException e) {
return;
}
}
});
stdInThread.start();
// Start a thread to ignore stderr
stdErrThread = new Thread(new Runnable() {
@Override
public void run() {
byte[] buffer = new byte[128];
while (true) {
Process ep = engineProc;
if ((ep == null) || Thread.currentThread().isInterrupted())
return;
try {
ep.getErrorStream().read(buffer);
} catch (IOException e) {
return;
}
}
}
});
stdErrThread.start();
} catch (IOException ex) {
report.reportError(ex.getMessage());
}
}
/** @inheritDoc */
@Override
public void initOptions() {
setOption("Hash", 16);
}
/** @inheritDoc */
@Override
public void setStrength(int strength) {
}
/** @inheritDoc */
@Override
public String readLineFromEngine(int timeoutMillis) {
try {
synchronized (inLines) {
if (inLines.size() == 0) {
Thread inThread = stdInThread;
if ((inThread == null) || !inThread.isAlive())
return null;
inLines.wait(timeoutMillis);
}
}
synchronized (inLines) {
if (inLines.size() > 0) {
String ret = inLines.get(0);
inLines.remove(0);
// System.out.printf("Engine -> GUI: %s\n", ret);
return ret;
}
}
} catch (InterruptedException e) {
}
return "";
}
/** @inheritDoc */
@Override
public void writeLineToEngine(String data) {
// System.out.printf("GUI -> Engine: %s\n", data);
data += "\n";
try {
Process ep = engineProc;
if (ep != null)
ep.getOutputStream().write(data.getBytes());
} catch (IOException e) {
}
}
/** @inheritDoc */
@Override
public void shutDown() {
super.shutDown();
if (engineProc != null)
engineProc.destroy();
engineProc = null;
if (stdInThread != null)
stdInThread.interrupt();
if (stdErrThread != null)
stdErrThread.interrupt();
}
private final static void copyFile(File from, File to) throws IOException {
if (to.exists() && (from.length() == to.length()) && (from.lastModified() == to.lastModified()))
return;
if (to.exists())
to.delete();
to.createNewFile();
FileChannel inFC = null;
FileChannel outFC = null;
try {
inFC = new FileInputStream(from).getChannel();
outFC = new FileOutputStream(to).getChannel();
long cnt = outFC.transferFrom(inFC, 0, inFC.size());
if (cnt < inFC.size())
throw new IOException("File copy failed");
} finally {
if (inFC != null) { try { inFC.close(); } catch (IOException ex) {} }
if (outFC != null) { try { outFC.close(); } catch (IOException ex) {} }
to.setLastModified(from.lastModified());
}
}
private final void chmod(String exePath) throws IOException {
Process proc = Runtime.getRuntime().exec(new String[]{"chmod", "744", exePath});
try {
proc.waitFor();
} catch (InterruptedException e) {
proc.destroy();
throw new IOException("chmod failed");
}
}
}

View File

@ -23,18 +23,19 @@ public class StockFishJNI extends UCIEngineBase {
System.loadLibrary("stockfishjni");
}
/** @inheritDoc */
@Override
public void setStrength(int strength) {
public final void initOptions() {
setOption("Hash", 16);
}
/** @inheritDoc */
@Override
public final void setStrength(int strength) {
setOption("Skill Level", strength/50);
}
/**
* Read a line from the process.
* @param timeoutMillis Maximum time to wait for data
* @return The line, without terminating newline characters,
* or empty string if no data available,
* or null if I/O error.
*/
/** @inheritDoc */
@Override
public final String readLineFromEngine(int timeoutMillis) {
String ret = readFromProcess(timeoutMillis);
@ -46,14 +47,15 @@ public class StockFishJNI extends UCIEngineBase {
return ret;
}
/** Write a line to the process. \n will be added automatically. */
/** @inheritDoc */
@Override
public final synchronized void writeLineToEngine(String data) {
// System.out.printf("GUI -> Engine: %s\n", data);
writeToProcess(data + "\n");
}
/** Start the child process. */
/** @inheritDoc */
@Override
protected final native void startProcess();
/**

View File

@ -20,15 +20,24 @@ package org.petero.droidfish.engine;
public interface UCIEngine {
/** For reporting engine error messages. */
public interface Report {
/** Report error message to GUI. */
void reportError(String errMsg);
}
/** Start engine. */
public void initialize();
/** Initialize default options. */
public void initOptions();
/** Shut down engine. */
public void shutDown();
/**
* Read a line from the engine.
* @param timeoutMillis Maximum time to wait for data
* @param timeoutMillis Maximum time to wait for data.
* @return The line, without terminating newline characters,
* or empty string if no data available,
* or null if I/O error.

View File

@ -2,10 +2,21 @@ package org.petero.droidfish.engine;
import java.util.HashMap;
import org.petero.droidfish.engine.cuckoochess.CuckooChessEngine;
public abstract class UCIEngineBase implements UCIEngine {
private boolean processAlive;
public static UCIEngine getEngine(String engine, Report report) {
if ("cuckoochess".equals(engine))
return new CuckooChessEngine(report);
else if ("stockfish".equals(engine))
return new StockFishJNI();
else
return new ExternalEngine(engine, report);
}
protected UCIEngineBase() {
processAlive = false;
}
@ -21,7 +32,7 @@ public abstract class UCIEngineBase implements UCIEngine {
}
@Override
public final void shutDown() {
public void shutDown() {
if (processAlive) {
writeLineToEngine("quit");
processAlive = false;

View File

@ -51,12 +51,8 @@ public class CuckooChessEngine extends UCIEngineBase {
private NioInputStream inFromEngine;
private Thread engineThread;
public CuckooChessEngine() {
try {
pos = TextIO.readFEN(TextIO.startPosFEN);
} catch (ChessParseError ex) {
throw new RuntimeException();
}
public CuckooChessEngine(Report report) {
pos = null;
moves = new ArrayList<Move>();
quit = false;
try {
@ -64,14 +60,12 @@ public class CuckooChessEngine extends UCIEngineBase {
engineToGui = Pipe.open();
inFromEngine = new NioInputStream(engineToGui);
} catch (IOException e) {
report.reportError(e.getMessage());
}
}
/** @inheritDoc */
@Override
public void setStrength(int strength) {
setOption("strength", strength);
}
protected final void startProcess() {
engineThread = new Thread(new Runnable() {
public void run() {
@ -87,6 +81,17 @@ public class CuckooChessEngine extends UCIEngineBase {
engineThread.start();
}
/** @inheritDoc */
@Override
public final void initOptions() {
}
/** @inheritDoc */
@Override
public final void setStrength(int strength) {
setOption("strength", strength);
}
private final void mainLoop(NioInputStream is, NioPrintStream os) {
String line;
while ((line = is.readLine()) != null) {
@ -97,6 +102,7 @@ public class CuckooChessEngine extends UCIEngineBase {
}
}
/** @inheritDoc */
@Override
public final String readLineFromEngine(int timeoutMillis) {
if ((engineThread != null) && !engineThread.isAlive())
@ -110,6 +116,7 @@ public class CuckooChessEngine extends UCIEngineBase {
return ret;
}
/** @inheritDoc */
@Override
public final synchronized void writeLineToEngine(String data) {
// System.out.printf("GUI -> Engine: %s\n", data);
@ -224,6 +231,13 @@ public class CuckooChessEngine extends UCIEngineBase {
sPar.infinite = true;
}
}
if (pos == null) {
try {
pos = TextIO.readFEN(TextIO.startPosFEN);
} catch (ChessParseError ex) {
throw new RuntimeException();
}
}
if (ponder) {
engine.startPonder(pos, moves, sPar);
} else {

View File

@ -157,6 +157,11 @@ public class DroidChessController {
}
}
/** Return current engine identifier. */
public final synchronized String getEngine() {
return engine;
}
/** Notify controller that preferences has changed. */
public final synchronized void prefsChanged() {
updateBookHints();
@ -649,6 +654,8 @@ public class DroidChessController {
tmpPos.makeMove(ponderMove, ui);
}
for (Move m : pv.pv) {
if (m == null)
break;
String moveStr = TextIO.moveToString(tmpPos, m, false);
buf.append(String.format(" %s", moveStr));
tmpPos.makeMove(m, ui);
@ -701,6 +708,15 @@ public class DroidChessController {
}
});
}
@Override
public void reportEngineError(final String errMsg) {
gui.runOnUIThread(new Runnable() {
public void run() {
gui.reportEngineError(errMsg);
}
});
}
}
/** Discard current search. Return true if GUI update needed. */

View File

@ -90,4 +90,7 @@ public interface SearchListener {
/** Report engine name. */
public void notifyEngineName(String engineName);
/** Report engine error. */
public void reportEngineError(String errMsg);
}