diff --git a/DroidFish/res/menu/options_menu.xml b/DroidFish/res/menu/options_menu.xml index 84abaa8..f5c6cb1 100644 --- a/DroidFish/res/menu/options_menu.xml +++ b/DroidFish/res/menu/options_menu.xml @@ -44,6 +44,10 @@ android:title="@string/option_select_book"> + + diff --git a/DroidFish/res/values-de/strings.xml b/DroidFish/res/values-de/strings.xml index b71d5b0..a3b6030 100644 --- a/DroidFish/res/values-de/strings.xml +++ b/DroidFish/res/values-de/strings.xml @@ -1,14 +1,7 @@ DroidFish - - Stockfish - CuckooChess - - - stockfish - cuckoochess - + Automatisch 1 @@ -414,8 +407,6 @@ wenn Sie es nicht aktiv nutzen.\ Automatischer Seitenwechsel Seiten beim Start einer neuen Partie automatisch wechseln (Einstellung Ansicht drehen ignorieren) Engine-Einstellungen - Schach-Engine - Auswahl der Schach-Engine Spielstärke Vorausberechnung Vorausberechnung von Zügen durch die Engine, wenn der Spieler am Zug ist diff --git a/DroidFish/res/values/strings.xml b/DroidFish/res/values/strings.xml index 60b1ab1..a761dd3 100644 --- a/DroidFish/res/values/strings.xml +++ b/DroidFish/res/values/strings.xml @@ -1,15 +1,6 @@ DroidFish - - Stockfish - CuckooChess - - - stockfish - cuckoochess - - stockfish Automatic @@ -305,6 +296,7 @@ you are not actively using the program.\ Edit Move Counters <Internal Book> Select opening book file + Select Chess Engine Select PGN file to open Save to PGN file Select Scid file to open @@ -401,6 +393,9 @@ you are not actively using the program.\ After Selected Replace Selected Engine + Engine error + Stockfish + CuckooChess Too few spaces Invalid piece @@ -425,6 +420,7 @@ you are not actively using the program.\ Force Computer Move Claim/Offer/Accept Draw Select Opening Book + Select Chess Engine Set Color Theme About / Help @@ -434,8 +430,6 @@ you are not actively using the program.\ Auto Swap Sides Automatically swap sides when new game started. Also overrides Flip Board setting Engine Settings - Engine - Chess Engine Strength Pondering Let engine think while waiting for opponent\'s move diff --git a/DroidFish/res/xml/preferences.xml b/DroidFish/res/xml/preferences.xml index 72480eb..cf4be24 100644 --- a/DroidFish/res/xml/preferences.xml +++ b/DroidFish/res/xml/preferences.xml @@ -18,14 +18,6 @@ - - = 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) { diff --git a/DroidFish/src/org/petero/droidfish/GUIInterface.java b/DroidFish/src/org/petero/droidfish/GUIInterface.java index 7b6bc6b..85e7d38 100644 --- a/DroidFish/src/org/petero/droidfish/GUIInterface.java +++ b/DroidFish/src/org/petero/droidfish/GUIInterface.java @@ -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(); diff --git a/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java b/DroidFish/src/org/petero/droidfish/engine/DroidComputerPlayer.java index 3e68c60..92e3f52 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.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; diff --git a/DroidFish/src/org/petero/droidfish/engine/ExternalEngine.java b/DroidFish/src/org/petero/droidfish/engine/ExternalEngine.java new file mode 100644 index 0000000..f4de2e0 --- /dev/null +++ b/DroidFish/src/org/petero/droidfish/engine/ExternalEngine.java @@ -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 inLines; + + public ExternalEngine(String engine, Report report) { + this.report = report; + engineFileName = new File(engine); + engineProc = null; + stdInThread = null; + stdErrThread = null; + inLines = new LinkedList(); + } + + /** @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"); + } + } +} diff --git a/DroidFish/src/org/petero/droidfish/engine/StockFishJNI.java b/DroidFish/src/org/petero/droidfish/engine/StockFishJNI.java index 539e2a6..e5b7417 100644 --- a/DroidFish/src/org/petero/droidfish/engine/StockFishJNI.java +++ b/DroidFish/src/org/petero/droidfish/engine/StockFishJNI.java @@ -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(); /** diff --git a/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java b/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java index f965505..d38cc16 100644 --- a/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java +++ b/DroidFish/src/org/petero/droidfish/engine/UCIEngine.java @@ -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. diff --git a/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java b/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java index 37dafb6..f96c927 100644 --- a/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java +++ b/DroidFish/src/org/petero/droidfish/engine/UCIEngineBase.java @@ -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; diff --git a/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java b/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java index 205f93f..c12bc0b 100644 --- a/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java +++ b/DroidFish/src/org/petero/droidfish/engine/cuckoochess/CuckooChessEngine.java @@ -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(); 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 { diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java index 0bccb70..7907db5 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java @@ -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. */ diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java b/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java index c30b786..4de4755 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java @@ -90,4 +90,7 @@ public interface SearchListener { /** Report engine name. */ public void notifyEngineName(String engineName); + + /** Report engine error. */ + public void reportEngineError(String errMsg); }