From d475ad7aa656ffdadc6d63da850cc8f12421c097 Mon Sep 17 00:00:00 2001 From: zsalch Date: Tue, 18 Sep 2018 16:53:11 +0800 Subject: [PATCH] Support Switch Multiple Engine Heights --- src/main/java/featurecat/lizzie/Lizzie.java | 43 ++++++- src/main/java/featurecat/lizzie/Util.java | 33 +++++ .../featurecat/lizzie/analysis/Leelaz.java | 116 ++++++++++++++++-- .../java/featurecat/lizzie/gui/Input.java | 15 +++ .../featurecat/lizzie/gui/LizzieFrame.java | 29 ++++- .../java/featurecat/lizzie/rules/Board.java | 88 +++++++++++++ .../lizzie/rules/BoardHistoryNode.java | 20 ++- .../resources/l10n/DisplayStrings.properties | 1 + .../l10n/DisplayStrings_RO.properties | 1 + .../l10n/DisplayStrings_zh_CN.properties | 1 + 10 files changed, 331 insertions(+), 16 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index 71f0c968c..2ce803748 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -1,5 +1,6 @@ package featurecat.lizzie; +import org.json.JSONArray; import org.json.JSONException; import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.plugin.PluginManager; @@ -56,7 +57,7 @@ public static void main(String[] args) throws IOException, JSONException, ClassN try { leelaz = new Leelaz(); if(config.handicapInsteadOfWinrate) { - leelaz.estimatePassWinrate(); + leelaz.estimatePassWinrate(); } if (args.length == 1) { frame.loadFile(new File(args[0])); @@ -95,4 +96,44 @@ public static void shutdown() { System.exit(0); } + /** + * Switch the Engine by index number + * @param index engine index + */ + public static void switchEngine(int index) { + + String commandLine = null; + if (index == 0) { + commandLine = Lizzie.config.leelazConfig.getString("engine-command"); + commandLine = commandLine.replaceAll("%network-file", Lizzie.config.leelazConfig.getString("network-file")); + } else { + JSONArray commandList = Lizzie.config.leelazConfig.getJSONArray("engine-command-list"); + if (commandList != null && commandList.length() >= index) { + commandLine = commandList.getString(index - 1); + } else { + index = -1; + } + } + if (index < 0 || commandLine == null || commandLine.trim().isEmpty() || index == Lizzie.leelaz.currentEngineN()) { + return; + } + + // Workaround for leelaz cannot exit when restarting + if (leelaz.isThinking) { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; + Lizzie.leelaz.togglePonder(); // we must toggle twice for it to restart pondering + Lizzie.leelaz.isThinking = false; + } + Lizzie.leelaz.togglePonder(); + } + + board.saveMoveNumber(); + try { + leelaz.restartEngine(commandLine, index); + board.restoreMoveNumber(); + } catch (IOException e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/featurecat/lizzie/Util.java b/src/main/java/featurecat/lizzie/Util.java index 395dfd73a..fd0728d21 100644 --- a/src/main/java/featurecat/lizzie/Util.java +++ b/src/main/java/featurecat/lizzie/Util.java @@ -1,5 +1,6 @@ package featurecat.lizzie; +import java.awt.FontMetrics; import java.io.*; import java.net.URL; import java.nio.channels.Channels; @@ -80,4 +81,36 @@ public static void saveAsFile(URL url, File file) { e.printStackTrace(); } } + + /** + * Truncate text that is too long for the given width + * + * @param line + * @param fm + * @param fitWidth + * @return fitted + */ + public static String truncateStringByWidth(String line, FontMetrics fm, int fitWidth) { + if (line == null || line.length() == 0) { + return ""; + } + int width = fm.stringWidth(line); + if (width > fitWidth) { + int guess = line.length() * fitWidth / width; + String before = line.substring(0, guess).trim(); + width = fm.stringWidth(before); + if (width > fitWidth) { + int diff = width - fitWidth; + int i = 0; + for (; (diff > 0 && i < 5); i++) { + diff = diff - fm.stringWidth(line.substring(guess - i - 1, guess - i)); + } + return line.substring(0, guess - i).trim(); + } else { + return before; + } + } else { + return line; + } + } } diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 0d08328e4..66d52c664 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -17,6 +17,9 @@ import java.net.URL; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -60,6 +63,15 @@ public class Leelaz { private boolean isLoaded = false; private boolean isCheckingVersion; + // for Multiple Engine + private String engineCommand = null; + private List commands = null; + private JSONObject config = null; + private String currentWeight = null; + private boolean switching = false; + private int currentEngineN = -1; + private ScheduledExecutorService executor = null; + // dynamic komi and opponent komi as reported by dynamic-komi version of leelaz private float dynamicKomi = Float.NaN, dynamicOppKomi = Float.NaN; /** @@ -78,7 +90,8 @@ public Leelaz() throws IOException, JSONException { currentCmdNum = 0; cmdQueue = new ArrayDeque<>(); - JSONObject config = Lizzie.config.config.getJSONObject("leelaz"); + // Move config to member for other method call + config = Lizzie.config.config.getJSONObject("leelaz"); printCommunication = config.getBoolean("print-comms"); maxAnalyzeTimeMillis = MINUTE * config.getInt("max-analyze-time-minutes"); @@ -87,6 +100,24 @@ public Leelaz() throws IOException, JSONException { updateToLatestNetwork(); } + + // command string for starting the engine + engineCommand = config.getString("engine-command"); + // substitute in the weights file + engineCommand = engineCommand.replaceAll("%network-file", config.getString("network-file")); + + // Initialize current engine number and start engine + currentEngineN = 0; + startEngine(engineCommand); + Lizzie.frame.refreshBackground(); + } + + public void startEngine(String engineCommand) throws IOException { + // Check engine command + if (engineCommand == null || engineCommand.trim().isEmpty()) { + return; + } + String startfolder = new File(Config.getBestDefaultLeelazPath()).getParent(); // todo make this a little more obvious/less bug-prone // Check if network file is present @@ -95,13 +126,23 @@ public Leelaz() throws IOException, JSONException { JOptionPane.showMessageDialog(null, resourceBundle.getString("LizzieFrame.display.network-missing")); } - - // command string for starting the engine - String engineCommand = config.getString("engine-command"); - // substitute in the weights file - engineCommand = engineCommand.replaceAll("%network-file", config.getString("network-file")); // create this as a list which gets passed into the processbuilder - List commands = Arrays.asList(engineCommand.split(" ")); + commands = Arrays.asList(engineCommand.split(" ")); + + // get weight name + if (engineCommand != null) { + Pattern wPattern = Pattern.compile("(?s).*?(--weights |-w )([^ ]+)(?s).*"); + Matcher wMatcher = wPattern.matcher(engineCommand); + if (wMatcher.matches()) { + currentWeight = wMatcher.group(2); + if (currentWeight != null) { + String[] names = currentWeight.split("[\\\\|/]"); + if (names != null && names.length > 1) { + currentWeight = names[names.length - 1]; + } + } + } + } // run leelaz ProcessBuilder processBuilder = new ProcessBuilder(commands); @@ -117,8 +158,42 @@ public Leelaz() throws IOException, JSONException { sendCommand("version"); // start a thread to continuously read Leelaz output - new Thread(this::read).start(); - Lizzie.frame.refreshBackground(); + //new Thread(this::read).start(); + //can stop engine for switching weights + executor = Executors.newSingleThreadScheduledExecutor(); + executor.execute(this::read); + } + + public void restartEngine(String engineCommand, int index) throws IOException { + if (engineCommand == null || engineCommand.trim().isEmpty()) { + return; + } + switching = true; + this.engineCommand = engineCommand; + // stop the ponder + if (Lizzie.leelaz.isPondering()) { + Lizzie.leelaz.togglePonder(); + } + normalQuit(); + startEngine(engineCommand); + currentEngineN = index; + togglePonder(); + } + + public void normalQuit() { + sendCommand("quit"); + executor.shutdown(); + try { + while (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + if (executor.awaitTermination(1, TimeUnit.SECONDS)) { + shutdown(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } } private void updateToLatestNetwork() { @@ -204,6 +279,10 @@ else if (line.equals("\n")) { // End of response } else if (line.startsWith("info")) { isLoaded = true; + // Clear switching prompt + switching = false; + // Display engine command in the title + if (Lizzie.frame != null) Lizzie.frame.updateTitle(); if (isResponseUpToDate()) { // This should not be stale data when the command number match parseInfo(line.substring(5)); @@ -294,7 +373,8 @@ private void read() { System.out.println("Leelaz process ended."); shutdown(); - System.exit(-1); + // Do no exit for switching weights + //System.exit(-1); } catch (IOException e) { e.printStackTrace(); System.exit(-1); @@ -561,4 +641,20 @@ private synchronized void notifyBestMoveListeners() { public boolean isLoaded() { return isLoaded; } + + public String currentWeight() { + return currentWeight; + } + + public boolean switching() { + return switching; + } + + public int currentEngineN() { + return currentEngineN; + } + + public String engineCommand() { + return this.engineCommand; + } } diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index ee32f376a..108f4594a 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -366,6 +366,21 @@ public void keyPressed(KeyEvent e) { toggleShowDynamicKomi(); break; + // Use Ctrl+Num to switching multiple engine + case VK_0: + case VK_1: + case VK_2: + case VK_3: + case VK_4: + case VK_5: + case VK_6: + case VK_7: + case VK_8: + case VK_9: + if (controlIsPressed(e)) { + Lizzie.switchEngine(e.getKeyCode() - VK_0); + } + break; default: shouldDisableAnalysis = false; } diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 3bb067a5d..8e53bdbb4 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -95,6 +95,9 @@ public class LizzieFrame extends JFrame { private long lastAutosaveTime = System.currentTimeMillis(); + // Save the player title + private String playerTitle = null; + static { // load fonts try { @@ -395,8 +398,10 @@ public void paint(Graphics g0) { if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { if (Lizzie.config.showStatus) { + // Display switching prompt drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.pondering") + - (Lizzie.leelaz.isPondering()?resourceBundle.getString("LizzieFrame.display.on"):resourceBundle.getString("LizzieFrame.display.off")), + (Lizzie.leelaz.isPondering()?resourceBundle.getString("LizzieFrame.display.on"):resourceBundle.getString("LizzieFrame.display.off")) + + " " + Lizzie.leelaz.currentWeight() + (Lizzie.leelaz.switching() ? resourceBundle.getString("LizzieFrame.prompt.switching") : ""), ponderingX, ponderingY, ponderingSize); } @@ -491,6 +496,14 @@ private void drawPonderingState(Graphics2D g, String text, int x, int y, double Font font = new Font(systemDefaultFontName, Font.PLAIN, (int)(Math.max(getWidth(), getHeight()) * size)); FontMetrics fm = g.getFontMetrics(font); int stringWidth = fm.stringWidth(text); + // Truncate too long text when display switching prompt + if (Lizzie.leelaz.isLoaded()) { + int mainBoardX = (boardRenderer != null && boardRenderer.getLocation() != null) ? boardRenderer.getLocation().x : 0; + if ((mainBoardX > x) && stringWidth > (mainBoardX - x) ) { + text = Util.truncateStringByWidth(text, fm, mainBoardX - x); + stringWidth = fm.stringWidth(text); + } + } int stringHeight = fm.getAscent() - fm.getDescent(); int width = stringWidth; int height = (int)(stringHeight * 1.2); @@ -881,8 +894,15 @@ public void toggleCoordinates() { } public void setPlayers(String whitePlayer, String blackPlayer) { - setTitle(String.format("%s (%s [W] vs %s [B])", DEFAULT_TITLE, - whitePlayer, blackPlayer)); + this.playerTitle = String.format("(%s [W] vs %s [B])", whitePlayer, blackPlayer); + this.updateTitle(); + } + + public void updateTitle() { + StringBuilder sb = new StringBuilder(DEFAULT_TITLE); + sb.append(this.playerTitle != null ? " " + this.playerTitle.trim() : ""); + sb.append(Lizzie.leelaz.engineCommand() != null ? " [" + Lizzie.leelaz.engineCommand() + "]" : ""); + setTitle(sb.toString()); } private void setDisplayedBranchLength(int n) { @@ -904,7 +924,8 @@ public boolean incrementDisplayedBranchLength(int n) { } public void resetTitle() { - setTitle(DEFAULT_TITLE); + this.playerTitle = null; + this.updateTitle(); } public void copySgf() { diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 81fe97825..6549c2473 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -26,6 +26,8 @@ public class Board implements LeelazListener { private boolean analysisMode = false; private int playoutsAnalysis = 100; + // Save the node for restore move when in the branch + private BoardHistoryNode saveNode = null; public Board() { initialize(); @@ -433,6 +435,92 @@ public boolean nextMove() { } } + /** + * Goes to the next coordinate, thread safe + * @param fromBackChildren by back children branch + * @return true when has next variation + */ + public boolean nextMove(int fromBackChildren) { + synchronized (this) { + // Update win rate statistics + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + if (stats.totalPlayouts >= history.getData().playouts) { + history.getData().winrate = stats.maxWinrate; + history.getData().playouts = stats.totalPlayouts; + } + return nextVariation(fromBackChildren); + } + } + + /** + * Save the move number for restore + * If in the branch, save the back routing from children + */ + public void saveMoveNumber() { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + int curMoveNum = curNode.getData().moveNumber; + if (curMoveNum > 0) { + if (!BoardHistoryList.isMainTrunk(curNode)) { + // If in branch, save the back routing from children + saveBackRouting(curNode); + } + goToMoveNumber(0); + } + saveNode = curNode; + } + + /** + * Save the back routing from children + */ + public void saveBackRouting(BoardHistoryNode node) { + if (node != null && node.previous() != null) { + node.previous().setFromBackChildren(node.previous().getNexts().indexOf(node)); + saveBackRouting(node.previous()); + } + } + + /** + * Restore move number by saved node + */ + public void restoreMoveNumber() { + restoreMoveNumber(saveNode); + } + + /** + * Restore move number by node + */ + public void restoreMoveNumber(BoardHistoryNode node) { + if (node == null) { + return; + } + int moveNumber = node.getData().moveNumber; + if (moveNumber > 0) { + if (BoardHistoryList.isMainTrunk(node)) { + goToMoveNumber(moveNumber); + } else { + // If in Branch, restore by the back routing + goToMoveNumberByBackChildren(moveNumber); + } + } + } + + /** + * Go to move number by back routing from children when in branch + */ + public void goToMoveNumberByBackChildren(int moveNumber) { + int delta = moveNumber - history.getMoveNumber(); + for (int i = 0; i < Math.abs(delta); i++) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + if (curNode.numberOfChildren() > 1 && delta > 0) { + nextMove(curNode.getFromBackChildren()); + } else { + if (!(delta > 0 ? nextMove() : previousMove())) { + break; + } + } + } + } + public boolean goToMoveNumber(int moveNumber) { return goToMoveNumberHelper(moveNumber, false); } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java index 95cf436f0..1dbc4c5b5 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java @@ -11,6 +11,9 @@ public class BoardHistoryNode { private BoardData data; + // Save the children for restore to branch + private int fromBackChildren; + /** * Initializes a new list node */ @@ -173,4 +176,19 @@ public void deleteChild(int idx) { nexts.remove(idx); } } -} + + /** + * @param fromBackChildren the fromBackChildren to set + */ + public void setFromBackChildren(int fromBackChildren) { + this.fromBackChildren = fromBackChildren; + } + + /** + * @return the fromBackChildren + */ + public int getFromBackChildren() { + return fromBackChildren; + } + +} \ No newline at end of file diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index 3efe45210..07a8e0905 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -44,6 +44,7 @@ LizzieFrame.prompt.failedToOpenFile=Failed to open file. LizzieFrame.prompt.failedToSaveFile=Failed to save file. LizzieFrame.prompt.sgfExists=The SGF file already exists, do you want to replace it? LizzieFrame.prompt.showControlsHint=hold x = view controls +LizzieFrame.prompt.switching=switching... LizzieFrame.display.lastMove=Last move LizzieFrame.display.pondering=Pondering LizzieFrame.display.on=on diff --git a/src/main/resources/l10n/DisplayStrings_RO.properties b/src/main/resources/l10n/DisplayStrings_RO.properties index 07b385516..6e55241f6 100644 --- a/src/main/resources/l10n/DisplayStrings_RO.properties +++ b/src/main/resources/l10n/DisplayStrings_RO.properties @@ -43,6 +43,7 @@ LizzieFrame.prompt.failedToOpenSgf=Nu s-a reușit deschiderea fișierului SGF LizzieFrame.prompt.failedToSaveSgf=Nu s-a reușit salvarea fișierului SGF LizzieFrame.prompt.sgfExists=Fișierul SGF există deja, doriți să-l înlocuiți? LizzieFrame.prompt.showControlsHint=x apăsat = afișează comenzi +LizzieFrame.prompt.switching=comutare... LizzieFrame.display.lastMove=Ulima mutare LizzieFrame.display.pondering=Analizeză LizzieFrame.display.on=pornit diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index 6a39c6660..a8a557a2f 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -32,6 +32,7 @@ LizzieFrame.prompt.failedToOpenFile=\u4E0D\u80FD\u6253\u5F00SGF\u6587\u4EF6. LizzieFrame.prompt.failedToSaveFile=\u4E0D\u80FD\u4FDD\u5B58SGF\u6587\u4EF6. LizzieFrame.prompt.sgfExists=SGF\u6587\u4EF6\u5DF2\u7ECF\u5B58\u5728, \u9700\u8981\u66FF\u6362\u5417? LizzieFrame.prompt.showControlsHint=\u6309\u4F4FX\u4E0D\u653E\u67E5\u770B\u5FEB\u6377\u952E\u63D0\u793A +LizzieFrame.prompt.switching=\u5207\u6362\u4E2D... LizzieFrame.display.lastMove=\u6700\u540E\u4E00\u624B LizzieFrame.display.pondering=\u5206\u6790 LizzieFrame.display.on=\u5F00\u542F