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