diff --git a/pom.xml b/pom.xml index ab70ad3bc..699e4d2f2 100644 --- a/pom.xml +++ b/pom.xml @@ -64,18 +64,6 @@ false - - com.coveo - fmt-maven-plugin - 2.5.1 - - - - format - - - - diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index 285595d31..15a3b6de2 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -6,6 +6,8 @@ import java.io.File; import java.io.IOException; import javax.swing.*; +import org.json.JSONArray; +import org.json.JSONException; /** Main class. */ public class Lizzie { @@ -80,4 +82,51 @@ public static void shutdown() { if (leelaz != null) leelaz.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 432ebb1f5..94def40a9 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; @@ -74,4 +75,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 8e8a2378a..f9224919b 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -10,6 +10,11 @@ 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; import javax.swing.*; import org.json.JSONException; import org.json.JSONObject; @@ -55,6 +60,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; /** @@ -73,7 +87,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"); @@ -82,13 +97,44 @@ 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; + } + File startfolder = new File(config.optString("engine-start-location", ".")); - String engineCommand = config.getString("engine-command"); String networkFile = config.getString("network-file"); // substitute in the weights file engineCommand = engineCommand.replaceAll("%network-file", networkFile); // 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]; + } + } + } + } // Check if engine is present File lef = startfolder.toPath().resolve(new File(commands.get(0)).toPath()).toFile(); @@ -123,8 +169,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() { @@ -201,6 +281,10 @@ private void parseLine(String line) { // 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)); @@ -301,7 +385,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); @@ -568,4 +653,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 4bbcd035c..fc36096ad 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -342,6 +342,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 15341845b..1893a6414 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -93,6 +93,9 @@ public class LizzieFrame extends JFrame { private long lastAutosaveTime = System.currentTimeMillis(); + // Save the player title + private String playerTitle = null; + // Display Comment private JScrollPane scrollPane = null; private JTextPane commentPane = null; @@ -436,8 +439,11 @@ public void paint(Graphics g0) { if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { if (Lizzie.config.showStatus) { - String key = "LizzieFrame.display." + (Lizzie.leelaz.isPondering() ? "on" : "off"); - String text = resourceBundle.getString(key); + String pondKey = "LizzieFrame.display." + (Lizzie.leelaz.isPondering() ? "on" : "off"); + String pondText = resourceBundle.getString(pondKey); + String switchText = resourceBundle.getString("LizzieFrame.prompt.switching"); + String weightText = Lizzie.leelaz.currentWeight().toString(); + String text = pondText + " " + weightText + (Lizzie.leelaz.switching() ? switchText : ""); drawPonderingState(g, text, ponderingX, ponderingY, ponderingSize); } @@ -545,6 +551,17 @@ private void drawPonderingState(Graphics2D g, String text, int x, int y, double Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); 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); @@ -1017,7 +1034,16 @@ 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) { @@ -1039,7 +1065,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 65c9f51a8..2de079d57 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -25,6 +25,9 @@ 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(); } @@ -473,6 +476,82 @@ 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 32d86d4e4..01ac257af 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java @@ -10,6 +10,9 @@ public class BoardHistoryNode { private BoardData data; + // Save the children for restore to branch + private int fromBackChildren; + /** Initializes a new list node */ public BoardHistoryNode(BoardData data) { previous = null; @@ -167,4 +170,14 @@ 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; + } } diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index c8cf8079e..be3e5dce7 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -45,6 +45,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 d01b4059a..14c54dce5 100644 --- a/src/main/resources/l10n/DisplayStrings_RO.properties +++ b/src/main/resources/l10n/DisplayStrings_RO.properties @@ -44,6 +44,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 2309ac564..c7effab3e 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -33,6 +33,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