From 68928137e004208a21542674d04a20ada4adb935 Mon Sep 17 00:00:00 2001 From: Magnus Lundmark Date: Thu, 10 May 2018 16:20:22 +0200 Subject: [PATCH 01/75] Start script --- lizzie.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100755 lizzie.sh diff --git a/lizzie.sh b/lizzie.sh new file mode 100755 index 000000000..01747a508 --- /dev/null +++ b/lizzie.sh @@ -0,0 +1,2 @@ +java -jar -Dsun.java2d.opengl=true ./target/lizzie-0.4-shaded.jar 2>/dev/null & + From 496a3be3cd66969c5c7335750494eddefed2cb93 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Sat, 2 Jun 2018 21:08:31 +0900 Subject: [PATCH 02/75] Skip empty content of KM[] in SGF --- src/main/java/featurecat/lizzie/rules/SGFParser.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index d496c23a3..1c5898f45 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -164,7 +164,11 @@ private static boolean parse(String value) { } else if (tag.equals("PW")) { whitePlayer = tagContent; } else if (tag.equals("KM")) { - Lizzie.board.getHistory().getGameInfo().setKomi(Double.parseDouble(tagContent)); + try { + Lizzie.board.getHistory().getGameInfo().setKomi(Double.parseDouble(tagContent)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } } break; case ';': From 2121e955ad7edb70dd005c3218a41c6ec178be19 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Sat, 2 Jun 2018 21:08:39 +0900 Subject: [PATCH 03/75] Keep GameInfo for handicapped games --- src/main/java/featurecat/lizzie/rules/Board.java | 2 ++ src/main/java/featurecat/lizzie/rules/BoardHistoryList.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index fcac07cc7..f04e96767 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -283,8 +283,10 @@ public void flatten() { Stone[] stones = history.getStones(); boolean blackToPlay = history.isBlacksTurn(); Zobrist zobrist = history.getZobrist().clone(); + BoardHistoryList oldHistory = history; history = new BoardHistoryList(new BoardData(stones, null, Stone.EMPTY, blackToPlay, zobrist, 0, new int[BOARD_SIZE * BOARD_SIZE], 0, 0, 0.0, 0)); + history.setGameInfo(oldHistory.getGameInfo()); } /** diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index aaf6764d5..4ecee7644 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -25,6 +25,10 @@ public GameInfo getGameInfo() { return gameInfo; } + public void setGameInfo(GameInfo gameInfo) { + this.gameInfo = gameInfo; + } + /** * Clear history. */ From 153c4e7ec44361b387f65027c542556928354ffe Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Sat, 2 Jun 2018 21:08:46 +0900 Subject: [PATCH 04/75] Assume komi = 0.0 for empty content of KM[] in SGF --- src/main/java/featurecat/lizzie/rules/SGFParser.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 1c5898f45..dbe5e5f16 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -165,6 +165,9 @@ private static boolean parse(String value) { whitePlayer = tagContent; } else if (tag.equals("KM")) { try { + if (tagContent.trim().isEmpty()) { + tagContent = "0.0"; + } Lizzie.board.getHistory().getGameInfo().setKomi(Double.parseDouble(tagContent)); } catch (NumberFormatException e) { e.printStackTrace(); From b123e076eb454d8de7f6f2f831da269dfbb80127 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Sat, 2 Jun 2018 21:10:04 +0900 Subject: [PATCH 05/75] Check pass in variation. Maybe fix #249 --- src/main/java/featurecat/lizzie/analysis/Branch.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/featurecat/lizzie/analysis/Branch.java b/src/main/java/featurecat/lizzie/analysis/Branch.java index e6958254f..ac2439c3f 100644 --- a/src/main/java/featurecat/lizzie/analysis/Branch.java +++ b/src/main/java/featurecat/lizzie/analysis/Branch.java @@ -26,6 +26,8 @@ public Branch(Board board, List variation) { for (int i = 0; i < variation.size(); i++) { int[] coord = Board.convertNameToCoordinates(variation.get(i)); + if (coord == null) + break; data.lastMove = coord; data.stones[Board.getIndex(coord[0], coord[1])] = data.blackToPlay ? Stone.BLACK_GHOST : Stone.WHITE_GHOST; data.moveNumberList[Board.getIndex(coord[0], coord[1])] = i + 1; From e9f2d8377818f551bd9271240edb6d49a07a0017 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Sat, 2 Jun 2018 21:10:49 +0900 Subject: [PATCH 06/75] Fix #251 (Handicap stones do not appear) --- src/main/java/featurecat/lizzie/analysis/Leelaz.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index ca6a92f1f..507b6d78e 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -216,7 +216,7 @@ private void parseLine(String line) { if (isSettingHandicap) { - for (int i = 2; i < params.length; i++) { + for (int i = 1; i < params.length; i++) { int[] coordinates = Lizzie.board.convertNameToCoordinates(params[i]); Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); } @@ -289,13 +289,13 @@ private void read() { * @param command a GTP command containing no newline characters */ public void sendCommand(String command) { + if (command.startsWith("fixed_handicap")) + isSettingHandicap = true; command = cmdNumber + " " + command; cmdNumber++; if (printCommunication) { System.out.printf("> %s\n", command); } - if (command.startsWith("fixed_handicap")) - isSettingHandicap = true; try { outputStream.write((command + "\n").getBytes()); outputStream.flush(); From a79466999989339af43eaae8e5cf1a44e013be23 Mon Sep 17 00:00:00 2001 From: Adrian Petrescu Date: Sun, 3 Jun 2018 23:26:27 -0400 Subject: [PATCH 07/75] Pick a reasonable default leelaz binary. Now that Leela-Zero has the new GTP extensions that Lizzie requires, it's no longer mandatory to compile a custom leelaz binary, which means it's no longer a hard requirement for there to be a leelaz binary in Lizzie's current working directory. To make life easier for packagers (like me), when constructing the default config.txt, Lizzie should examine the PATH and find the system-wide leelaz. Of course, a local ./leelaz should still take priority (even if `.` is not actually on your PATH). This makes that the default behaviour. Works for me on a fresh install with leela-zero installed in /usr/bin. --- src/main/java/featurecat/lizzie/Config.java | 30 +++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 97d3e0789..19398b26d 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -5,7 +5,10 @@ import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; public class Config { @@ -100,7 +103,7 @@ private boolean validateAndCorrectSettings(JSONObject config) { // Check engine configs JSONObject leelaz = config.getJSONObject("leelaz"); // Check if the engine program exists. - String enginePath = leelaz.optString("engine-program", "./leelaz"); + String enginePath = leelaz.optString("engine-program", getBestDefaultLeelazPath()); if (!Files.exists(Paths.get(enginePath)) && !Files.exists(Paths.get(enginePath + ".exe" /* For windows */))) { // FIXME: I don't know how to handle it properly.. Possibly showing a warning dialog may be a good idea? leelaz.put("engine-program", "./leelaz"); @@ -194,6 +197,28 @@ public boolean showLargeSubBoard() { } + /** + * Scans the current directory as well as the current PATH to find a reasonable default leelaz binary. + * + * @return A working path to a leelaz binary. If there are none on the PATH, "./leelaz" is returned for backwards + * compatibility. + */ + private String getBestDefaultLeelazPath() { + List potentialPaths = new ArrayList<>(); + potentialPaths.add("."); + potentialPaths.addAll(Arrays.asList(System.getenv("PATH").split(":"))); + + for (String potentialPath : potentialPaths) { + for (String potentialExtension : Arrays.asList(new String[] {"", ".exe"})) { + File potentialLeelaz = new File(potentialPath, "leelaz" + potentialExtension); + if (potentialLeelaz.exists() && potentialLeelaz.canExecute()) { + return potentialLeelaz.getPath(); + } + } + } + + return "./leelaz"; + } private JSONObject createDefaultConfig() { @@ -202,7 +227,8 @@ private JSONObject createDefaultConfig() { // About engine parameter JSONObject leelaz = new JSONObject(); leelaz.put("network-file", "network.gz"); - leelaz.put("engine-command", "./leelaz --gtp --lagbuffer 0 --weights %network-file --threads 2"); + leelaz.put("engine-command", String.format("%s --gtp --lagbuffer 0 --weights %%network-file --threads 2", + getBestDefaultLeelazPath())); leelaz.put("engine-start-location", "."); leelaz.put("max-analyze-time-minutes", 5); leelaz.put("max-game-thinking-time-seconds", 2); From 06995878d6b7f7521d701e2e0f950f6cb1afae01 Mon Sep 17 00:00:00 2001 From: Adrian Petrescu Date: Sun, 3 Jun 2018 23:31:39 -0400 Subject: [PATCH 08/75] Fix parsing of /network-profiles page Sometime recently, http://zero.sjeng.org/network-profiles changed their page, the links are no longer hard-coded in the HTML but instead populated dynamically in Javascript. So the old regex was broken and just never found a match. Fix the regex to now grab the first hash from the inline JSON that Vue uses to populate the table. Works on my own manual testing. --- src/main/java/featurecat/lizzie/analysis/Leelaz.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index ca6a92f1f..f811f329f 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -124,7 +124,7 @@ private void updateToLatestNetwork() { private String getBestNetworkHash() throws IOException { // finds a valid network hash - Pattern networkHashFinder = Pattern.compile("(?<=/networks/)[a-f0-9]+"); + Pattern networkHashFinder = Pattern.compile("\"hash\":\"([a-f0-9]+)\","); String networks = null; try { @@ -139,7 +139,7 @@ private String getBestNetworkHash() throws IOException { // get the first match - this will be the best network. m.find(); - return m.group(0); + return m.group(1); } private boolean needToDownloadLatestNetwork() throws IOException { From 5cb07835b1c7c6c885326e64a5552c3577975103 Mon Sep 17 00:00:00 2001 From: Adrian Petrescu Date: Sun, 3 Jun 2018 23:35:33 -0400 Subject: [PATCH 09/75] Default automatically-download-latest-network to true Now that getBestNetworkHash() works again, I don't see why we wouldn't automatically download the latest network, just to get closer to a 0-setup situation. The GUI prompts the user in any case, and doesn't do anything if there is already a %network-file present, so it is never harmful. --- src/main/java/featurecat/lizzie/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 97d3e0789..a605ebf87 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -208,7 +208,7 @@ private JSONObject createDefaultConfig() { leelaz.put("max-game-thinking-time-seconds", 2); leelaz.put("print-comms", false); leelaz.put("analyze-update-interval-centisec", 10); - leelaz.put("automatically-download-latest-network", false); + leelaz.put("automatically-download-latest-network", true); config.put("leelaz", leelaz); From 81fb17f3387ebe2b5b59d1ae82056be51add2935 Mon Sep 17 00:00:00 2001 From: Adrian Petrescu Date: Mon, 4 Jun 2018 00:00:24 -0400 Subject: [PATCH 10/75] Package assets as part of the JAR In my continuing effort to make Lizzie relocatable, the assets should be included as part of the JAR, rather than requiring the user to run Lizzie from inside a directory that has the /assets folder. This accomplishes that. Tested and works locally. --- .../java/featurecat/lizzie/theme/DefaultTheme.java | 8 ++++---- .../main/resources/assets}/background.jpg | Bin {assets => src/main/resources/assets}/black0.png | Bin {assets => src/main/resources/assets}/board.png | Bin {assets => src/main/resources/assets}/white0.png | Bin 5 files changed, 4 insertions(+), 4 deletions(-) rename {assets => src/main/resources/assets}/background.jpg (100%) rename {assets => src/main/resources/assets}/black0.png (100%) rename {assets => src/main/resources/assets}/board.png (100%) rename {assets => src/main/resources/assets}/white0.png (100%) diff --git a/src/main/java/featurecat/lizzie/theme/DefaultTheme.java b/src/main/java/featurecat/lizzie/theme/DefaultTheme.java index f70bc1fa7..e6ea85ccd 100644 --- a/src/main/java/featurecat/lizzie/theme/DefaultTheme.java +++ b/src/main/java/featurecat/lizzie/theme/DefaultTheme.java @@ -20,7 +20,7 @@ public class DefaultTheme implements ITheme { public BufferedImage getBlackStone(int[] position) { if (blackStoneCached == null) { try { - blackStoneCached = ImageIO.read(new File("assets/black0.png")); + blackStoneCached = ImageIO.read(getClass().getResourceAsStream("/assets/black0.png")); } catch (IOException e) { e.printStackTrace(); } @@ -32,7 +32,7 @@ public BufferedImage getBlackStone(int[] position) { public BufferedImage getWhiteStone(int[] position) { if (whiteStoneCached == null) { try { - whiteStoneCached = ImageIO.read(new File("assets/white0.png")); + whiteStoneCached = ImageIO.read(getClass().getResourceAsStream("/assets/white0.png")); } catch (IOException e) { e.printStackTrace(); } @@ -44,7 +44,7 @@ public BufferedImage getWhiteStone(int[] position) { public BufferedImage getBoard() { if (boardCached == null) { try { - boardCached = ImageIO.read(new File("assets/board.png")); + boardCached = ImageIO.read(getClass().getResourceAsStream("/assets/board.png")); } catch (IOException e) { e.printStackTrace(); } @@ -56,7 +56,7 @@ public BufferedImage getBoard() { public BufferedImage getBackground() { if (backgroundCached == null) { try { - backgroundCached = ImageIO.read(new File("assets/background.jpg")); + backgroundCached = ImageIO.read(getClass().getResourceAsStream("/assets/background.jpg")); } catch (IOException e) { e.printStackTrace(); } diff --git a/assets/background.jpg b/src/main/resources/assets/background.jpg similarity index 100% rename from assets/background.jpg rename to src/main/resources/assets/background.jpg diff --git a/assets/black0.png b/src/main/resources/assets/black0.png similarity index 100% rename from assets/black0.png rename to src/main/resources/assets/black0.png diff --git a/assets/board.png b/src/main/resources/assets/board.png similarity index 100% rename from assets/board.png rename to src/main/resources/assets/board.png diff --git a/assets/white0.png b/src/main/resources/assets/white0.png similarity index 100% rename from assets/white0.png rename to src/main/resources/assets/white0.png From e471fef16e6dbabb61451c161c56ae0493d391d0 Mon Sep 17 00:00:00 2001 From: bittsitt Date: Mon, 4 Jun 2018 18:40:36 +0200 Subject: [PATCH 11/75] Check that leela zero is installed and network file exists. If not, warn user. --- src/main/java/featurecat/lizzie/Lizzie.java | 18 ++++++++++++++++++ .../featurecat/lizzie/analysis/Leelaz.java | 11 ++++++++++- .../resources/l10n/DisplayStrings.properties | 4 +++- .../l10n/DisplayStrings_zh_CN.properties | 2 ++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index f5c1b54cf..bbf33a7d9 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -5,9 +5,12 @@ import featurecat.lizzie.plugin.PluginManager; import featurecat.lizzie.rules.Board; import featurecat.lizzie.gui.LizzieFrame; +import org.json.JSONObject; import javax.swing.*; +import java.io.File; import java.io.IOException; +import java.util.ResourceBundle; /** * Main class. @@ -27,6 +30,21 @@ public static void main(String[] args) throws IOException, JSONException, ClassN config = new Config(); + // Check that user has installed leela zero + JSONObject leelazconfig = Lizzie.config.config.getJSONObject("leelaz"); + ResourceBundle resourceBundle = ResourceBundle.getBundle("l10n.DisplayStrings"); + String startfolder = leelazconfig.optString("engine-start-location", "."); + + // Check if engine is present + File lef = new File(startfolder + '/' + "leelaz"); + if (!lef.exists()) { + File leexe = new File(startfolder + '/' + "leelaz.exe"); + if (!leexe.exists()) { + JOptionPane.showMessageDialog(null, resourceBundle.getString("LizzieFrame.display.leelaz-missing"), "Lizzie - Error!", JOptionPane.ERROR_MESSAGE); + return; + } + } + PluginManager.loadPlugins(); board = new Board(); diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 507b6d78e..264f22b03 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -82,6 +82,15 @@ public Leelaz() throws IOException, JSONException { updateToLatestNetwork(); } + String startfolder = config.optString("engine-start-location", "."); + + // Check if network file is present + File wf = new File(startfolder + '/' + config.getString("network-file")); + if (!wf.exists()) { + 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 @@ -91,7 +100,7 @@ public Leelaz() throws IOException, JSONException { // run leelaz ProcessBuilder processBuilder = new ProcessBuilder(commands); - processBuilder.directory(new File(config.optString("engine-start-location", "."))); + processBuilder.directory(new File(startfolder)); processBuilder.redirectErrorStream(true); process = processBuilder.start(); diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index ca7686f4a..70e3d4405 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -48,4 +48,6 @@ LizzieFrame.display.pondering=Pondering LizzieFrame.display.on=on LizzieFrame.display.off=off LizzieFrame.display.loading=Leela Zero is loading... -LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. \ No newline at end of file +LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. +LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage +LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage \ No newline at end of file diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index 3c26237c0..b07c46c64 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -37,3 +37,5 @@ LizzieFrame.display.on=\u5F00\u542F LizzieFrame.display.off=\u6682\u505C LizzieFrame.display.loading=Leela Zero\u6B63\u5728\u8F7D\u5165\u4E2D... LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. +LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage +LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage From 6a326a6c9dbfc1ed4779aa7dc6d463e7bbca56bc Mon Sep 17 00:00:00 2001 From: featurecat Date: Mon, 4 Jun 2018 18:37:40 -0400 Subject: [PATCH 12/75] Update Config.java actually the reason the default is false is because the current download function can be exceptionally slow. this will be default true when we bundle wget or curl with lizzie. --- src/main/java/featurecat/lizzie/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index a605ebf87..97d3e0789 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -208,7 +208,7 @@ private JSONObject createDefaultConfig() { leelaz.put("max-game-thinking-time-seconds", 2); leelaz.put("print-comms", false); leelaz.put("analyze-update-interval-centisec", 10); - leelaz.put("automatically-download-latest-network", true); + leelaz.put("automatically-download-latest-network", false); config.put("leelaz", leelaz); From 8eb4b32629457f76b837358fee10365bf835533c Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Tue, 5 Jun 2018 21:12:32 +0900 Subject: [PATCH 13/75] Fix: suggestion appears on a existing stone after Ctrl-C --- .../java/featurecat/lizzie/rules/BoardHistoryList.java | 7 +++++++ src/main/java/featurecat/lizzie/rules/SGFParser.java | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index 4ecee7644..30ba2b81c 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -29,6 +29,13 @@ public void setGameInfo(GameInfo gameInfo) { this.gameInfo = gameInfo; } + public BoardHistoryList shallowCopy() { + BoardHistoryList copy = new BoardHistoryList(null); + copy.head = head; + copy.gameInfo = gameInfo; + return copy; + } + /** * Clear history. */ diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index dbe5e5f16..61853d1db 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -217,7 +217,7 @@ public static void save(Board board, String filename) throws IOException { private static void saveToStream(Board board, Writer writer) throws IOException { // collect game info - BoardHistoryList history = board.getHistory(); + BoardHistoryList history = board.getHistory().shallowCopy(); GameInfo gameInfo = history.getGameInfo(); String playerBlack = gameInfo.getPlayerBlack(); String playerWhite = gameInfo.getPlayerWhite(); From f8cec279b6aa8436cfdfe1fffffa9dadd8883869 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Tue, 5 Jun 2018 21:45:13 +0900 Subject: [PATCH 14/75] Increase/decrease alpha value by ctrl-shift-pgup/pgdown --- .../java/featurecat/lizzie/gui/BoardRenderer.java | 8 +++++++- src/main/java/featurecat/lizzie/gui/Input.java | 12 ++++++++++-- src/main/java/featurecat/lizzie/gui/LizzieFrame.java | 4 ++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 8a85a4e51..da28817a9 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -61,6 +61,8 @@ public class BoardRenderer { private boolean showingBranch = false; private boolean isMainBoard = false; + private int maxAlpha = 240; + public BoardRenderer(boolean isMainBoard) { uiConfig = Lizzie.config.config.getJSONObject("ui"); theme = ITheme.loadTheme(uiConfig.getString("theme")); @@ -497,7 +499,7 @@ private void drawLeelazSuggestions(Graphics2D g) { final int MIN_ALPHA = 32; final int MIN_ALPHA_TO_DISPLAY_TEXT = 64; - final int MAX_ALPHA = 240; + final int MAX_ALPHA = maxAlpha = Math.max(maxAlpha, MIN_ALPHA_TO_DISPLAY_TEXT); final double HUE_SCALING_FACTOR = 3.0; final double ALPHA_SCALING_FACTOR = 5.0; final float GREEN_HUE = Color.RGBtoHSB(0,1,0,null)[0]; @@ -1002,4 +1004,8 @@ public boolean isInside(int x1, int y1) { private boolean showCoordinates() { return isMainBoard && Lizzie.frame.showCoordinates; } + + public void increaseMaxAlpha(int k) { + maxAlpha = Math.min(maxAlpha + k, 255); + } } diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index 044146db5..6f55b5888 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -197,7 +197,11 @@ public void keyPressed(KeyEvent e) { break; case VK_PAGE_DOWN: - redo(10); + if (controlIsPressed(e) && e.isShiftDown()) { + Lizzie.frame.increaseMaxAlpha(-5); + } else { + redo(10); + } break; case VK_DOWN: @@ -245,7 +249,11 @@ public void keyPressed(KeyEvent e) { break; case VK_PAGE_UP: - undo(10); + if (controlIsPressed(e) && e.isShiftDown()) { + Lizzie.frame.increaseMaxAlpha(5); + } else { + undo(10); + } break; case VK_I: diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index fa45ff9de..47565fbb3 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -896,4 +896,8 @@ public void pasteSgf() { e.printStackTrace(); } } + + public void increaseMaxAlpha(int k) { + boardRenderer.increaseMaxAlpha(k); + } } From dd5c10480629c019cf5549b77a41ac3c4ba41c8d Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Tue, 5 Jun 2018 21:45:25 +0900 Subject: [PATCH 15/75] Make modified alpha value persistent --- src/main/java/featurecat/lizzie/Config.java | 19 +++++++++++-------- .../featurecat/lizzie/gui/BoardRenderer.java | 8 +++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 19398b26d..1b0303c74 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -32,7 +32,7 @@ public class Config { private String configFilename = "config.txt"; private String persistFilename = "persist"; - private JSONObject loadAndMergeConfig(JSONObject defaultCfg, String fileName) throws IOException { + private JSONObject loadAndMergeConfig(JSONObject defaultCfg, String fileName, boolean needValidation) throws IOException { File file = new File(fileName); if (!file.canRead()) { System.err.printf("Creating config file %s\n", fileName); @@ -59,7 +59,7 @@ private JSONObject loadAndMergeConfig(JSONObject defaultCfg, String fileName) th fp.close(); // Validate and correct settings - if (validateAndCorrectSettings(mergedcfg)) { + if (needValidation && validateAndCorrectSettings(mergedcfg)) { modified = true; } @@ -82,8 +82,7 @@ private JSONObject loadAndMergeConfig(JSONObject defaultCfg, String fileName) th * @return if any correction has been made. */ private boolean validateAndCorrectSettings(JSONObject config) { - // We don't care persisted settings. They are managed automatically. - if (config == null || !config.has("ui")) { + if (config == null) { return false; } @@ -125,9 +124,9 @@ public Config() throws IOException { JSONObject persistConfig = createPersistConfig(); // Main properties - this.config = loadAndMergeConfig(defaultConfig, configFilename); + this.config = loadAndMergeConfig(defaultConfig, configFilename, true); // Persisted properties - this.persisted = loadAndMergeConfig(persistConfig, persistFilename); + this.persisted = loadAndMergeConfig(persistConfig, persistFilename, false); leelazConfig = config.getJSONObject("leelaz"); uiConfig = config.getJSONObject("ui"); @@ -274,12 +273,16 @@ private JSONObject createPersistConfig() { config.put("filesystem", filesys); // About User Interface display - //JSONObject ui = new JSONObject(); + JSONObject ui = new JSONObject(); //ui.put("window-height", 657); //ui.put("window-width", 687); + //ui.put("max-alpha", 240); - //config.put("ui", ui); + // Avoid the key "ui" because it was used to distinguish "config" and "persist" + // in old version of validateAndCorrectSettings(). + // If we use "ui" here, we will have trouble to run old lizzie. + config.put("ui-persist", ui); return config; } diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index da28817a9..3bbdc618e 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -1,6 +1,7 @@ package featurecat.lizzie.gui; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Branch; @@ -30,7 +31,7 @@ public class BoardRenderer { private int x, y; private int boardLength; - private JSONObject uiConfig; + private JSONObject uiConfig, uiPersist; private int scaledMargin, availableLength, squareLength, stoneRadius; private Branch branch; @@ -69,6 +70,10 @@ public BoardRenderer(boolean isMainBoard) { if (theme == null) { theme = new DefaultTheme(); } + uiPersist = Lizzie.config.persisted.getJSONObject("ui-persist"); + try { + maxAlpha = uiPersist.getInt("max-alpha"); + } catch (JSONException e) {} this.isMainBoard = isMainBoard; } @@ -1007,5 +1012,6 @@ private boolean showCoordinates() { public void increaseMaxAlpha(int k) { maxAlpha = Math.min(maxAlpha + k, 255); + uiPersist.put("max-alpha", maxAlpha); } } From 69dedf48bd3857a9b21ba97266718356f9f762bd Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Tue, 5 Jun 2018 21:46:26 +0900 Subject: [PATCH 16/75] Fix too slow "End", "Ctrl-V", etc. by omitting unnecessary lz-analyze --- .../featurecat/lizzie/analysis/Leelaz.java | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 507b6d78e..27eb8ec1c 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -33,6 +33,7 @@ public class Leelaz { private long maxAnalyzeTimeMillis;//, maxThinkingTimeMillis; private int cmdNumber; private int currentCmdNum; + private ArrayDeque cmdQueue; private Process process; @@ -71,7 +72,8 @@ public Leelaz() throws IOException, JSONException { isPondering = false; startPonderTime = System.currentTimeMillis(); cmdNumber = 1; - currentCmdNum = -1; + currentCmdNum = 0; + cmdQueue = new ArrayDeque<>(); JSONObject config = Lizzie.config.config.getJSONObject("leelaz"); @@ -188,7 +190,7 @@ private void parseLine(String line) { // End of response } else if (line.startsWith("info")) { isLoaded = true; - if (currentCmdNum == cmdNumber - 1) { + if (isResponseUpToDate()) { // This should not be stale data when the command number match parseInfo(line.substring(5)); notifyBestMoveListeners(); @@ -205,14 +207,16 @@ private void parseLine(String line) { } isThinking = false; - } else if (Lizzie.frame != null && line.startsWith("=")) { + } else if (Lizzie.frame != null && (line.startsWith("=") || line.startsWith("?"))) { if (printCommunication) { System.out.print(line); } String[] params = line.trim().split(" "); currentCmdNum = Integer.parseInt(params[0].substring(1).trim()); - if (params.length == 1) return; + trySendCommandFromQueue(); + + if (line.startsWith("?") || params.length == 1) return; if (isSettingHandicap) { @@ -284,11 +288,43 @@ private void read() { } /** - * Sends a command for leelaz to execute + * Sends a command to command queue for leelaz to execute * * @param command a GTP command containing no newline characters */ public void sendCommand(String command) { + synchronized(cmdQueue) { + String lastCommand = cmdQueue.peekLast(); + // For efficiency, delete unnecessary "lz-analyze" that will be stopped immediately + if (lastCommand != null && lastCommand.startsWith("lz-analyze")) { + cmdQueue.removeLast(); + } + cmdQueue.addLast(command); + trySendCommandFromQueue(); + } + } + + /** + * Sends a command from command queue for leelaz to execute if it is ready + */ + private void trySendCommandFromQueue() { + if (!isResponseUpToDate()) { + return; // leelaz is not ready yet + } + synchronized(cmdQueue) { + String command = cmdQueue.pollFirst(); + if (command != null) { + sendCommandToLeelaz(command); + } + } + } + + /** + * Sends a command for leelaz to execute + * + * @param command a GTP command containing no newline characters + */ + private void sendCommandToLeelaz(String command) { if (command.startsWith("fixed_handicap")) isSettingHandicap = true; command = cmdNumber + " " + command; @@ -304,6 +340,14 @@ public void sendCommand(String command) { } } + /** + * Check whether leelaz is responding to the last command + */ + private boolean isResponseUpToDate() { + // Use >= instead of == for avoiding hang-up, though it cannot happen + return currentCmdNum >= cmdNumber - 1; + } + /** * @param color color of stone to play * @param move coordinate of the coordinate From 526c9f7cf3f653b0bbb3228ee432a2960cb28870 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Tue, 5 Jun 2018 21:46:38 +0900 Subject: [PATCH 17/75] Send most commands immediately for fear of possible hang-up --- .../java/featurecat/lizzie/analysis/Leelaz.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 27eb8ec1c..18b76a976 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -308,14 +308,19 @@ public void sendCommand(String command) { * Sends a command from command queue for leelaz to execute if it is ready */ private void trySendCommandFromQueue() { - if (!isResponseUpToDate()) { - return; // leelaz is not ready yet - } + // Defer sending "lz-analyze" if leelaz is not ready yet. + // Though all commands should be deferred theoretically, + // only "lz-analyze" is differed here for fear of + // possible hang-up by missing response for some reason. + // cmdQueue can be replaced with a mere String variable in this case, + // but it is kept for future change of our mind. synchronized(cmdQueue) { - String command = cmdQueue.pollFirst(); - if (command != null) { - sendCommandToLeelaz(command); + String command = cmdQueue.peekFirst(); + if (command == null || (command.startsWith("lz-analyze") && !isResponseUpToDate())) { + return; } + cmdQueue.removeFirst(); + sendCommandToLeelaz(command); } } From bd4c12e9b0e5e921be363dcf90e8c80b782a3821 Mon Sep 17 00:00:00 2001 From: Magnus Lundmark Date: Tue, 5 Jun 2018 15:00:36 +0200 Subject: [PATCH 18/75] Added config switch for default window size and whether to start maximized. --- lizzie.sh | 2 +- src/main/java/featurecat/lizzie/Config.java | 7 +++++-- src/main/java/featurecat/lizzie/gui/LizzieFrame.java | 10 +++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lizzie.sh b/lizzie.sh index 01747a508..56f2b2891 100755 --- a/lizzie.sh +++ b/lizzie.sh @@ -1,2 +1,2 @@ -java -jar -Dsun.java2d.opengl=true ./target/lizzie-0.4-shaded.jar 2>/dev/null & +java -jar -Dsun.java2d.opengl=true ./target/lizzie-0.5-shaded.jar 2>/dev/null & diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 19398b26d..7e25aa03c 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -23,7 +23,8 @@ public class Config { public boolean showNextMoves = true; public boolean showSubBoard = true; public boolean largeSubBoard = false; - + public boolean startMaximized = true; + public JSONObject config; public JSONObject leelazConfig; public JSONObject uiConfig; @@ -141,6 +142,7 @@ public Config() throws IOException { showSubBoard = uiConfig.getBoolean("show-subboard"); largeSubBoard = uiConfig.getBoolean("large-subboard"); handicapInsteadOfWinrate = uiConfig.getBoolean("handicap-instead-of-winrate"); + startMaximized = uiConfig.getBoolean("window-maximized"); } // Modifies config by adding in values from default_config that are missing. @@ -196,7 +198,6 @@ public boolean showLargeSubBoard() { return showSubBoard && largeSubBoard; } - /** * Scans the current directory as well as the current PATH to find a reasonable default leelaz binary. * @@ -259,6 +260,8 @@ private JSONObject createDefaultConfig() { ui.put("confirm-exit", false); ui.put("handicap-instead-of-winrate",false); ui.put("board-size", 19); + ui.put("window-size", new JSONArray("[0, 0]")); + ui.put("window-maximized", false); config.put("ui", ui); return config; diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index fa45ff9de..7dcd9453c 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -20,6 +20,7 @@ import featurecat.lizzie.rules.GIBParser; import featurecat.lizzie.rules.SGFParser; import org.json.JSONObject; +import org.json.JSONArray; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; @@ -112,11 +113,14 @@ public LizzieFrame() { variationTree = new VariationTree(); winrateGraph = new WinrateGraph(); - // on 1080p screens in Windows, this is a good width/height. removing a default size causes problems in Linux - setSize(657, 687); setMinimumSize( new Dimension(640,480) ); setLocationRelativeTo(null); // start centered - setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized + JSONArray windowSize = Lizzie.config.uiConfig.getJSONArray("window-size"); + setSize(windowSize.getInt(0), windowSize.getInt(1)); // use config file window size + + if (Lizzie.config.startMaximized) { + setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized + } setVisible(true); From fffefb5d2a4d34e6a709b6ee50fc1bccd152c259 Mon Sep 17 00:00:00 2001 From: Magnus Lundmark Date: Tue, 5 Jun 2018 15:06:02 +0200 Subject: [PATCH 19/75] Default window size is 1024x768 --- src/main/java/featurecat/lizzie/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 7e25aa03c..c2b5685b7 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -260,7 +260,7 @@ private JSONObject createDefaultConfig() { ui.put("confirm-exit", false); ui.put("handicap-instead-of-winrate",false); ui.put("board-size", 19); - ui.put("window-size", new JSONArray("[0, 0]")); + ui.put("window-size", new JSONArray("[1024, 768]")); ui.put("window-maximized", false); config.put("ui", ui); From b8d390f59bcc1e95c1140e8cc83d2df5b9f7c55f Mon Sep 17 00:00:00 2001 From: Adrian Petrescu Date: Wed, 6 Jun 2018 21:46:11 -0400 Subject: [PATCH 20/75] Use /best-network-hash instead of parsing the webpage. --- .../featurecat/lizzie/analysis/Leelaz.java | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index f811f329f..108dc8810 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -123,23 +123,7 @@ private void updateToLatestNetwork() { } private String getBestNetworkHash() throws IOException { - // finds a valid network hash - Pattern networkHashFinder = Pattern.compile("\"hash\":\"([a-f0-9]+)\","); - - String networks = null; - try { - networks = Util.downloadAsString(new URL(baseURL + "/network-profiles")); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - if (networks == null) { - throw new IOException("Could not determine the best network URL"); - } - Matcher m = networkHashFinder.matcher(networks); - // get the first match - this will be the best network. - m.find(); - - return m.group(1); + return Util.downloadAsString(new URL(baseURL + "/best-network-hash")).split("\n")[0]; } private boolean needToDownloadLatestNetwork() throws IOException { From bc2ed0e19369d1f60070be59a51c5df26eb8076b Mon Sep 17 00:00:00 2001 From: TFiFiE Date: Fri, 8 Jun 2018 21:25:52 +0200 Subject: [PATCH 21/75] Always show statistics for blue-ringed moves. --- src/main/java/featurecat/lizzie/gui/BoardRenderer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 8a85a4e51..0ca3f42fb 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -535,6 +535,7 @@ private void drawLeelazSuggestions(Graphics2D g) { continue; boolean isBestMove = bestMoves.get(0) == move; + boolean hasMaxWinrate = move.winrate == maxWinrate; if (move.playouts == 0) // this actually can happen continue; @@ -566,7 +567,7 @@ private void drawLeelazSuggestions(Graphics2D g) { if (branch == null || (isBestMove && Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { int strokeWidth = 1; - if (isBestMove != (move.winrate == maxWinrate)) { + if (isBestMove != hasMaxWinrate) { strokeWidth = 2; g.setColor(isBestMove ? Color.RED : Color.BLUE); g.setStroke(new BasicStroke(strokeWidth)); @@ -578,7 +579,7 @@ private void drawLeelazSuggestions(Graphics2D g) { } - if (branch == null && alpha >= MIN_ALPHA_TO_DISPLAY_TEXT || (Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { + if (branch == null && (alpha >= MIN_ALPHA_TO_DISPLAY_TEXT || hasMaxWinrate) || (Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { double roundedWinrate = Math.round(move.winrate * 10) / 10.0; if (uiConfig.getBoolean("win-rate-always-black") && !Lizzie.board.getData().blackToPlay) { roundedWinrate = 100.0 - roundedWinrate; From c44af771fd24099ee8013714201f7bf4c83fd9a5 Mon Sep 17 00:00:00 2001 From: featurecat Date: Sat, 9 Jun 2018 22:50:52 -0400 Subject: [PATCH 22/75] merged bittsitt's and apetresc's changes --- src/main/java/featurecat/lizzie/Config.java | 4 ++-- src/main/java/featurecat/lizzie/analysis/Leelaz.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 19398b26d..4b302481a 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -111,7 +111,7 @@ private boolean validateAndCorrectSettings(JSONObject config) { } // Similar checks for startup directory. It should exist and should be a directory. - String engineStartLocation = leelaz.optString("engine-start-location", "."); + String engineStartLocation = getBestDefaultLeelazPath(); if (!(Files.exists(Paths.get(engineStartLocation)) && Files.isDirectory(Paths.get(engineStartLocation)))) { leelaz.put("engine-start-location", "."); madeCorrections = true; @@ -203,7 +203,7 @@ public boolean showLargeSubBoard() { * @return A working path to a leelaz binary. If there are none on the PATH, "./leelaz" is returned for backwards * compatibility. */ - private String getBestDefaultLeelazPath() { + public static String getBestDefaultLeelazPath() { List potentialPaths = new ArrayList<>(); potentialPaths.add("."); potentialPaths.addAll(Arrays.asList(System.getenv("PATH").split(":"))); diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 5de2fc4f2..29bab4450 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -1,5 +1,6 @@ package featurecat.lizzie.analysis; +import featurecat.lizzie.Config; import featurecat.lizzie.Lizzie; import featurecat.lizzie.Util; import org.json.JSONArray; @@ -82,7 +83,7 @@ public Leelaz() throws IOException, JSONException { updateToLatestNetwork(); } - String startfolder = config.optString("engine-start-location", "."); + String startfolder = Config.getBestDefaultLeelazPath(); // todo make this a little more obvious/less bug-prone // Check if network file is present File wf = new File(startfolder + '/' + config.getString("network-file")); From 0cb3e4ea9e1ca7717256b2a530533a74a2441888 Mon Sep 17 00:00:00 2001 From: featurecat Date: Mon, 11 Jun 2018 08:39:15 -0400 Subject: [PATCH 23/75] Delete lizzie.sh --- lizzie.sh | 2 -- 1 file changed, 2 deletions(-) delete mode 100755 lizzie.sh diff --git a/lizzie.sh b/lizzie.sh deleted file mode 100755 index 56f2b2891..000000000 --- a/lizzie.sh +++ /dev/null @@ -1,2 +0,0 @@ -java -jar -Dsun.java2d.opengl=true ./target/lizzie-0.5-shaded.jar 2>/dev/null & - From 375a5cd780af0f351be784e4acaaa798da7c1ce9 Mon Sep 17 00:00:00 2001 From: featurecat Date: Mon, 11 Jun 2018 08:39:57 -0400 Subject: [PATCH 24/75] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 86188e699..79e484680 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ leelaz src/main/resources/META-INF target leelaz_opencl_tuning +lizzie.sh From c7995410652bb26ff28630385cc2ba31b47f42de Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Wed, 13 Jun 2018 21:31:38 +0900 Subject: [PATCH 25/75] Fix: startfolder was "./leelaz" ("." is correct) --- src/main/java/featurecat/lizzie/analysis/Leelaz.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 29bab4450..fb03224ac 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -83,7 +83,7 @@ public Leelaz() throws IOException, JSONException { updateToLatestNetwork(); } - String startfolder = Config.getBestDefaultLeelazPath(); // todo make this a little more obvious/less bug-prone + String startfolder = new File(Config.getBestDefaultLeelazPath()).getParent(); // todo make this a little more obvious/less bug-prone // Check if network file is present File wf = new File(startfolder + '/' + config.getString("network-file")); From 73d076d38eefeabacef95ef006695e627b707bc3 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Wed, 13 Jun 2018 21:41:02 +0900 Subject: [PATCH 26/75] Add autosave and resume (option "resume-previous-game") --- src/main/java/featurecat/lizzie/Config.java | 1 + src/main/java/featurecat/lizzie/Lizzie.java | 7 ++++++ .../java/featurecat/lizzie/rules/Board.java | 23 +++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index ef45c3d76..ca06cead6 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -257,6 +257,7 @@ private JSONObject createDefaultConfig() { ui.put("large-subboard", false); ui.put("win-rate-always-black", false); ui.put("confirm-exit", false); + ui.put("resume-previous-game", false); ui.put("handicap-instead-of-winrate",false); ui.put("board-size", 19); ui.put("window-size", new JSONArray("[1024, 768]")); diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index bbf33a7d9..7b7bee280 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -4,6 +4,7 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.plugin.PluginManager; import featurecat.lizzie.rules.Board; +import featurecat.lizzie.rules.SGFParser; import featurecat.lizzie.gui.LizzieFrame; import org.json.JSONObject; @@ -57,6 +58,9 @@ public static void main(String[] args) throws IOException, JSONException, ClassN if(config.handicapInsteadOfWinrate) { leelaz.estimatePassWinrate(); } + if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { + board.resumePreviousGame(); + } leelaz.togglePonder(); } catch (IOException e) { e.printStackTrace(); @@ -74,6 +78,9 @@ public static void shutdown() { LizzieFrame.saveSgf(); } } + if (board != null) { + board.autosaveToMemory(); + } try { config.persist(); diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index f04e96767..3c10dfb5a 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -4,12 +4,15 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.analysis.LeelazListener; import featurecat.lizzie.analysis.MoveData; +import featurecat.lizzie.rules.SGFParser; +import java.io.IOException; import javax.swing.*; import java.util.ArrayDeque; import java.util.Deque; import java.util.Queue; import java.util.List; +import org.json.JSONException; public class Board implements LeelazListener { public static final int BOARD_SIZE = Lizzie.config.config.getJSONObject("ui").optInt("board-size", 19); @@ -1051,4 +1054,24 @@ public void bestMoveNotification(List bestMoves) { } } } + + public void autosave() { + autosaveToMemory(); + try { + Lizzie.config.persist(); + } catch (IOException err) {} + } + + public void autosaveToMemory() { + try { + Lizzie.config.persisted.put("autosave", SGFParser.saveToString()); + } catch (IOException err) {} + } + + public void resumePreviousGame() { + try { + SGFParser.loadFromString(Lizzie.config.persisted.getString("autosave")); + while (nextMove()) ; + } catch (JSONException err) {} + } } From 131cc118ba4e8bb9a9fb00958e10eeeac476417e Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Wed, 13 Jun 2018 21:41:10 +0900 Subject: [PATCH 27/75] Autosave board periodically (option "autosave-interval-seconds") --- src/main/java/featurecat/lizzie/Config.java | 1 + src/main/java/featurecat/lizzie/gui/LizzieFrame.java | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index ca06cead6..768910e99 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -258,6 +258,7 @@ private JSONObject createDefaultConfig() { ui.put("win-rate-always-black", false); ui.put("confirm-exit", false); ui.put("resume-previous-game", false); + ui.put("autosave-interval-seconds", -1); ui.put("handicap-instead-of-winrate",false); ui.put("board-size", 19); ui.put("window-size", new JSONArray("[1024, 768]")); diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 366e0d645..172fc34a3 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -92,6 +92,8 @@ public class LizzieFrame extends JFrame { // Get the font name in current system locale private String systemDefaultFontName = new JLabel().getFont().getFontName(); + private long lastAutosaveTime = System.currentTimeMillis(); + static { // load fonts try { @@ -252,6 +254,7 @@ public static void openSgf() { * @param g0 not used */ public void paint(Graphics g0) { + autosaveMaybe(); if (bs == null) return; @@ -835,6 +838,15 @@ public void onMouseDragged(int x, int y) { } } + private void autosaveMaybe() { + int interval = Lizzie.config.config.getJSONObject("ui").getInt("autosave-interval-seconds") * 1000; + long currentTime = System.currentTimeMillis(); + if (interval > 0 && currentTime - lastAutosaveTime >= interval) { + Lizzie.board.autosave(); + lastAutosaveTime = currentTime; + } + } + public void toggleCoordinates() { showCoordinates = !showCoordinates; } From 53632832dbde6624766061d6184e64611154b3f4 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Wed, 13 Jun 2018 21:41:19 +0900 Subject: [PATCH 28/75] Autosave only when the board is modified --- .../java/featurecat/lizzie/rules/Board.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 3c10dfb5a..81fe97825 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -1056,16 +1056,24 @@ public void bestMoveNotification(List bestMoves) { } public void autosave() { - autosaveToMemory(); - try { - Lizzie.config.persist(); - } catch (IOException err) {} + if (autosaveToMemory()) { + try { + Lizzie.config.persist(); + } catch (IOException err) {} + } } - public void autosaveToMemory() { + public boolean autosaveToMemory() { try { - Lizzie.config.persisted.put("autosave", SGFParser.saveToString()); - } catch (IOException err) {} + String sgf = SGFParser.saveToString(); + if (sgf.equals(Lizzie.config.persisted.getString("autosave"))) { + return false; + } + Lizzie.config.persisted.put("autosave", sgf); + } catch (Exception err) { // IOException or JSONException + return false; + } + return true; } public void resumePreviousGame() { From 16d0a41ad79060d23ce5bda200b6e2e56f69b635 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Sat, 23 Jun 2018 21:40:17 +0900 Subject: [PATCH 29/75] Fix #296 (MultiGo SGF detection) --- src/main/java/featurecat/lizzie/rules/SGFParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 61853d1db..f570d3eeb 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -72,7 +72,7 @@ private static boolean parse(String value) { StringBuilder tagContentBuilder = new StringBuilder(); // MultiGo 's branch: (Main Branch (Main Branch) (Branch) ) // Other 's branch: (Main Branch (Branch) Main Branch) - if (value.charAt(value.length() - 2) == ')') { + if (value.matches("(?m)(?s).*\\)\\s*\\)")) { isMultiGo = true; } From 5257231912c97ceac3609763cbfe93250b9d5218 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Sat, 23 Jun 2018 22:53:31 +0900 Subject: [PATCH 30/75] Delete unnecessary flag in regexp --- src/main/java/featurecat/lizzie/rules/SGFParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index f570d3eeb..c5f274b12 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -72,7 +72,7 @@ private static boolean parse(String value) { StringBuilder tagContentBuilder = new StringBuilder(); // MultiGo 's branch: (Main Branch (Main Branch) (Branch) ) // Other 's branch: (Main Branch (Branch) Main Branch) - if (value.matches("(?m)(?s).*\\)\\s*\\)")) { + if (value.matches("(?s).*\\)\\s*\\)")) { isMultiGo = true; } From b5bb15a76ea7bc1800b2229fc748e9ffa8ebcafb Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sat, 14 Jul 2018 10:41:11 +0200 Subject: [PATCH 31/75] Add setting to hide status text I found it particularly annoying to have the "Pondering on" text on top of the goban. --- src/main/java/featurecat/lizzie/Config.java | 3 +++ .../java/featurecat/lizzie/gui/LizzieFrame.java | 15 +++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 768910e99..14b5c155d 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -18,6 +18,7 @@ public class Config { public boolean showRawBoard = false; public boolean handicapInsteadOfWinrate = false; + public boolean showStatus = true; public boolean showBranch = true; public boolean showBestMoves = true; public boolean showNextMoves = true; @@ -133,6 +134,7 @@ public Config() throws IOException { uiConfig = config.getJSONObject("ui"); showMoveNumber = uiConfig.getBoolean("show-move-number"); + showStatus = uiConfig.getBoolean("show-status"); showBranch = uiConfig.getBoolean("show-leelaz-variation"); showWinrate = uiConfig.getBoolean("show-winrate"); showVariationGraph = uiConfig.getBoolean("show-variation-graph"); @@ -248,6 +250,7 @@ private JSONObject createDefaultConfig() { ui.put("fancy-board", true); ui.put("shadow-size", 100); ui.put("show-move-number", false); + ui.put("show-status", true); ui.put("show-leelaz-variation", true); ui.put("show-winrate", true); ui.put("show-variation-graph", true); diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 172fc34a3..e206368d3 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -115,7 +115,7 @@ public LizzieFrame() { variationTree = new VariationTree(); winrateGraph = new WinrateGraph(); - setMinimumSize( new Dimension(640,480) ); + setMinimumSize( new Dimension(640,480) ); setLocationRelativeTo(null); // start centered JSONArray windowSize = Lizzie.config.uiConfig.getJSONArray("window-size"); setSize(windowSize.getInt(0), windowSize.getInt(1)); // use config file window size @@ -373,16 +373,19 @@ public void paint(Graphics g0) { cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) cachedImage.getGraphics(); - drawCommandString(g); + if (Lizzie.config.showStatus) + drawCommandString(g); boardRenderer.setLocation(boardX, boardY); boardRenderer.setBoardLength(maxSize); boardRenderer.draw(g); if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.pondering") + - (Lizzie.leelaz.isPondering()?resourceBundle.getString("LizzieFrame.display.on"):resourceBundle.getString("LizzieFrame.display.off")), - ponderingX, ponderingY, ponderingSize); + if (Lizzie.config.showStatus) { + drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.pondering") + + (Lizzie.leelaz.isPondering()?resourceBundle.getString("LizzieFrame.display.on"):resourceBundle.getString("LizzieFrame.display.off")), + ponderingX, ponderingY, ponderingSize); + } // Todo: Make board move over when there is no space beside the board if (Lizzie.config.showWinrate) { @@ -404,7 +407,7 @@ public void paint(Graphics g0) { // This can happen when no space is left for subboard. } } - } else { + } else if (Lizzie.config.showStatus) { drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.loading"), loadingX, loadingY, loadingSize); } From b20aa59affc8cd8f35f34f9a2f201b7fb5b0820f Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sat, 14 Jul 2018 10:48:33 +0200 Subject: [PATCH 32/75] Add setting to hide captured stones panel --- src/main/java/featurecat/lizzie/Config.java | 3 +++ src/main/java/featurecat/lizzie/gui/LizzieFrame.java | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 768910e99..e6c8819a0 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -16,6 +16,7 @@ public class Config { public boolean showWinrate = true; public boolean showVariationGraph = true; public boolean showRawBoard = false; + public boolean showCaptured = true; public boolean handicapInsteadOfWinrate = false; public boolean showBranch = true; @@ -136,6 +137,7 @@ public Config() throws IOException { showBranch = uiConfig.getBoolean("show-leelaz-variation"); showWinrate = uiConfig.getBoolean("show-winrate"); showVariationGraph = uiConfig.getBoolean("show-variation-graph"); + showCaptured = uiConfig.getBoolean("show-captured"); showBestMoves = uiConfig.getBoolean("show-best-moves"); showNextMoves = uiConfig.getBoolean("show-next-moves"); showSubBoard = uiConfig.getBoolean("show-subboard"); @@ -251,6 +253,7 @@ private JSONObject createDefaultConfig() { ui.put("show-leelaz-variation", true); ui.put("show-winrate", true); ui.put("show-variation-graph", true); + ui.put("show-captured", true); ui.put("show-best-moves", true); ui.put("show-next-moves", true); ui.put("show-subboard", true); diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 172fc34a3..4d79865ca 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -115,7 +115,7 @@ public LizzieFrame() { variationTree = new VariationTree(); winrateGraph = new WinrateGraph(); - setMinimumSize( new Dimension(640,480) ); + setMinimumSize( new Dimension(640,480) ); setLocationRelativeTo(null); // start centered JSONArray windowSize = Lizzie.config.uiConfig.getJSONArray("window-size"); setSize(windowSize.getInt(0), windowSize.getInt(1)); // use config file window size @@ -408,7 +408,8 @@ public void paint(Graphics g0) { drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.loading"), loadingX, loadingY, loadingSize); } - drawCaptured(g, capx, capy, capw, caph); + if (Lizzie.config.showCaptured) + drawCaptured(g, capx, capy, capw, caph); // cleanup g.dispose(); From 11ec0aa045f0f5fbb8d976b7fe31bdd1c0eada93 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sat, 14 Jul 2018 11:01:18 +0200 Subject: [PATCH 33/75] Rename {save,open}Sgf to {.}File --- src/main/java/featurecat/lizzie/Lizzie.java | 4 ++-- src/main/java/featurecat/lizzie/gui/Input.java | 8 ++++---- src/main/java/featurecat/lizzie/gui/LizzieFrame.java | 10 +++++----- src/main/resources/l10n/DisplayStrings.properties | 8 ++++---- .../resources/l10n/DisplayStrings_zh_CN.properties | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index 7b7bee280..8f39c44d9 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -67,7 +67,7 @@ public static void main(String[] args) throws IOException, JSONException, ClassN } }).start(); - + } public static void shutdown() { @@ -75,7 +75,7 @@ public static void shutdown() { if (board != null && config.config.getJSONObject("ui").getBoolean("confirm-exit")) { int ret = JOptionPane.showConfirmDialog(null, "Do you want to save this SGF?", "Save SGF?", JOptionPane.OK_CANCEL_OPTION); if (ret == JOptionPane.OK_OPTION) { - LizzieFrame.saveSgf(); + LizzieFrame.saveFile(); } } if (board != null) { diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index 6f55b5888..94e76d87b 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -195,7 +195,7 @@ public void keyPressed(KeyEvent e) { undo(); } break; - + case VK_PAGE_DOWN: if (controlIsPressed(e) && e.isShiftDown()) { Lizzie.frame.increaseMaxAlpha(-5); @@ -247,7 +247,7 @@ public void keyPressed(KeyEvent e) { case VK_H: Lizzie.config.toggleHandicapInsteadOfWinrate(); break; - + case VK_PAGE_UP: if (controlIsPressed(e) && e.isShiftDown()) { Lizzie.frame.increaseMaxAlpha(5); @@ -266,13 +266,13 @@ public void keyPressed(KeyEvent e) { // stop the ponder if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); - LizzieFrame.saveSgf(); + LizzieFrame.saveFile(); break; case VK_O: if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); - LizzieFrame.openSgf(); + LizzieFrame.openFile(); break; case VK_V: diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 172fc34a3..8200818ad 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -115,7 +115,7 @@ public LizzieFrame() { variationTree = new VariationTree(); winrateGraph = new WinrateGraph(); - setMinimumSize( new Dimension(640,480) ); + setMinimumSize( new Dimension(640,480) ); setLocationRelativeTo(null); // start centered JSONArray windowSize = Lizzie.config.uiConfig.getJSONArray("window-size"); setSize(windowSize.getInt(0), windowSize.getInt(1)); // use config file window size @@ -185,7 +185,7 @@ public static void editGameInfo() { gameInfoDialog.dispose(); } - public static void saveSgf() { + public static void saveFile() { FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf", "SGF"); JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); @@ -207,12 +207,12 @@ public static void saveSgf() { SGFParser.save(Lizzie.board, file.getPath()); filesystem.put("last-folder", file.getParent()); } catch (IOException err) { - JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToSaveSgf"), "Error", JOptionPane.ERROR); + JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedTosaveFile"), "Error", JOptionPane.ERROR); } } } - public static void openSgf() { + public static void openFile() { FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf or *.gib", "SGF", "GIB"); JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); @@ -234,7 +234,7 @@ public static void openSgf() { } filesystem.put("last-folder", file.getParent()); } catch (IOException err) { - JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToOpenSgf"), "Error", JOptionPane.ERROR); + JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToOpenFile"), "Error", JOptionPane.ERROR); } } } diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index 70e3d4405..421a2ecd3 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -39,15 +39,15 @@ LizzieFrame.commands.keyW=w|toggle winrate display LizzieFrame.commands.keyPeriod=.|score game LizzieFrame.commands.mouseWheelScroll=scrollwheel|undo/redo LizzieFrame.commands.rightClick=right click|undo -LizzieFrame.prompt.failedToOpenSgf=Failed to open the SGF file. -LizzieFrame.prompt.failedToSaveSgf=Failed to save the SGF file. +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.display.lastMove=Last move -LizzieFrame.display.pondering=Pondering +LizzieFrame.display.pondering=Pondering LizzieFrame.display.on=on LizzieFrame.display.off=off LizzieFrame.display.loading=Leela Zero is loading... LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage -LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage \ No newline at end of file +LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index b07c46c64..7d5069288 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -27,8 +27,8 @@ LizzieFrame.commands.keyW=w|\u663E\u793A/\u9690\u85CF\u80DC\u7387\u56FE LizzieFrame.commands.keyPeriod=.(\u5C0F\u6570\u70B9)|\u70B9\u76EE LizzieFrame.commands.mouseWheelScroll=\u9F20\u6807\u6EDA\u8F6E|\u5728\u68CB\u8C31\u4E2D\u5411\u524D/\u5411\u540E\u79FB\u52A8 LizzieFrame.commands.rightClick=\u9F20\u6807\u53F3\u952E|\u56DE\u4E0A\u4E00\u624B -LizzieFrame.prompt.failedToOpenSgf=\u4E0D\u80FD\u6253\u5F00SGF\u6587\u4EF6. -LizzieFrame.prompt.failedToSaveSgf=\u4E0D\u80FD\u4FDD\u5B58SGF\u6587\u4EF6. +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.display.lastMove=\u6700\u540E\u4E00\u624B From a475f3ea77ba384b8e9d492429a34821a22d70b5 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sat, 14 Jul 2018 11:08:14 +0200 Subject: [PATCH 34/75] Refactor file loading logic in loadFile method --- .../featurecat/lizzie/gui/LizzieFrame.java | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 8200818ad..ed6639663 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -220,23 +220,26 @@ public static void openFile() { chooser.setFileFilter(filter); chooser.setMultiSelectionEnabled(false); int result = chooser.showOpenDialog(null); - if (result == JFileChooser.APPROVE_OPTION) { - File file = chooser.getSelectedFile(); - if (!(file.getPath().endsWith(".sgf") || file.getPath().endsWith(".gib"))) { - file = new File(file.getPath() + ".sgf"); - } - try { - System.out.println(file.getPath()); - if (file.getPath().endsWith(".sgf")) { - SGFParser.load(file.getPath()); - } else { - GIBParser.load(file.getPath()); - } - filesystem.put("last-folder", file.getParent()); - } catch (IOException err) { - JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToOpenFile"), "Error", JOptionPane.ERROR); - } - } + if (result == JFileChooser.APPROVE_OPTION) + loadFile(chooser.getSelectedFile()); + } + + public static void loadFile(File file) { + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + if (!(file.getPath().endsWith(".sgf") || file.getPath().endsWith(".gib"))) { + file = new File(file.getPath() + ".sgf"); + } + try { + System.out.println(file.getPath()); + if (file.getPath().endsWith(".sgf")) { + SGFParser.load(file.getPath()); + } else { + GIBParser.load(file.getPath()); + } + filesystem.put("last-folder", file.getParent()); + } catch (IOException err) { + JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToOpenFile"), "Error", JOptionPane.ERROR); + } } private BufferedImage cachedImage = null; From 11dcfb35ba3b4a5afa07d6c0f0508baafb7c0110 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sat, 14 Jul 2018 11:09:20 +0200 Subject: [PATCH 35/75] Allow loading file from the CLI With this change it becomes possible to quickly load files from the CLI: java -jar Lizzie.jar /path/to/game.sgf Which is equivalant to - java -jar Lizzie.jar - press o - select /path/to/game.sgf using the java dialog --- src/main/java/featurecat/lizzie/Lizzie.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index 8f39c44d9..71f0c968c 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -58,7 +58,9 @@ public static void main(String[] args) throws IOException, JSONException, ClassN if(config.handicapInsteadOfWinrate) { leelaz.estimatePassWinrate(); } - if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { + if (args.length == 1) { + frame.loadFile(new File(args[0])); + } else if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { board.resumePreviousGame(); } leelaz.togglePonder(); From 814cfd3b005d088ea1f65c1c19b60d66075da35e Mon Sep 17 00:00:00 2001 From: Jakub Straszewski Date: Thu, 12 Jul 2018 09:57:37 +0200 Subject: [PATCH 36/75] Improve parsing of MoveData to allow for unknown tags --- .../featurecat/lizzie/analysis/MoveData.java | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/MoveData.java b/src/main/java/featurecat/lizzie/analysis/MoveData.java index 0ba3ef154..4ae9c2490 100644 --- a/src/main/java/featurecat/lizzie/analysis/MoveData.java +++ b/src/main/java/featurecat/lizzie/analysis/MoveData.java @@ -22,13 +22,30 @@ public MoveData(String line) throws ArrayIndexOutOfBoundsException { String[] data = line.trim().split(" "); // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed - coordinate = data[1]; - playouts = Integer.parseInt(data[3]); - winrate = Integer.parseInt(data[5])/100.0; - order = Integer.parseInt(data[7]); - variation = new ArrayList<>(Arrays.asList(data)); - variation = variation.subList(9, variation.size()); + for (int i=0; i(Arrays.asList(data)); + variation = variation.subList(i+1, data.length); + break; + } else { + String value = data[++i]; + if (key.equals("move")) { + coordinate = value; + } + if (key.equals("visits")) { + playouts = Integer.parseInt(value); + } + if (key.equals("winrate")) { + winrate = Integer.parseInt(value)/100.0; + } + if (key.equals("order")) { + order = Integer.parseInt(value); + } + } + } } public int compareTo(MoveData b) { From 20611dbfdd3349204bfc14792544b62f95d97b26 Mon Sep 17 00:00:00 2001 From: Jakub Straszewski Date: Thu, 12 Jul 2018 09:58:16 +0200 Subject: [PATCH 37/75] Print dynamic komi if present --- .../featurecat/lizzie/analysis/Leelaz.java | 29 ++++++++++++++++++- .../featurecat/lizzie/gui/LizzieFrame.java | 10 +++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index fd69e6c49..0d08328e4 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -60,6 +60,8 @@ public class Leelaz { private boolean isLoaded = false; private boolean isCheckingVersion; + // dynamic komi and opponent komi as reported by dynamic-komi version of leelaz + private float dynamicKomi = Float.NaN, dynamicOppKomi = Float.NaN; /** * Initializes the leelaz process and starts reading output * @@ -180,7 +182,25 @@ private void parseInfo(String line) { */ private void parseLine(String line) { synchronized (this) { - if (line.equals("\n")) { + if (line.startsWith("komi=")) + { + try { + dynamicKomi = Float.parseFloat(line.substring("komi=".length()).trim()); + } + catch (NumberFormatException nfe) { + dynamicKomi = Float.NaN; + } + } + else if (line.startsWith("opp_komi=")) + { + try { + dynamicOppKomi = Float.parseFloat(line.substring("opp_komi=".length()).trim()); + } + catch (NumberFormatException nfe) { + dynamicOppKomi = Float.NaN; + } + } + else if (line.equals("\n")) { // End of response } else if (line.startsWith("info")) { isLoaded = true; @@ -425,6 +445,13 @@ public List getBestMoves() { } } + public String getDynamicKomi() { + if (Float.isNaN(dynamicKomi) || Float.isNaN(dynamicOppKomi)) { + return null; + } + return String.format("%.1f / %.1f", dynamicKomi, dynamicOppKomi); + } + public boolean isPondering() { return isPondering; } diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 37cd07366..d4df3fa2c 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -322,6 +322,12 @@ public void paint(Graphics g0) { int ponderingY = boardY + (int) (maxSize*0.93); double ponderingSize = .02; + // dynamic komi + int dynamicKomiX = this.getInsets().left; + int dynamicKomiY = boardY + (int) (maxSize*0.86); + double dynamicKomiSize = .02; + + // loading message int loadingX = ponderingX; int loadingY = ponderingY; @@ -390,6 +396,10 @@ public void paint(Graphics g0) { ponderingX, ponderingY, ponderingSize); } + if (Lizzie.leelaz.getDynamicKomi() != null) { + drawPonderingState(g, Lizzie.leelaz.getDynamicKomi(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); + } + // Todo: Make board move over when there is no space beside the board if (Lizzie.config.showWinrate) { drawWinrateGraphContainer(backgroundG, contx, conty, contw, conth); From c0707b26c510002586e1217f871208e94fe0dfcf Mon Sep 17 00:00:00 2001 From: Jakub Straszewski Date: Fri, 20 Jul 2018 15:00:09 +0200 Subject: [PATCH 38/75] add a label for dynamic komi --- src/main/java/featurecat/lizzie/gui/LizzieFrame.java | 6 +++++- src/main/resources/l10n/DisplayStrings.properties | 1 + src/main/resources/l10n/DisplayStrings_zh_CN.properties | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index d4df3fa2c..fd1803f5d 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -323,8 +323,11 @@ public void paint(Graphics g0) { double ponderingSize = .02; // dynamic komi + int dynamicKomiLabelX = this.getInsets().left; + int dynamicKomiLabelY = boardY + (int) (maxSize*0.86); + int dynamicKomiX = this.getInsets().left; - int dynamicKomiY = boardY + (int) (maxSize*0.86); + int dynamicKomiY = boardY + (int) (maxSize*0.89); double dynamicKomiSize = .02; @@ -397,6 +400,7 @@ public void paint(Graphics g0) { } if (Lizzie.leelaz.getDynamicKomi() != null) { + drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.dynamic-komi"), dynamicKomiLabelX, dynamicKomiLabelY, dynamicKomiSize); drawPonderingState(g, Lizzie.leelaz.getDynamicKomi(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); } diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index 421a2ecd3..63c898f8d 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -51,3 +51,4 @@ LizzieFrame.display.loading=Leela Zero is loading... LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage +LizzieFrame.display.dynamic-komi=dyn. komi: \ No newline at end of file diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index 7d5069288..59cf0e710 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -39,3 +39,4 @@ LizzieFrame.display.loading=Leela Zero\u6B63\u5728\u8F7D\u5165\u4E2D... LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage +LizzieFrame.display.dynamic-komi=dyn. komi: \ No newline at end of file From da5357cb643d7d0831bacfb1900e946b52f8a8d6 Mon Sep 17 00:00:00 2001 From: Kuba Date: Sat, 21 Jul 2018 10:32:56 +0200 Subject: [PATCH 39/75] make showing dynamic komi optional --- src/main/java/featurecat/lizzie/Config.java | 3 +++ .../java/featurecat/lizzie/gui/Input.java | 8 +++++++ .../featurecat/lizzie/gui/LizzieFrame.java | 22 +++++++++++++------ .../resources/l10n/DisplayStrings.properties | 3 ++- .../l10n/DisplayStrings_zh_CN.properties | 1 + 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index b6cb578cc..ba25e3330 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -18,6 +18,7 @@ public class Config { public boolean showRawBoard = false; public boolean showCaptured = true; public boolean handicapInsteadOfWinrate = false; + public boolean showDynamicKomi = true; public boolean showStatus = true; public boolean showBranch = true; @@ -146,6 +147,7 @@ public Config() throws IOException { largeSubBoard = uiConfig.getBoolean("large-subboard"); handicapInsteadOfWinrate = uiConfig.getBoolean("handicap-instead-of-winrate"); startMaximized = uiConfig.getBoolean("window-maximized"); + showDynamicKomi = uiConfig.getBoolean("show-dynamic-komi"); } // Modifies config by adding in values from default_config that are missing. @@ -269,6 +271,7 @@ private JSONObject createDefaultConfig() { ui.put("board-size", 19); ui.put("window-size", new JSONArray("[1024, 768]")); ui.put("window-maximized", false); + ui.put("show-dynamic-komi", true); config.put("ui", ui); return config; diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index 94e76d87b..ee32f376a 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -160,6 +160,10 @@ private boolean controlIsPressed(KeyEvent e) { return e.isControlDown() || (mac && e.isMetaDown()); } + private void toggleShowDynamicKomi() { + Lizzie.config.showDynamicKomi = !Lizzie.config.showDynamicKomi; + } + @Override public void keyPressed(KeyEvent e) { @@ -358,6 +362,10 @@ public void keyPressed(KeyEvent e) { } break; + case VK_D: + toggleShowDynamicKomi(); + 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 fd1803f5d..3bb067a5d 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -35,6 +35,7 @@ import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.io.*; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.ResourceBundle; @@ -399,7 +400,7 @@ public void paint(Graphics g0) { ponderingX, ponderingY, ponderingSize); } - if (Lizzie.leelaz.getDynamicKomi() != null) { + if (Lizzie.config.showDynamicKomi && Lizzie.leelaz.getDynamicKomi() != null) { drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.dynamic-komi"), dynamicKomiLabelX, dynamicKomiLabelY, dynamicKomiSize); drawPonderingState(g, Lizzie.leelaz.getDynamicKomi(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); } @@ -531,17 +532,22 @@ void drawControls() { // redraw background createBackground(); + List commandsToShow = new ArrayList<>(Arrays.asList(commands)); + if (Lizzie.leelaz.getDynamicKomi() != null) { + commandsToShow.add(resourceBundle.getString("LizzieFrame.commands.keyD")); + } + Graphics2D g = cachedImage.createGraphics(); int maxSize = Math.min(getWidth(), getHeight()); Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.034)); g.setFont(font); FontMetrics metrics = g.getFontMetrics(font); - int maxCommandWidth = Arrays.asList(commands).stream().reduce(0, (Integer i, String command) -> Math.max(i, metrics.stringWidth(command)), (Integer a, Integer b) -> Math.max(a, b)); + int maxCommandWidth = commandsToShow.stream().reduce(0, (Integer i, String command) -> Math.max(i, metrics.stringWidth(command)), (Integer a, Integer b) -> Math.max(a, b)); int lineHeight = (int) (font.getSize() * 1.15); int boxWidth = Util.clamp((int) (maxCommandWidth * 1.4), 0, getWidth()); - int boxHeight = Util.clamp(commands.length * lineHeight, 0, getHeight()); + int boxHeight = Util.clamp(commandsToShow.size() * lineHeight, 0, getHeight()); int commandsX = Util.clamp(getWidth() / 2 - boxWidth / 2, 0, getWidth()); int commandsY = Util.clamp(getHeight() / 2 - boxHeight / 2, 0, getHeight()); @@ -567,10 +573,12 @@ void drawControls() { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(Color.WHITE); - for (int i = 0; i < commands.length; i++) { - String[] split = commands[i].split("\\|"); - g.drawString(split[0], verticalLineX - metrics.stringWidth(split[0]) - strokeRadius * 4, font.getSize() + (commandsY + i * lineHeight)); - g.drawString(split[1], verticalLineX + strokeRadius * 4, font.getSize() + (commandsY + i * lineHeight)); + int lineOffset = commandsY; + for (String command : commandsToShow) { + String[] split = command.split("\\|"); + g.drawString(split[0], verticalLineX - metrics.stringWidth(split[0]) - strokeRadius * 4, font.getSize() + lineOffset); + g.drawString(split[1], verticalLineX + strokeRadius * 4, font.getSize() + lineOffset); + lineOffset += lineHeight; } refreshBackground(); diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index 63c898f8d..a807ba8bd 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -19,6 +19,7 @@ LizzieFrame.commands.keyAltC=ctrl-c|copy SGF to clipboard LizzieFrame.commands.keyAltV=ctrl-v|paste SGF from clipboard LizzieFrame.commands.keyC=c|toggle coordinates LizzieFrame.commands.keyControl=ctrl|undo/redo 10 moves +LizzieFrame.commands.keyD=d|show/hide dynamic komi LizzieFrame.commands.keyDownArrow=down arrow|redo LizzieFrame.commands.keyEnd=end|go to end LizzieFrame.commands.keyEnter=enter|force Leela Zero move @@ -51,4 +52,4 @@ LizzieFrame.display.loading=Leela Zero is loading... LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage -LizzieFrame.display.dynamic-komi=dyn. komi: \ No newline at end of file +LizzieFrame.display.dynamic-komi=dyn. komi: diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index 59cf0e710..6a39c6660 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -6,6 +6,7 @@ LizzieFrame.commands.keyAltC=ctrl-c|\u62F7\u8D1DSGF\u5230\u526A\u8D34\u677F LizzieFrame.commands.keyAltV=ctrl-v|\u4ECE\u526A\u8D34\u677F\u7C98\u8D34SGF\u5C40\u9762 LizzieFrame.commands.keyC=c|\u663E\u793A/\u9690\u85CF\u5750\u6807 +LizzieFrame.commands.keyD=d|show/hide dynamic komi LizzieFrame.commands.keyControl=ctrl|\u5411\u524D/\u5411\u540E10\u6B65 LizzieFrame.commands.keyDownArrow=\u4E0B\u65B9\u5411\u952E|\u5411\u540E\u4E00\u6B65 LizzieFrame.commands.keyEnd=end|\u8DF3\u5230\u68CB\u8C31\u672B\u5C3E From 965e198b24b16d17e03f668abfd7790b34cc7444 Mon Sep 17 00:00:00 2001 From: Kuba Date: Sat, 21 Jul 2018 10:33:52 +0200 Subject: [PATCH 40/75] bring back trailing space after 'Pondering' in DisplayStrings --- src/main/resources/l10n/DisplayStrings.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index a807ba8bd..3efe45210 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -45,7 +45,7 @@ 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.display.lastMove=Last move -LizzieFrame.display.pondering=Pondering +LizzieFrame.display.pondering=Pondering LizzieFrame.display.on=on LizzieFrame.display.off=off LizzieFrame.display.loading=Leela Zero is loading... From 5f23fe020645f52fed2b7f16e1ff9cc968a6cf71 Mon Sep 17 00:00:00 2001 From: TFiFiE Date: Sat, 28 Jul 2018 19:18:37 +0200 Subject: [PATCH 41/75] Configurable threshold for displaying move statistics. --- src/main/java/featurecat/lizzie/Config.java | 1 + .../java/featurecat/lizzie/gui/BoardRenderer.java | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index ba25e3330..c44467aa3 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -272,6 +272,7 @@ private JSONObject createDefaultConfig() { ui.put("window-size", new JSONArray("[1024, 768]")); ui.put("window-maximized", false); ui.put("show-dynamic-komi", true); + ui.put("min-playout-ratio-for-stats", 0.0); config.put("ui", ui); return config; diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 51835c1f0..bdc437d7f 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -503,12 +503,10 @@ private void drawLeelazSuggestions(Graphics2D g) { return; final int MIN_ALPHA = 32; - final int MIN_ALPHA_TO_DISPLAY_TEXT = 64; - final int MAX_ALPHA = maxAlpha = Math.max(maxAlpha, MIN_ALPHA_TO_DISPLAY_TEXT); final double HUE_SCALING_FACTOR = 3.0; final double ALPHA_SCALING_FACTOR = 5.0; - final float GREEN_HUE = Color.RGBtoHSB(0,1,0,null)[0]; - final float CYAN_HUE = Color.RGBtoHSB(0,1,1,null)[0]; + final float GREEN_HUE = Color.RGBtoHSB(0, 1, 0, null)[0]; + final float CYAN_HUE = Color.RGBtoHSB(0, 1, 1, null)[0]; if (!bestMoves.isEmpty()) { @@ -558,7 +556,7 @@ private void drawLeelazSuggestions(Graphics2D g) { float hue = isBestMove ? CYAN_HUE : (float) (-GREEN_HUE * Math.max(0, Math.log(percentPlayouts) / HUE_SCALING_FACTOR + 1)); float saturation = 0.75f; //saturation float brightness = 0.85f; //brightness - int alpha = (int) (MIN_ALPHA + (MAX_ALPHA - MIN_ALPHA) * Math.max(0, Math.log(percentPlayouts) / + int alpha = (int) (MIN_ALPHA + (maxAlpha - MIN_ALPHA) * Math.max(0, Math.log(percentPlayouts) / ALPHA_SCALING_FACTOR + 1)); // if (uiConfig.getBoolean("shadows-enabled")) // alpha = 255; @@ -586,7 +584,8 @@ private void drawLeelazSuggestions(Graphics2D g) { } - if (branch == null && (alpha >= MIN_ALPHA_TO_DISPLAY_TEXT || hasMaxWinrate) || (Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { + if ((branch == null && (hasMaxWinrate || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats"))) || + (Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { double roundedWinrate = Math.round(move.winrate * 10) / 10.0; if (uiConfig.getBoolean("win-rate-always-black") && !Lizzie.board.getData().blackToPlay) { roundedWinrate = 100.0 - roundedWinrate; From e2b46600fff1a3469e140d4392deae315b4742e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20G=C4=83tejescu?= Date: Fri, 3 Aug 2018 20:58:01 +0300 Subject: [PATCH 42/75] Fix a typo in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a6043895..7174719a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Active developmoent is on the next branch! Not master. Please clone next when contributing! +# Active development is on the next branch! Not master. Please clone next when contributing! TODO: add more specific contributing guidelines. In general... From d330e004bba835ffe8a9611fd779219f6c76069f Mon Sep 17 00:00:00 2001 From: odCat Date: Sat, 4 Aug 2018 01:06:52 +0300 Subject: [PATCH 43/75] Add Romanian localization file The translation is only partially done. --- .../l10n/DisplayStrings_RO.properties | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/main/resources/l10n/DisplayStrings_RO.properties diff --git a/src/main/resources/l10n/DisplayStrings_RO.properties b/src/main/resources/l10n/DisplayStrings_RO.properties new file mode 100644 index 000000000..28d416d9c --- /dev/null +++ b/src/main/resources/l10n/DisplayStrings_RO.properties @@ -0,0 +1,51 @@ +# Note for localization +# +# The localization file set(resource bundle) consist of one base file(this file) and several translated property files. +# Each localized string is assigned a unique key. All keys referenced in codes should be present in the base file. +# If a key is present in the translated file matching the current system locale, the value in the translated +# file will be used. Otherwise the value in the base file will be used instead. +# Usually strings in the base file are written in English. So that if a string has no translations, it will be +# displayed in English by default. +# +# In Java 8 or before, the encoding of the resource bundle files has no standards. +# To avoid encoding inconsistencies, it is recommended to use "native2ascii" tool in JDK +# to encode native translations in raw unicode codes, such as \u0001 \u0002 if the native strings +# cannot be encoded in ascii. Typically east Asian translations, such as Chinese(both simplified and traditional), +# Korean or Japanese, need the encoding process. +# +# You can directly use the original display texts as the key, but it is recommended to name the key properly. + +LizzieFrame.commands.keyAltC=control-c|copy SGF to clipboard +LizzieFrame.commands.keyAltV=control-v|paste SGF from clipboard +LizzieFrame.commands.keyC=c|afișează/ascunde coordonatele +LizzieFrame.commands.keyControl=control|undo/redo 10 moves +LizzieFrame.commands.keyDownArrow=săgeată jos|redo +LizzieFrame.commands.keyEnd=end|mergi la sfârșit +LizzieFrame.commands.keyEnter=enter|force Leela Zero move +LizzieFrame.commands.keyF=f|toggle next move display +LizzieFrame.commands.keyG=g|toggle variation graph +LizzieFrame.commands.keyHome=home|mergi la început +LizzieFrame.commands.keyI=i|editează informațiile jocului +LizzieFrame.commands.keyA=a|rulează analiza automată a jocului +LizzieFrame.commands.keyM=m|afișează/ascunde numărul mutarilor +LizzieFrame.commands.keyN=n|începe un joc împotriva lui Leela Zero +LizzieFrame.commands.keyO=o|deschide SGF +LizzieFrame.commands.keyP=p|pass +LizzieFrame.commands.keyS=s|salvează SGF +LizzieFrame.commands.keySpace=spațiu|pornește/oprește analiza +LizzieFrame.commands.keyUpArrow=săgeată sus|undo +LizzieFrame.commands.keyV=v|afișează/ascunde variante +LizzieFrame.commands.keyW=w|afișează/ascunde probabilitate victorie +LizzieFrame.commands.keyPeriod=.|score game +LizzieFrame.commands.mouseWheelScroll=scrollwheel|înainte/înapoi +LizzieFrame.commands.rightClick=right click|undo +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=hold x = afișează comenzi +LizzieFrame.display.lastMove=Ulima mutare +LizzieFrame.display.pondering=Analizeză +LizzieFrame.display.on=pornit +LizzieFrame.display.off=oprit +LizzieFrame.display.loading=Leela Zero se încarcă... +LizzieFrame.display.download-latest-network-prompt=Descărcați cel mai nou fișier rețea? This may take some time. From 8240521e819004ec1ad1d2a784d7e4f8561a0c3b Mon Sep 17 00:00:00 2001 From: odCat Date: Sat, 4 Aug 2018 06:44:36 +0300 Subject: [PATCH 44/75] Finish translation --- .../l10n/DisplayStrings_RO.properties | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/resources/l10n/DisplayStrings_RO.properties b/src/main/resources/l10n/DisplayStrings_RO.properties index 28d416d9c..07b385516 100644 --- a/src/main/resources/l10n/DisplayStrings_RO.properties +++ b/src/main/resources/l10n/DisplayStrings_RO.properties @@ -15,37 +15,37 @@ # # You can directly use the original display texts as the key, but it is recommended to name the key properly. -LizzieFrame.commands.keyAltC=control-c|copy SGF to clipboard -LizzieFrame.commands.keyAltV=control-v|paste SGF from clipboard -LizzieFrame.commands.keyC=c|afișează/ascunde coordonatele -LizzieFrame.commands.keyControl=control|undo/redo 10 moves -LizzieFrame.commands.keyDownArrow=săgeată jos|redo +LizzieFrame.commands.keyAltC=control-c|copiere SGF în memoria temporară +LizzieFrame.commands.keyAltV=control-v|lipire SGF din memoria temporară +LizzieFrame.commands.keyC=c|afișează/ascunde coordonate +LizzieFrame.commands.keyControl=control|înainte/înapoi 10 mutări +LizzieFrame.commands.keyDownArrow=săgeată jos|înainte LizzieFrame.commands.keyEnd=end|mergi la sfârșit -LizzieFrame.commands.keyEnter=enter|force Leela Zero move -LizzieFrame.commands.keyF=f|toggle next move display -LizzieFrame.commands.keyG=g|toggle variation graph +LizzieFrame.commands.keyEnter=enter|forțează mutarea lui Leela Zero +LizzieFrame.commands.keyF=f|afișează/ascunde următoarea mutare +LizzieFrame.commands.keyG=g|afișează/ascunde graficul cu variante LizzieFrame.commands.keyHome=home|mergi la început LizzieFrame.commands.keyI=i|editează informațiile jocului LizzieFrame.commands.keyA=a|rulează analiza automată a jocului -LizzieFrame.commands.keyM=m|afișează/ascunde numărul mutarilor +LizzieFrame.commands.keyM=m|afișează/ascunde numărul mutărilor LizzieFrame.commands.keyN=n|începe un joc împotriva lui Leela Zero LizzieFrame.commands.keyO=o|deschide SGF -LizzieFrame.commands.keyP=p|pass +LizzieFrame.commands.keyP=p|pas LizzieFrame.commands.keyS=s|salvează SGF LizzieFrame.commands.keySpace=spațiu|pornește/oprește analiza -LizzieFrame.commands.keyUpArrow=săgeată sus|undo +LizzieFrame.commands.keyUpArrow=săgeată sus|înapoi LizzieFrame.commands.keyV=v|afișează/ascunde variante LizzieFrame.commands.keyW=w|afișează/ascunde probabilitate victorie -LizzieFrame.commands.keyPeriod=.|score game -LizzieFrame.commands.mouseWheelScroll=scrollwheel|înainte/înapoi -LizzieFrame.commands.rightClick=right click|undo +LizzieFrame.commands.keyPeriod=.|scor +LizzieFrame.commands.mouseWheelScroll=rotiță mouse|înainte/înapoi +LizzieFrame.commands.rightClick=clic dreapta|înapoi 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=hold x = afișează comenzi +LizzieFrame.prompt.showControlsHint=x apăsat = afișează comenzi LizzieFrame.display.lastMove=Ulima mutare LizzieFrame.display.pondering=Analizeză LizzieFrame.display.on=pornit LizzieFrame.display.off=oprit LizzieFrame.display.loading=Leela Zero se încarcă... -LizzieFrame.display.download-latest-network-prompt=Descărcați cel mai nou fișier rețea? This may take some time. +LizzieFrame.display.download-latest-network-prompt=Descărcați cel mai nou fișier rețea? Poate să dureze. From dfe6101ec58bb3bcc63090b93ec63f245fd0c939 Mon Sep 17 00:00:00 2001 From: TFiFiE Date: Wed, 29 Aug 2018 03:11:15 +0200 Subject: [PATCH 45/75] Fixed inversion of named y-coordinate. --- src/main/java/featurecat/lizzie/rules/Board.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 81fe97825..15d34eaa1 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -73,7 +73,7 @@ public static int[] convertNameToCoordinates(String namedCoordinate) { } // coordinates take the form C16 A19 Q5 K10 etc. I is not used. int x = alphabet.indexOf(namedCoordinate.charAt(0)); - int y = Integer.parseInt(namedCoordinate.substring(1)) - 1; + int y = BOARD_SIZE - Integer.parseInt(namedCoordinate.substring(1)); return new int[]{x, y}; } @@ -86,7 +86,7 @@ public static int[] convertNameToCoordinates(String namedCoordinate) { */ public static String convertCoordinatesToName(int x, int y) { // coordinates take the form C16 A19 Q5 K10 etc. I is not used. - return alphabet.charAt(x) + "" + (y + 1); + return alphabet.charAt(x) + "" + (BOARD_SIZE - y); } /** From 684a052f1e1b7564d74ce8706aace6f3bedba76e Mon Sep 17 00:00:00 2001 From: Ximin Luo Date: Mon, 3 Sep 2018 14:30:24 -0700 Subject: [PATCH 46/75] Fix location checks, and use engine-start-location directly. Fixes #323 --- src/main/java/featurecat/lizzie/Config.java | 10 +------ src/main/java/featurecat/lizzie/Lizzie.java | 22 +------------- .../featurecat/lizzie/analysis/Leelaz.java | 30 +++++++++++-------- .../resources/l10n/DisplayStrings.properties | 2 +- .../l10n/DisplayStrings_zh_CN.properties | 4 +-- 5 files changed, 23 insertions(+), 45 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index c44467aa3..8f6d7a3f1 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -105,15 +105,7 @@ private boolean validateAndCorrectSettings(JSONObject config) { // Check engine configs JSONObject leelaz = config.getJSONObject("leelaz"); - // Check if the engine program exists. - String enginePath = leelaz.optString("engine-program", getBestDefaultLeelazPath()); - if (!Files.exists(Paths.get(enginePath)) && !Files.exists(Paths.get(enginePath + ".exe" /* For windows */))) { - // FIXME: I don't know how to handle it properly.. Possibly showing a warning dialog may be a good idea? - leelaz.put("engine-program", "./leelaz"); - madeCorrections = true; - } - - // Similar checks for startup directory. It should exist and should be a directory. + // Checks for startup directory. It should exist and should be a directory. String engineStartLocation = getBestDefaultLeelazPath(); if (!(Files.exists(Paths.get(engineStartLocation)) && Files.isDirectory(Paths.get(engineStartLocation)))) { leelaz.put("engine-start-location", "."); diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index 71f0c968c..f88df1e3a 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -28,28 +28,9 @@ public class Lizzie { */ public static void main(String[] args) throws IOException, JSONException, ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException, InterruptedException { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - config = new Config(); - - // Check that user has installed leela zero - JSONObject leelazconfig = Lizzie.config.config.getJSONObject("leelaz"); - ResourceBundle resourceBundle = ResourceBundle.getBundle("l10n.DisplayStrings"); - String startfolder = leelazconfig.optString("engine-start-location", "."); - - // Check if engine is present - File lef = new File(startfolder + '/' + "leelaz"); - if (!lef.exists()) { - File leexe = new File(startfolder + '/' + "leelaz.exe"); - if (!leexe.exists()) { - JOptionPane.showMessageDialog(null, resourceBundle.getString("LizzieFrame.display.leelaz-missing"), "Lizzie - Error!", JOptionPane.ERROR_MESSAGE); - return; - } - } - PluginManager.loadPlugins(); - board = new Board(); - frame = new LizzieFrame(); new Thread( () -> { @@ -66,10 +47,9 @@ public static void main(String[] args) throws IOException, JSONException, ClassN leelaz.togglePonder(); } catch (IOException e) { e.printStackTrace(); + System.exit(-1); } }).start(); - - } public static void shutdown() { diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 0d08328e4..d70c2e64f 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -87,25 +87,31 @@ public Leelaz() throws IOException, JSONException { updateToLatestNetwork(); } - String startfolder = new File(Config.getBestDefaultLeelazPath()).getParent(); // todo make this a little more obvious/less bug-prone - - // Check if network file is present - File wf = new File(startfolder + '/' + config.getString("network-file")); - if (!wf.exists()) { - JOptionPane.showMessageDialog(null, resourceBundle.getString("LizzieFrame.display.network-missing")); - } - - - // command string for starting the engine + 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", config.getString("network-file")); + engineCommand = engineCommand.replaceAll("%network-file", networkFile); // create this as a list which gets passed into the processbuilder List commands = Arrays.asList(engineCommand.split(" ")); + // Check if engine is present + File lef = startfolder.toPath().resolve(new File(commands.get(0)).toPath()).toFile(); + if (!lef.exists()) { + JOptionPane.showMessageDialog(null, resourceBundle.getString("LizzieFrame.display.leelaz-missing"), "Lizzie - Error!", JOptionPane.ERROR_MESSAGE); + throw new IOException("engine not present"); + } + + // Check if network file is present + File wf = startfolder.toPath().resolve(new File(networkFile).toPath()).toFile(); + if (!wf.exists()) { + JOptionPane.showMessageDialog(null, resourceBundle.getString("LizzieFrame.display.network-missing")); + throw new IOException("network-file not present"); + } + // run leelaz ProcessBuilder processBuilder = new ProcessBuilder(commands); - processBuilder.directory(new File(startfolder)); + processBuilder.directory(startfolder); processBuilder.redirectErrorStream(true); process = processBuilder.start(); diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index 3efe45210..05deb875a 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -51,5 +51,5 @@ LizzieFrame.display.off=off LizzieFrame.display.loading=Leela Zero is loading... LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage -LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage +LizzieFrame.display.network-missing=Did not find network weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage LizzieFrame.display.dynamic-komi=dyn. komi: diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index 6a39c6660..95d9c079d 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -39,5 +39,5 @@ LizzieFrame.display.off=\u6682\u505C LizzieFrame.display.loading=Leela Zero\u6B63\u5728\u8F7D\u5165\u4E2D... LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage -LizzieFrame.display.network-missing=Did not find network/weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage -LizzieFrame.display.dynamic-komi=dyn. komi: \ No newline at end of file +LizzieFrame.display.network-missing=Did not find network weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage +LizzieFrame.display.dynamic-komi=dyn. komi: From 89bbdb58f873c3cf51f5769175be5895611af88d Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Mon, 17 Sep 2018 11:17:54 +0200 Subject: [PATCH 47/75] Refactor MoveData constructor into a static method Plus some minor cosmecis improvements. --- .../featurecat/lizzie/analysis/Leelaz.java | 18 +++----- .../featurecat/lizzie/analysis/MoveData.java | 45 ++++++++++++------- .../java/featurecat/lizzie/rules/Board.java | 1 + 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 0d08328e4..31d3e3c6d 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -21,7 +21,7 @@ import java.util.regex.Pattern; /** - * an interface with leelaz.exe go engine. Can be adapted for GTP, but is specifically designed for GCP's Leela Zero. + * An interface with leelaz go engine. Can be adapted for GTP, but is specifically designed for GCP's Leela Zero. * leelaz is modified to output information as it ponders * see www.github.com/gcp/leela-zero */ @@ -168,7 +168,7 @@ private void parseInfo(String line) { bestMoves = new ArrayList<>(); String[] variations = line.split(" info "); for (String var : variations) { - bestMoves.add(new MoveData(var)); + bestMoves.add(MoveData.fromInfo(var)); } // Not actually necessary to sort with current version of LZ (0.15) // but not guaranteed to be ordered in the future @@ -182,25 +182,21 @@ private void parseInfo(String line) { */ private void parseLine(String line) { synchronized (this) { - if (line.startsWith("komi=")) - { + if (line.startsWith("komi=")) { try { dynamicKomi = Float.parseFloat(line.substring("komi=".length()).trim()); } catch (NumberFormatException nfe) { dynamicKomi = Float.NaN; } - } - else if (line.startsWith("opp_komi=")) - { + } else if (line.startsWith("opp_komi=")) { try { dynamicOppKomi = Float.parseFloat(line.substring("opp_komi=".length()).trim()); } catch (NumberFormatException nfe) { dynamicOppKomi = Float.NaN; } - } - else if (line.equals("\n")) { + } else if (line.equals("\n")) { // End of response } else if (line.startsWith("info")) { isLoaded = true; @@ -268,7 +264,7 @@ private void parseMoveDataLine(String line) { if (line.length() > 0 && Character.isLetter(line.charAt(0)) && !line.startsWith("pass")) { if (!(Lizzie.frame != null && Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay)) { try { - bestMovesTemp.add(new MoveData(line)); + bestMovesTemp.add(MoveData.fromInfo(line)); } catch (ArrayIndexOutOfBoundsException e) { // this is very rare but is possible. ignore } @@ -415,7 +411,7 @@ public void undo() { } /** - * this initializes leelaz's pondering mode at its current position + * This initializes leelaz's pondering mode at its current position */ private void ponder() { isPondering = true; diff --git a/src/main/java/featurecat/lizzie/analysis/MoveData.java b/src/main/java/featurecat/lizzie/analysis/MoveData.java index 4ae9c2490..a9ccc62ee 100644 --- a/src/main/java/featurecat/lizzie/analysis/MoveData.java +++ b/src/main/java/featurecat/lizzie/analysis/MoveData.java @@ -5,7 +5,7 @@ import java.util.List; /** - * holds the data from Leelaz's pondering mode + * Holds the data from Leelaz's pondering mode */ public class MoveData implements Comparable { public String coordinate; @@ -14,41 +14,56 @@ public class MoveData implements Comparable { public int order; public List variation; + private MoveData() {} + + public int compareTo(MoveData b) { + return order - b.order; + } + /** - * Parses a leelaz ponder output line + * Parses a leelaz ponder output line. For example: + * + * info move R5 visits 38 winrate 5404 order 0 pv R5 Q5 R6 S4 Q10 C3 D3 C4 C6 C5 D5 + * * @param line line of ponder output */ - public MoveData(String line) throws ArrayIndexOutOfBoundsException { + public static MoveData fromInfo(String line) throws ArrayIndexOutOfBoundsException { + MoveData result = new MoveData(); String[] data = line.trim().split(" "); // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed - for (int i=0; i(Arrays.asList(data)); - variation = variation.subList(i+1, data.length); + // Read variation to the end of line + result.variation = new ArrayList<>(Arrays.asList(data)); + result.variation = result.variation.subList(i + 1, data.length); break; } else { String value = data[++i]; if (key.equals("move")) { - coordinate = value; + result.coordinate = value; } if (key.equals("visits")) { - playouts = Integer.parseInt(value); + result.playouts = Integer.parseInt(value); } if (key.equals("winrate")) { - winrate = Integer.parseInt(value)/100.0; - } - if (key.equals("order")) { - order = Integer.parseInt(value); + result.winrate = Integer.parseInt(value) / 100.0; } } } + + return result; } - public int compareTo(MoveData b) { - return order - b.order; + /** + * Parses a leelaz summary output line. For example: + * + * P16 -> 4 (V: 50.94%) (N: 5.79%) PV: P16 N18 R5 Q5 + * + * @param line line of summary output + */ + public static MoveData fromSummary(String summary) { + return new MoveData(); // TODO } } diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 81fe97825..279e629be 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -745,6 +745,7 @@ public boolean previousMove() { if (inScoreMode()) setScoreMode(false); // 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; From efc8fe31e9d6cba3a12f351b66ab847be6c34932 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Mon, 17 Sep 2018 11:19:22 +0200 Subject: [PATCH 48/75] Setup JUnit; unit test MoveData constructor --- pom.xml | 19 +++++++++++++++ .../lizzie/analysis/MoveDataTest.java | 23 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/test/java/featurecat/lizzie/analysis/MoveDataTest.java diff --git a/pom.xml b/pom.xml index 6f02aa141..49139edda 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,9 @@ 1.8 1.8 + + -Xlint:all + @@ -52,6 +55,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.9 + + + false + + @@ -63,5 +75,12 @@ 20180130 + + + junit + junit + 4.11 + test + diff --git a/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java new file mode 100644 index 000000000..e7d7b0dfc --- /dev/null +++ b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java @@ -0,0 +1,23 @@ +package featurecat.lizzie.analysis; + +import org.junit.Test; +import java.util.List; +import java.util.Arrays; +import static org.junit.Assert.assertEquals; + +public class MoveDataTest { + @Test public void testFromInfoLine() { + String info = "move R5 visits 38 winrate 5404 order 0 pv R5 Q5 R6 S4 Q10 C3 D3 C4 C6 C5 D5"; + MoveData moveData = MoveData.fromInfo(info); + + assertEquals(moveData.coordinate, "R5"); + assertEquals(moveData.playouts, 38); + assertEquals(moveData.winrate, 54.04, 0.01); + assertEquals(moveData.order, 0); + + List expected = Arrays.asList( + "R5", "Q5", "R6", "S4", "Q10", "C3", "D3", "C4", "C6", "C5", "D5"); + + assertEquals(moveData.variation, expected); + } +} From 5b0e7558dcfe8f818c328446f2c4452d75496e45 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Mon, 17 Sep 2018 11:51:33 +0200 Subject: [PATCH 49/75] Don't parse MoveData order Moves are always outputed in order by leelaz, so this processing this info is redundent. Furthermore, summary lines do not include any order, so we can only rely on print order for these. This commit remove the order field, and remove the Comparable interface from MoveData. --- src/main/java/featurecat/lizzie/analysis/Leelaz.java | 4 ---- src/main/java/featurecat/lizzie/analysis/MoveData.java | 8 +------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 31d3e3c6d..02bcb8dc6 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -164,15 +164,11 @@ private void initializeStreams() { } private void parseInfo(String line) { - bestMoves = new ArrayList<>(); String[] variations = line.split(" info "); for (String var : variations) { bestMoves.add(MoveData.fromInfo(var)); } - // Not actually necessary to sort with current version of LZ (0.15) - // but not guaranteed to be ordered in the future - Collections.sort(bestMoves); } /** diff --git a/src/main/java/featurecat/lizzie/analysis/MoveData.java b/src/main/java/featurecat/lizzie/analysis/MoveData.java index a9ccc62ee..b6578fc15 100644 --- a/src/main/java/featurecat/lizzie/analysis/MoveData.java +++ b/src/main/java/featurecat/lizzie/analysis/MoveData.java @@ -7,19 +7,14 @@ /** * Holds the data from Leelaz's pondering mode */ -public class MoveData implements Comparable { +public class MoveData { public String coordinate; public int playouts; public double winrate; - public int order; public List variation; private MoveData() {} - public int compareTo(MoveData b) { - return order - b.order; - } - /** * Parses a leelaz ponder output line. For example: * @@ -52,7 +47,6 @@ public static MoveData fromInfo(String line) throws ArrayIndexOutOfBoundsExcepti } } } - return result; } From 191c54b828e3eeb295a0c0568392ee2106aec69b Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Mon, 17 Sep 2018 14:03:56 +0200 Subject: [PATCH 50/75] Implement MoveData summary parser --- .../featurecat/lizzie/analysis/MoveData.java | 17 ++++++++- .../lizzie/analysis/MoveDataTest.java | 37 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/main/java/featurecat/lizzie/analysis/MoveData.java b/src/main/java/featurecat/lizzie/analysis/MoveData.java index b6578fc15..f66486dbb 100644 --- a/src/main/java/featurecat/lizzie/analysis/MoveData.java +++ b/src/main/java/featurecat/lizzie/analysis/MoveData.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Holds the data from Leelaz's pondering mode @@ -58,6 +60,19 @@ public static MoveData fromInfo(String line) throws ArrayIndexOutOfBoundsExcepti * @param line line of summary output */ public static MoveData fromSummary(String summary) { - return new MoveData(); // TODO + Matcher match = summaryPattern.matcher(summary.trim()); + if (!match.matches()) { + throw new IllegalArgumentException("Unexpected summary format: " + summary); + } else { + MoveData result = new MoveData(); + result.coordinate = match.group(1); + result.playouts = Integer.parseInt(match.group(2)); + result.winrate = Double.parseDouble(match.group(3)); + result.variation = Arrays.asList(match.group(4).split(" ")); + return result; + } } + + private static Pattern summaryPattern = Pattern.compile( + "^ *(\\w\\d*) -> *(\\d+) \\(V: ([^%)]+)%\\) \\([^\\)]+\\) PV: (.+).*$"); } diff --git a/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java index e7d7b0dfc..1c71ec958 100644 --- a/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java +++ b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java @@ -13,11 +13,40 @@ public class MoveDataTest { assertEquals(moveData.coordinate, "R5"); assertEquals(moveData.playouts, 38); assertEquals(moveData.winrate, 54.04, 0.01); - assertEquals(moveData.order, 0); + assertEquals(moveData.variation, Arrays.asList( + "R5", "Q5", "R6", "S4", "Q10", "C3", "D3", "C4", "C6", "C5", "D5")); + } + + private void testSummary(String summary, String coordinate, int playouts, double winrate, List variation) { + MoveData moveData = MoveData.fromSummary(summary); + assertEquals(moveData.coordinate, coordinate); + assertEquals(moveData.playouts, playouts); + assertEquals(moveData.winrate, winrate, 0.01); + assertEquals(moveData.variation, variation); + } + + @Test public void summaryLine1() { + testSummary(" P16 -> 4 (V: 50.94%) (N: 5.79%) PV: P16 N18 R5 Q5", + "P16", 4, 50.94, Arrays.asList("P16", "N18", "R5", "Q5")); + } - List expected = Arrays.asList( - "R5", "Q5", "R6", "S4", "Q10", "C3", "D3", "C4", "C6", "C5", "D5"); + @Test public void summaryLine2() { + testSummary(" D9 -> 59 (V: 60.61%) (N: 52.59%) PV: D9 D12 E9 C13 C15 F17", + "D9", 59, 60.61, Arrays.asList("D9", "D12", "E9", "C13", "C15", "F17")); + } + + @Test public void summaryLine3() { + testSummary(" B2 -> 1 (V: 46.52%) (N: 86.74%) PV: B2", + "B2", 1, 46.52, Arrays.asList("B2")); + } + + @Test public void summaryLine4() { + testSummary(" D16 -> 33 (V: 53.63%) (N: 27.64%) PV: D16 D4 Q16 O4 C3 C4", + "D16", 33, 53.63, Arrays.asList("D16", "D4", "Q16", "O4", "C3", "C4")); + } - assertEquals(moveData.variation, expected); + @Test public void summaryLine5() { + testSummary(" Q16 -> 0 (V: 0.00%) (N: 0.52%) PV: Q16\n", + "Q16", 0, 0.0, Arrays.asList("Q16")); } } From 0c936f6f5734ebfb0209d59f97905f0da91cfaf2 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Mon, 17 Sep 2018 16:59:32 +0200 Subject: [PATCH 51/75] Fix #362: parse " -> " lines from leelaz With the new "summary parser", Lizzie will now properly show best moves even after exhaustion of the --visit visits. --- src/main/java/featurecat/lizzie/analysis/Leelaz.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 02bcb8dc6..58873e7cf 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -206,6 +206,13 @@ private void parseLine(String line) { togglePonder(); } } + } else if (line.contains(" -> ")) { + isLoaded = true; + if (isResponseUpToDate()) { + bestMoves.add(MoveData.fromSummary(line)); + if (Lizzie.frame != null) + Lizzie.frame.repaint(); + } } else if (line.startsWith("play")) { // In lz-genmove_analyze if (Lizzie.frame.isPlayingAgainstLeelaz) { From d45406b09336b3401e1be33529ab2b6d267dda35 Mon Sep 17 00:00:00 2001 From: zsalch Date: Tue, 18 Sep 2018 13:41:07 +0800 Subject: [PATCH 52/75] Support Read/Write Sgf with Variation, AW/AB & Comment --- .../java/featurecat/lizzie/rules/Board.java | 13 ++ .../featurecat/lizzie/rules/BoardData.java | 3 + .../lizzie/rules/BoardHistoryList.java | 14 ++ .../featurecat/lizzie/rules/SGFParser.java | 180 ++++++++++++------ 4 files changed, 151 insertions(+), 59 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 81fe97825..c58a4d433 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -100,6 +100,19 @@ public static boolean isValid(int x, int y) { return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE; } + /** + * The comment. Thread safe + * @param comment the comment of stone + */ + public void comment(String comment) { + synchronized (this) { + + if (history.getData() != null) { + history.getData().comment = comment; + } + } + } + /** * The pass. Thread safe * diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index 1fce99366..be77d1619 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -18,6 +18,9 @@ public class BoardData { public int blackCaptures; public int whiteCaptures; + // Comment in the Sgf move + public String comment; + public BoardData(Stone[] stones, int[] lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, int moveNumber, int[] moveNumberList, int blackCaptures, int whiteCaptures, double winrate, int playouts) { this.moveNumber = moveNumber; this.lastMove = lastMove; diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index 30ba2b81c..d1be0a3db 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -88,6 +88,20 @@ public BoardData next() { return head.getData(); } + /** + * moves the pointer to the right, returns the node stored there + * + * @return the next node, null if there is no next node + */ + public BoardHistoryNode nextNode() { + if (head.next() == null) + return null; + else + head = head.next(); + + return head; + } + /** * moves the pointer to the variation number idx, returns the data stored there * diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index c5f274b12..73f6eceb5 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -1,5 +1,7 @@ package featurecat.lizzie.rules; +import java.util.HashMap; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -66,6 +68,12 @@ private static boolean parse(String value) { return false; } int subTreeDepth = 0; + // Save the variation step count + Map subTreeStepMap = new HashMap(); + // Comment of the AW/AB (Add White/Add Black) stone + String awabComment = null; + // Previous Tag + String prevTag = null; boolean inTag = false, isMultiGo = false, escaping = false; String tag = null; StringBuilder tagBuilder = new StringBuilder(); @@ -79,12 +87,9 @@ private static boolean parse(String value) { String blackPlayer = "", whitePlayer = ""; PARSE_LOOP: - for (byte b : value.getBytes()) { - // Check unicode charactors (UTF-8) - char c = (char) b; - if (((int) b & 0x80) != 0) { - continue; - } + // Suppoert unicode charactors (UTF-8) + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); if (escaping) { // Any char following "\" is inserted verbatim // (ref) "3.2. Text" in https://www.red-bean.com/sgf/sgf4.html @@ -96,14 +101,27 @@ private static boolean parse(String value) { case '(': if (!inTag) { subTreeDepth += 1; + // Initialize the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(0)); + } else { + if (i > 0) { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); + } } break; case ')': if (!inTag) { - subTreeDepth -= 1; if (isMultiGo) { - break PARSE_LOOP; + // Restore to the variation node + for (int s = 0; s < subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue(); s++) { + Lizzie.board.previousMove(); + } } + subTreeDepth -= 1; + } else { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); } break; case '[': @@ -134,6 +152,8 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.BLACK); } else { + // Save the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.BLACK); } } else if (tag.equals("W")) { @@ -141,8 +161,17 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.WHITE); } else { + // Save the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.WHITE); } + } else if (tag.equals("C")) { + // Support comment + if ("AW".equals(prevTag) || "AB".equals(prevTag)) { + awabComment = tagContent; + } else { + Lizzie.board.comment(tagContent); + } } else if (tag.equals("AB")) { int[] move = convertSgfPosToCoord(tagContent); if (move == null) { @@ -173,6 +202,7 @@ private static boolean parse(String value) { e.printStackTrace(); } } + prevTag = tag; break; case ';': break; @@ -199,6 +229,11 @@ private static boolean parse(String value) { // Rewind to game start while (Lizzie.board.previousMove()) ; + // Set AW/AB Comment + if (awabComment != null) { + Lizzie.board.comment(awabComment); + } + return true; } @@ -250,65 +285,92 @@ private static void saveToStream(Board board, Writer writer) throws IOException builder.append(String.format("[%c%c]", x, y)); } } + } else { + // Process the AW/AB stone + Stone[] stones = history.getStones(); + StringBuilder abStone = new StringBuilder(); + StringBuilder awStone = new StringBuilder(); + for (int i = 0; i < stones.length; i++) { + Stone stone = stones[i]; + if (stone.isBlack() || stone.isWhite()) { + // i = x * Board.BOARD_SIZE + y; + int corY = i % Board.BOARD_SIZE; + int corX = (i - corY) / Board.BOARD_SIZE; + + char x = (char) (corX + 'a'); + char y = (char) (corY + 'a'); + + if (stone.isBlack()) { + abStone.append(String.format("[%c%c]", x, y)); + } else { + awStone.append(String.format("[%c%c]", x, y)); + } + } + } + if (abStone.length() > 0) { + builder.append("AB").append(abStone); + } + if (awStone.length() > 0) { + builder.append("AW").append(awStone); + } } + // The AW/AB Comment + if (history.getData().comment != null) { + builder.append(String.format("C[%s]", history.getData().comment)); + } + // replay moves, and convert them to tags. // * format: ";B[xy]" or ";W[xy]" // * with 'xy' = coordinates ; or 'tt' for pass. - BoardData data; - - // TODO: this code comes from cngoodboy's plugin PR #65. It looks like it might be useful for handling - // AB/AW commands for sgfs in general -- we can extend it beyond just handicap. TODO integrate it -// data = history.getData(); -// -// // For handicap -// ArrayList abList = new ArrayList(); -// ArrayList awList = new ArrayList(); -// -// for (int i = 0; i < Board.BOARD_SIZE; i++) { -// for (int j = 0; j < Board.BOARD_SIZE; j++) { -// switch (data.stones[Board.getIndex(i, j)]) { -// case BLACK: -// abList.add(new int[]{i, j}); -// break; -// case WHITE: -// awList.add(new int[]{i, j}); -// break; -// default: -// break; -// } -// } -// } -// -// if (!abList.isEmpty()) { -// builder.append(";AB"); -// for (int i = 0; i < abList.size(); i++) { -// builder.append(String.format("[%s]", convertCoordToSgfPos(abList.get(i)))); -// } -// } -// -// if (!awList.isEmpty()) { -// builder.append(";AW"); -// for (int i = 0; i < awList.size(); i++) { -// builder.append(String.format("[%s]", convertCoordToSgfPos(awList.get(i)))); -// } -// } - - while ((data = history.next()) != null) { - - String stone; - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - else continue; - - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); - - builder.append(String.format(";%s[%c%c]", stone, x, y)); - } + + + // Write variation tree + builder.append(generateNode(board, writer, history.nextNode())); // close file builder.append(')'); writer.append(builder.toString()); } + + // Generate node + private static String generateNode(Board board, Writer writer, BoardHistoryNode node) throws IOException { + StringBuilder builder = new StringBuilder(""); + + if (node != null) { + + BoardData data = node.getData(); + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { + + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + builder.append(String.format(";%s[%c%c]", stone, x, y)); + + // Write the comment + if (data.comment != null) { + builder.append(String.format("C[%s]", data.comment)); + } + + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, writer, sub)); + builder.append(")"); + } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, writer, node.next())); + } else { + return builder.toString(); + } + } + } + + return builder.toString(); + } } From c81e28668c3d899dd3f1c13f5f2b692a0677041f Mon Sep 17 00:00:00 2001 From: zsalch Date: Tue, 18 Sep 2018 14:24:39 +0800 Subject: [PATCH 53/75] Support Seamless Texture & Enhance Stone Quality --- .../featurecat/lizzie/gui/BoardRenderer.java | 34 ++++++++++++++----- .../featurecat/lizzie/gui/LizzieFrame.java | 6 ++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index bdc437d7f..77efd6b08 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -639,8 +639,12 @@ private void drawWoodenBoard(Graphics2D g) { if (uiConfig.getBoolean("fancy-board")) { // fancy version int shadowRadius = (int) (boardLength * MARGIN / 6); - Image boardImage = theme.getBoard(); - g.drawImage(boardImage == null ? theme.getBoard() : boardImage, x - 2 * shadowRadius, y - 2 * shadowRadius, boardLength + 4 * shadowRadius, boardLength + 4 * shadowRadius, null); + // Support seamless texture + BufferedImage boardImage = theme.getBoard(); + TexturePaint paint = new TexturePaint(boardImage, new Rectangle(0, 0, boardImage.getWidth(), boardImage.getHeight())); + g.setPaint(paint); + g.fill(new Rectangle(x - 2 * shadowRadius, y - 2 * shadowRadius, boardLength + 4 * shadowRadius, boardLength + 4 * shadowRadius)); + g.setStroke(new BasicStroke(shadowRadius * 2)); // draw border g.setColor(new Color(0, 0, 0, 50)); @@ -751,8 +755,11 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center case BLACK_CAPTURED: if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, false); - Image stone = theme.getBlackStone(new int[]{x, y}); - g.drawImage(stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + // Enhance draw quality + BufferedImage stone = theme.getBlackStone(new int[]{x, y}); + BufferedImage newstone = new BufferedImage(stoneRadius * 2 + 1, stoneRadius * 2 + 1, BufferedImage.TYPE_INT_ARGB); + newstone.getGraphics().drawImage(stone.getScaledInstance(stoneRadius * 2 + 1, stoneRadius * 2 + 1, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g.drawImage(newstone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(Color.BLACK); @@ -765,7 +772,10 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, false); Image stone = theme.getWhiteStone(new int[]{x, y}); - g.drawImage(stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + // Enhance draw quality + BufferedImage newstone = new BufferedImage(stoneRadius * 2 + 1, stoneRadius * 2 + 1, BufferedImage.TYPE_INT_ARGB); + newstone.getGraphics().drawImage(stone.getScaledInstance(stoneRadius * 2 + 1, stoneRadius * 2 + 1, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g.drawImage(newstone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(Color.WHITE); @@ -778,8 +788,11 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center case BLACK_GHOST: if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, true); - Image stone = theme.getBlackStone(new int[]{x, y}); - g.drawImage(stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + // Enhance draw quality + BufferedImage stone = theme.getBlackStone(new int[]{x, y}); + BufferedImage newstone = new BufferedImage(stoneRadius * 2 + 1, stoneRadius * 2 + 1, BufferedImage.TYPE_INT_ARGB); + newstone.getGraphics().drawImage(stone.getScaledInstance(stoneRadius * 2 + 1, stoneRadius * 2 + 1, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g.drawImage(newstone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(new Color(0, 0, 0));//, uiConfig.getInt("branch-stone-alpha"))); @@ -790,8 +803,11 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center case WHITE_GHOST: if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, true); - Image stone = theme.getWhiteStone(new int[]{x, y}); - g.drawImage(stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + // Enhance draw quality + BufferedImage stone = theme.getWhiteStone(new int[]{x, y}); + BufferedImage newstone = new BufferedImage(stoneRadius * 2 + 1, stoneRadius * 2 + 1, BufferedImage.TYPE_INT_ARGB); + newstone.getGraphics().drawImage(stone.getScaledInstance(stoneRadius * 2 + 1, stoneRadius * 2 + 1, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g.drawImage(newstone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(new Color(255, 255, 255));//, uiConfig.getInt("branch-stone-alpha"))); diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 3bb067a5d..998b053c9 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -470,8 +470,10 @@ private Graphics2D createBackground() { BufferedImage background = boardRenderer.theme.getBackground(); int drawWidth = Math.max(background.getWidth(), getWidth()); int drawHeight = Math.max(background.getHeight(), getHeight()); - - g.drawImage(background, 0, 0, drawWidth, drawHeight, null); + // Support seamless texture + TexturePaint paint = new TexturePaint(background, new Rectangle(0, 0, background.getWidth(), background.getHeight())); + g.setPaint(paint); + g.fill(new Rectangle(0, 0, drawWidth, drawHeight)); return g; } From 271e5def409acaff4209c39ef9f26ff303aee48c Mon Sep 17 00:00:00 2001 From: zsalch Date: Tue, 18 Sep 2018 14:31:31 +0800 Subject: [PATCH 54/75] Support Display Only Last Move Number --- src/main/java/featurecat/lizzie/gui/BoardRenderer.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index bdc437d7f..3c87a2b5d 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -469,11 +469,20 @@ private void drawMoveNumbers(Graphics2D g) { int[] moveNumberList = branch == null ? Lizzie.board.getMoveNumberList() : branch.data.moveNumberList; + // Allow to display only last move number + int lastMoveNumber = Lizzie.board.getData().moveNumber; + int onlyLastMoveNumber = (!Lizzie.config.uiConfig.isNull("only-last-move-number")) ? Lizzie.config.uiConfig.getInt("only-last-move-number") : 9999; + for (int i = 0; i < Board.BOARD_SIZE; i++) { for (int j = 0; j < Board.BOARD_SIZE; j++) { int stoneX = x + scaledMargin + squareLength * i; int stoneY = y + scaledMargin + squareLength * j; + // Allow to display only last move number + if (lastMoveNumber - moveNumberList[Board.getIndex(i, j)] >= onlyLastMoveNumber) { + continue; + } + Stone stoneAtThisPoint = branch == null ? Lizzie.board.getStones()[Board.getIndex(i, j)] : branch.data.stones[Board.getIndex(i, j)]; From c5e326a223695e15a7edb4c85fd52874ba65e066 Mon Sep 17 00:00:00 2001 From: zsalch Date: Wed, 19 Sep 2018 07:42:49 +0800 Subject: [PATCH 55/75] Change uiConfig.optInt Method --- src/main/java/featurecat/lizzie/gui/BoardRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 3c87a2b5d..7b44a7191 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -471,8 +471,8 @@ private void drawMoveNumbers(Graphics2D g) { // Allow to display only last move number int lastMoveNumber = Lizzie.board.getData().moveNumber; - int onlyLastMoveNumber = (!Lizzie.config.uiConfig.isNull("only-last-move-number")) ? Lizzie.config.uiConfig.getInt("only-last-move-number") : 9999; - + int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); + for (int i = 0; i < Board.BOARD_SIZE; i++) { for (int j = 0; j < Board.BOARD_SIZE; j++) { int stoneX = x + scaledMargin + squareLength * i; From 336ede02ec4fb1126ef1b8997443edbbb39d47c6 Mon Sep 17 00:00:00 2001 From: zsalch Date: Wed, 19 Sep 2018 10:39:43 +0800 Subject: [PATCH 56/75] Texture & Scale Moonth Method --- .../featurecat/lizzie/gui/BoardRenderer.java | 47 +++++++++++-------- .../featurecat/lizzie/gui/LizzieFrame.java | 4 +- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 77efd6b08..a1c004a75 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -16,6 +16,7 @@ import java.awt.font.TextAttribute; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -639,11 +640,9 @@ private void drawWoodenBoard(Graphics2D g) { if (uiConfig.getBoolean("fancy-board")) { // fancy version int shadowRadius = (int) (boardLength * MARGIN / 6); - // Support seamless texture BufferedImage boardImage = theme.getBoard(); - TexturePaint paint = new TexturePaint(boardImage, new Rectangle(0, 0, boardImage.getWidth(), boardImage.getHeight())); - g.setPaint(paint); - g.fill(new Rectangle(x - 2 * shadowRadius, y - 2 * shadowRadius, boardLength + 4 * shadowRadius, boardLength + 4 * shadowRadius)); + // Support seamless texture + drawTextureImage(g, boardImage == null ? theme.getBoard() : boardImage, x - 2 * shadowRadius, y - 2 * shadowRadius, boardLength + 4 * shadowRadius, boardLength + 4 * shadowRadius); g.setStroke(new BasicStroke(shadowRadius * 2)); // draw border @@ -755,11 +754,9 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center case BLACK_CAPTURED: if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, false); + Image stone = theme.getBlackStone(new int[]{x, y}); // Enhance draw quality - BufferedImage stone = theme.getBlackStone(new int[]{x, y}); - BufferedImage newstone = new BufferedImage(stoneRadius * 2 + 1, stoneRadius * 2 + 1, BufferedImage.TYPE_INT_ARGB); - newstone.getGraphics().drawImage(stone.getScaledInstance(stoneRadius * 2 + 1, stoneRadius * 2 + 1, java.awt.Image.SCALE_SMOOTH), 0, 0, null); - g.drawImage(newstone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(Color.BLACK); @@ -773,9 +770,7 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center drawShadow(gShadow, centerX, centerY, false); Image stone = theme.getWhiteStone(new int[]{x, y}); // Enhance draw quality - BufferedImage newstone = new BufferedImage(stoneRadius * 2 + 1, stoneRadius * 2 + 1, BufferedImage.TYPE_INT_ARGB); - newstone.getGraphics().drawImage(stone.getScaledInstance(stoneRadius * 2 + 1, stoneRadius * 2 + 1, java.awt.Image.SCALE_SMOOTH), 0, 0, null); - g.drawImage(newstone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(Color.WHITE); @@ -788,11 +783,9 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center case BLACK_GHOST: if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, true); + Image stone = theme.getBlackStone(new int[]{x, y}); // Enhance draw quality - BufferedImage stone = theme.getBlackStone(new int[]{x, y}); - BufferedImage newstone = new BufferedImage(stoneRadius * 2 + 1, stoneRadius * 2 + 1, BufferedImage.TYPE_INT_ARGB); - newstone.getGraphics().drawImage(stone.getScaledInstance(stoneRadius * 2 + 1, stoneRadius * 2 + 1, java.awt.Image.SCALE_SMOOTH), 0, 0, null); - g.drawImage(newstone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(new Color(0, 0, 0));//, uiConfig.getInt("branch-stone-alpha"))); @@ -803,11 +796,9 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center case WHITE_GHOST: if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, true); + Image stone = theme.getWhiteStone(new int[]{x, y}); // Enhance draw quality - BufferedImage stone = theme.getWhiteStone(new int[]{x, y}); - BufferedImage newstone = new BufferedImage(stoneRadius * 2 + 1, stoneRadius * 2 + 1, BufferedImage.TYPE_INT_ARGB); - newstone.getGraphics().drawImage(stone.getScaledInstance(stoneRadius * 2 + 1, stoneRadius * 2 + 1, java.awt.Image.SCALE_SMOOTH), 0, 0, null); - g.drawImage(newstone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(new Color(255, 255, 255));//, uiConfig.getInt("branch-stone-alpha"))); @@ -821,6 +812,24 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center } } + /** + * Draw scale smooth image, enhanced display quality + */ + public void drawScaleSmoothImage(Graphics2D g, Image img, int x, int y, int width, int height, ImageObserver observer) { + BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + newstone.getGraphics().drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, observer); + g.drawImage(newstone, x, y, width, height, observer); + } + + /** + * Draw texture image + */ + public void drawTextureImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height) { + TexturePaint paint = new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight())); + g.setPaint(paint); + g.fill(new Rectangle(x, y, width, height)); + } + /** * Fills in a circle centered at (centerX, centerY) with radius $radius$ */ diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 998b053c9..68f879196 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -471,9 +471,7 @@ private Graphics2D createBackground() { int drawWidth = Math.max(background.getWidth(), getWidth()); int drawHeight = Math.max(background.getHeight(), getHeight()); // Support seamless texture - TexturePaint paint = new TexturePaint(background, new Rectangle(0, 0, background.getWidth(), background.getHeight())); - g.setPaint(paint); - g.fill(new Rectangle(0, 0, drawWidth, drawHeight)); + boardRenderer.drawTextureImage(g, background, 0, 0, drawWidth, drawHeight); return g; } From 2b6358aebd6e90d46fb739d73f7b762ffce629c6 Mon Sep 17 00:00:00 2001 From: zsalch Date: Wed, 19 Sep 2018 17:30:07 +0800 Subject: [PATCH 57/75] Use Spaces instead of Tab --- .../lizzie/rules/BoardHistoryList.java | 4 +- .../featurecat/lizzie/rules/SGFParser.java | 99 +++++++++---------- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index d1be0a3db..044ec0f02 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -101,7 +101,7 @@ public BoardHistoryNode nextNode() { return head; } - + /** * moves the pointer to the variation number idx, returns the data stored there * @@ -134,7 +134,7 @@ public BoardData getNext() { public List getNexts() { return head.getNexts(); } - + /** * Does not change the pointer position * diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 73f6eceb5..27e83a2ff 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -72,7 +72,7 @@ private static boolean parse(String value) { Map subTreeStepMap = new HashMap(); // Comment of the AW/AB (Add White/Add Black) stone String awabComment = null; - // Previous Tag + // Previous Tag String prevTag = null; boolean inTag = false, isMultiGo = false, escaping = false; String tag = null; @@ -86,7 +86,6 @@ private static boolean parse(String value) { String blackPlayer = "", whitePlayer = ""; - PARSE_LOOP: // Suppoert unicode charactors (UTF-8) for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); @@ -104,10 +103,10 @@ private static boolean parse(String value) { // Initialize the step count subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(0)); } else { - if (i > 0) { - // Allow the comment tag includes '(' - tagContentBuilder.append(c); - } + if (i > 0) { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); + } } break; case ')': @@ -152,8 +151,8 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.BLACK); } else { - // Save the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); + // Save the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.BLACK); } } else if (tag.equals("W")) { @@ -161,17 +160,17 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.WHITE); } else { - // Save the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); + // Save the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.WHITE); } } else if (tag.equals("C")) { - // Support comment - if ("AW".equals(prevTag) || "AB".equals(prevTag)) { - awabComment = tagContent; - } else { - Lizzie.board.comment(tagContent); - } + // Support comment + if ("AW".equals(prevTag) || "AB".equals(prevTag)) { + awabComment = tagContent; + } else { + Lizzie.board.comment(tagContent); + } } else if (tag.equals("AB")) { int[] move = convertSgfPosToCoord(tagContent); if (move == null) { @@ -231,7 +230,7 @@ private static boolean parse(String value) { // Set AW/AB Comment if (awabComment != null) { - Lizzie.board.comment(awabComment); + Lizzie.board.comment(awabComment); } return true; @@ -286,7 +285,7 @@ private static void saveToStream(Board board, Writer writer) throws IOException } } } else { - // Process the AW/AB stone + // Process the AW/AB stone Stone[] stones = history.getStones(); StringBuilder abStone = new StringBuilder(); StringBuilder awStone = new StringBuilder(); @@ -301,24 +300,24 @@ private static void saveToStream(Board board, Writer writer) throws IOException char y = (char) (corY + 'a'); if (stone.isBlack()) { - abStone.append(String.format("[%c%c]", x, y)); + abStone.append(String.format("[%c%c]", x, y)); } else { - awStone.append(String.format("[%c%c]", x, y)); + awStone.append(String.format("[%c%c]", x, y)); } } } if (abStone.length() > 0) { - builder.append("AB").append(abStone); + builder.append("AB").append(abStone); } if (awStone.length() > 0) { - builder.append("AW").append(awStone); + builder.append("AW").append(awStone); } } // The AW/AB Comment if (history.getData().comment != null) { - builder.append(String.format("C[%s]", history.getData().comment)); - } + builder.append(String.format("C[%s]", history.getData().comment)); + } // replay moves, and convert them to tags. // * format: ";B[xy]" or ";W[xy]" @@ -339,35 +338,35 @@ private static String generateNode(Board board, Writer writer, BoardHistoryNode if (node != null) { - BoardData data = node.getData(); + BoardData data = node.getData(); String stone = ""; if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); - - builder.append(String.format(";%s[%c%c]", stone, x, y)); - - // Write the comment - if (data.comment != null) { - builder.append(String.format("C[%s]", data.comment)); - } - - if (node.numberOfChildren() > 1) { - // Variation - for (BoardHistoryNode sub : node.getNexts()) { - builder.append("("); - builder.append(generateNode(board, writer, sub)); - builder.append(")"); - } - } else if (node.numberOfChildren() == 1) { - builder.append(generateNode(board, writer, node.next())); - } else { - return builder.toString(); - } + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + builder.append(String.format(";%s[%c%c]", stone, x, y)); + + // Write the comment + if (data.comment != null) { + builder.append(String.format("C[%s]", data.comment)); + } + + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, writer, sub)); + builder.append(")"); + } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, writer, node.next())); + } else { + return builder.toString(); + } } } From 4ba9c13f080c8af8797be21c58fe44e7e4576e23 Mon Sep 17 00:00:00 2001 From: zsalch Date: Wed, 19 Sep 2018 17:41:53 +0800 Subject: [PATCH 58/75] Use Spaces instead of Tab --- .../featurecat/lizzie/gui/BoardRenderer.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index a1c004a75..670870d86 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -755,7 +755,7 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, false); Image stone = theme.getBlackStone(new int[]{x, y}); - // Enhance draw quality + // Enhance draw quality drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); @@ -769,7 +769,7 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, false); Image stone = theme.getWhiteStone(new int[]{x, y}); - // Enhance draw quality + // Enhance draw quality drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); @@ -784,7 +784,7 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, true); Image stone = theme.getBlackStone(new int[]{x, y}); - // Enhance draw quality + // Enhance draw quality drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); @@ -797,7 +797,7 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, true); Image stone = theme.getWhiteStone(new int[]{x, y}); - // Enhance draw quality + // Enhance draw quality drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); } else { drawShadow(gShadow, centerX, centerY, true); @@ -816,18 +816,18 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center * Draw scale smooth image, enhanced display quality */ public void drawScaleSmoothImage(Graphics2D g, Image img, int x, int y, int width, int height, ImageObserver observer) { - BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - newstone.getGraphics().drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, observer); - g.drawImage(newstone, x, y, width, height, observer); - } + BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + newstone.getGraphics().drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, observer); + g.drawImage(newstone, x, y, width, height, observer); + } /** * Draw texture image */ public void drawTextureImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height) { - TexturePaint paint = new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight())); - g.setPaint(paint); - g.fill(new Rectangle(x, y, width, height)); + TexturePaint paint = new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight())); + g.setPaint(paint); + g.fill(new Rectangle(x, y, width, height)); } /** From 8ebe43eff15cfe778f29c913b30bfa176f1e6aa2 Mon Sep 17 00:00:00 2001 From: zsalch Date: Wed, 19 Sep 2018 18:06:04 +0800 Subject: [PATCH 59/75] Remove unnecessary code --- .../featurecat/lizzie/rules/SGFParser.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 27e83a2ff..3311b34fc 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -86,7 +86,7 @@ private static boolean parse(String value) { String blackPlayer = "", whitePlayer = ""; - // Suppoert unicode charactors (UTF-8) + // Support unicode characters (UTF-8) for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (escaping) { @@ -101,7 +101,7 @@ private static boolean parse(String value) { if (!inTag) { subTreeDepth += 1; // Initialize the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(0)); + subTreeStepMap.put(subTreeDepth, 0); } else { if (i > 0) { // Allow the comment tag includes '(' @@ -113,7 +113,8 @@ private static boolean parse(String value) { if (!inTag) { if (isMultiGo) { // Restore to the variation node - for (int s = 0; s < subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue(); s++) { + int varStep = subTreeStepMap.get(subTreeDepth); + for (int s = 0; s < varStep; s++) { Lizzie.board.previousMove(); } } @@ -152,7 +153,7 @@ private static boolean parse(String value) { Lizzie.board.pass(Stone.BLACK); } else { // Save the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); Lizzie.board.place(move[0], move[1], Stone.BLACK); } } else if (tag.equals("W")) { @@ -161,7 +162,7 @@ private static boolean parse(String value) { Lizzie.board.pass(Stone.WHITE); } else { // Save the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); Lizzie.board.place(move[0], move[1], Stone.WHITE); } } else if (tag.equals("C")) { @@ -325,15 +326,17 @@ private static void saveToStream(Board board, Writer writer) throws IOException // Write variation tree - builder.append(generateNode(board, writer, history.nextNode())); + builder.append(generateNode(board, history.nextNode())); // close file builder.append(')'); writer.append(builder.toString()); } - // Generate node - private static String generateNode(Board board, Writer writer, BoardHistoryNode node) throws IOException { + /** + * Generate node with variations + */ + private static String generateNode(Board board, BoardHistoryNode node) throws IOException { StringBuilder builder = new StringBuilder(""); if (node != null) { @@ -359,11 +362,11 @@ private static String generateNode(Board board, Writer writer, BoardHistoryNode // Variation for (BoardHistoryNode sub : node.getNexts()) { builder.append("("); - builder.append(generateNode(board, writer, sub)); + builder.append(generateNode(board, sub)); builder.append(")"); } } else if (node.numberOfChildren() == 1) { - builder.append(generateNode(board, writer, node.next())); + builder.append(generateNode(board, node.next())); } else { return builder.toString(); } From 9c68cf8c7902037103d12108148aa68aafafd3c9 Mon Sep 17 00:00:00 2001 From: zsalch Date: Thu, 20 Sep 2018 13:53:45 +0800 Subject: [PATCH 60/75] Stone performance improvement --- .../featurecat/lizzie/gui/BoardRenderer.java | 89 +++++++++++++++++-- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 670870d86..c6c290917 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -46,6 +46,9 @@ public class BoardRenderer { private BufferedImage cachedStonesShadowImage = null; private Zobrist cachedZhash = new Zobrist(); // defaults to an empty board + private BufferedImage cachedBlackStoneImage = null; + private BufferedImage cachedWhiteStoneImage = null; + private BufferedImage branchStonesImage = null; private BufferedImage branchStonesShadowImage = null; @@ -756,7 +759,8 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center drawShadow(gShadow, centerX, centerY, false); Image stone = theme.getBlackStone(new int[]{x, y}); // Enhance draw quality - drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + int size = stoneRadius * 2 + 1; + g.drawImage(getScaleStone(stone, color, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(Color.BLACK); @@ -770,7 +774,8 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center drawShadow(gShadow, centerX, centerY, false); Image stone = theme.getWhiteStone(new int[]{x, y}); // Enhance draw quality - drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + int size = stoneRadius * 2 + 1; + g.drawImage(getScaleStone(stone, color, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(Color.WHITE); @@ -785,7 +790,8 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center drawShadow(gShadow, centerX, centerY, true); Image stone = theme.getBlackStone(new int[]{x, y}); // Enhance draw quality - drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + int size = stoneRadius * 2 + 1; + g.drawImage(getScaleStone(stone, color, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(new Color(0, 0, 0));//, uiConfig.getInt("branch-stone-alpha"))); @@ -798,7 +804,8 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center drawShadow(gShadow, centerX, centerY, true); Image stone = theme.getWhiteStone(new int[]{x, y}); // Enhance draw quality - drawScaleSmoothImage(g, stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); + int size = stoneRadius * 2 + 1; + g.drawImage(getScaleStone(stone, color, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(new Color(255, 255, 255));//, uiConfig.getInt("branch-stone-alpha"))); @@ -812,12 +819,82 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center } } + /** + * Get scaled stone, if cached then return cached + */ + public BufferedImage getScaleStone(Image img, Stone color, int width, int height) { + BufferedImage stone = null; + switch (color) { + case BLACK: + case BLACK_CAPTURED: + case BLACK_GHOST: + if (cachedBlackStoneImage == null || cachedBlackStoneImage.getWidth() != width || cachedBlackStoneImage.getHeight() != height) { + stone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = stone.createGraphics(); + g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g2.dispose(); + cachedBlackStoneImage = stone; + } else { + return cachedBlackStoneImage; + } + break; + case WHITE: + case WHITE_CAPTURED: + case WHITE_GHOST: + if (cachedWhiteStoneImage == null || cachedWhiteStoneImage.getWidth() != width || cachedWhiteStoneImage.getHeight() != height) { + stone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = stone.createGraphics(); + g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g2.dispose(); + cachedWhiteStoneImage = stone; + } else { + return cachedWhiteStoneImage; + } + break; + default: + } + return stone; + } + /** * Draw scale smooth image, enhanced display quality */ - public void drawScaleSmoothImage(Graphics2D g, Image img, int x, int y, int width, int height, ImageObserver observer) { + public void drawScaleSmoothImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height, ImageObserver observer) { BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - newstone.getGraphics().drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, observer); + Graphics2D g2 = newstone.createGraphics(); + g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, observer); + g2.dispose(); + g.drawImage(newstone, x, y, width, height, observer); + } + + /** + * Draw scale smooth image, enhanced display quality, less and faster than drawScaleSmoothImage + */ + public void drawScaleImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height, ImageObserver observer) { + BufferedImage newstone = (BufferedImage)img; + int w = img.getWidth(); + int h = img.getHeight(); + do { + if (w > width) { + w /= 2; + if (w < width) { + w = width; + } + } + if (h > height) { + h /= 2; + if (h < height) { + h = height; + } + } + BufferedImage tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = tmp.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2.drawImage(newstone, 0, 0, w, h, null); + g2.dispose(); + newstone = tmp; + } + while (w != width || h != height); g.drawImage(newstone, x, y, width, height, observer); } From 1d9a2d5952e6073f6fe620d8e6017c5956dd867d Mon Sep 17 00:00:00 2001 From: zsalch Date: Thu, 20 Sep 2018 23:00:12 +0800 Subject: [PATCH 61/75] Simplified code --- .../featurecat/lizzie/gui/BoardRenderer.java | 121 ++++-------------- 1 file changed, 28 insertions(+), 93 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index c6c290917..54475a044 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -752,106 +752,41 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center if (gShadow == null) gShadow = g; - switch (color) { - case BLACK: - case BLACK_CAPTURED: - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, false); - Image stone = theme.getBlackStone(new int[]{x, y}); - // Enhance draw quality - int size = stoneRadius * 2 + 1; - g.drawImage(getScaleStone(stone, color, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(Color.BLACK); - fillCircle(g, centerX, centerY, stoneRadius); - } - break; - - case WHITE: - case WHITE_CAPTURED: - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, false); - Image stone = theme.getWhiteStone(new int[]{x, y}); - // Enhance draw quality - int size = stoneRadius * 2 + 1; - g.drawImage(getScaleStone(stone, color, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(Color.WHITE); - fillCircle(g, centerX, centerY, stoneRadius); - g.setColor(Color.BLACK); - drawCircle(g, centerX, centerY, stoneRadius); - } - break; - - case BLACK_GHOST: - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, true); - Image stone = theme.getBlackStone(new int[]{x, y}); - // Enhance draw quality - int size = stoneRadius * 2 + 1; - g.drawImage(getScaleStone(stone, color, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(new Color(0, 0, 0));//, uiConfig.getInt("branch-stone-alpha"))); - fillCircle(g, centerX, centerY, stoneRadius); - } - break; - - case WHITE_GHOST: - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, true); - Image stone = theme.getWhiteStone(new int[]{x, y}); - // Enhance draw quality - int size = stoneRadius * 2 + 1; - g.drawImage(getScaleStone(stone, color, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(new Color(255, 255, 255));//, uiConfig.getInt("branch-stone-alpha"))); - fillCircle(g, centerX, centerY, stoneRadius); - g.setColor(new Color(0, 0, 0));//, uiConfig.getInt("branch-stone-alpha"))); - drawCircle(g, centerX, centerY, stoneRadius); + if (color.isBlack() || color.isWhite()) { + boolean isBlack = color.isBlack(); + boolean isGhost = (color == Stone.BLACK_GHOST || color == Stone.WHITE_GHOST); + if (uiConfig.getBoolean("fancy-stones")) { + drawShadow(gShadow, centerX, centerY, isGhost); + Image stone = isBlack ? theme.getBlackStone(new int[]{x, y}) : theme.getWhiteStone(new int[]{x, y}); + int size = stoneRadius * 2 + 1; + g.drawImage(getScaleStone(stone, isBlack, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); + } else { + drawShadow(gShadow, centerX, centerY, true); + g.setColor(isBlack ? (isGhost ? new Color(0, 0, 0) : Color.BLACK) : (isGhost ? new Color(255, 255, 255) : Color.WHITE)); + fillCircle(g, centerX, centerY, stoneRadius); + if (!isBlack) { + g.setColor(isGhost ? new Color(0, 0, 0) : Color.BLACK); + drawCircle(g, centerX, centerY, stoneRadius); } - break; - - default: + } } } /** * Get scaled stone, if cached then return cached */ - public BufferedImage getScaleStone(Image img, Stone color, int width, int height) { - BufferedImage stone = null; - switch (color) { - case BLACK: - case BLACK_CAPTURED: - case BLACK_GHOST: - if (cachedBlackStoneImage == null || cachedBlackStoneImage.getWidth() != width || cachedBlackStoneImage.getHeight() != height) { - stone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = stone.createGraphics(); - g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, null); - g2.dispose(); - cachedBlackStoneImage = stone; - } else { - return cachedBlackStoneImage; - } - break; - case WHITE: - case WHITE_CAPTURED: - case WHITE_GHOST: - if (cachedWhiteStoneImage == null || cachedWhiteStoneImage.getWidth() != width || cachedWhiteStoneImage.getHeight() != height) { - stone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = stone.createGraphics(); - g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, null); - g2.dispose(); - cachedWhiteStoneImage = stone; - } else { - return cachedWhiteStoneImage; - } - break; - default: + public BufferedImage getScaleStone(Image img, boolean isBlack, int width, int height) { + BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; + if (stone == null || stone.getWidth() != width || stone.getHeight() != height) { + stone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = stone.createGraphics(); + g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g2.dispose(); + if (isBlack) { + cachedBlackStoneImage = stone; + } else { + cachedWhiteStoneImage = stone; + } } return stone; } From c8f10f4437a8d2a2cf37dfa03e0eb8a53887dbdc Mon Sep 17 00:00:00 2001 From: zsalch Date: Fri, 21 Sep 2018 09:56:14 +0800 Subject: [PATCH 62/75] Describe & disabled not use function --- .../featurecat/lizzie/gui/BoardRenderer.java | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 54475a044..b5e112d53 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -792,46 +792,52 @@ public BufferedImage getScaleStone(Image img, boolean isBlack, int width, int he } /** - * Draw scale smooth image, enhanced display quality + * Draw scale smooth image, enhanced display quality (Not use, for future) + * This function use the traditional Image.getScaledInstance() method + * to provide the nice quality, but the performance is poor. + * Recommended for use in a few drawings */ - public void drawScaleSmoothImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height, ImageObserver observer) { - BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = newstone.createGraphics(); - g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, observer); - g2.dispose(); - g.drawImage(newstone, x, y, width, height, observer); - } +// public void drawScaleSmoothImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height, ImageObserver observer) { +// BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); +// Graphics2D g2 = newstone.createGraphics(); +// g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, observer); +// g2.dispose(); +// g.drawImage(newstone, x, y, width, height, observer); +// } /** - * Draw scale smooth image, enhanced display quality, less and faster than drawScaleSmoothImage + * Draw scale smooth image, enhanced display quality (Not use, for future) + * This functions use a multi-step approach to prevent the information loss + * and produces a much higher quality that is close to the Image.getScaledInstance() + * and faster than Image.getScaledInstance() method. */ - public void drawScaleImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height, ImageObserver observer) { - BufferedImage newstone = (BufferedImage)img; - int w = img.getWidth(); - int h = img.getHeight(); - do { - if (w > width) { - w /= 2; - if (w < width) { - w = width; - } - } - if (h > height) { - h /= 2; - if (h < height) { - h = height; - } - } - BufferedImage tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = tmp.createGraphics(); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); - g2.drawImage(newstone, 0, 0, w, h, null); - g2.dispose(); - newstone = tmp; - } - while (w != width || h != height); - g.drawImage(newstone, x, y, width, height, observer); - } +// public void drawScaleImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height, ImageObserver observer) { +// BufferedImage newstone = (BufferedImage)img; +// int w = img.getWidth(); +// int h = img.getHeight(); +// do { +// if (w > width) { +// w /= 2; +// if (w < width) { +// w = width; +// } +// } +// if (h > height) { +// h /= 2; +// if (h < height) { +// h = height; +// } +// } +// BufferedImage tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); +// Graphics2D g2 = tmp.createGraphics(); +// g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); +// g2.drawImage(newstone, 0, 0, w, h, null); +// g2.dispose(); +// newstone = tmp; +// } +// while (w != width || h != height); +// g.drawImage(newstone, x, y, width, height, observer); +// } /** * Draw texture image From bf7587f744959fce878b2eaacdf135c0f4ff4762 Mon Sep 17 00:00:00 2001 From: zsalch Date: Sat, 22 Sep 2018 23:59:03 +0800 Subject: [PATCH 63/75] Add Test Case --- pom.xml | 19 +++ src/test/java/common/Util.java | 149 ++++++++++++++++++ .../lizzie/rules/SGFParserTest.java | 124 +++++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 src/test/java/common/Util.java create mode 100644 src/test/java/featurecat/lizzie/rules/SGFParserTest.java diff --git a/pom.xml b/pom.xml index 6f02aa141..49139edda 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,9 @@ 1.8 1.8 + + -Xlint:all + @@ -52,6 +55,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.9 + + + false + + @@ -63,5 +75,12 @@ 20180130 + + + junit + junit + 4.11 + test + diff --git a/src/test/java/common/Util.java b/src/test/java/common/Util.java new file mode 100644 index 000000000..e201d57f3 --- /dev/null +++ b/src/test/java/common/Util.java @@ -0,0 +1,149 @@ +package common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.UnsupportedLookAndFeelException; + +import org.json.JSONException; +import org.junit.Test; + +import featurecat.lizzie.Config; +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.analysis.Leelaz; +import featurecat.lizzie.analysis.MoveData; +import featurecat.lizzie.gui.LizzieFrame; +import featurecat.lizzie.rules.Board; +import featurecat.lizzie.rules.BoardData; +import featurecat.lizzie.rules.BoardHistoryList; +import featurecat.lizzie.rules.BoardHistoryNode; +import featurecat.lizzie.rules.SGFParser; +import featurecat.lizzie.rules.Stone; + +public class Util { + + private static ArrayList laneUsageList = new ArrayList(); + + /** + * Get Variation Tree as String List + * The logic is same as the function VariationTree.drawTree + * + * @param startLane + * @param startNode + * @param variationNumber + * @param isMain + */ + public static void getVariationTree(List moveList, int startLane, BoardHistoryNode startNode, int variationNumber, boolean isMain) { + // Finds depth on leftmost variation of this tree + int depth = BoardHistoryList.getDepth(startNode) + 1; + int lane = startLane; + // Figures out how far out too the right (which lane) we have to go not to collide with other variations + while (lane < laneUsageList.size() && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { + // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" + laneUsageList.set(lane, startNode.getData().moveNumber - 1); + lane++; + } + if (lane >= laneUsageList.size()) + { + laneUsageList.add(0); + } + if (variationNumber > 1) + laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); + laneUsageList.set(lane, startNode.getData().moveNumber); + + // At this point, lane contains the lane we should use (the main branch is in lane 0) + BoardHistoryNode cur = startNode; + + // Draw main line + StringBuilder sb = new StringBuilder(); + sb.append(formatMove(cur.getData())); + while (cur.next() != null) { + cur = cur.next(); + sb.append(formatMove(cur.getData())); + } + moveList.add(sb.toString()); + // Now we have drawn all the nodes in this variation, and has reached the bottom of this variation + // Move back up, and for each, draw any variations we find + while (cur.previous() != null && cur != startNode) { + cur = cur.previous(); + int curwidth = lane; + // Draw each variation, uses recursion + for (int i = 1; i < cur.numberOfChildren(); i++) { + curwidth++; + // Recursion, depth of recursion will normally not be very deep (one recursion level for every variation that has a variation (sort of)) + getVariationTree(moveList, curwidth, cur.getVariation(i), i, false); + } + } + } + + private static String formatMove(BoardData data) { + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + else return stone; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + String comment = ""; + if (data.comment != null && data.comment.trim().length() > 0) { + comment = String.format("C[%s]", data.comment); + } + return String.format(";%s[%c%c]%s", stone, x, y, comment); + } + + public static String trimGameInfo(String sgf) { + String gameInfo = String.format("(?s).*AP\\[Lizzie: %s\\]", + Lizzie.lizzieVersion); + return sgf.replaceFirst(gameInfo, "("); + } + + public static String[] splitAwAbSgf(String sgf) { + String[] ret = new String[2]; + String regex = "(A[BW]{1}(\\[[a-z]{2}\\])+)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(sgf); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + sb.append(matcher.group(0)); + } + ret[0] = sb.toString(); + ret[1] = sgf.replaceAll(regex, ""); + return ret; + } + + public static Stone[] convertStones(String awAb) { + Stone[] stones = new Stone[Board.BOARD_SIZE * Board.BOARD_SIZE]; + for (int i = 0; i < stones.length; i++) { + stones[i] = Stone.EMPTY; + } + String regex = "(A[BW]{1})|(?<=\\[)([a-z]{2})(?=\\])"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(awAb); + StringBuilder sb = new StringBuilder(); + Stone stone = Stone.EMPTY; + while (matcher.find()) { + String str = matcher.group(0); + if("AB".equals(str)) { + stone = Stone.BLACK; + } else if("AW".equals(str)) { + stone = Stone.WHITE; + } else { + int[] move = SGFParser.convertSgfPosToCoord(str); + int index = Board.getIndex(move[0], move[1]); + stones[index] = stone; + } + } + return stones; + } +} diff --git a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java new file mode 100644 index 000000000..893cb64e2 --- /dev/null +++ b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java @@ -0,0 +1,124 @@ +package featurecat.lizzie.rules; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertArrayEquals; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import common.Util; + +import featurecat.lizzie.Config; +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.analysis.Leelaz; +import featurecat.lizzie.gui.LizzieFrame; + +public class SGFParserTest { + + private Lizzie lizzie = null; + + @Test + + public void run() throws IOException { + lizzie = new Lizzie(); + lizzie.config = new Config(); + lizzie.board = new Board(); + lizzie.frame = new LizzieFrame(); + // new Thread( () -> { + lizzie.leelaz = new Leelaz(); + // }).start(); + + testVariaionOnly1(); + testFull1(); + } + + public void testVariaionOnly1() throws IOException { + + String sgfString = "(;B[pd];W[dp];B[pp];W[dd];B[fq]" + + "(;W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd]" + + "(;B[gb]" + + "(;W[hc];B[nq])" + + "(;W[gc];B[ec];W[hc];B[hb];W[ic]))" + + "(;B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]))" + + "(;W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]))"; + + int variationNum = 4; + String mainBranch = ";B[pd];W[dp];B[pp];W[dd];B[fq];W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd];B[gb];W[hc];B[nq]"; + String variation1 = ";W[gc];B[ec];W[hc];B[hb];W[ic]"; + String variation2 = ";B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]"; + String variation3 = ";W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]"; + + // Load correctly + boolean loaded = SGFParser.loadFromString(sgfString); + assertTrue(loaded); + + // Variations + List moveList = new ArrayList(); + Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); + + assertTrue(moveList != null); + assertEquals(moveList.size(), variationNum); + + assertEquals(moveList.get(0), mainBranch); + assertEquals(moveList.get(1), variation1); + assertEquals(moveList.get(2), variation2); + assertEquals(moveList.get(3), variation3); + + // Save correctly + String saveSgf = SGFParser.saveToString(); + assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + + assertEquals(sgfString, Util.trimGameInfo(saveSgf)); + } + + public void testFull1() throws IOException { + + String sgfInfo = "(;CA[utf8]AP[MultiGo:4.4.4]SZ[19]"; + String sgfAwAb = "AB[pe][pq][oq][nq][mq][cp][dq][eq][fp]AB[qd]AW[dc][cf][oc][qo][op][np][mp][ep][fq]"; + String sgfContent = ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?]" + + "(;B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.])" + + "(;B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]))"; + String sgfString = sgfInfo + sgfAwAb + sgfContent; + + int variationNum = 2; + String mainBranch = ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?];B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.]"; + String variation1 = ";B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]"; + + Stone[] expectStones = Util.convertStones(sgfAwAb); + + // Load correctly + boolean loaded = SGFParser.loadFromString(sgfString); + assertTrue(loaded); + + // Variations + List moveList = new ArrayList(); + Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); + + assertTrue(moveList != null); + assertEquals(moveList.size(), variationNum); + assertEquals(moveList.get(0), mainBranch); + assertEquals(moveList.get(1), variation1); + + // AW/AB + assertArrayEquals(expectStones, Lizzie.board.getHistory().getStones()); + + // Save correctly + String saveSgf = SGFParser.saveToString(); + assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + + String sgf = Util.trimGameInfo(saveSgf); + String[] ret = Util.splitAwAbSgf(sgf); + Stone[] actualStones = Util.convertStones(ret[0]); + + // AW/AB + assertArrayEquals(expectStones, actualStones); + + // Content + assertEquals("(" + sgfContent, ret[1]); + } + +} From b8af7a44ae78a506579d7c1db78bacb25822a226 Mon Sep 17 00:00:00 2001 From: zsalch Date: Tue, 25 Sep 2018 13:11:32 +0800 Subject: [PATCH 64/75] Fix branch move number when mouse moveover --- src/main/java/featurecat/lizzie/gui/BoardRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 7b44a7191..0fa1d3fd5 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -470,7 +470,7 @@ private void drawMoveNumbers(Graphics2D g) { int[] moveNumberList = branch == null ? Lizzie.board.getMoveNumberList() : branch.data.moveNumberList; // Allow to display only last move number - int lastMoveNumber = Lizzie.board.getData().moveNumber; + int lastMoveNumber = branch == null ? Lizzie.board.getData().moveNumber : branch.data.moveNumber; int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); for (int i = 0; i < Board.BOARD_SIZE; i++) { From 339feef547c951f0b2986839e690afb2699fdaac Mon Sep 17 00:00:00 2001 From: zsalch Date: Thu, 27 Sep 2018 16:19:42 +0800 Subject: [PATCH 65/75] Allow top branch --- .../featurecat/lizzie/rules/SGFParser.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 3311b34fc..42b725814 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -324,9 +324,8 @@ private static void saveToStream(Board board, Writer writer) throws IOException // * format: ";B[xy]" or ";W[xy]" // * with 'xy' = coordinates ; or 'tt' for pass. - // Write variation tree - builder.append(generateNode(board, history.nextNode())); + builder.append(generateNode(board, history.getCurrentHistoryNode())); // close file builder.append(')'); @@ -357,19 +356,19 @@ private static String generateNode(Board board, BoardHistoryNode node) throws IO if (data.comment != null) { builder.append(String.format("C[%s]", data.comment)); } + } - if (node.numberOfChildren() > 1) { - // Variation - for (BoardHistoryNode sub : node.getNexts()) { - builder.append("("); - builder.append(generateNode(board, sub)); - builder.append(")"); - } - } else if (node.numberOfChildren() == 1) { - builder.append(generateNode(board, node.next())); - } else { - return builder.toString(); + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, sub)); + builder.append(")"); } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, node.next())); + } else { + return builder.toString(); } } From 45f213bcdd3bc4800cbf40a04b9da16ade81add5 Mon Sep 17 00:00:00 2001 From: zsalch Date: Wed, 3 Oct 2018 20:13:57 +0800 Subject: [PATCH 66/75] Fixed the code by review --- src/main/java/featurecat/lizzie/Config.java | 5 + .../java/featurecat/lizzie/gui/Input.java | 7 ++ .../featurecat/lizzie/gui/LizzieFrame.java | 104 +++++++++++++++++- .../featurecat/lizzie/gui/VariationTree.java | 2 +- .../lizzie/rules/BoardHistoryList.java | 14 --- .../resources/l10n/DisplayStrings.properties | 1 + .../l10n/DisplayStrings_RO.properties | 1 + .../l10n/DisplayStrings_zh_CN.properties | 1 + 8 files changed, 119 insertions(+), 16 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 8f6d7a3f1..c7314f430 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -15,6 +15,7 @@ public class Config { public boolean showMoveNumber = false; public boolean showWinrate = true; public boolean showVariationGraph = true; + public boolean showComment = false; public boolean showRawBoard = false; public boolean showCaptured = true; public boolean handicapInsteadOfWinrate = false; @@ -132,6 +133,7 @@ public Config() throws IOException { showBranch = uiConfig.getBoolean("show-leelaz-variation"); showWinrate = uiConfig.getBoolean("show-winrate"); showVariationGraph = uiConfig.getBoolean("show-variation-graph"); + showComment = uiConfig.optBoolean("show-comment", false); showCaptured = uiConfig.getBoolean("show-captured"); showBestMoves = uiConfig.getBoolean("show-best-moves"); showNextMoves = uiConfig.getBoolean("show-next-moves"); @@ -179,6 +181,9 @@ public void toggleShowWinrate() { public void toggleShowVariationGraph() { this.showVariationGraph = !this.showVariationGraph; } + public void toggleShowComment() { + this.showComment = !this.showComment; + } public void toggleShowBestMoves() { this.showBestMoves = !this.showBestMoves; } diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index ee32f376a..6c5f077c5 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -316,6 +316,10 @@ public void keyPressed(KeyEvent e) { Lizzie.config.toggleShowVariationGraph(); break; + case VK_T: + Lizzie.config.toggleShowComment(); + break; + case VK_C: if (controlIsPressed(e)) { Lizzie.frame.copySgf(); @@ -399,6 +403,9 @@ public void keyReleased(KeyEvent e) { @Override public void mouseWheelMoved(MouseWheelEvent e) { + if (Lizzie.frame.processCommentMouseWheelMoved(e)) { + return; + } if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); if (e.getWheelRotation() > 0) { diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 68f879196..34e0e279e 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -21,6 +21,7 @@ import featurecat.lizzie.rules.SGFParser; import org.json.JSONObject; import org.json.JSONArray; +import org.json.JSONException; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; @@ -30,6 +31,7 @@ import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; +import java.awt.event.MouseWheelEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferStrategy; @@ -68,6 +70,7 @@ public class LizzieFrame extends JFrame { resourceBundle.getString("LizzieFrame.commands.keyV"), resourceBundle.getString("LizzieFrame.commands.keyW"), resourceBundle.getString("LizzieFrame.commands.keyG"), + resourceBundle.getString("LizzieFrame.commands.keyT"), resourceBundle.getString("LizzieFrame.commands.keyHome"), resourceBundle.getString("LizzieFrame.commands.keyEnd"), resourceBundle.getString("LizzieFrame.commands.keyControl"), @@ -95,6 +98,13 @@ public class LizzieFrame extends JFrame { private long lastAutosaveTime = System.currentTimeMillis(); + // Display Comment + private JScrollPane scrollPane = null; + private JTextPane commentPane = null; + private BufferedImage commentImage = null; + private String cachedComment = null; + private Rectangle commentRect = null; + static { // load fonts try { @@ -125,6 +135,17 @@ public LizzieFrame() { setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized } + // Comment Pane + commentPane = new JTextPane(); + commentPane.setEditable(false); + commentPane.setMargin(new Insets(5, 5, 5, 5)); + commentPane.setBackground(new Color(0, 0, 0, 200)); + commentPane.setForeground(Color.WHITE); + scrollPane = new JScrollPane(); + scrollPane.setViewportView(commentPane); + scrollPane.setBorder(null); + scrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + setVisible(true); createBufferStrategy(2); @@ -414,7 +435,17 @@ public void paint(Graphics g0) { if (Lizzie.config.showVariationGraph) { drawVariationTreeContainer(backgroundG, vx, vy, vw, vh); - variationTree.draw(g, treex, treey, treew, treeh); + int cHeight = 0; + if (Lizzie.config.showComment) { + // Draw the Comment of the Sgf + cHeight = drawComment(g, vx, vy, vw, vh, false); + } + variationTree.draw(g, treex, treey, treew, treeh - cHeight); + } else { + if (Lizzie.config.showComment) { + // Draw the Comment of the Sgf + drawComment(g, vx, topInset, vw, vh - topInset + vy, true); + } } if (Lizzie.config.showSubBoard) { try { @@ -867,6 +898,44 @@ public void onMouseDragged(int x, int y) { } } + /** + * Process Comment Mouse Wheel Moved + * + * @return true when the scroll event was processed by this method + */ + public boolean processCommentMouseWheelMoved(MouseWheelEvent e) { + if (Lizzie.config.showComment && commentRect != null && commentRect.contains(e.getX(), e.getY())) { + scrollPane.dispatchEvent(e); + createCommentImage(true, 0, 0); + getGraphics().drawImage(commentImage, commentRect.x, commentRect.y, commentRect.width, commentRect.height, null); + return true; + } else { + return false; + } + } + + /** + * Create comment cached image + * + * @param forceRefresh + * @param w + * @param h + */ + public void createCommentImage(boolean forceRefresh, int w, int h) { + if (forceRefresh || commentImage == null || scrollPane.getWidth() != w || scrollPane.getHeight() != h) { + if (w > 0 && h > 0) { + scrollPane.setSize(w, h); + } + commentImage = new BufferedImage(scrollPane.getWidth(), scrollPane.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = commentImage.createGraphics(); + scrollPane.doLayout(); + scrollPane.addNotify(); + scrollPane.validate(); + scrollPane.printAll(g2); + g2.dispose(); + } + } + private void autosaveMaybe() { int interval = Lizzie.config.config.getJSONObject("ui").getInt("autosave-interval-seconds") * 1000; long currentTime = System.currentTimeMillis(); @@ -945,4 +1014,37 @@ public void pasteSgf() { public void increaseMaxAlpha(int k) { boardRenderer.increaseMaxAlpha(k); } + + /** + * Draw the Comment of the Sgf file + * + * @param g + * @param x + * @param y + * @param w + * @param h + * @param full + * @return + */ + private int drawComment(Graphics2D g, int x, int y, int w, int h, boolean full) { + String comment = (Lizzie.board.getHistory().getData() != null && Lizzie.board.getHistory().getData().comment != null) ? Lizzie.board.getHistory().getData().comment : ""; + int cHeight = full ? h : (int)(h*0.5); + int fontSize = (int)(Math.min(getWidth(), getHeight()) * 0.98 * 0.03); + try { + fontSize = Lizzie.config.uiConfig.getInt("comment-font-size"); + } catch (JSONException e) { + if (fontSize < 16) { + fontSize = 16; + } + } + Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); + commentPane.setFont(font); + commentPane.setText(comment); + commentPane.setSize(w, cHeight); + createCommentImage((comment != null && !comment.equals(this.cachedComment)) ? true : false, w, cHeight); + commentRect = new Rectangle(x, y + (h - cHeight), scrollPane.getWidth(), scrollPane.getHeight()); + g.drawImage(commentImage, commentRect.x, commentRect.y, commentRect.width, commentRect.height, null); + cachedComment = comment; + return cHeight; + } } diff --git a/src/main/java/featurecat/lizzie/gui/VariationTree.java b/src/main/java/featurecat/lizzie/gui/VariationTree.java index 6c873dc0e..ff05d30bc 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -79,7 +79,7 @@ public void drawTree(Graphics2D g, int posx, int posy, int startLane, int maxpos g.setColor(curcolor); // Draw main line - while (cur.next() != null && posy + YSPACING < maxposy) { + while (cur.next() != null && ((posy + YSPACING + DOT_DIAM) < maxposy)) { // Fix oval cover issue sometimes posy += YSPACING; cur = cur.next(); if (cur == curMove) { diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index 044ec0f02..643079036 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -88,20 +88,6 @@ public BoardData next() { return head.getData(); } - /** - * moves the pointer to the right, returns the node stored there - * - * @return the next node, null if there is no next node - */ - public BoardHistoryNode nextNode() { - if (head.next() == null) - return null; - else - head = head.next(); - - return head; - } - /** * moves the pointer to the variation number idx, returns the data stored there * diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index 05deb875a..c8cf8079e 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -25,6 +25,7 @@ LizzieFrame.commands.keyEnd=end|go to end LizzieFrame.commands.keyEnter=enter|force Leela Zero move LizzieFrame.commands.keyF=f|toggle next move display LizzieFrame.commands.keyG=g|toggle variation graph +LizzieFrame.commands.keyT=t|toggle comment display LizzieFrame.commands.keyHome=home|go to start LizzieFrame.commands.keyI=i|edit game info LizzieFrame.commands.keyA=a|run automatic analysis of game diff --git a/src/main/resources/l10n/DisplayStrings_RO.properties b/src/main/resources/l10n/DisplayStrings_RO.properties index 07b385516..d01b4059a 100644 --- a/src/main/resources/l10n/DisplayStrings_RO.properties +++ b/src/main/resources/l10n/DisplayStrings_RO.properties @@ -24,6 +24,7 @@ LizzieFrame.commands.keyEnd=end|mergi la sfârșit LizzieFrame.commands.keyEnter=enter|forțează mutarea lui Leela Zero LizzieFrame.commands.keyF=f|afișează/ascunde următoarea mutare LizzieFrame.commands.keyG=g|afișează/ascunde graficul cu variante +LizzieFrame.commands.keyT=t|afișează/ascunde comentariul LizzieFrame.commands.keyHome=home|mergi la început LizzieFrame.commands.keyI=i|editează informațiile jocului LizzieFrame.commands.keyA=a|rulează analiza automată a jocului diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index 95d9c079d..2309ac564 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -13,6 +13,7 @@ LizzieFrame.commands.keyEnd=end|\u8DF3\u5230\u68CB\u8C31\u672B\u5C3E LizzieFrame.commands.keyEnter=enter|\u8BA9Leela Zero\u843D\u5B50 LizzieFrame.commands.keyF=f|\u663E\u793A/\u9690\u85CF\u4E0B\u4E00\u624B LizzieFrame.commands.keyG=g|\u663E\u793A/\u9690\u85CF\u5206\u652F\u56FE +LizzieFrame.commands.keyT=t|\u663E\u793A/\u9690\u85CF\u8BC4\u8BBA LizzieFrame.commands.keyHome=home|\u8DF3\u8F6C\u5230\u68CB\u8C31\u5F00\u5934 LizzieFrame.commands.keyI=i|\u7F16\u8F91\u68CB\u5C40\u4FE1\u606F LizzieFrame.commands.keyA=a|\u8FD0\u884C\u7B80\u5355\u7684\u81EA\u52A8\u5168\u76D8\u5206\u6790 From 3aae8b0a14e4ef7a883d539e378a08b44fd72dc3 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Wed, 3 Oct 2018 22:13:48 +0200 Subject: [PATCH 67/75] Remove plugin infrastructure --- doc/plugin.md | 115 ------------------ src/main/java/featurecat/lizzie/Lizzie.java | 3 - .../featurecat/lizzie/gui/BoardRenderer.java | 12 +- .../java/featurecat/lizzie/gui/Input.java | 49 ++------ .../featurecat/lizzie/plugin/IPlugin.java | 62 ---------- .../lizzie/plugin/PluginManager.java | 100 --------------- .../featurecat/lizzie/rules/GIBParser.java | 3 - .../featurecat/lizzie/rules/SGFParser.java | 4 - 8 files changed, 14 insertions(+), 334 deletions(-) delete mode 100644 doc/plugin.md delete mode 100644 src/main/java/featurecat/lizzie/plugin/IPlugin.java delete mode 100644 src/main/java/featurecat/lizzie/plugin/PluginManager.java diff --git a/doc/plugin.md b/doc/plugin.md deleted file mode 100644 index e5053390c..000000000 --- a/doc/plugin.md +++ /dev/null @@ -1,115 +0,0 @@ -# Developing plug-ins for Lizzie - - -The Jar file for a plugin should look like this: - -./plugin -./plugin/Plugin.class -./META-INF -./META-INF/MANIFEST.MF -./...Your other classes - -### Start first step - -Create a "HelloWorld" directory and create a "plugin" directory in it, and create a new "Plugin.java" file in the "plugin" directory. - -And write the following - -```java -package plugin; - -import java.io.IOException; - -import IPlugin; - -/** - * Plugin - */ -public class Plugin implements IPlugin { - public static String name = "Hello World"; - public static String version = "0.0.0"; - - @Override - public String getName() { - return name; - } - - @Override - public String getVersion() { - return version; - } -} -``` - -This is the most basic framework of a plugin where ``` name ``` and ``` version ``` are used to generate a hash of this plugin. - -Let's edit Plugin.java to have a message box pop up when we press 'H'. - -```java - @Override - public void onKeyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_H) { - JOptionPane.showConfirmDialog(null, "Hello World!"); - } - } -``` - -And add the import statement at the beginning of the file - -```java -import java.swt.KeyEvent; -import javax.swing.JOptionPane; -``` - -Copy lizzie.jar to "HelloWorld" directory, and execute the command: - -``` -javac -classpath lizzie.jar ./plugin/Plugin.java -jar -cvf HelloWorld.jar ./plugin/Plugin.class -``` - -Copy the generated "HelloWorld.jar" to the "plugin" directory under the Lizzie directory. - -Start the lizzie and press 'H'! - -### More - -The plugin can freely call Lizzie's own classes and interfaces, so it can do a lot of functions. - -In the future, plugins may be able to enable and disable certain functions, so it is possible to do very complete functions with plug-ins. - -### Methods for plug-ins - -In ``` Lizzie.config ```: - -``` public boolean showBranch ``` - -``` public boolean showBestMoves ``` - -``` public void toggleShowBranch() ``` - -``` public void toggleShowBestMoves() ``` - -### Callback functions - -Basic callback function - -``` public abstract String getName() ``` Should be override, return plug-in 's name. -``` public abstract String getVersion() ``` Should be override, return plug-in 's version. - -``` public void onInit() throws IOException ``` Called when the plugin is loaded, the plugin is loaded after loading the configuration file. - -``` public void onMousePressed(MouseEvent e) ``` -``` public void onMouseReleased(MouseEvent e) ``` -``` public void onMouseMoved(MouseEvent e) ``` -``` public void onKeyPressed(KeyEvent e) ``` -``` public void onKeyReleased(KeyEvent e) ``` Like its name, it is called in the corresponding event. - -``` public boolean onDraw(Graphics2D g) ``` Called when drawing, the return value represents whether to submit the drawing result. - -``` public void onShutdown() throws IOException ``` Called when the program is shut down. - - -Special callback function - -``` public void onSgfLoaded() ``` diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index f88df1e3a..087667933 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -2,7 +2,6 @@ import org.json.JSONException; import featurecat.lizzie.analysis.Leelaz; -import featurecat.lizzie.plugin.PluginManager; import featurecat.lizzie.rules.Board; import featurecat.lizzie.rules.SGFParser; import featurecat.lizzie.gui.LizzieFrame; @@ -29,7 +28,6 @@ public class Lizzie { public static void main(String[] args) throws IOException, JSONException, ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException, InterruptedException { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); config = new Config(); - PluginManager.loadPlugins(); board = new Board(); frame = new LizzieFrame(); @@ -53,7 +51,6 @@ public static void main(String[] args) throws IOException, JSONException, ClassN } public static void shutdown() { - PluginManager.onShutdown(); if (board != null && config.config.getJSONObject("ui").getBoolean("confirm-exit")) { int ret = JOptionPane.showConfirmDialog(null, "Do you want to save this SGF?", "Save SGF?", JOptionPane.OK_CANCEL_OPTION); if (ret == JOptionPane.OK_OPTION) { diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 909fba7fe..9ef659f2d 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -6,12 +6,12 @@ import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Branch; import featurecat.lizzie.analysis.MoveData; -import featurecat.lizzie.plugin.PluginManager; import featurecat.lizzie.rules.Board; import featurecat.lizzie.rules.BoardHistoryNode; import featurecat.lizzie.rules.Stone; import featurecat.lizzie.rules.Zobrist; - +import featurecat.lizzie.theme.DefaultTheme; +import featurecat.lizzie.theme.ITheme; import java.awt.*; import java.awt.font.TextAttribute; import java.awt.geom.Point2D; @@ -21,9 +21,6 @@ import java.util.List; import java.util.Map; -import featurecat.lizzie.theme.DefaultTheme; -import featurecat.lizzie.theme.ITheme; - public class BoardRenderer { private static final double MARGIN = 0.03; // percentage of the boardLength to offset before drawing black lines private static final double MARGIN_WITH_COORDINATES = 0.06; @@ -121,7 +118,6 @@ public void draw(Graphics2D g) { } } - PluginManager.onDraw(g); // timer.lap("leelaz"); // timer.print(); @@ -606,14 +602,14 @@ private void drawLeelazSuggestions(Graphics2D g) { g.setColor(Color.BLACK); if (branch != null && Lizzie.board.getData().blackToPlay) g.setColor(Color.WHITE); - + String text; if (Lizzie.config.handicapInsteadOfWinrate) { text=String.format("%.2f", Lizzie.leelaz.winrateToHandicap(move.winrate)); } else { text=String.format("%.1f", roundedWinrate); } - + drawString(g, suggestionX, suggestionY, LizzieFrame.OpenSansSemiboldBase, Font.PLAIN, text, stoneRadius, stoneRadius * 1.5, 1); drawString(g, suggestionX, suggestionY + stoneRadius * 2 / 5, LizzieFrame.OpenSansRegularBase, getPlayoutsString(move.playouts), (float) (stoneRadius * 0.8), stoneRadius * 1.4); } diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index ee32f376a..c0862a4fd 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -1,68 +1,43 @@ package featurecat.lizzie.gui; import featurecat.lizzie.Lizzie; - import java.awt.event.*; - import static java.awt.event.KeyEvent.*; -import featurecat.lizzie.plugin.PluginManager; - import javax.swing.*; public class Input implements MouseListener, KeyListener, MouseWheelListener, MouseMotionListener { @Override - public void mouseClicked(MouseEvent e) { - - } + public void mouseClicked(MouseEvent e) {} @Override public void mousePressed(MouseEvent e) { - PluginManager.onMousePressed(e); - int x = e.getX(); - int y = e.getY(); - - if (e.getButton() == MouseEvent.BUTTON1) // left mouse click - Lizzie.frame.onClicked(x, y); - else if (e.getButton() == MouseEvent.BUTTON3) // right mouse click + if (e.getButton() == MouseEvent.BUTTON1) // left click + Lizzie.frame.onClicked(e.getX(), e.getY()); + else if (e.getButton() == MouseEvent.BUTTON3) // right click undo(); } @Override - public void mouseReleased(MouseEvent e) { - PluginManager.onMouseReleased(e); - } + public void mouseReleased(MouseEvent e) {} @Override - public void mouseEntered(MouseEvent e) { - - } + public void mouseEntered(MouseEvent e) {} @Override - public void mouseExited(MouseEvent e) { - - } + public void mouseExited(MouseEvent e) {} @Override public void mouseDragged(MouseEvent e) { - int x = e.getX(); - int y = e.getY(); - - Lizzie.frame.onMouseDragged(x, y); + Lizzie.frame.onMouseDragged(e.getX(), e.getY()); } @Override public void mouseMoved(MouseEvent e) { - PluginManager.onMouseMoved(e); - int x = e.getX(); - int y = e.getY(); - - Lizzie.frame.onMouseMoved(x, y); + Lizzie.frame.onMouseMoved(e.getX(), e.getY()); } @Override - public void keyTyped(KeyEvent e) { - - } + public void keyTyped(KeyEvent e) {} private void undo() { undo(1); @@ -166,9 +141,6 @@ private void toggleShowDynamicKomi() { @Override public void keyPressed(KeyEvent e) { - - PluginManager.onKeyPressed(e); - // If any controls key is pressed, let's disable analysis mode. // This is probably the user attempting to exit analysis mode. boolean shouldDisableAnalysis = true; @@ -379,7 +351,6 @@ public void keyPressed(KeyEvent e) { private boolean wasPonderingWhenControlsShown = false; @Override public void keyReleased(KeyEvent e) { - PluginManager.onKeyReleased(e); switch (e.getKeyCode()) { case VK_X: if (wasPonderingWhenControlsShown) diff --git a/src/main/java/featurecat/lizzie/plugin/IPlugin.java b/src/main/java/featurecat/lizzie/plugin/IPlugin.java deleted file mode 100644 index 190d4b54d..000000000 --- a/src/main/java/featurecat/lizzie/plugin/IPlugin.java +++ /dev/null @@ -1,62 +0,0 @@ -package featurecat.lizzie.plugin; - - -import java.awt.Graphics2D; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; - -public interface IPlugin { - - public static IPlugin load(String uri) throws Exception { - URLClassLoader loader = new URLClassLoader(new URL[] {new File(uri).toURI().toURL()}); - IPlugin plugin = (IPlugin) loader.loadClass("plugin.Plugin").newInstance(); - plugin.onInit(); - - loader.close(); - - return plugin; - } - - public default void onInit() throws IOException { - - } - - public default void onMousePressed(MouseEvent e) { - - } - - public default void onMouseReleased(MouseEvent e) { - - } - - public default void onMouseMoved(MouseEvent e) { - - } - - public default void onKeyPressed(KeyEvent e) { - - } - - public default void onKeyReleased(KeyEvent e) { - - } - - public default boolean onDraw(Graphics2D g) { - return false; - } - - public default void onShutdown() throws IOException { - - } - - public default void onSgfLoaded() { - - } - - public String getName(); - public String getVersion(); -} diff --git a/src/main/java/featurecat/lizzie/plugin/PluginManager.java b/src/main/java/featurecat/lizzie/plugin/PluginManager.java deleted file mode 100644 index 426a36a77..000000000 --- a/src/main/java/featurecat/lizzie/plugin/PluginManager.java +++ /dev/null @@ -1,100 +0,0 @@ -package featurecat.lizzie.plugin; - -import java.awt.Graphics2D; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.io.File; -import java.util.HashSet; -import java.io.IOException; - -import featurecat.lizzie.Lizzie; - -public class PluginManager { - public static HashSet plugins; - - public static void loadPlugins() throws IOException { - if (plugins != null) { - for (IPlugin plugin : plugins) { - plugin.onShutdown(); - } - } - plugins = new HashSet(); - File path = new File("./plugin/"); - path.mkdirs(); - - for (File jarFile : path.listFiles()) { - if (jarFile.isDirectory()) { - continue; - } - try { - plugins.add(IPlugin.load(jarFile.getPath())); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public static void onMousePressed(MouseEvent e) { - for (IPlugin plugin : plugins) { - plugin.onMousePressed(e); - } - } - - public static void onMouseReleased(MouseEvent e) { - for (IPlugin plugin : plugins) { - plugin.onMousePressed(e); - } - } - - public static void onMouseMoved(MouseEvent e) { - for (IPlugin plugin : plugins) { - plugin.onMousePressed(e); - } - } - - public static void onKeyPressed(KeyEvent e) { - for (IPlugin plugin : plugins) { - plugin.onKeyPressed(e); - } - } - - public static void onKeyReleased(KeyEvent e) { - for (IPlugin plugin : plugins) { - plugin.onKeyReleased(e); - } - } - - public static void onShutdown(){ - - for (IPlugin plugin : plugins) { - try {plugin.onShutdown(); - } catch(IOException e) { - e.printStackTrace(); - } - } - } - - public static void onSgfLoaded() { - for (IPlugin plugin : plugins) { - plugin.onSgfLoaded(); - } - } - - public static void onDraw(Graphics2D g0) { - int width = Lizzie.frame.getWidth(); - int height = Lizzie.frame.getHeight(); - BufferedImage cachedImageParent = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = cachedImageParent.createGraphics(); - for (IPlugin plugin : plugins) { - BufferedImage cachedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = cachedImage.createGraphics(); - if (plugin.onDraw(graphics)) { - g.drawImage(cachedImage, 0, 0, null); - } - graphics.dispose(); - } - g0.drawImage(cachedImageParent, 0, 0, null); - g.dispose(); - } -} diff --git a/src/main/java/featurecat/lizzie/rules/GIBParser.java b/src/main/java/featurecat/lizzie/rules/GIBParser.java index 7961a5230..7f0c08aff 100644 --- a/src/main/java/featurecat/lizzie/rules/GIBParser.java +++ b/src/main/java/featurecat/lizzie/rules/GIBParser.java @@ -1,8 +1,6 @@ package featurecat.lizzie.rules; import featurecat.lizzie.Lizzie; -import featurecat.lizzie.plugin.PluginManager; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -35,7 +33,6 @@ public static boolean load(String filename) throws IOException { } boolean returnValue = parse(value); - PluginManager.onSgfLoaded(); return returnValue; } diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 42b725814..52b4faf15 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -4,11 +4,8 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; - import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.GameInfo; -import featurecat.lizzie.plugin.PluginManager; - import java.io.*; import java.text.SimpleDateFormat; @@ -38,7 +35,6 @@ public static boolean load(String filename) throws IOException { } boolean returnValue = parse(value); - PluginManager.onSgfLoaded(); return returnValue; } From 8512ebc4470fb16638b44c796612db3ed29ecf0f Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Wed, 3 Oct 2018 23:06:40 +0200 Subject: [PATCH 68/75] Remove jar based theme infrastructure To be replaced by #378 --- doc/theme.md | 19 ------ src/main/java/featurecat/lizzie/Config.java | 1 - .../featurecat/lizzie/gui/BoardRenderer.java | 58 ++++++++++------ .../featurecat/lizzie/gui/LizzieFrame.java | 8 +-- .../featurecat/lizzie/theme/DefaultTheme.java | 66 ------------------- .../java/featurecat/lizzie/theme/ITheme.java | 51 -------------- 6 files changed, 42 insertions(+), 161 deletions(-) delete mode 100644 doc/theme.md delete mode 100644 src/main/java/featurecat/lizzie/theme/DefaultTheme.java delete mode 100644 src/main/java/featurecat/lizzie/theme/ITheme.java diff --git a/doc/theme.md b/doc/theme.md deleted file mode 100644 index 20f006348..000000000 --- a/doc/theme.md +++ /dev/null @@ -1,19 +0,0 @@ -# Theme -The theme's function is to make Lizzie's display richer, so simply change the assets file can not do things. - -The way to load a theme is to copy the theme's Jar file to the theme directory, and then set the theme's class name in the configuration file. - -Any class that implements the ITheme interface can be used as a theme class. - -Use ```javac -classpath ...``` to ensure that the theme's source files can be imported into the ITheme class. - -A theme class needs to implement the following methods: -```java -public Image getBlackStone(int[] position) throws IOException; - -public Image getWhiteStone(int[] position) throws IOException; - - public Image getBoard() throws IOException; - - public Image getBackground() throws IOException; -``` diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 8f6d7a3f1..71f1315fa 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -240,7 +240,6 @@ private JSONObject createDefaultConfig() { JSONObject ui = new JSONObject(); ui.put("board-color", new JSONArray("[217, 152, 77]")); - ui.put("theme", "DefaultTheme"); ui.put("shadows-enabled", true); ui.put("fancy-stones", true); ui.put("fancy-board", true); diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 9ef659f2d..53b3895f7 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -10,16 +10,16 @@ import featurecat.lizzie.rules.BoardHistoryNode; import featurecat.lizzie.rules.Stone; import featurecat.lizzie.rules.Zobrist; -import featurecat.lizzie.theme.DefaultTheme; -import featurecat.lizzie.theme.ITheme; import java.awt.*; import java.awt.font.TextAttribute; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.util.HashMap; +import java.io.IOException; import java.util.List; import java.util.Map; +import javax.imageio.ImageIO; public class BoardRenderer { private static final double MARGIN = 0.03; // percentage of the boardLength to offset before drawing black lines @@ -40,6 +40,8 @@ public class BoardRenderer { private int cachedX, cachedY; private BufferedImage cachedStonesImage = null; + private BufferedImage cachedBoardImage = null; + private BufferedImage cachedWallpaperImage = null; private BufferedImage cachedStonesShadowImage = null; private Zobrist cachedZhash = new Zobrist(); // defaults to an empty board @@ -51,7 +53,6 @@ public class BoardRenderer { private boolean lastInScoreMode = false; - public ITheme theme; public List variation; // special values of displayedBranchLength @@ -67,10 +68,6 @@ public class BoardRenderer { public BoardRenderer(boolean isMainBoard) { uiConfig = Lizzie.config.config.getJSONObject("ui"); - theme = ITheme.loadTheme(uiConfig.getString("theme")); - if (theme == null) { - theme = new DefaultTheme(); - } uiPersist = Lizzie.config.persisted.getJSONObject("ui-persist"); try { maxAlpha = uiPersist.getInt("max-alpha"); @@ -88,7 +85,7 @@ public void draw(Graphics2D g) { setupSizeParameters(); // Stopwatch timer = new Stopwatch(); - drawBackground(g); + drawGoban(g); // timer.lap("background"); drawStones(); // timer.lap("stones"); @@ -157,7 +154,7 @@ private void setupSizeParameters() { /** * Draw the green background and go board with lines. We cache the image for a performance boost. */ - private void drawBackground(Graphics2D g0) { + private void drawGoban(Graphics2D g0) { // draw the cached background image if frame size changes if (cachedBackgroundImage == null || cachedBackgroundImage.getWidth() != Lizzie.frame.getWidth() || cachedBackgroundImage.getHeight() != Lizzie.frame.getHeight() || @@ -646,11 +643,16 @@ private void drawNextMoves(Graphics2D g) { private void drawWoodenBoard(Graphics2D g) { if (uiConfig.getBoolean("fancy-board")) { - // fancy version + if (cachedBoardImage == null) { + try { + cachedBoardImage = ImageIO.read(getClass().getResourceAsStream("/assets/board.png")); + } catch (IOException e) { + e.printStackTrace(); + } + } + int shadowRadius = (int) (boardLength * MARGIN / 6); - BufferedImage boardImage = theme.getBoard(); - // Support seamless texture - drawTextureImage(g, boardImage == null ? theme.getBoard() : boardImage, x - 2 * shadowRadius, y - 2 * shadowRadius, boardLength + 4 * shadowRadius, boardLength + 4 * shadowRadius); + drawTextureImage(g, cachedBoardImage, x - 2 * shadowRadius, y - 2 * shadowRadius, boardLength + 4 * shadowRadius, boardLength + 4 * shadowRadius); g.setStroke(new BasicStroke(shadowRadius * 2)); // draw border @@ -659,7 +661,6 @@ private void drawWoodenBoard(Graphics2D g) { g.setStroke(new BasicStroke(1)); } else { - // simple version JSONArray boardColor = uiConfig.getJSONArray("board-color"); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g.setColor(new Color(boardColor.getInt(0), boardColor.getInt(1), boardColor.getInt(2))); @@ -762,9 +763,8 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center boolean isGhost = (color == Stone.BLACK_GHOST || color == Stone.WHITE_GHOST); if (uiConfig.getBoolean("fancy-stones")) { drawShadow(gShadow, centerX, centerY, isGhost); - Image stone = isBlack ? theme.getBlackStone(new int[]{x, y}) : theme.getWhiteStone(new int[]{x, y}); int size = stoneRadius * 2 + 1; - g.drawImage(getScaleStone(stone, isBlack, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); + g.drawImage(getScaleStone(isBlack, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); } else { drawShadow(gShadow, centerX, centerY, true); g.setColor(isBlack ? (isGhost ? new Color(0, 0, 0) : Color.BLACK) : (isGhost ? new Color(255, 255, 255) : Color.WHITE)); @@ -780,12 +780,19 @@ private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int center /** * Get scaled stone, if cached then return cached */ - public BufferedImage getScaleStone(Image img, boolean isBlack, int width, int height) { + private BufferedImage getScaleStone(boolean isBlack, int size) { BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; - if (stone == null || stone.getWidth() != width || stone.getHeight() != height) { - stone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + if (stone == null) { + stone = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + String imgPath = isBlack ? "/assets/black0.png" : "/assets/white0.png"; + Image img = null; + try { + img = ImageIO.read(getClass().getResourceAsStream(imgPath)); + } catch (IOException e) { + e.printStackTrace(); + } Graphics2D g2 = stone.createGraphics(); - g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g2.drawImage(img.getScaledInstance(size, size, java.awt.Image.SCALE_SMOOTH), 0, 0, null); g2.dispose(); if (isBlack) { cachedBlackStoneImage = stone; @@ -796,6 +803,17 @@ public BufferedImage getScaleStone(Image img, boolean isBlack, int width, int he return stone; } + public BufferedImage getWallpaper() { + if (cachedWallpaperImage == null) { + try { + cachedWallpaperImage = ImageIO.read(getClass().getResourceAsStream("/assets/background.jpg")); + } catch (IOException e) { + e.printStackTrace(); + } + } + return cachedWallpaperImage; + } + /** * Draw scale smooth image, enhanced display quality (Not use, for future) * This function use the traditional Image.getScaledInstance() method diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 68f879196..8f2dfbb34 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -467,11 +467,11 @@ private Graphics2D createBackground() { Graphics2D g = cachedBackground.createGraphics(); - BufferedImage background = boardRenderer.theme.getBackground(); - int drawWidth = Math.max(background.getWidth(), getWidth()); - int drawHeight = Math.max(background.getHeight(), getHeight()); + BufferedImage wallpaper = boardRenderer.getWallpaper(); + int drawWidth = Math.max(wallpaper.getWidth(), getWidth()); + int drawHeight = Math.max(wallpaper.getHeight(), getHeight()); // Support seamless texture - boardRenderer.drawTextureImage(g, background, 0, 0, drawWidth, drawHeight); + boardRenderer.drawTextureImage(g, wallpaper, 0, 0, drawWidth, drawHeight); return g; } diff --git a/src/main/java/featurecat/lizzie/theme/DefaultTheme.java b/src/main/java/featurecat/lizzie/theme/DefaultTheme.java deleted file mode 100644 index e6ea85ccd..000000000 --- a/src/main/java/featurecat/lizzie/theme/DefaultTheme.java +++ /dev/null @@ -1,66 +0,0 @@ -package featurecat.lizzie.theme; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; - - -/** - * DefaultTheme - */ -public class DefaultTheme implements ITheme { - BufferedImage blackStoneCached = null; - BufferedImage whiteStoneCached = null; - BufferedImage boardCached = null; - BufferedImage backgroundCached = null; - - @Override - public BufferedImage getBlackStone(int[] position) { - if (blackStoneCached == null) { - try { - blackStoneCached = ImageIO.read(getClass().getResourceAsStream("/assets/black0.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return blackStoneCached; - } - - @Override - public BufferedImage getWhiteStone(int[] position) { - if (whiteStoneCached == null) { - try { - whiteStoneCached = ImageIO.read(getClass().getResourceAsStream("/assets/white0.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return whiteStoneCached; - } - - @Override - public BufferedImage getBoard() { - if (boardCached == null) { - try { - boardCached = ImageIO.read(getClass().getResourceAsStream("/assets/board.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return boardCached; - } - - @Override - public BufferedImage getBackground() { - if (backgroundCached == null) { - try { - backgroundCached = ImageIO.read(getClass().getResourceAsStream("/assets/background.jpg")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return backgroundCached; - } -} \ No newline at end of file diff --git a/src/main/java/featurecat/lizzie/theme/ITheme.java b/src/main/java/featurecat/lizzie/theme/ITheme.java deleted file mode 100644 index e820a4e44..000000000 --- a/src/main/java/featurecat/lizzie/theme/ITheme.java +++ /dev/null @@ -1,51 +0,0 @@ -package featurecat.lizzie.theme; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; - -/** - * ITheme - */ -public interface ITheme { - static ITheme loadTheme(String name) { - ITheme ret = _loadTheme(name); - if (ret == null) { - return new DefaultTheme(); - } - return ret; - } - - static ITheme _loadTheme(String name) { - try { - File themes = new File("theme"); - if (!themes.isDirectory()) { - return null; - } - ArrayList jarFileList = new ArrayList(); - for (File file : themes.listFiles()) { - if (file.canRead() && file.getName().endsWith(".jar")) { - jarFileList.add(file.toURI().toURL()); - } - } - URLClassLoader loader = new URLClassLoader(jarFileList.toArray(new URL[jarFileList.size()])); - Class theme = loader.loadClass(name); - loader.close(); - ITheme ret = (ITheme) theme.newInstance(); - return ret; - } catch (Exception e) { - return new DefaultTheme(); - } - } - - // Considering that the theme may implement different pieces for each coordinate, you need to pass in the coordinates. - BufferedImage getBlackStone(int[] position); - - BufferedImage getWhiteStone(int[] position); - - BufferedImage getBoard(); - - BufferedImage getBackground(); -} From 406164af20ff06aad57ffd47d31299168dab6f83 Mon Sep 17 00:00:00 2001 From: zsalch Date: Thu, 4 Oct 2018 19:44:30 +0800 Subject: [PATCH 69/75] Change comment font size to config --- src/main/java/featurecat/lizzie/Config.java | 2 ++ src/main/java/featurecat/lizzie/gui/LizzieFrame.java | 12 ++---------- .../java/featurecat/lizzie/gui/VariationTree.java | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index c7314f430..898f4e220 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -16,6 +16,7 @@ public class Config { public boolean showWinrate = true; public boolean showVariationGraph = true; public boolean showComment = false; + public int commentFontSize = 0; public boolean showRawBoard = false; public boolean showCaptured = true; public boolean handicapInsteadOfWinrate = false; @@ -134,6 +135,7 @@ public Config() throws IOException { showWinrate = uiConfig.getBoolean("show-winrate"); showVariationGraph = uiConfig.getBoolean("show-variation-graph"); showComment = uiConfig.optBoolean("show-comment", false); + commentFontSize = uiConfig.optInt("comment-font-size", 0); showCaptured = uiConfig.getBoolean("show-captured"); showBestMoves = uiConfig.getBoolean("show-best-moves"); showNextMoves = uiConfig.getBoolean("show-next-moves"); diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 34e0e279e..758d5d218 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -21,7 +21,6 @@ import featurecat.lizzie.rules.SGFParser; import org.json.JSONObject; import org.json.JSONArray; -import org.json.JSONException; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; @@ -1029,19 +1028,12 @@ public void increaseMaxAlpha(int k) { private int drawComment(Graphics2D g, int x, int y, int w, int h, boolean full) { String comment = (Lizzie.board.getHistory().getData() != null && Lizzie.board.getHistory().getData().comment != null) ? Lizzie.board.getHistory().getData().comment : ""; int cHeight = full ? h : (int)(h*0.5); - int fontSize = (int)(Math.min(getWidth(), getHeight()) * 0.98 * 0.03); - try { - fontSize = Lizzie.config.uiConfig.getInt("comment-font-size"); - } catch (JSONException e) { - if (fontSize < 16) { - fontSize = 16; - } - } + int fontSize = Lizzie.config.commentFontSize > 0 ? Lizzie.config.commentFontSize : (int)(Math.min(getWidth(), getHeight()) * 0.98 * 0.03); Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); commentPane.setFont(font); commentPane.setText(comment); commentPane.setSize(w, cHeight); - createCommentImage((comment != null && !comment.equals(this.cachedComment)) ? true : false, w, cHeight); + createCommentImage(comment != null && !comment.equals(this.cachedComment), w, cHeight); commentRect = new Rectangle(x, y + (h - cHeight), scrollPane.getWidth(), scrollPane.getHeight()); g.drawImage(commentImage, commentRect.x, commentRect.y, commentRect.width, commentRect.height, null); cachedComment = comment; diff --git a/src/main/java/featurecat/lizzie/gui/VariationTree.java b/src/main/java/featurecat/lizzie/gui/VariationTree.java index ff05d30bc..6c873dc0e 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -79,7 +79,7 @@ public void drawTree(Graphics2D g, int posx, int posy, int startLane, int maxpos g.setColor(curcolor); // Draw main line - while (cur.next() != null && ((posy + YSPACING + DOT_DIAM) < maxposy)) { // Fix oval cover issue sometimes + while (cur.next() != null && posy + YSPACING < maxposy) { posy += YSPACING; cur = cur.next(); if (cur == curMove) { From d475ad7aa656ffdadc6d63da850cc8f12421c097 Mon Sep 17 00:00:00 2001 From: zsalch Date: Tue, 18 Sep 2018 16:53:11 +0800 Subject: [PATCH 70/75] 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 From 40969e3c349a7e2c773f16ac9677d4341ce6379b Mon Sep 17 00:00:00 2001 From: zsalch Date: Fri, 5 Oct 2018 12:07:19 +0800 Subject: [PATCH 71/75] Restore comment font minimum size --- src/main/java/featurecat/lizzie/gui/LizzieFrame.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 758d5d218..e2dd900df 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -1028,7 +1028,12 @@ public void increaseMaxAlpha(int k) { private int drawComment(Graphics2D g, int x, int y, int w, int h, boolean full) { String comment = (Lizzie.board.getHistory().getData() != null && Lizzie.board.getHistory().getData().comment != null) ? Lizzie.board.getHistory().getData().comment : ""; int cHeight = full ? h : (int)(h*0.5); - int fontSize = Lizzie.config.commentFontSize > 0 ? Lizzie.config.commentFontSize : (int)(Math.min(getWidth(), getHeight()) * 0.98 * 0.03); + int fontSize = (int)(Math.min(getWidth(), getHeight()) * 0.0294); + if (Lizzie.config.commentFontSize > 0) { + fontSize = Lizzie.config.commentFontSize; + } else if (fontSize < 16) { + fontSize = 16; + } Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); commentPane.setFont(font); commentPane.setText(comment); From 055743fadf4ff7e8d232bf9799ec2b3ea0ff3f8c Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sat, 6 Oct 2018 11:54:32 +0200 Subject: [PATCH 72/75] Add com.jhlabs.filters as a proper dependency --- pom.xml | 7 + .../jhlabs/image/AbstractBufferedImageOp.java | 59 -- .../java/com/jhlabs/image/ConvolveFilter.java | 285 --------- .../java/com/jhlabs/image/GaussianFilter.java | 142 ----- src/main/java/com/jhlabs/image/ImageMath.java | 602 ------------------ .../java/com/jhlabs/image/PixelUtils.java | 211 ------ 6 files changed, 7 insertions(+), 1299 deletions(-) delete mode 100644 src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java delete mode 100644 src/main/java/com/jhlabs/image/ConvolveFilter.java delete mode 100644 src/main/java/com/jhlabs/image/GaussianFilter.java delete mode 100644 src/main/java/com/jhlabs/image/ImageMath.java delete mode 100644 src/main/java/com/jhlabs/image/PixelUtils.java diff --git a/pom.xml b/pom.xml index 49139edda..699e4d2f2 100644 --- a/pom.xml +++ b/pom.xml @@ -82,5 +82,12 @@ 4.11 test + + + + com.jhlabs + filters + 2.0.235 + diff --git a/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java b/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java deleted file mode 100644 index dc1257fb3..000000000 --- a/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java +++ /dev/null @@ -1,59 +0,0 @@ -/* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ - -package com.jhlabs.image; - -import java.awt.*; -import java.awt.geom.*; -import java.awt.image.*; - -/** - * A convenience class which implements those methods of BufferedImageOp which are rarely changed. - */ -public abstract class AbstractBufferedImageOp implements BufferedImageOp { - - public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) { - if ( dstCM == null ) - dstCM = src.getColorModel(); - return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null); - } - - public Rectangle2D getBounds2D( BufferedImage src ) { - return new Rectangle(0, 0, src.getWidth(), src.getHeight()); - } - - public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { - if ( dstPt == null ) - dstPt = new Point2D.Double(); - dstPt.setLocation( srcPt.getX(), srcPt.getY() ); - return dstPt; - } - - public RenderingHints getRenderingHints() { - return null; - } - - /** - * A convenience method for getting ARGB pixels from an image. This tries to avoid the performance - * penalty of BufferedImage.getRGB unmanaging the image. - */ - public int[] getRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) { - int type = image.getType(); - if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB ) - return (int [])image.getRaster().getDataElements( x, y, width, height, pixels ); - return image.getRGB( x, y, width, height, pixels, 0, width ); - } - - /** - * A convenience method for setting ARGB pixels in an image. This tries to avoid the performance - * penalty of BufferedImage.setRGB unmanaging the image. - */ - public void setRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) { - int type = image.getType(); - if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB ) - image.getRaster().setDataElements( x, y, width, height, pixels ); - else - image.setRGB( x, y, width, height, pixels, 0, width ); - } -} \ No newline at end of file diff --git a/src/main/java/com/jhlabs/image/ConvolveFilter.java b/src/main/java/com/jhlabs/image/ConvolveFilter.java deleted file mode 100644 index 46a947dad..000000000 --- a/src/main/java/com/jhlabs/image/ConvolveFilter.java +++ /dev/null @@ -1,285 +0,0 @@ -/* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ - -package com.jhlabs.image; - -import java.awt.*; -import java.awt.image.*; -import java.awt.geom.*; - -/** - * A filter which applies a convolution kernel to an image. - * @author Jerry Huxtable - */ -public class ConvolveFilter extends AbstractBufferedImageOp { - - static final long serialVersionUID = 2239251672685254626L; - - public static int ZERO_EDGES = 0; - public static int CLAMP_EDGES = 1; - public static int WRAP_EDGES = 2; - - protected Kernel kernel = null; - public boolean alpha = true; - private int edgeAction = CLAMP_EDGES; - - /** - * Construct a filter with a null kernel. This is only useful if you're going to change the kernel later on. - */ - public ConvolveFilter() { - this(new float[9]); - } - - /** - * Construct a filter with the given 3x3 kernel. - * @param matrix an array of 9 floats containing the kernel - */ - public ConvolveFilter(float[] matrix) { - this(new Kernel(3, 3, matrix)); - } - - /** - * Construct a filter with the given kernel. - * @param rows the number of rows in the kernel - * @param cols the number of columns in the kernel - * @param matrix an array of rows*cols floats containing the kernel - */ - public ConvolveFilter(int rows, int cols, float[] matrix) { - this(new Kernel(cols, rows, matrix)); - } - - /** - * Construct a filter with the given 3x3 kernel. - * @param matrix an array of 9 floats containing the kernel - */ - public ConvolveFilter(Kernel kernel) { - this.kernel = kernel; - } - - public void setKernel(Kernel kernel) { - this.kernel = kernel; - } - - public Kernel getKernel() { - return kernel; - } - - public void setEdgeAction(int edgeAction) { - this.edgeAction = edgeAction; - } - - public int getEdgeAction() { - return edgeAction; - } - - public BufferedImage filter( BufferedImage src, BufferedImage dst ) { - int width = src.getWidth(); - int height = src.getHeight(); - - if ( dst == null ) - dst = createCompatibleDestImage( src, null ); - - int[] inPixels = new int[width*height]; - int[] outPixels = new int[width*height]; - getRGB( src, 0, 0, width, height, inPixels ); - - convolve(kernel, inPixels, outPixels, width, height, alpha, edgeAction); - - setRGB( dst, 0, 0, width, height, outPixels ); - return dst; - } - - public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) { - if ( dstCM == null ) - dstCM = src.getColorModel(); - return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null); - } - - public Rectangle2D getBounds2D( BufferedImage src ) { - return new Rectangle(0, 0, src.getWidth(), src.getHeight()); - } - - public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { - if ( dstPt == null ) - dstPt = new Point2D.Double(); - dstPt.setLocation( srcPt.getX(), srcPt.getY() ); - return dstPt; - } - - public RenderingHints getRenderingHints() { - return null; - } - - public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, int edgeAction) { - convolve(kernel, inPixels, outPixels, width, height, true, edgeAction); - } - - public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - if (kernel.getHeight() == 1) - convolveH(kernel, inPixels, outPixels, width, height, alpha, edgeAction); - else if (kernel.getWidth() == 1) - convolveV(kernel, inPixels, outPixels, width, height, alpha, edgeAction); - else - convolveHV(kernel, inPixels, outPixels, width, height, alpha, edgeAction); - } - - /** - * Convolve with a 2D kernel - */ - public static void convolveHV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - int index = 0; - float[] matrix = kernel.getKernelData( null ); - int rows = kernel.getHeight(); - int cols = kernel.getWidth(); - int rows2 = rows/2; - int cols2 = cols/2; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float r = 0, g = 0, b = 0, a = 0; - - for (int row = -rows2; row <= rows2; row++) { - int iy = y+row; - int ioffset; - if (0 <= iy && iy < height) - ioffset = iy*width; - else if ( edgeAction == CLAMP_EDGES ) - ioffset = y*width; - else if ( edgeAction == WRAP_EDGES ) - ioffset = ((iy+height) % height) * width; - else - continue; - int moffset = cols*(row+rows2)+cols2; - for (int col = -cols2; col <= cols2; col++) { - float f = matrix[moffset+col]; - - if (f != 0) { - int ix = x+col; - if (!(0 <= ix && ix < width)) { - if ( edgeAction == CLAMP_EDGES ) - ix = x; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - else - continue; - } - int rgb = inPixels[ioffset+ix]; - a += f * ((rgb >> 24) & 0xff); - r += f * ((rgb >> 16) & 0xff); - g += f * ((rgb >> 8) & 0xff); - b += f * (rgb & 0xff); - } - } - } - int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; - int ir = PixelUtils.clamp((int)(r+0.5)); - int ig = PixelUtils.clamp((int)(g+0.5)); - int ib = PixelUtils.clamp((int)(b+0.5)); - outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; - } - } - } - - /** - * Convolve with a kernel consisting of one row - */ - public static void convolveH(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - int index = 0; - float[] matrix = kernel.getKernelData( null ); - int cols = kernel.getWidth(); - int cols2 = cols/2; - - for (int y = 0; y < height; y++) { - int ioffset = y*width; - for (int x = 0; x < width; x++) { - float r = 0, g = 0, b = 0, a = 0; - int moffset = cols2; - for (int col = -cols2; col <= cols2; col++) { - float f = matrix[moffset+col]; - - if (f != 0) { - int ix = x+col; - if ( ix < 0 ) { - if ( edgeAction == CLAMP_EDGES ) - ix = 0; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - } else if ( ix >= width) { - if ( edgeAction == CLAMP_EDGES ) - ix = width-1; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - } - int rgb = inPixels[ioffset+ix]; - a += f * ((rgb >> 24) & 0xff); - r += f * ((rgb >> 16) & 0xff); - g += f * ((rgb >> 8) & 0xff); - b += f * (rgb & 0xff); - } - } - int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; - int ir = PixelUtils.clamp((int)(r+0.5)); - int ig = PixelUtils.clamp((int)(g+0.5)); - int ib = PixelUtils.clamp((int)(b+0.5)); - outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; - } - } - } - - /** - * Convolve with a kernel consisting of one column - */ - public static void convolveV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - int index = 0; - float[] matrix = kernel.getKernelData( null ); - int rows = kernel.getHeight(); - int rows2 = rows/2; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float r = 0, g = 0, b = 0, a = 0; - - for (int row = -rows2; row <= rows2; row++) { - int iy = y+row; - int ioffset; - if ( iy < 0 ) { - if ( edgeAction == CLAMP_EDGES ) - ioffset = 0; - else if ( edgeAction == WRAP_EDGES ) - ioffset = ((y+height) % height)*width; - else - ioffset = iy*width; - } else if ( iy >= height) { - if ( edgeAction == CLAMP_EDGES ) - ioffset = (height-1)*width; - else if ( edgeAction == WRAP_EDGES ) - ioffset = ((y+height) % height)*width; - else - ioffset = iy*width; - } else - ioffset = iy*width; - - float f = matrix[row+rows2]; - - if (f != 0) { - int rgb = inPixels[ioffset+x]; - a += f * ((rgb >> 24) & 0xff); - r += f * ((rgb >> 16) & 0xff); - g += f * ((rgb >> 8) & 0xff); - b += f * (rgb & 0xff); - } - } - int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; - int ir = PixelUtils.clamp((int)(r+0.5)); - int ig = PixelUtils.clamp((int)(g+0.5)); - int ib = PixelUtils.clamp((int)(b+0.5)); - outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; - } - } - } - - public String toString() { - return "Blur/Convolve..."; - } -} \ No newline at end of file diff --git a/src/main/java/com/jhlabs/image/GaussianFilter.java b/src/main/java/com/jhlabs/image/GaussianFilter.java deleted file mode 100644 index fb04e6f99..000000000 --- a/src/main/java/com/jhlabs/image/GaussianFilter.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.jhlabs.image; -import java.awt.image.*; - -/** - * A filter which applies Gaussian blur to an image. This is a subclass of ConvolveFilter - * which simply creates a kernel with a Gaussian distribution for blurring. - * @author Jerry Huxtable - */ -public class GaussianFilter extends ConvolveFilter { - - static final long serialVersionUID = 5377089073023183684L; - - private float radius; - private Kernel kernel; - - /** - * Construct a Gaussian filter - */ - public GaussianFilter() { - this(2); - } - - /** - * Construct a Gaussian filter - * @param radius blur radius in pixels - */ - public GaussianFilter(float radius) { - setRadius(radius); - } - - /** - * Set the radius of the kernel, and hence the amount of blur. The bigger the radius, the longer this filter will take. - * @param radius the radius of the blur in pixels. - */ - public void setRadius(float radius) { - this.radius = radius; - kernel = makeKernel(radius); - } - - /** - * Get the radius of the kernel. - * @return the radius - */ - public float getRadius() { - return radius; - } - - public BufferedImage filter( BufferedImage src, BufferedImage dst ) { - int width = src.getWidth(); - int height = src.getHeight(); - - if ( dst == null ) - dst = createCompatibleDestImage( src, null ); - - int[] inPixels = new int[width*height]; - int[] outPixels = new int[width*height]; - src.getRGB( 0, 0, width, height, inPixels, 0, width ); - - convolveAndTranspose(kernel, inPixels, outPixels, width, height, alpha, CLAMP_EDGES); - convolveAndTranspose(kernel, outPixels, inPixels, height, width, alpha, CLAMP_EDGES); - - dst.setRGB( 0, 0, width, height, inPixels, 0, width ); - return dst; - } - - public static void convolveAndTranspose(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - float[] matrix = kernel.getKernelData( null ); - int cols = kernel.getWidth(); - int cols2 = cols/2; - - for (int y = 0; y < height; y++) { - int index = y; - int ioffset = y*width; - for (int x = 0; x < width; x++) { - float r = 0, g = 0, b = 0, a = 0; - int moffset = cols2; - for (int col = -cols2; col <= cols2; col++) { - float f = matrix[moffset+col]; - - if (f != 0) { - int ix = x+col; - if ( ix < 0 ) { - if ( edgeAction == CLAMP_EDGES ) - ix = 0; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - } else if ( ix >= width) { - if ( edgeAction == CLAMP_EDGES ) - ix = width-1; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - } - int rgb = inPixels[ioffset+ix]; - a += f * ((rgb >> 24) & 0xff); - r += f * ((rgb >> 16) & 0xff); - g += f * ((rgb >> 8) & 0xff); - b += f * (rgb & 0xff); - } - } - int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; - int ir = PixelUtils.clamp((int)(r+0.5)); - int ig = PixelUtils.clamp((int)(g+0.5)); - int ib = PixelUtils.clamp((int)(b+0.5)); - outPixels[index] = (ia << 24) | (ir << 16) | (ig << 8) | ib; - index += height; - } - } - } - - /** - * Make a Gaussian blur kernel. - */ - public static Kernel makeKernel(float radius) { - int r = (int)Math.ceil(radius); - int rows = r*2+1; - float[] matrix = new float[rows]; - float sigma = radius/3; - float sigma22 = 2*sigma*sigma; - float sigmaPi2 = 2*ImageMath.PI*sigma; - float sqrtSigmaPi2 = (float)Math.sqrt(sigmaPi2); - float radius2 = radius*radius; - float total = 0; - int index = 0; - for (int row = -r; row <= r; row++) { - float distance = row*row; - if (distance > radius2) - matrix[index] = 0; - else - matrix[index] = (float)Math.exp(-(distance)/sigma22) / sqrtSigmaPi2; - total += matrix[index]; - index++; - } - for (int i = 0; i < rows; i++) - matrix[i] /= total; - - return new Kernel(rows, 1, matrix); - } - - public String toString() { - return "Blur/Gaussian Blur..."; - } -} \ No newline at end of file diff --git a/src/main/java/com/jhlabs/image/ImageMath.java b/src/main/java/com/jhlabs/image/ImageMath.java deleted file mode 100644 index 1e95374d0..000000000 --- a/src/main/java/com/jhlabs/image/ImageMath.java +++ /dev/null @@ -1,602 +0,0 @@ -/* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ - -package com.jhlabs.image; - -/** - * A class containing static math methods useful for image processing. - */ -public class ImageMath { - - public final static float PI = (float)Math.PI; - public final static float HALF_PI = (float)Math.PI/2.0f; - public final static float QUARTER_PI = (float)Math.PI/4.0f; - public final static float TWO_PI = (float)Math.PI*2.0f; - - /** - * Apply a bias to a number in the unit interval, moving numbers towards 0 or 1 - * according to the bias parameter. - * @param a the number to bias - * @param b the bias parameter. 0.5 means no change, smaller values bias towards 0, larger towards 1. - * @return the output value - */ - public static float bias(float a, float b) { -// return (float)Math.pow(a, Math.log(b) / Math.log(0.5)); - return a/((1.0f/b-2)*(1.0f-a)+1); - } - - /** - * A variant of the gamma function. - * @param a the number to apply gain to - * @param b the gain parameter. 0.5 means no change, smaller values reduce gain, larger values increase gain. - * @return the output value - */ - public static float gain(float a, float b) { -/* - float p = (float)Math.log(1.0 - b) / (float)Math.log(0.5); - - if (a < .001) - return 0.0f; - else if (a > .999) - return 1.0f; - if (a < 0.5) - return (float)Math.pow(2 * a, p) / 2; - else - return 1.0f - (float)Math.pow(2 * (1. - a), p) / 2; -*/ - float c = (1.0f/b-2.0f) * (1.0f-2.0f*a); - if (a < 0.5) - return a/(c+1.0f); - else - return (c-a)/(c-1.0f); - } - - /** - * The step function. Returns 0 below a threshold, 1 above. - * @param a the threshold position - * @param x the input parameter - * @return the output value - 0 or 1 - */ - public static float step(float a, float x) { - return (x < a) ? 0.0f : 1.0f; - } - - /** - * The pulse function. Returns 1 between two thresholds, 0 outside. - * @param a the lower threshold position - * @param b the upper threshold position - * @param x the input parameter - * @return the output value - 0 or 1 - */ - public static float pulse(float a, float b, float x) { - return (x < a || x >= b) ? 0.0f : 1.0f; - } - - /** - * A smoothed pulse function. A cubic function is used to smooth the step between two thresholds. - * @param a1 the lower threshold position for the start of the pulse - * @param a2 the upper threshold position for the start of the pulse - * @param b1 the lower threshold position for the end of the pulse - * @param b2 the upper threshold position for the end of the pulse - * @param x the input parameter - * @return the output value - */ - public static float smoothPulse(float a1, float a2, float b1, float b2, float x) { - if (x < a1 || x >= b2) - return 0; - if (x >= a2) { - if (x < b1) - return 1.0f; - x = (x - b1) / (b2 - b1); - return 1.0f - (x*x * (3.0f - 2.0f*x)); - } - x = (x - a1) / (a2 - a1); - return x*x * (3.0f - 2.0f*x); - } - - /** - * A smoothed step function. A cubic function is used to smooth the step between two thresholds. - * @param a the lower threshold position - * @param b the upper threshold position - * @param x the input parameter - * @return the output value - */ - public static float smoothStep(float a, float b, float x) { - if (x < a) - return 0; - if (x >= b) - return 1; - x = (x - a) / (b - a); - return x*x * (3 - 2*x); - } - - /** - * A "circle up" function. Returns y on a unit circle given 1-x. Useful for forming bevels. - * @param x the input parameter in the range 0..1 - * @return the output value - */ - public static float circleUp(float x) { - x = 1-x; - return (float)Math.sqrt(1-x*x); - } - - /** - * A "circle down" function. Returns 1-y on a unit circle given x. Useful for forming bevels. - * @param x the input parameter in the range 0..1 - * @return the output value - */ - public static float circleDown(float x) { - return 1.0f-(float)Math.sqrt(1-x*x); - } - - /** - * Clamp a value to an interval. - * @param a the lower clamp threshold - * @param b the upper clamp threshold - * @param x the input parameter - * @return the clamped value - */ - public static float clamp(float x, float a, float b) { - return (x < a) ? a : (x > b) ? b : x; - } - - /** - * Clamp a value to an interval. - * @param a the lower clamp threshold - * @param b the upper clamp threshold - * @param x the input parameter - * @return the clamped value - */ - public static int clamp(int x, int a, int b) { - return (x < a) ? a : (x > b) ? b : x; - } - - /** - * Return a mod b. This differs from the % operator with respect to negative numbers. - * @param a the dividend - * @param b the divisor - * @return a mod b - */ - public static double mod(double a, double b) { - int n = (int)(a/b); - - a -= n*b; - if (a < 0) - return a + b; - return a; - } - - /** - * Return a mod b. This differs from the % operator with respect to negative numbers. - * @param a the dividend - * @param b the divisor - * @return a mod b - */ - public static float mod(float a, float b) { - int n = (int)(a/b); - - a -= n*b; - if (a < 0) - return a + b; - return a; - } - - /** - * Return a mod b. This differs from the % operator with respect to negative numbers. - * @param a the dividend - * @param b the divisor - * @return a mod b - */ - public static int mod(int a, int b) { - int n = a/b; - - a -= n*b; - if (a < 0) - return a + b; - return a; - } - - /** - * The triangle function. Returns a repeating triangle shape in the range 0..1 with wavelength 1.0 - * @param x the input parameter - * @return the output value - */ - public static float triangle(float x) { - float r = mod(x, 1.0f); - return 2.0f*(r < 0.5 ? r : 1-r); - } - - /** - * Linear interpolation. - * @param t the interpolation parameter - * @param a the lower interpolation range - * @param b the upper interpolation range - * @return the interpolated value - */ - public static float lerp(float t, float a, float b) { - return a + t * (b - a); - } - - /** - * Linear interpolation. - * @param t the interpolation parameter - * @param a the lower interpolation range - * @param b the upper interpolation range - * @return the interpolated value - */ - public static int lerp(float t, int a, int b) { - return (int)(a + t * (b - a)); - } - - /** - * Linear interpolation of ARGB values. - * @param t the interpolation parameter - * @param rgb1 the lower interpolation range - * @param rgb2 the upper interpolation range - * @return the interpolated value - */ - public static int mixColors(float t, int rgb1, int rgb2) { - int a1 = (rgb1 >> 24) & 0xff; - int r1 = (rgb1 >> 16) & 0xff; - int g1 = (rgb1 >> 8) & 0xff; - int b1 = rgb1 & 0xff; - int a2 = (rgb2 >> 24) & 0xff; - int r2 = (rgb2 >> 16) & 0xff; - int g2 = (rgb2 >> 8) & 0xff; - int b2 = rgb2 & 0xff; - a1 = lerp(t, a1, a2); - r1 = lerp(t, r1, r2); - g1 = lerp(t, g1, g2); - b1 = lerp(t, b1, b2); - return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; - } - - /** - * Bilinear interpolation of ARGB values. - * @param x the X interpolation parameter 0..1 - * @param y the y interpolation parameter 0..1 - * @param rgb array of four ARGB values in the order NW, NE, SW, SE - * @return the interpolated value - */ - public static int bilinearInterpolate(float x, float y, int[] p) { - float m0, m1; - int a0 = (p[0] >> 24) & 0xff; - int r0 = (p[0] >> 16) & 0xff; - int g0 = (p[0] >> 8) & 0xff; - int b0 = p[0] & 0xff; - int a1 = (p[1] >> 24) & 0xff; - int r1 = (p[1] >> 16) & 0xff; - int g1 = (p[1] >> 8) & 0xff; - int b1 = p[1] & 0xff; - int a2 = (p[2] >> 24) & 0xff; - int r2 = (p[2] >> 16) & 0xff; - int g2 = (p[2] >> 8) & 0xff; - int b2 = p[2] & 0xff; - int a3 = (p[3] >> 24) & 0xff; - int r3 = (p[3] >> 16) & 0xff; - int g3 = (p[3] >> 8) & 0xff; - int b3 = p[3] & 0xff; - - float cx = 1.0f-x; - float cy = 1.0f-y; - - m0 = cx * a0 + x * a1; - m1 = cx * a2 + x * a3; - int a = (int)(cy * m0 + y * m1); - - m0 = cx * r0 + x * r1; - m1 = cx * r2 + x * r3; - int r = (int)(cy * m0 + y * m1); - - m0 = cx * g0 + x * g1; - m1 = cx * g2 + x * g3; - int g = (int)(cy * m0 + y * m1); - - m0 = cx * b0 + x * b1; - m1 = cx * b2 + x * b3; - int b = (int)(cy * m0 + y * m1); - - return (a << 24) | (r << 16) | (g << 8) | b; - } - - /** - * Return the NTSC gray level of an RGB value. - * @param rgb1 the input pixel - * @return the gray level (0-255) - */ - public static int brightnessNTSC(int rgb) { - int r = (rgb >> 16) & 0xff; - int g = (rgb >> 8) & 0xff; - int b = rgb & 0xff; - return (int)(r*0.299f + g*0.587f + b*0.114f); - } - - // Catmull-Rom splines - private final static float m00 = -0.5f; - private final static float m01 = 1.5f; - private final static float m02 = -1.5f; - private final static float m03 = 0.5f; - private final static float m10 = 1.0f; - private final static float m11 = -2.5f; - private final static float m12 = 2.0f; - private final static float m13 = -0.5f; - private final static float m20 = -0.5f; - private final static float m21 = 0.0f; - private final static float m22 = 0.5f; - private final static float m23 = 0.0f; - private final static float m30 = 0.0f; - private final static float m31 = 1.0f; - private final static float m32 = 0.0f; - private final static float m33 = 0.0f; - - /** - * Compute a Catmull-Rom spline. - * @param x the input parameter - * @param numKnots the number of knots in the spline - * @param knots the array of knots - * @return the spline value - */ - public static float spline(float x, int numKnots, float[] knots) { - int span; - int numSpans = numKnots - 3; - float k0, k1, k2, k3; - float c0, c1, c2, c3; - - if (numSpans < 1) - throw new IllegalArgumentException("Too few knots in spline"); - - x = clamp(x, 0, 1) * numSpans; - span = (int)x; - if (span > numKnots-4) - span = numKnots-4; - x -= span; - - k0 = knots[span]; - k1 = knots[span+1]; - k2 = knots[span+2]; - k3 = knots[span+3]; - - c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3; - c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3; - c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3; - c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3; - - return ((c3*x + c2)*x + c1)*x + c0; - } - - /** - * Compute a Catmull-Rom spline, but with variable knot spacing. - * @param x the input parameter - * @param numKnots the number of knots in the spline - * @param xknots the array of knot x values - * @param yknots the array of knot y values - * @return the spline value - */ - public static float spline(float x, int numKnots, int[] xknots, int[] yknots) { - int span; - int numSpans = numKnots - 3; - float k0, k1, k2, k3; - float c0, c1, c2, c3; - - if (numSpans < 1) - throw new IllegalArgumentException("Too few knots in spline"); - - for (span = 0; span < numSpans; span++) - if (xknots[span+1] > x) - break; - if (span > numKnots-3) - span = numKnots-3; - float t = (float)(x-xknots[span]) / (xknots[span+1]-xknots[span]); - span--; - if (span < 0) { - span = 0; - t = 0; - } - - k0 = yknots[span]; - k1 = yknots[span+1]; - k2 = yknots[span+2]; - k3 = yknots[span+3]; - - c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3; - c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3; - c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3; - c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3; - - return ((c3*t + c2)*t + c1)*t + c0; - } - - /** - * Compute a Catmull-Rom spline for RGB values. - * @param x the input parameter - * @param numKnots the number of knots in the spline - * @param knots the array of knots - * @return the spline value - */ - public static int colorSpline(float x, int numKnots, int[] knots) { - int span; - int numSpans = numKnots - 3; - float k0, k1, k2, k3; - float c0, c1, c2, c3; - - if (numSpans < 1) - throw new IllegalArgumentException("Too few knots in spline"); - - x = clamp(x, 0, 1) * numSpans; - span = (int)x; - if (span > numKnots-4) - span = numKnots-4; - x -= span; - - int v = 0; - for (int i = 0; i < 4; i++) { - int shift = i * 8; - - k0 = (knots[span] >> shift) & 0xff; - k1 = (knots[span+1] >> shift) & 0xff; - k2 = (knots[span+2] >> shift) & 0xff; - k3 = (knots[span+3] >> shift) & 0xff; - - c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3; - c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3; - c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3; - c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3; - int n = (int)(((c3*x + c2)*x + c1)*x + c0); - if (n < 0) - n = 0; - else if (n > 255) - n = 255; - v |= n << shift; - } - - return v; - } - - /** - * Compute a Catmull-Rom spline for RGB values, but with variable knot spacing. - * @param x the input parameter - * @param numKnots the number of knots in the spline - * @param xknots the array of knot x values - * @param yknots the array of knot y values - * @return the spline value - */ - public static int colorSpline(int x, int numKnots, int[] xknots, int[] yknots) { - int span; - int numSpans = numKnots - 3; - float k0, k1, k2, k3; - float c0, c1, c2, c3; - - if (numSpans < 1) - throw new IllegalArgumentException("Too few knots in spline"); - - for (span = 0; span < numSpans; span++) - if (xknots[span+1] > x) - break; - if (span > numKnots-3) - span = numKnots-3; - float t = (float)(x-xknots[span]) / (xknots[span+1]-xknots[span]); - span--; - if (span < 0) { - span = 0; - t = 0; - } - - int v = 0; - for (int i = 0; i < 4; i++) { - int shift = i * 8; - - k0 = (yknots[span] >> shift) & 0xff; - k1 = (yknots[span+1] >> shift) & 0xff; - k2 = (yknots[span+2] >> shift) & 0xff; - k3 = (yknots[span+3] >> shift) & 0xff; - - c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3; - c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3; - c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3; - c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3; - int n = (int)(((c3*t + c2)*t + c1)*t + c0); - if (n < 0) - n = 0; - else if (n > 255) - n = 255; - v |= n << shift; - } - - return v; - } - - /** - * An implementation of Fant's resampling algorithm. - * @param source the source pixels - * @param dest the destination pixels - * @param length the length of the scanline to resample - * @param offset the start offset into the arrays - * @param stride the offset between pixels in consecutive rows - * @param out an array of output positions for each pixel - */ - public static void resample(int[] source, int[] dest, int length, int offset, int stride, float[] out) { - int i, j; - float intensity; - float sizfac; - float inSegment; - float outSegment; - int a, r, g, b, nextA, nextR, nextG, nextB; - float aSum, rSum, gSum, bSum; - float[] in; - int srcIndex = offset; - int destIndex = offset; - int lastIndex = source.length; - int rgb; - - in = new float[length+1]; - i = 0; - for (j = 0; j < length; j++) { - while (out[i+1] < j) - i++; - in[j] = i + (float) (j - out[i]) / (out[i + 1] - out[i]); - } - in[length] = length; - - inSegment = 1.0f; - outSegment = in[1]; - sizfac = outSegment; - aSum = rSum = gSum = bSum = 0.0f; - rgb = source[srcIndex]; - a = (rgb >> 24) & 0xff; - r = (rgb >> 16) & 0xff; - g = (rgb >> 8) & 0xff; - b = rgb & 0xff; - srcIndex += stride; - rgb = source[srcIndex]; - nextA = (rgb >> 24) & 0xff; - nextR = (rgb >> 16) & 0xff; - nextG = (rgb >> 8) & 0xff; - nextB = rgb & 0xff; - srcIndex += stride; - i = 1; - - while (i < length) { - float aIntensity = inSegment * a + (1.0f - inSegment) * nextA; - float rIntensity = inSegment * r + (1.0f - inSegment) * nextR; - float gIntensity = inSegment * g + (1.0f - inSegment) * nextG; - float bIntensity = inSegment * b + (1.0f - inSegment) * nextB; - if (inSegment < outSegment) { - aSum += (aIntensity * inSegment); - rSum += (rIntensity * inSegment); - gSum += (gIntensity * inSegment); - bSum += (bIntensity * inSegment); - outSegment -= inSegment; - inSegment = 1.0f; - a = nextA; - r = nextR; - g = nextG; - b = nextB; - if (srcIndex < lastIndex) - rgb = source[srcIndex]; - nextA = (rgb >> 24) & 0xff; - nextR = (rgb >> 16) & 0xff; - nextG = (rgb >> 8) & 0xff; - nextB = rgb & 0xff; - srcIndex += stride; - } else { - aSum += (aIntensity * outSegment); - rSum += (rIntensity * outSegment); - gSum += (gIntensity * outSegment); - bSum += (bIntensity * outSegment); - dest[destIndex] = - ((int)Math.min(aSum/sizfac, 255) << 24) | - ((int)Math.min(rSum/sizfac, 255) << 16) | - ((int)Math.min(gSum/sizfac, 255) << 8) | - (int)Math.min(bSum/sizfac, 255); - destIndex += stride; - rSum = gSum = bSum = 0.0f; - inSegment -= outSegment; - outSegment = in[i+1] - in[i]; - sizfac = outSegment; - i++; - } - } - } - -} \ No newline at end of file diff --git a/src/main/java/com/jhlabs/image/PixelUtils.java b/src/main/java/com/jhlabs/image/PixelUtils.java deleted file mode 100644 index daf6f9d98..000000000 --- a/src/main/java/com/jhlabs/image/PixelUtils.java +++ /dev/null @@ -1,211 +0,0 @@ -/* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ - -package com.jhlabs.image; - -import java.util.*; -import java.awt.Color; - -/** - * Some more useful math functions for image processing. - * These are becoming obsolete as we move to Java2D. Use MiscComposite instead. - */ -public class PixelUtils { - - public final static int REPLACE = 0; - public final static int NORMAL = 1; - public final static int MIN = 2; - public final static int MAX = 3; - public final static int ADD = 4; - public final static int SUBTRACT = 5; - public final static int DIFFERENCE = 6; - public final static int MULTIPLY = 7; - public final static int HUE = 8; - public final static int SATURATION = 9; - public final static int VALUE = 10; - public final static int COLOR = 11; - public final static int SCREEN = 12; - public final static int AVERAGE = 13; - public final static int OVERLAY = 14; - public final static int CLEAR = 15; - public final static int EXCHANGE = 16; - public final static int DISSOLVE = 17; - public final static int DST_IN = 18; - public final static int ALPHA = 19; - public final static int ALPHA_TO_GRAY = 20; - - private static Random randomGenerator = new Random(); - - /** - * Clamp a value to the range 0..255 - */ - public static int clamp(int c) { - if (c < 0) - return 0; - if (c > 255) - return 255; - return c; - } - - public static int interpolate(int v1, int v2, float f) { - return clamp((int)(v1+f*(v2-v1))); - } - - public static int brightness(int rgb) { - int r = (rgb >> 16) & 0xff; - int g = (rgb >> 8) & 0xff; - int b = rgb & 0xff; - return (r+g+b)/3; - } - - public static boolean nearColors(int rgb1, int rgb2, int tolerance) { - int r1 = (rgb1 >> 16) & 0xff; - int g1 = (rgb1 >> 8) & 0xff; - int b1 = rgb1 & 0xff; - int r2 = (rgb2 >> 16) & 0xff; - int g2 = (rgb2 >> 8) & 0xff; - int b2 = rgb2 & 0xff; - return Math.abs(r1-r2) <= tolerance && Math.abs(g1-g2) <= tolerance && Math.abs(b1-b2) <= tolerance; - } - - private final static float hsb1[] = new float[3];//FIXME-not thread safe - private final static float hsb2[] = new float[3];//FIXME-not thread safe - - // Return rgb1 painted onto rgb2 - public static int combinePixels(int rgb1, int rgb2, int op) { - return combinePixels(rgb1, rgb2, op, 0xff); - } - - public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha, int channelMask) { - return (rgb2 & ~channelMask) | combinePixels(rgb1 & channelMask, rgb2, op, extraAlpha); - } - - public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha) { - if (op == REPLACE) - return rgb1; - int a1 = (rgb1 >> 24) & 0xff; - int r1 = (rgb1 >> 16) & 0xff; - int g1 = (rgb1 >> 8) & 0xff; - int b1 = rgb1 & 0xff; - int a2 = (rgb2 >> 24) & 0xff; - int r2 = (rgb2 >> 16) & 0xff; - int g2 = (rgb2 >> 8) & 0xff; - int b2 = rgb2 & 0xff; - - switch (op) { - case NORMAL: - break; - case MIN: - r1 = Math.min(r1, r2); - g1 = Math.min(g1, g2); - b1 = Math.min(b1, b2); - break; - case MAX: - r1 = Math.max(r1, r2); - g1 = Math.max(g1, g2); - b1 = Math.max(b1, b2); - break; - case ADD: - r1 = clamp(r1+r2); - g1 = clamp(g1+g2); - b1 = clamp(b1+b2); - break; - case SUBTRACT: - r1 = clamp(r2-r1); - g1 = clamp(g2-g1); - b1 = clamp(b2-b1); - break; - case DIFFERENCE: - r1 = clamp(Math.abs(r1-r2)); - g1 = clamp(Math.abs(g1-g2)); - b1 = clamp(Math.abs(b1-b2)); - break; - case MULTIPLY: - r1 = clamp(r1*r2/255); - g1 = clamp(g1*g2/255); - b1 = clamp(b1*b2/255); - break; - case DISSOLVE: - if ((randomGenerator.nextInt() & 0xff) <= a1) { - r1 = r2; - g1 = g2; - b1 = b2; - } - break; - case AVERAGE: - r1 = (r1+r2)/2; - g1 = (g1+g2)/2; - b1 = (b1+b2)/2; - break; - case HUE: - case SATURATION: - case VALUE: - case COLOR: - Color.RGBtoHSB(r1, g1, b1, hsb1); - Color.RGBtoHSB(r2, g2, b2, hsb2); - switch (op) { - case HUE: - hsb2[0] = hsb1[0]; - break; - case SATURATION: - hsb2[1] = hsb1[1]; - break; - case VALUE: - hsb2[2] = hsb1[2]; - break; - case COLOR: - hsb2[0] = hsb1[0]; - hsb2[1] = hsb1[1]; - break; - } - rgb1 = Color.HSBtoRGB(hsb2[0], hsb2[1], hsb2[2]); - r1 = (rgb1 >> 16) & 0xff; - g1 = (rgb1 >> 8) & 0xff; - b1 = rgb1 & 0xff; - break; - case SCREEN: - r1 = 255 - ((255 - r1) * (255 - r2)) / 255; - g1 = 255 - ((255 - g1) * (255 - g2)) / 255; - b1 = 255 - ((255 - b1) * (255 - b2)) / 255; - break; - case OVERLAY: - int m, s; - s = 255 - ((255 - r1) * (255 - r2)) / 255; - m = r1 * r2 / 255; - r1 = (s * r1 + m * (255 - r1)) / 255; - s = 255 - ((255 - g1) * (255 - g2)) / 255; - m = g1 * g2 / 255; - g1 = (s * g1 + m * (255 - g1)) / 255; - s = 255 - ((255 - b1) * (255 - b2)) / 255; - m = b1 * b2 / 255; - b1 = (s * b1 + m * (255 - b1)) / 255; - break; - case CLEAR: - r1 = g1 = b1 = 0xff; - break; - case DST_IN: - r1 = clamp((r2*a1)/255); - g1 = clamp((g2*a1)/255); - b1 = clamp((b2*a1)/255); - a1 = clamp((a2*a1)/255); - return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; - case ALPHA: - a1 = a1*a2/255; - return (a1 << 24) | (r2 << 16) | (g2 << 8) | b2; - case ALPHA_TO_GRAY: - int na = 255-a1; - return (a1 << 24) | (na << 16) | (na << 8) | na; - } - if (extraAlpha != 0xff || a1 != 0xff) { - a1 = a1*extraAlpha/255; - int a3 = (255-a1)*a2/255; - r1 = clamp((r1*a1+r2*a3)/255); - g1 = clamp((g1*a1+g2*a3)/255); - b1 = clamp((b1*a1+b2*a3)/255); - a1 = clamp(a1+a3); - } - return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; - } - -} \ No newline at end of file From 8e4c2275b111717656e96df101af065ee36c8aae Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Fri, 5 Oct 2018 00:42:19 +0200 Subject: [PATCH 73/75] Setup fmt-maven-plugin --- pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pom.xml b/pom.xml index 699e4d2f2..ab70ad3bc 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,18 @@ false + + com.coveo + fmt-maven-plugin + 2.5.1 + + + + format + + + + From cf9a5ff5e330fce89225cbb880df49bd6259be08 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sat, 6 Oct 2018 11:16:21 +0200 Subject: [PATCH 74/75] Auto format everything using Google Java Style --- .../java/featurecat/benchmark/Stopwatch.java | 91 +- src/main/java/featurecat/lizzie/Config.java | 579 ++--- src/main/java/featurecat/lizzie/Lizzie.java | 100 +- src/main/java/featurecat/lizzie/Util.java | 114 +- .../featurecat/lizzie/analysis/Branch.java | 56 +- .../featurecat/lizzie/analysis/GameInfo.java | 99 +- .../featurecat/lizzie/analysis/Leelaz.java | 990 ++++---- .../lizzie/analysis/LeelazListener.java | 2 +- .../featurecat/lizzie/analysis/MoveData.java | 118 +- .../featurecat/lizzie/gui/BoardRenderer.java | 2070 +++++++++-------- .../featurecat/lizzie/gui/GameInfoDialog.java | 242 +- .../java/featurecat/lizzie/gui/Input.java | 690 +++--- .../featurecat/lizzie/gui/LizzieFrame.java | 2005 ++++++++-------- .../featurecat/lizzie/gui/NewGameDialog.java | 355 +-- .../featurecat/lizzie/gui/VariationTree.java | 281 +-- .../featurecat/lizzie/gui/WinrateGraph.java | 404 ++-- .../java/featurecat/lizzie/rules/Board.java | 2070 +++++++++-------- .../featurecat/lizzie/rules/BoardData.java | 83 +- .../lizzie/rules/BoardHistoryList.java | 651 +++--- .../lizzie/rules/BoardHistoryNode.java | 334 ++- .../featurecat/lizzie/rules/GIBParser.java | 168 +- .../featurecat/lizzie/rules/SGFParser.java | 659 +++--- .../java/featurecat/lizzie/rules/Stone.java | 139 +- .../java/featurecat/lizzie/rules/Zobrist.java | 108 +- src/test/java/common/Util.java | 238 +- .../lizzie/analysis/MoveDataTest.java | 66 +- .../lizzie/rules/SGFParserTest.java | 221 +- 27 files changed, 6620 insertions(+), 6313 deletions(-) diff --git a/src/main/java/featurecat/benchmark/Stopwatch.java b/src/main/java/featurecat/benchmark/Stopwatch.java index d7349e627..354b333a7 100644 --- a/src/main/java/featurecat/benchmark/Stopwatch.java +++ b/src/main/java/featurecat/benchmark/Stopwatch.java @@ -2,58 +2,55 @@ import java.util.LinkedHashMap; -/** - * Simple stopwatch profiler to benchmark how long code takes to run - */ +/** Simple stopwatch profiler to benchmark how long code takes to run */ public class Stopwatch { - private LinkedHashMap times; - private long startTime; - private long lastTime; + private LinkedHashMap times; + private long startTime; + private long lastTime; - /** - * Begins timing from the moment this object is created. - */ - public Stopwatch() { - times = new LinkedHashMap<>(); - startTime = System.nanoTime(); - lastTime = startTime; - } + /** Begins timing from the moment this object is created. */ + public Stopwatch() { + times = new LinkedHashMap<>(); + startTime = System.nanoTime(); + lastTime = startTime; + } - /** - * Mark down the current time that it took $marker$ to run. - * @param marker a tag to describe what section of code is being profiled - */ - public void lap(String marker) { - long currentTime = System.nanoTime(); - times.put(marker, currentTime - lastTime); - lastTime = currentTime; - } + /** + * Mark down the current time that it took $marker$ to run. + * + * @param marker a tag to describe what section of code is being profiled + */ + public void lap(String marker) { + long currentTime = System.nanoTime(); + times.put(marker, currentTime - lastTime); + lastTime = currentTime; + } - /** - * Print the recorded profiler statistics - */ - public void print() { - System.out.println("\n======== profiler ========"); - long totalTime = lastTime - startTime; - for (String marker : times.keySet()) { - System.out.printf("%5.1f%% %s\n", 100.0 * times.get(marker) / totalTime, marker); - } - System.out.println("in " + totalTime / 1_000_000.0 + " ms"); + /** Print the recorded profiler statistics */ + public void print() { + System.out.println("\n======== profiler ========"); + long totalTime = lastTime - startTime; + for (String marker : times.keySet()) { + System.out.printf("%5.1f%% %s\n", 100.0 * times.get(marker) / totalTime, marker); } + System.out.println("in " + totalTime / 1_000_000.0 + " ms"); + } - public void printTimePerAction(int numActionsExecuted) { - System.out.println("\n======== profiler ========"); - long totalTime = System.nanoTime() - startTime; - System.out.println((totalTime / 1_000_000.0 / numActionsExecuted) + " ms per action"); - System.out.println(numActionsExecuted + " total actions executed in " + totalTime / 1_000_000_000.0 + " s"); - } + public void printTimePerAction(int numActionsExecuted) { + System.out.println("\n======== profiler ========"); + long totalTime = System.nanoTime() - startTime; + System.out.println((totalTime / 1_000_000.0 / numActionsExecuted) + " ms per action"); + System.out.println( + numActionsExecuted + " total actions executed in " + totalTime / 1_000_000_000.0 + " s"); + } - /** - * Reset the Stopwatch so it can be used again. Begins timing from the moment this method is executed. - */ - public void reset() { - times = new LinkedHashMap<>(); - startTime = System.nanoTime(); - lastTime = startTime; - } + /** + * Reset the Stopwatch so it can be used again. Begins timing from the moment this method is + * executed. + */ + public void reset() { + times = new LinkedHashMap<>(); + startTime = System.nanoTime(); + lastTime = startTime; + } } diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 9ccb39b3d..169880605 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -1,7 +1,5 @@ package featurecat.lizzie; -import org.json.*; - import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; @@ -9,310 +7,321 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; +import org.json.*; public class Config { - public boolean showMoveNumber = false; - public boolean showWinrate = true; - public boolean showVariationGraph = true; - public boolean showComment = false; - public int commentFontSize = 0; - public boolean showRawBoard = false; - public boolean showCaptured = true; - public boolean handicapInsteadOfWinrate = false; - public boolean showDynamicKomi = true; - - public boolean showStatus = true; - public boolean showBranch = true; - public boolean showBestMoves = true; - public boolean showNextMoves = true; - public boolean showSubBoard = true; - public boolean largeSubBoard = false; - public boolean startMaximized = true; - - public JSONObject config; - public JSONObject leelazConfig; - public JSONObject uiConfig; - public JSONObject persisted; - - private String configFilename = "config.txt"; - private String persistFilename = "persist"; - - private JSONObject loadAndMergeConfig(JSONObject defaultCfg, String fileName, boolean needValidation) throws IOException { - File file = new File(fileName); - if (!file.canRead()) { - System.err.printf("Creating config file %s\n", fileName); - try { - writeConfig(defaultCfg, file); - } catch (JSONException e) { - e.printStackTrace(); - System.exit(1); - } - } - - FileInputStream fp = new FileInputStream(file); - - JSONObject mergedcfg = null; - boolean modified = false; - try { - mergedcfg = new JSONObject(new JSONTokener(fp)); - modified = merge_defaults(mergedcfg, defaultCfg); - } catch (JSONException e) { - mergedcfg = null; - e.printStackTrace(); - } - - fp.close(); - - // Validate and correct settings - if (needValidation && validateAndCorrectSettings(mergedcfg)) { - modified = true; - } - - if (modified) { - writeConfig(mergedcfg, file); - } - return mergedcfg; - - + public boolean showMoveNumber = false; + public boolean showWinrate = true; + public boolean showVariationGraph = true; + public boolean showComment = false; + public int commentFontSize = 0; + public boolean showRawBoard = false; + public boolean showCaptured = true; + public boolean handicapInsteadOfWinrate = false; + public boolean showDynamicKomi = true; + + public boolean showStatus = true; + public boolean showBranch = true; + public boolean showBestMoves = true; + public boolean showNextMoves = true; + public boolean showSubBoard = true; + public boolean largeSubBoard = false; + public boolean startMaximized = true; + + public JSONObject config; + public JSONObject leelazConfig; + public JSONObject uiConfig; + public JSONObject persisted; + + private String configFilename = "config.txt"; + private String persistFilename = "persist"; + + private JSONObject loadAndMergeConfig( + JSONObject defaultCfg, String fileName, boolean needValidation) throws IOException { + File file = new File(fileName); + if (!file.canRead()) { + System.err.printf("Creating config file %s\n", fileName); + try { + writeConfig(defaultCfg, file); + } catch (JSONException e) { + e.printStackTrace(); + System.exit(1); + } } - /** - * Check settings to ensure its consistency, especially for those whose types are not boolean. - * If any inconsistency is found, try to correct it or to report it. - *
- * For example, we only support 9x9, 13x13 or 19x19(default) sized boards. If the configured board size - * is not in the list above, we should correct it. - * - * @param config The config json object to check - * @return if any correction has been made. - */ - private boolean validateAndCorrectSettings(JSONObject config) { - if (config == null) { - return false; - } - - boolean madeCorrections = false; - - // Check ui configs - JSONObject ui = config.getJSONObject("ui"); + FileInputStream fp = new FileInputStream(file); - // Check board-size. We support only 9x9, 13x13 or 19x19 - int boardSize = ui.optInt("board-size", 19); - if (boardSize != 19 && boardSize != 13 && boardSize != 9) { - // Correct it to default 19x19 - ui.put("board-size", 19); - madeCorrections = true; - } - - // Check engine configs - JSONObject leelaz = config.getJSONObject("leelaz"); - // Checks for startup directory. It should exist and should be a directory. - String engineStartLocation = getBestDefaultLeelazPath(); - if (!(Files.exists(Paths.get(engineStartLocation)) && Files.isDirectory(Paths.get(engineStartLocation)))) { - leelaz.put("engine-start-location", "."); - madeCorrections = true; - } - - return madeCorrections; + JSONObject mergedcfg = null; + boolean modified = false; + try { + mergedcfg = new JSONObject(new JSONTokener(fp)); + modified = merge_defaults(mergedcfg, defaultCfg); + } catch (JSONException e) { + mergedcfg = null; + e.printStackTrace(); } - public Config() throws IOException { - JSONObject defaultConfig = createDefaultConfig(); - JSONObject persistConfig = createPersistConfig(); - - // Main properties - this.config = loadAndMergeConfig(defaultConfig, configFilename, true); - // Persisted properties - this.persisted = loadAndMergeConfig(persistConfig, persistFilename, false); - - leelazConfig = config.getJSONObject("leelaz"); - uiConfig = config.getJSONObject("ui"); - - showMoveNumber = uiConfig.getBoolean("show-move-number"); - showStatus = uiConfig.getBoolean("show-status"); - showBranch = uiConfig.getBoolean("show-leelaz-variation"); - showWinrate = uiConfig.getBoolean("show-winrate"); - showVariationGraph = uiConfig.getBoolean("show-variation-graph"); - showComment = uiConfig.optBoolean("show-comment", false); - commentFontSize = uiConfig.optInt("comment-font-size", 0); - showCaptured = uiConfig.getBoolean("show-captured"); - showBestMoves = uiConfig.getBoolean("show-best-moves"); - showNextMoves = uiConfig.getBoolean("show-next-moves"); - showSubBoard = uiConfig.getBoolean("show-subboard"); - largeSubBoard = uiConfig.getBoolean("large-subboard"); - handicapInsteadOfWinrate = uiConfig.getBoolean("handicap-instead-of-winrate"); - startMaximized = uiConfig.getBoolean("window-maximized"); - showDynamicKomi = uiConfig.getBoolean("show-dynamic-komi"); - } + fp.close(); - // Modifies config by adding in values from default_config that are missing. - // Returns whether it added anything. - public boolean merge_defaults(JSONObject config, JSONObject defaults_config) { - boolean modified = false; - Iterator keys = defaults_config.keys(); - while (keys.hasNext()) { - String key = keys.next(); - Object new_val = defaults_config.get(key); - if (new_val instanceof JSONObject) { - if (!config.has(key)) { - config.put(key, new JSONObject()); - modified = true; - } - Object old_val = config.get(key); - modified |= merge_defaults((JSONObject) old_val, (JSONObject) new_val); - } else { - if (!config.has(key)) { - config.put(key, new_val); - modified = true; - } - } - } - return modified; + // Validate and correct settings + if (needValidation && validateAndCorrectSettings(mergedcfg)) { + modified = true; } - public void toggleShowMoveNumber() { - this.showMoveNumber = !this.showMoveNumber; + if (modified) { + writeConfig(mergedcfg, file); } - public void toggleShowBranch() { - this.showBranch = !this.showBranch; + return mergedcfg; + } + + /** + * Check settings to ensure its consistency, especially for those whose types are not + * boolean. If any inconsistency is found, try to correct it or to report it.
+ * For example, we only support 9x9, 13x13 or 19x19(default) sized boards. If the configured board + * size is not in the list above, we should correct it. + * + * @param config The config json object to check + * @return if any correction has been made. + */ + private boolean validateAndCorrectSettings(JSONObject config) { + if (config == null) { + return false; } - public void toggleShowWinrate() { - this.showWinrate = !this.showWinrate; - } - public void toggleShowVariationGraph() { - this.showVariationGraph = !this.showVariationGraph; - } - public void toggleShowComment() { - this.showComment = !this.showComment; - } - public void toggleShowBestMoves() { - this.showBestMoves = !this.showBestMoves; - } - public void toggleShowNextMoves() { - this.showNextMoves = !this.showNextMoves; - } - public void toggleHandicapInsteadOfWinrate() { - this.handicapInsteadOfWinrate = !this.handicapInsteadOfWinrate; - } - public void toggleLargeSubBoard() { - this.largeSubBoard = !this.largeSubBoard; - } - public boolean showLargeSubBoard() { - return showSubBoard && largeSubBoard; - } - - /** - * Scans the current directory as well as the current PATH to find a reasonable default leelaz binary. - * - * @return A working path to a leelaz binary. If there are none on the PATH, "./leelaz" is returned for backwards - * compatibility. - */ - public static String getBestDefaultLeelazPath() { - List potentialPaths = new ArrayList<>(); - potentialPaths.add("."); - potentialPaths.addAll(Arrays.asList(System.getenv("PATH").split(":"))); - - for (String potentialPath : potentialPaths) { - for (String potentialExtension : Arrays.asList(new String[] {"", ".exe"})) { - File potentialLeelaz = new File(potentialPath, "leelaz" + potentialExtension); - if (potentialLeelaz.exists() && potentialLeelaz.canExecute()) { - return potentialLeelaz.getPath(); - } - } - } - return "./leelaz"; - } + boolean madeCorrections = false; + // Check ui configs + JSONObject ui = config.getJSONObject("ui"); - private JSONObject createDefaultConfig() { - JSONObject config = new JSONObject(); - - // About engine parameter - JSONObject leelaz = new JSONObject(); - leelaz.put("network-file", "network.gz"); - leelaz.put("engine-command", String.format("%s --gtp --lagbuffer 0 --weights %%network-file --threads 2", - getBestDefaultLeelazPath())); - leelaz.put("engine-start-location", "."); - leelaz.put("max-analyze-time-minutes", 5); - leelaz.put("max-game-thinking-time-seconds", 2); - leelaz.put("print-comms", false); - leelaz.put("analyze-update-interval-centisec", 10); - leelaz.put("automatically-download-latest-network", false); - - config.put("leelaz", leelaz); - - // About User Interface display - JSONObject ui = new JSONObject(); - - ui.put("board-color", new JSONArray("[217, 152, 77]")); - ui.put("shadows-enabled", true); - ui.put("fancy-stones", true); - ui.put("fancy-board", true); - ui.put("shadow-size", 100); - ui.put("show-move-number", false); - ui.put("show-status", true); - ui.put("show-leelaz-variation", true); - ui.put("show-winrate", true); - ui.put("show-variation-graph", true); - ui.put("show-captured", true); - ui.put("show-best-moves", true); - ui.put("show-next-moves", true); - ui.put("show-subboard", true); - ui.put("large-subboard", false); - ui.put("win-rate-always-black", false); - ui.put("confirm-exit", false); - ui.put("resume-previous-game", false); - ui.put("autosave-interval-seconds", -1); - ui.put("handicap-instead-of-winrate",false); - ui.put("board-size", 19); - ui.put("window-size", new JSONArray("[1024, 768]")); - ui.put("window-maximized", false); - ui.put("show-dynamic-komi", true); - ui.put("min-playout-ratio-for-stats", 0.0); - - config.put("ui", ui); - return config; + // Check board-size. We support only 9x9, 13x13 or 19x19 + int boardSize = ui.optInt("board-size", 19); + if (boardSize != 19 && boardSize != 13 && boardSize != 9) { + // Correct it to default 19x19 + ui.put("board-size", 19); + madeCorrections = true; } - private JSONObject createPersistConfig() { - JSONObject config = new JSONObject(); - - // About engine parameter - JSONObject filesys = new JSONObject(); - filesys.put("last-folder", ""); - - config.put("filesystem", filesys); - - // About User Interface display - JSONObject ui = new JSONObject(); - - //ui.put("window-height", 657); - //ui.put("window-width", 687); - //ui.put("max-alpha", 240); - - // Avoid the key "ui" because it was used to distinguish "config" and "persist" - // in old version of validateAndCorrectSettings(). - // If we use "ui" here, we will have trouble to run old lizzie. - config.put("ui-persist", ui); - return config; + // Check engine configs + JSONObject leelaz = config.getJSONObject("leelaz"); + // Checks for startup directory. It should exist and should be a directory. + String engineStartLocation = getBestDefaultLeelazPath(); + if (!(Files.exists(Paths.get(engineStartLocation)) + && Files.isDirectory(Paths.get(engineStartLocation)))) { + leelaz.put("engine-start-location", "."); + madeCorrections = true; } - private void writeConfig(JSONObject config, File file) throws IOException, JSONException { - file.createNewFile(); - - FileOutputStream fp = new FileOutputStream(file); - OutputStreamWriter writer = new OutputStreamWriter(fp); - - writer.write(config.toString(2)); - - writer.close(); - fp.close(); + return madeCorrections; + } + + public Config() throws IOException { + JSONObject defaultConfig = createDefaultConfig(); + JSONObject persistConfig = createPersistConfig(); + + // Main properties + this.config = loadAndMergeConfig(defaultConfig, configFilename, true); + // Persisted properties + this.persisted = loadAndMergeConfig(persistConfig, persistFilename, false); + + leelazConfig = config.getJSONObject("leelaz"); + uiConfig = config.getJSONObject("ui"); + + showMoveNumber = uiConfig.getBoolean("show-move-number"); + showStatus = uiConfig.getBoolean("show-status"); + showBranch = uiConfig.getBoolean("show-leelaz-variation"); + showWinrate = uiConfig.getBoolean("show-winrate"); + showVariationGraph = uiConfig.getBoolean("show-variation-graph"); + showComment = uiConfig.optBoolean("show-comment", false); + commentFontSize = uiConfig.optInt("comment-font-size", 0); + showCaptured = uiConfig.getBoolean("show-captured"); + showBestMoves = uiConfig.getBoolean("show-best-moves"); + showNextMoves = uiConfig.getBoolean("show-next-moves"); + showSubBoard = uiConfig.getBoolean("show-subboard"); + largeSubBoard = uiConfig.getBoolean("large-subboard"); + handicapInsteadOfWinrate = uiConfig.getBoolean("handicap-instead-of-winrate"); + startMaximized = uiConfig.getBoolean("window-maximized"); + showDynamicKomi = uiConfig.getBoolean("show-dynamic-komi"); + } + + // Modifies config by adding in values from default_config that are missing. + // Returns whether it added anything. + public boolean merge_defaults(JSONObject config, JSONObject defaults_config) { + boolean modified = false; + Iterator keys = defaults_config.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object new_val = defaults_config.get(key); + if (new_val instanceof JSONObject) { + if (!config.has(key)) { + config.put(key, new JSONObject()); + modified = true; + } + Object old_val = config.get(key); + modified |= merge_defaults((JSONObject) old_val, (JSONObject) new_val); + } else { + if (!config.has(key)) { + config.put(key, new_val); + modified = true; + } + } } - - public void persist() throws IOException { - writeConfig(this.persisted, new File(persistFilename)); + return modified; + } + + public void toggleShowMoveNumber() { + this.showMoveNumber = !this.showMoveNumber; + } + + public void toggleShowBranch() { + this.showBranch = !this.showBranch; + } + + public void toggleShowWinrate() { + this.showWinrate = !this.showWinrate; + } + + public void toggleShowVariationGraph() { + this.showVariationGraph = !this.showVariationGraph; + } + + public void toggleShowComment() { + this.showComment = !this.showComment; + } + + public void toggleShowBestMoves() { + this.showBestMoves = !this.showBestMoves; + } + + public void toggleShowNextMoves() { + this.showNextMoves = !this.showNextMoves; + } + + public void toggleHandicapInsteadOfWinrate() { + this.handicapInsteadOfWinrate = !this.handicapInsteadOfWinrate; + } + + public void toggleLargeSubBoard() { + this.largeSubBoard = !this.largeSubBoard; + } + + public boolean showLargeSubBoard() { + return showSubBoard && largeSubBoard; + } + + /** + * Scans the current directory as well as the current PATH to find a reasonable default leelaz + * binary. + * + * @return A working path to a leelaz binary. If there are none on the PATH, "./leelaz" is + * returned for backwards compatibility. + */ + public static String getBestDefaultLeelazPath() { + List potentialPaths = new ArrayList<>(); + potentialPaths.add("."); + potentialPaths.addAll(Arrays.asList(System.getenv("PATH").split(":"))); + + for (String potentialPath : potentialPaths) { + for (String potentialExtension : Arrays.asList(new String[] {"", ".exe"})) { + File potentialLeelaz = new File(potentialPath, "leelaz" + potentialExtension); + if (potentialLeelaz.exists() && potentialLeelaz.canExecute()) { + return potentialLeelaz.getPath(); + } + } } + return "./leelaz"; + } + + private JSONObject createDefaultConfig() { + JSONObject config = new JSONObject(); + + // About engine parameter + JSONObject leelaz = new JSONObject(); + leelaz.put("network-file", "network.gz"); + leelaz.put( + "engine-command", + String.format( + "%s --gtp --lagbuffer 0 --weights %%network-file --threads 2", + getBestDefaultLeelazPath())); + leelaz.put("engine-start-location", "."); + leelaz.put("max-analyze-time-minutes", 5); + leelaz.put("max-game-thinking-time-seconds", 2); + leelaz.put("print-comms", false); + leelaz.put("analyze-update-interval-centisec", 10); + leelaz.put("automatically-download-latest-network", false); + + config.put("leelaz", leelaz); + + // About User Interface display + JSONObject ui = new JSONObject(); + + ui.put("board-color", new JSONArray("[217, 152, 77]")); + ui.put("shadows-enabled", true); + ui.put("fancy-stones", true); + ui.put("fancy-board", true); + ui.put("shadow-size", 100); + ui.put("show-move-number", false); + ui.put("show-status", true); + ui.put("show-leelaz-variation", true); + ui.put("show-winrate", true); + ui.put("show-variation-graph", true); + ui.put("show-captured", true); + ui.put("show-best-moves", true); + ui.put("show-next-moves", true); + ui.put("show-subboard", true); + ui.put("large-subboard", false); + ui.put("win-rate-always-black", false); + ui.put("confirm-exit", false); + ui.put("resume-previous-game", false); + ui.put("autosave-interval-seconds", -1); + ui.put("handicap-instead-of-winrate", false); + ui.put("board-size", 19); + ui.put("window-size", new JSONArray("[1024, 768]")); + ui.put("window-maximized", false); + ui.put("show-dynamic-komi", true); + ui.put("min-playout-ratio-for-stats", 0.0); + + config.put("ui", ui); + return config; + } + + private JSONObject createPersistConfig() { + JSONObject config = new JSONObject(); + + // About engine parameter + JSONObject filesys = new JSONObject(); + filesys.put("last-folder", ""); + + config.put("filesystem", filesys); + + // About User Interface display + JSONObject ui = new JSONObject(); + + // ui.put("window-height", 657); + // ui.put("window-width", 687); + // ui.put("max-alpha", 240); + + // Avoid the key "ui" because it was used to distinguish "config" and "persist" + // in old version of validateAndCorrectSettings(). + // If we use "ui" here, we will have trouble to run old lizzie. + config.put("ui-persist", ui); + return config; + } + + private void writeConfig(JSONObject config, File file) throws IOException, JSONException { + file.createNewFile(); + + FileOutputStream fp = new FileOutputStream(file); + OutputStreamWriter writer = new OutputStreamWriter(fp); + + writer.write(config.toString(2)); + + writer.close(); + fp.close(); + } + + public void persist() throws IOException { + writeConfig(this.persisted, new File(persistFilename)); + } } diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index 087667933..fb85a4873 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -1,75 +1,71 @@ package featurecat.lizzie; -import org.json.JSONException; import featurecat.lizzie.analysis.Leelaz; -import featurecat.lizzie.rules.Board; -import featurecat.lizzie.rules.SGFParser; import featurecat.lizzie.gui.LizzieFrame; -import org.json.JSONObject; - -import javax.swing.*; +import featurecat.lizzie.rules.Board; import java.io.File; import java.io.IOException; -import java.util.ResourceBundle; +import javax.swing.*; +import org.json.JSONException; -/** - * Main class. - */ +/** Main class. */ public class Lizzie { - public static LizzieFrame frame; - public static Leelaz leelaz; - public static Board board; - public static Config config; - public static String lizzieVersion = "0.5"; + public static LizzieFrame frame; + public static Leelaz leelaz; + public static Board board; + public static Config config; + public static String lizzieVersion = "0.5"; - /** - * Launches the game window, and runs the game. - */ - public static void main(String[] args) throws IOException, JSONException, ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException, InterruptedException { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - config = new Config(); - board = new Board(); - frame = new LizzieFrame(); + /** Launches the game window, and runs the game. */ + public static void main(String[] args) + throws IOException, JSONException, ClassNotFoundException, UnsupportedLookAndFeelException, + InstantiationException, IllegalAccessException, InterruptedException { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + config = new Config(); + board = new Board(); + frame = new LizzieFrame(); - new Thread( () -> { - try { + new Thread( + () -> { + try { leelaz = new Leelaz(); - if(config.handicapInsteadOfWinrate) { - leelaz.estimatePassWinrate(); + if (config.handicapInsteadOfWinrate) { + leelaz.estimatePassWinrate(); } if (args.length == 1) { - frame.loadFile(new File(args[0])); + frame.loadFile(new File(args[0])); } else if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { - board.resumePreviousGame(); + board.resumePreviousGame(); } leelaz.togglePonder(); - } catch (IOException e) { + } catch (IOException e) { e.printStackTrace(); System.exit(-1); - } - }).start(); - } - - public static void shutdown() { - if (board != null && config.config.getJSONObject("ui").getBoolean("confirm-exit")) { - int ret = JOptionPane.showConfirmDialog(null, "Do you want to save this SGF?", "Save SGF?", JOptionPane.OK_CANCEL_OPTION); - if (ret == JOptionPane.OK_OPTION) { - LizzieFrame.saveFile(); - } - } - if (board != null) { - board.autosaveToMemory(); - } + } + }) + .start(); + } - try { - config.persist(); - } catch (IOException err) { - // Failed to save config - } + public static void shutdown() { + if (board != null && config.config.getJSONObject("ui").getBoolean("confirm-exit")) { + int ret = + JOptionPane.showConfirmDialog( + null, "Do you want to save this SGF?", "Save SGF?", JOptionPane.OK_CANCEL_OPTION); + if (ret == JOptionPane.OK_OPTION) { + LizzieFrame.saveFile(); + } + } + if (board != null) { + board.autosaveToMemory(); + } - if (leelaz != null) - leelaz.shutdown(); - System.exit(0); + try { + config.persist(); + } catch (IOException err) { + // Failed to save config } + if (leelaz != null) leelaz.shutdown(); + System.exit(0); + } } diff --git a/src/main/java/featurecat/lizzie/Util.java b/src/main/java/featurecat/lizzie/Util.java index 395dfd73a..432ebb1f5 100644 --- a/src/main/java/featurecat/lizzie/Util.java +++ b/src/main/java/featurecat/lizzie/Util.java @@ -11,73 +11,67 @@ import java.util.zip.GZIPInputStream; public class Util { - /** - * @param val the value we want to clamp - * @param min the minimum value that will be returned - * @param max the maximum value that will be returned - * @return the closest number to val within the range [min, max] - */ - public static > T clamp(T val, T min, T max) { - if (val.compareTo(min) < 0) return min; - else if (val.compareTo(max) > 0) return max; - else return val; - } + /** + * @param val the value we want to clamp + * @param min the minimum value that will be returned + * @param max the maximum value that will be returned + * @return the closest number to val within the range [min, max] + */ + public static > T clamp(T val, T min, T max) { + if (val.compareTo(min) < 0) return min; + else if (val.compareTo(max) > 0) return max; + else return val; + } - /** - * @return the sha 256 checksum of decompressed contents from a GZIPed file - */ - public static String getSha256Sum(File file) { - try (InputStream inputStream = new GZIPInputStream(new FileInputStream(file))) { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - DigestInputStream digestInputStream = new DigestInputStream(inputStream, messageDigest); + /** @return the sha 256 checksum of decompressed contents from a GZIPed file */ + public static String getSha256Sum(File file) { + try (InputStream inputStream = new GZIPInputStream(new FileInputStream(file))) { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + DigestInputStream digestInputStream = new DigestInputStream(inputStream, messageDigest); - // we have to read the file. additionally, using a buffer is efficient. - // 8192 was tested as the best value among 4096, 8192, 16384, and 32768. - byte[] buffer = new byte[8192]; - while (digestInputStream.read(buffer) != -1) ; + // we have to read the file. additionally, using a buffer is efficient. + // 8192 was tested as the best value among 4096, 8192, 16384, and 32768. + byte[] buffer = new byte[8192]; + while (digestInputStream.read(buffer) != -1) ; - MessageDigest digest = digestInputStream.getMessageDigest(); - digestInputStream.close(); + MessageDigest digest = digestInputStream.getMessageDigest(); + digestInputStream.close(); - byte[] sha256 = digest.digest(); - StringBuilder sb = new StringBuilder(); - for (byte b : sha256) { - sb.append(String.format("%02X", b)); - } - return sb.toString().toLowerCase(); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } catch (EOFException e) { - // do nothing, just means we need to download a new one - } catch (IOException e) { - e.printStackTrace(); - } - return null; + byte[] sha256 = digest.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : sha256) { + sb.append(String.format("%02X", b)); + } + return sb.toString().toLowerCase(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (EOFException e) { + // do nothing, just means we need to download a new one + } catch (IOException e) { + e.printStackTrace(); } + return null; + } - /** - * @return the url's contents, downloaded as a string - */ - public static String downloadAsString(URL url) { - try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { - return br.lines().collect(Collectors.joining("\n")); - } catch (IOException e) { - e.printStackTrace(); - } - - return null; + /** @return the url's contents, downloaded as a string */ + public static String downloadAsString(URL url) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + e.printStackTrace(); } - /** - * Downloads the contents of the url, and saves them in a file. - */ - public static void saveAsFile(URL url, File file) { - try { - ReadableByteChannel rbc = Channels.newChannel(url.openStream()); - FileOutputStream fos = new FileOutputStream(file); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - } catch (IOException e) { - e.printStackTrace(); - } + return null; + } + + /** Downloads the contents of the url, and saves them in a file. */ + public static void saveAsFile(URL url, File file) { + try { + ReadableByteChannel rbc = Channels.newChannel(url.openStream()); + FileOutputStream fos = new FileOutputStream(file); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } catch (IOException e) { + e.printStackTrace(); } + } } diff --git a/src/main/java/featurecat/lizzie/analysis/Branch.java b/src/main/java/featurecat/lizzie/analysis/Branch.java index ac2439c3f..a0f61d1d5 100644 --- a/src/main/java/featurecat/lizzie/analysis/Branch.java +++ b/src/main/java/featurecat/lizzie/analysis/Branch.java @@ -4,35 +4,45 @@ import featurecat.lizzie.rules.BoardData; import featurecat.lizzie.rules.Stone; import featurecat.lizzie.rules.Zobrist; - import java.util.List; public class Branch { - public BoardData data; + public BoardData data; - public Branch(Board board, List variation) { - int moveNumber = 0; - int[] lastMove = board.getLastMove(); - int[] moveNumberList = new int[Board.BOARD_SIZE * Board.BOARD_SIZE]; - boolean blackToPlay = board.getData().blackToPlay; + public Branch(Board board, List variation) { + int moveNumber = 0; + int[] lastMove = board.getLastMove(); + int[] moveNumberList = new int[Board.BOARD_SIZE * Board.BOARD_SIZE]; + boolean blackToPlay = board.getData().blackToPlay; - Stone lastMoveColor = board.getData().lastMoveColor; - Stone[] stones = board.getStones().clone(); - Zobrist zobrist = board.getData().zobrist == null ? null : board.getData().zobrist.clone(); + Stone lastMoveColor = board.getData().lastMoveColor; + Stone[] stones = board.getStones().clone(); + Zobrist zobrist = board.getData().zobrist == null ? null : board.getData().zobrist.clone(); - // Dont care about winrate for branch - this.data = new BoardData(stones, lastMove, lastMoveColor, blackToPlay, - zobrist, moveNumber, moveNumberList, board.getData().blackCaptures, board.getData().whiteCaptures, 0.0, 0); + // Dont care about winrate for branch + this.data = + new BoardData( + stones, + lastMove, + lastMoveColor, + blackToPlay, + zobrist, + moveNumber, + moveNumberList, + board.getData().blackCaptures, + board.getData().whiteCaptures, + 0.0, + 0); - for (int i = 0; i < variation.size(); i++) { - int[] coord = Board.convertNameToCoordinates(variation.get(i)); - if (coord == null) - break; - data.lastMove = coord; - data.stones[Board.getIndex(coord[0], coord[1])] = data.blackToPlay ? Stone.BLACK_GHOST : Stone.WHITE_GHOST; - data.moveNumberList[Board.getIndex(coord[0], coord[1])] = i + 1; - data.lastMoveColor = data.blackToPlay ? Stone.WHITE : Stone.BLACK; - data.blackToPlay = !data.blackToPlay; - } + for (int i = 0; i < variation.size(); i++) { + int[] coord = Board.convertNameToCoordinates(variation.get(i)); + if (coord == null) break; + data.lastMove = coord; + data.stones[Board.getIndex(coord[0], coord[1])] = + data.blackToPlay ? Stone.BLACK_GHOST : Stone.WHITE_GHOST; + data.moveNumberList[Board.getIndex(coord[0], coord[1])] = i + 1; + data.lastMoveColor = data.blackToPlay ? Stone.WHITE : Stone.BLACK; + data.blackToPlay = !data.blackToPlay; } + } } diff --git a/src/main/java/featurecat/lizzie/analysis/GameInfo.java b/src/main/java/featurecat/lizzie/analysis/GameInfo.java index 7910bc1a8..198eff393 100644 --- a/src/main/java/featurecat/lizzie/analysis/GameInfo.java +++ b/src/main/java/featurecat/lizzie/analysis/GameInfo.java @@ -1,56 +1,55 @@ package featurecat.lizzie.analysis; - import java.util.Date; public class GameInfo { - public static final String DEFAULT_NAME_HUMAN_PLAYER = "Human"; - public static final String DEFAULT_NAME_CPU_PLAYER = "Leela Zero"; - public static final double DEFAULT_KOMI = 7.5; - - private String playerBlack = ""; - private String playerWhite = ""; - private Date date = new Date(); - private double komi = DEFAULT_KOMI; - private int handicap = 0; - - public String getPlayerBlack() { - return playerBlack; - } - - public Date getDate() { - return date; - } - - public void setDate(Date date) { - this.date = date; - } - - public void setPlayerBlack(String playerBlack) { - this.playerBlack = playerBlack; - } - - public String getPlayerWhite() { - return playerWhite; - } - - public void setPlayerWhite(String playerWhite) { - this.playerWhite = playerWhite; - } - - public double getKomi() { - return komi; - } - - public void setKomi(double komi) { - this.komi = komi; - } - - public int getHandicap() { - return handicap; - } - - public void setHandicap(int handicap) { - this.handicap = handicap; - } + public static final String DEFAULT_NAME_HUMAN_PLAYER = "Human"; + public static final String DEFAULT_NAME_CPU_PLAYER = "Leela Zero"; + public static final double DEFAULT_KOMI = 7.5; + + private String playerBlack = ""; + private String playerWhite = ""; + private Date date = new Date(); + private double komi = DEFAULT_KOMI; + private int handicap = 0; + + public String getPlayerBlack() { + return playerBlack; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public void setPlayerBlack(String playerBlack) { + this.playerBlack = playerBlack; + } + + public String getPlayerWhite() { + return playerWhite; + } + + public void setPlayerWhite(String playerWhite) { + this.playerWhite = playerWhite; + } + + public double getKomi() { + return komi; + } + + public void setKomi(double komi) { + this.komi = komi; + } + + public int getHandicap() { + return handicap; + } + + public void setHandicap(int handicap) { + this.handicap = handicap; + } } diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 2c8c79a93..5c2774437 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -1,569 +1,583 @@ package featurecat.lizzie.analysis; -import featurecat.lizzie.Config; import featurecat.lizzie.Lizzie; import featurecat.lizzie.Util; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import featurecat.lizzie.rules.Stone; - -import javax.swing.*; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import javax.swing.*; +import org.json.JSONException; +import org.json.JSONObject; /** - * An interface with leelaz go engine. Can be adapted for GTP, but is specifically designed for GCP's Leela Zero. - * leelaz is modified to output information as it ponders - * see www.github.com/gcp/leela-zero + * An interface with leelaz go engine. Can be adapted for GTP, but is specifically designed for + * GCP's Leela Zero. leelaz is modified to output information as it ponders see + * www.github.com/gcp/leela-zero */ public class Leelaz { - private static final ResourceBundle resourceBundle = ResourceBundle.getBundle("l10n.DisplayStrings"); + private static final ResourceBundle resourceBundle = + ResourceBundle.getBundle("l10n.DisplayStrings"); - private static final long MINUTE = 60 * 1000; // number of milliseconds in a minute - private static final String baseURL = "http://zero.sjeng.org"; + private static final long MINUTE = 60 * 1000; // number of milliseconds in a minute + private static final String baseURL = "http://zero.sjeng.org"; - private long maxAnalyzeTimeMillis;//, maxThinkingTimeMillis; - private int cmdNumber; - private int currentCmdNum; - private ArrayDeque cmdQueue; + private long maxAnalyzeTimeMillis; // , maxThinkingTimeMillis; + private int cmdNumber; + private int currentCmdNum; + private ArrayDeque cmdQueue; - private Process process; + private Process process; - private BufferedInputStream inputStream; - private BufferedOutputStream outputStream; + private BufferedInputStream inputStream; + private BufferedOutputStream outputStream; - private boolean printCommunication; + private boolean printCommunication; - private List bestMoves; - private List bestMovesTemp; + private List bestMoves; + private List bestMovesTemp; - private List listeners; + private List listeners; - private boolean isPondering; - private long startPonderTime; + private boolean isPondering; + private long startPonderTime; - // fixed_handicap - public boolean isSettingHandicap = false; + // fixed_handicap + public boolean isSettingHandicap = false; - // genmove - public boolean isThinking = false; + // genmove + public boolean isThinking = false; - private boolean isLoaded = false; - private boolean isCheckingVersion; + private boolean isLoaded = false; + private boolean isCheckingVersion; - // dynamic komi and opponent komi as reported by dynamic-komi version of leelaz - private float dynamicKomi = Float.NaN, dynamicOppKomi = Float.NaN; - /** - * Initializes the leelaz process and starts reading output - * - * @throws IOException - */ - public Leelaz() throws IOException, JSONException { - bestMoves = new ArrayList<>(); - bestMovesTemp = new ArrayList<>(); - listeners = new CopyOnWriteArrayList<>(); + // dynamic komi and opponent komi as reported by dynamic-komi version of leelaz + private float dynamicKomi = Float.NaN, dynamicOppKomi = Float.NaN; + /** + * Initializes the leelaz process and starts reading output + * + * @throws IOException + */ + public Leelaz() throws IOException, JSONException { + bestMoves = new ArrayList<>(); + bestMovesTemp = new ArrayList<>(); + listeners = new CopyOnWriteArrayList<>(); - isPondering = false; - startPonderTime = System.currentTimeMillis(); - cmdNumber = 1; - currentCmdNum = 0; - cmdQueue = new ArrayDeque<>(); + isPondering = false; + startPonderTime = System.currentTimeMillis(); + cmdNumber = 1; + currentCmdNum = 0; + cmdQueue = new ArrayDeque<>(); - JSONObject config = Lizzie.config.config.getJSONObject("leelaz"); + JSONObject config = Lizzie.config.config.getJSONObject("leelaz"); - printCommunication = config.getBoolean("print-comms"); - maxAnalyzeTimeMillis = MINUTE * config.getInt("max-analyze-time-minutes"); - - if (config.getBoolean("automatically-download-latest-network")) { - updateToLatestNetwork(); - } + printCommunication = config.getBoolean("print-comms"); + maxAnalyzeTimeMillis = MINUTE * config.getInt("max-analyze-time-minutes"); - 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(" ")); - - // Check if engine is present - File lef = startfolder.toPath().resolve(new File(commands.get(0)).toPath()).toFile(); - if (!lef.exists()) { - JOptionPane.showMessageDialog(null, resourceBundle.getString("LizzieFrame.display.leelaz-missing"), "Lizzie - Error!", JOptionPane.ERROR_MESSAGE); - throw new IOException("engine not present"); - } - - // Check if network file is present - File wf = startfolder.toPath().resolve(new File(networkFile).toPath()).toFile(); - if (!wf.exists()) { - JOptionPane.showMessageDialog(null, resourceBundle.getString("LizzieFrame.display.network-missing")); - throw new IOException("network-file not present"); - } - - // run leelaz - ProcessBuilder processBuilder = new ProcessBuilder(commands); - processBuilder.directory(startfolder); - processBuilder.redirectErrorStream(true); - process = processBuilder.start(); - - initializeStreams(); - - // Send a version request to check that we have a supported version - // Response handled in parseLine - isCheckingVersion = true; - sendCommand("version"); - - // start a thread to continuously read Leelaz output - new Thread(this::read).start(); - Lizzie.frame.refreshBackground(); + if (config.getBoolean("automatically-download-latest-network")) { + updateToLatestNetwork(); } - private void updateToLatestNetwork() { - try { - if (needToDownloadLatestNetwork()) { - int dialogResult = JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.display.download-latest-network-prompt")); - if (dialogResult == JOptionPane.YES_OPTION) { - Util.saveAsFile(new URL(baseURL + "/networks/" + getBestNetworkHash() + ".gz"), - new File(Lizzie.config.leelazConfig.getString("network-file"))); - } - } - } catch (IOException e) { - e.printStackTrace(); - // now we're probably still ok. Maybe we're offline -- then it's not a big problem. - } + 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(" ")); + + // Check if engine is present + File lef = startfolder.toPath().resolve(new File(commands.get(0)).toPath()).toFile(); + if (!lef.exists()) { + JOptionPane.showMessageDialog( + null, + resourceBundle.getString("LizzieFrame.display.leelaz-missing"), + "Lizzie - Error!", + JOptionPane.ERROR_MESSAGE); + throw new IOException("engine not present"); } - private String getBestNetworkHash() throws IOException { - return Util.downloadAsString(new URL(baseURL + "/best-network-hash")).split("\n")[0]; + // Check if network file is present + File wf = startfolder.toPath().resolve(new File(networkFile).toPath()).toFile(); + if (!wf.exists()) { + JOptionPane.showMessageDialog( + null, resourceBundle.getString("LizzieFrame.display.network-missing")); + throw new IOException("network-file not present"); } - private boolean needToDownloadLatestNetwork() throws IOException { - File networkFile = new File(Lizzie.config.leelazConfig.getString("network-file")); - if (!networkFile.exists()) { - return true; - } else { - String currentNetworkHash = Util.getSha256Sum(networkFile); - if (currentNetworkHash == null) - return true; - - String bestNetworkHash = getBestNetworkHash(); - - return !currentNetworkHash.equals(bestNetworkHash); + // run leelaz + ProcessBuilder processBuilder = new ProcessBuilder(commands); + processBuilder.directory(startfolder); + processBuilder.redirectErrorStream(true); + process = processBuilder.start(); + + initializeStreams(); + + // Send a version request to check that we have a supported version + // Response handled in parseLine + isCheckingVersion = true; + sendCommand("version"); + + // start a thread to continuously read Leelaz output + new Thread(this::read).start(); + Lizzie.frame.refreshBackground(); + } + + private void updateToLatestNetwork() { + try { + if (needToDownloadLatestNetwork()) { + int dialogResult = + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.display.download-latest-network-prompt")); + if (dialogResult == JOptionPane.YES_OPTION) { + Util.saveAsFile( + new URL(baseURL + "/networks/" + getBestNetworkHash() + ".gz"), + new File(Lizzie.config.leelazConfig.getString("network-file"))); } + } + } catch (IOException e) { + e.printStackTrace(); + // now we're probably still ok. Maybe we're offline -- then it's not a big problem. } + } - /** - * Initializes the input and output streams - */ - private void initializeStreams() { - inputStream = new BufferedInputStream(process.getInputStream()); - outputStream = new BufferedOutputStream(process.getOutputStream()); - } + private String getBestNetworkHash() throws IOException { + return Util.downloadAsString(new URL(baseURL + "/best-network-hash")).split("\n")[0]; + } - private void parseInfo(String line) { - bestMoves = new ArrayList<>(); - String[] variations = line.split(" info "); - for (String var : variations) { - bestMoves.add(MoveData.fromInfo(var)); - } - } + private boolean needToDownloadLatestNetwork() throws IOException { + File networkFile = new File(Lizzie.config.leelazConfig.getString("network-file")); + if (!networkFile.exists()) { + return true; + } else { + String currentNetworkHash = Util.getSha256Sum(networkFile); + if (currentNetworkHash == null) return true; - /** - * Parse a line of Leelaz output - * - * @param line output line - */ - private void parseLine(String line) { - synchronized (this) { - if (line.startsWith("komi=")) { - try { - dynamicKomi = Float.parseFloat(line.substring("komi=".length()).trim()); - } - catch (NumberFormatException nfe) { - dynamicKomi = Float.NaN; - } - } else if (line.startsWith("opp_komi=")) { - try { - dynamicOppKomi = Float.parseFloat(line.substring("opp_komi=".length()).trim()); - } - catch (NumberFormatException nfe) { - dynamicOppKomi = Float.NaN; - } - } else if (line.equals("\n")) { - // End of response - } else if (line.startsWith("info")) { - isLoaded = true; - if (isResponseUpToDate()) { - // This should not be stale data when the command number match - parseInfo(line.substring(5)); - notifyBestMoveListeners(); - if (Lizzie.frame != null) Lizzie.frame.repaint(); - // don't follow the maxAnalyzeTime rule if we are in analysis mode - if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis && !Lizzie.board.inAnalysisMode()) { - togglePonder(); - } - } - } else if (line.contains(" -> ")) { - isLoaded = true; - if (isResponseUpToDate()) { - bestMoves.add(MoveData.fromSummary(line)); - if (Lizzie.frame != null) - Lizzie.frame.repaint(); - } - } else if (line.startsWith("play")) { - // In lz-genmove_analyze - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.board.place(line.substring(5).trim()); - } - isThinking = false; - - } else if (Lizzie.frame != null && (line.startsWith("=") || line.startsWith("?"))) { - if (printCommunication) { - System.out.print(line); - } - String[] params = line.trim().split(" "); - currentCmdNum = Integer.parseInt(params[0].substring(1).trim()); - - trySendCommandFromQueue(); - - if (line.startsWith("?") || params.length == 1) return; - - - if (isSettingHandicap) { - for (int i = 1; i < params.length; i++) { - int[] coordinates = Lizzie.board.convertNameToCoordinates(params[i]); - Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); - } - isSettingHandicap = false; - } else if (isThinking && !isPondering) { - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.board.place(params[1]); - isThinking = false; - } - } else if (isCheckingVersion) { - String[] ver = params[1].split("\\."); - int minor = Integer.parseInt(ver[1]); - // Gtp support added in version 15 - if (minor < 15) { - JOptionPane.showMessageDialog(Lizzie.frame, "Lizzie requires version 0.15 or later of Leela Zero for analysis (found " + params[1] + ")"); - } - isCheckingVersion = false; - } - } - } - } + String bestNetworkHash = getBestNetworkHash(); - /** - * Parse a move-data line of Leelaz output - * - * @param line output line - */ - private void parseMoveDataLine(String line) { - line = line.trim(); - // ignore passes, and only accept lines that start with a coordinate letter - if (line.length() > 0 && Character.isLetter(line.charAt(0)) && !line.startsWith("pass")) { - if (!(Lizzie.frame != null && Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay)) { - try { - bestMovesTemp.add(MoveData.fromInfo(line)); - } catch (ArrayIndexOutOfBoundsException e) { - // this is very rare but is possible. ignore - } - } - } + return !currentNetworkHash.equals(bestNetworkHash); } - - /** - * Continually reads and processes output from leelaz - */ - private void read() { + } + + /** Initializes the input and output streams */ + private void initializeStreams() { + inputStream = new BufferedInputStream(process.getInputStream()); + outputStream = new BufferedOutputStream(process.getOutputStream()); + } + + private void parseInfo(String line) { + bestMoves = new ArrayList<>(); + String[] variations = line.split(" info "); + for (String var : variations) { + bestMoves.add(MoveData.fromInfo(var)); + } + } + + /** + * Parse a line of Leelaz output + * + * @param line output line + */ + private void parseLine(String line) { + synchronized (this) { + if (line.startsWith("komi=")) { try { - int c; - StringBuilder line = new StringBuilder(); - while ((c = inputStream.read()) != -1) { - line.append((char) c); - if ((c == '\n')) { - parseLine(line.toString()); - line = new StringBuilder(); - } - } - // this line will be reached when Leelaz shuts down - System.out.println("Leelaz process ended."); - - shutdown(); - System.exit(-1); - } catch (IOException e) { - e.printStackTrace(); - System.exit(-1); + dynamicKomi = Float.parseFloat(line.substring("komi=".length()).trim()); + } catch (NumberFormatException nfe) { + dynamicKomi = Float.NaN; } - } - - /** - * Sends a command to command queue for leelaz to execute - * - * @param command a GTP command containing no newline characters - */ - public void sendCommand(String command) { - synchronized(cmdQueue) { - String lastCommand = cmdQueue.peekLast(); - // For efficiency, delete unnecessary "lz-analyze" that will be stopped immediately - if (lastCommand != null && lastCommand.startsWith("lz-analyze")) { - cmdQueue.removeLast(); - } - cmdQueue.addLast(command); - trySendCommandFromQueue(); + } else if (line.startsWith("opp_komi=")) { + try { + dynamicOppKomi = Float.parseFloat(line.substring("opp_komi=".length()).trim()); + } catch (NumberFormatException nfe) { + dynamicOppKomi = Float.NaN; } - } - - /** - * Sends a command from command queue for leelaz to execute if it is ready - */ - private void trySendCommandFromQueue() { - // Defer sending "lz-analyze" if leelaz is not ready yet. - // Though all commands should be deferred theoretically, - // only "lz-analyze" is differed here for fear of - // possible hang-up by missing response for some reason. - // cmdQueue can be replaced with a mere String variable in this case, - // but it is kept for future change of our mind. - synchronized(cmdQueue) { - String command = cmdQueue.peekFirst(); - if (command == null || (command.startsWith("lz-analyze") && !isResponseUpToDate())) { - return; - } - cmdQueue.removeFirst(); - sendCommandToLeelaz(command); + } else if (line.equals("\n")) { + // End of response + } else if (line.startsWith("info")) { + isLoaded = true; + if (isResponseUpToDate()) { + // This should not be stale data when the command number match + parseInfo(line.substring(5)); + notifyBestMoveListeners(); + if (Lizzie.frame != null) Lizzie.frame.repaint(); + // don't follow the maxAnalyzeTime rule if we are in analysis mode + if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis + && !Lizzie.board.inAnalysisMode()) { + togglePonder(); + } } - } + } else if (line.contains(" -> ")) { + isLoaded = true; + if (isResponseUpToDate()) { + bestMoves.add(MoveData.fromSummary(line)); + if (Lizzie.frame != null) Lizzie.frame.repaint(); + } + } else if (line.startsWith("play")) { + // In lz-genmove_analyze + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.board.place(line.substring(5).trim()); + } + isThinking = false; - /** - * Sends a command for leelaz to execute - * - * @param command a GTP command containing no newline characters - */ - private void sendCommandToLeelaz(String command) { - if (command.startsWith("fixed_handicap")) - isSettingHandicap = true; - command = cmdNumber + " " + command; - cmdNumber++; + } else if (Lizzie.frame != null && (line.startsWith("=") || line.startsWith("?"))) { if (printCommunication) { - System.out.printf("> %s\n", command); + System.out.print(line); } - try { - outputStream.write((command + "\n").getBytes()); - outputStream.flush(); - } catch (IOException e) { - e.printStackTrace(); + String[] params = line.trim().split(" "); + currentCmdNum = Integer.parseInt(params[0].substring(1).trim()); + + trySendCommandFromQueue(); + + if (line.startsWith("?") || params.length == 1) return; + + if (isSettingHandicap) { + for (int i = 1; i < params.length; i++) { + int[] coordinates = Lizzie.board.convertNameToCoordinates(params[i]); + Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); + } + isSettingHandicap = false; + } else if (isThinking && !isPondering) { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.board.place(params[1]); + isThinking = false; + } + } else if (isCheckingVersion) { + String[] ver = params[1].split("\\."); + int minor = Integer.parseInt(ver[1]); + // Gtp support added in version 15 + if (minor < 15) { + JOptionPane.showMessageDialog( + Lizzie.frame, + "Lizzie requires version 0.15 or later of Leela Zero for analysis (found " + + params[1] + + ")"); + } + isCheckingVersion = false; } + } } - - /** - * Check whether leelaz is responding to the last command - */ - private boolean isResponseUpToDate() { - // Use >= instead of == for avoiding hang-up, though it cannot happen - return currentCmdNum >= cmdNumber - 1; - } - - /** - * @param color color of stone to play - * @param move coordinate of the coordinate - */ - public void playMove(Stone color, String move) { - synchronized (this) { - String colorString; - switch (color) { - case BLACK: - colorString = "B"; - break; - case WHITE: - colorString = "W"; - break; - default: - throw new IllegalArgumentException("The stone color must be B or W, but was " + color.toString()); - } - - sendCommand("play " + colorString + " " + move); - bestMoves = new ArrayList<>(); - - if (isPondering && !Lizzie.frame.isPlayingAgainstLeelaz) - ponder(); + } + + /** + * Parse a move-data line of Leelaz output + * + * @param line output line + */ + private void parseMoveDataLine(String line) { + line = line.trim(); + // ignore passes, and only accept lines that start with a coordinate letter + if (line.length() > 0 && Character.isLetter(line.charAt(0)) && !line.startsWith("pass")) { + if (!(Lizzie.frame != null + && Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay)) { + try { + bestMovesTemp.add(MoveData.fromInfo(line)); + } catch (ArrayIndexOutOfBoundsException e) { + // this is very rare but is possible. ignore } + } } - - public void genmove(String color) { - String command = "genmove " + color; - /* - * We don't support displaying this while playing, so no reason to request it (for now) - if (isPondering) { - command = "lz-genmove_analyze " + color + " 10"; - }*/ - sendCommand(command); - isThinking = true; - isPondering = false; - } - - public void undo() { - synchronized (this) { - sendCommand("undo"); - bestMoves = new ArrayList<>(); - if (isPondering) - ponder(); + } + + /** Continually reads and processes output from leelaz */ + private void read() { + try { + int c; + StringBuilder line = new StringBuilder(); + while ((c = inputStream.read()) != -1) { + line.append((char) c); + if ((c == '\n')) { + parseLine(line.toString()); + line = new StringBuilder(); } + } + // this line will be reached when Leelaz shuts down + System.out.println("Leelaz process ended."); + + shutdown(); + System.exit(-1); + } catch (IOException e) { + e.printStackTrace(); + System.exit(-1); } - - /** - * This initializes leelaz's pondering mode at its current position - */ - private void ponder() { - isPondering = true; - startPonderTime = System.currentTimeMillis(); - sendCommand("lz-analyze " + Lizzie.config.config.getJSONObject("leelaz").getInt("analyze-update-interval-centisec")); // until it responds to this, incoming ponder results are obsolete + } + + /** + * Sends a command to command queue for leelaz to execute + * + * @param command a GTP command containing no newline characters + */ + public void sendCommand(String command) { + synchronized (cmdQueue) { + String lastCommand = cmdQueue.peekLast(); + // For efficiency, delete unnecessary "lz-analyze" that will be stopped immediately + if (lastCommand != null && lastCommand.startsWith("lz-analyze")) { + cmdQueue.removeLast(); + } + cmdQueue.addLast(command); + trySendCommandFromQueue(); } - - public void togglePonder() { - isPondering = !isPondering; - if (isPondering) { - ponder(); - } else { - sendCommand("name"); // ends pondering - } + } + + /** Sends a command from command queue for leelaz to execute if it is ready */ + private void trySendCommandFromQueue() { + // Defer sending "lz-analyze" if leelaz is not ready yet. + // Though all commands should be deferred theoretically, + // only "lz-analyze" is differed here for fear of + // possible hang-up by missing response for some reason. + // cmdQueue can be replaced with a mere String variable in this case, + // but it is kept for future change of our mind. + synchronized (cmdQueue) { + String command = cmdQueue.peekFirst(); + if (command == null || (command.startsWith("lz-analyze") && !isResponseUpToDate())) { + return; + } + cmdQueue.removeFirst(); + sendCommandToLeelaz(command); } - - /** - * End the process - */ - public void shutdown() { - process.destroy(); + } + + /** + * Sends a command for leelaz to execute + * + * @param command a GTP command containing no newline characters + */ + private void sendCommandToLeelaz(String command) { + if (command.startsWith("fixed_handicap")) isSettingHandicap = true; + command = cmdNumber + " " + command; + cmdNumber++; + if (printCommunication) { + System.out.printf("> %s\n", command); } - - public List getBestMoves() { - synchronized (this) { - return bestMoves; - } + try { + outputStream.write((command + "\n").getBytes()); + outputStream.flush(); + } catch (IOException e) { + e.printStackTrace(); } - - public String getDynamicKomi() { - if (Float.isNaN(dynamicKomi) || Float.isNaN(dynamicOppKomi)) { - return null; - } - return String.format("%.1f / %.1f", dynamicKomi, dynamicOppKomi); + } + + /** Check whether leelaz is responding to the last command */ + private boolean isResponseUpToDate() { + // Use >= instead of == for avoiding hang-up, though it cannot happen + return currentCmdNum >= cmdNumber - 1; + } + + /** + * @param color color of stone to play + * @param move coordinate of the coordinate + */ + public void playMove(Stone color, String move) { + synchronized (this) { + String colorString; + switch (color) { + case BLACK: + colorString = "B"; + break; + case WHITE: + colorString = "W"; + break; + default: + throw new IllegalArgumentException( + "The stone color must be B or W, but was " + color.toString()); + } + + sendCommand("play " + colorString + " " + move); + bestMoves = new ArrayList<>(); + + if (isPondering && !Lizzie.frame.isPlayingAgainstLeelaz) ponder(); } + } - public boolean isPondering() { - return isPondering; + public void genmove(String color) { + String command = "genmove " + color; + /* + * We don't support displaying this while playing, so no reason to request it (for now) + if (isPondering) { + command = "lz-genmove_analyze " + color + " 10"; + }*/ + sendCommand(command); + isThinking = true; + isPondering = false; + } + + public void undo() { + synchronized (this) { + sendCommand("undo"); + bestMoves = new ArrayList<>(); + if (isPondering) ponder(); } - - public class WinrateStats { - public double maxWinrate; - public int totalPlayouts; - - public WinrateStats(double maxWinrate, int totalPlayouts) { - this.maxWinrate = maxWinrate; - this.totalPlayouts = totalPlayouts; - } + } + + /** This initializes leelaz's pondering mode at its current position */ + private void ponder() { + isPondering = true; + startPonderTime = System.currentTimeMillis(); + sendCommand( + "lz-analyze " + + Lizzie.config + .config + .getJSONObject("leelaz") + .getInt( + "analyze-update-interval-centisec")); // until it responds to this, incoming + // ponder results are obsolete + } + + public void togglePonder() { + isPondering = !isPondering; + if (isPondering) { + ponder(); + } else { + sendCommand("name"); // ends pondering } + } - /* - * Return the best win rate and total number of playouts. - * If no analysis available, win rate is negative and playouts is 0. - */ - public WinrateStats getWinrateStats() { - WinrateStats stats = new WinrateStats(-100, 0); - - if (bestMoves != null && !bestMoves.isEmpty()) { - // we should match the Leelaz UCTNode get_eval, which is a weighted average - // copy the list to avoid concurrent modification exception... TODO there must be a better way - // (note the concurrent modification exception is very very rare) - final List moves = new ArrayList(bestMoves); - - // get the total number of playouts in moves - stats.totalPlayouts = moves.stream().reduce(0, - (Integer result, MoveData move) -> result + move.playouts, - (Integer a, Integer b) -> a + b); - - // set maxWinrate to the weighted average winrate of moves - stats.maxWinrate = moves.stream().reduce(0d, - (Double result, MoveData move) -> - result + move.winrate * move.playouts / stats.totalPlayouts, - (Double a, Double b) -> a + b); - } + /** End the process */ + public void shutdown() { + process.destroy(); + } - return stats; + public List getBestMoves() { + synchronized (this) { + return bestMoves; } + } - /* - * initializes the normalizing factor for winrate_to_handicap_stones conversion. - */ - public void estimatePassWinrate() { - // we use A1 instead of pass, because valuenetwork is more accurate for A1 on empty board than a pass. - // probably the reason for higher accuracy is that networks have randomness which produces occasionally A1 as first move, but never pass. - // for all practical purposes, A1 should equal pass for the value it provides, hence good replacement. - // this way we avoid having to run lots of playouts for accurate winrate for pass. - playMove(Stone.BLACK, "A1"); - togglePonder(); - WinrateStats stats = getWinrateStats(); - - // we could use a timelimit or higher minimum playouts to get a more accurate measurement. - while (stats.totalPlayouts < 1) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new Error(e); - } - stats = getWinrateStats(); - } - mHandicapWinrate = stats.maxWinrate; - togglePonder(); - undo(); - Lizzie.board.clear(); + public String getDynamicKomi() { + if (Float.isNaN(dynamicKomi) || Float.isNaN(dynamicOppKomi)) { + return null; } + return String.format("%.1f / %.1f", dynamicKomi, dynamicOppKomi); + } - public static double mHandicapWinrate = 25; - - /** - * Convert winrate to handicap stones, by normalizing winrate by first move pass winrate (one stone handicap). - */ - public static double winrateToHandicap(double pWinrate) { - // we assume each additional handicap lowers winrate by fixed percentage. - // this is pretty accurate for human handicap games at least. - // also this kind of property is a requirement for handicaps to determined based on rank difference. - - // lets convert the 0%-50% range and 100%-50% from both the move and and pass into range of 0-1 - double moveWinrateSymmetric = 1 - Math.abs(1 - (pWinrate / 100) * 2); - double passWinrateSymmetric = 1 - Math.abs(1 - (mHandicapWinrate / 100) * 2); + public boolean isPondering() { + return isPondering; + } - // convert the symmetric move winrate into correctly scaled log scale, so that winrate of passWinrate equals 1 handicap. - double handicapSymmetric = Math.log(moveWinrateSymmetric) / Math.log(passWinrateSymmetric); + public class WinrateStats { + public double maxWinrate; + public int totalPlayouts; - // make it negative if we had low winrate below 50. - return Math.signum(pWinrate - 50) * handicapSymmetric; + public WinrateStats(double maxWinrate, int totalPlayouts) { + this.maxWinrate = maxWinrate; + this.totalPlayouts = totalPlayouts; } - - public synchronized void addListener(LeelazListener listener) { - listeners.add(listener); + } + + /* + * Return the best win rate and total number of playouts. + * If no analysis available, win rate is negative and playouts is 0. + */ + public WinrateStats getWinrateStats() { + WinrateStats stats = new WinrateStats(-100, 0); + + if (bestMoves != null && !bestMoves.isEmpty()) { + // we should match the Leelaz UCTNode get_eval, which is a weighted average + // copy the list to avoid concurrent modification exception... TODO there must be a better way + // (note the concurrent modification exception is very very rare) + final List moves = new ArrayList(bestMoves); + + // get the total number of playouts in moves + stats.totalPlayouts = + moves + .stream() + .reduce( + 0, + (Integer result, MoveData move) -> result + move.playouts, + (Integer a, Integer b) -> a + b); + + // set maxWinrate to the weighted average winrate of moves + stats.maxWinrate = + moves + .stream() + .reduce( + 0d, + (Double result, MoveData move) -> + result + move.winrate * move.playouts / stats.totalPlayouts, + (Double a, Double b) -> a + b); } - // Beware, due to race conditions, bestMoveNotification can be called once even after item is removed - // with removeListener - public synchronized void removeListener(LeelazListener listener) { - listeners.remove(listener); + return stats; + } + + /* + * initializes the normalizing factor for winrate_to_handicap_stones conversion. + */ + public void estimatePassWinrate() { + // we use A1 instead of pass, because valuenetwork is more accurate for A1 on empty board than a + // pass. + // probably the reason for higher accuracy is that networks have randomness which produces + // occasionally A1 as first move, but never pass. + // for all practical purposes, A1 should equal pass for the value it provides, hence good + // replacement. + // this way we avoid having to run lots of playouts for accurate winrate for pass. + playMove(Stone.BLACK, "A1"); + togglePonder(); + WinrateStats stats = getWinrateStats(); + + // we could use a timelimit or higher minimum playouts to get a more accurate measurement. + while (stats.totalPlayouts < 1) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new Error(e); + } + stats = getWinrateStats(); } - - private synchronized void notifyBestMoveListeners() { - for (LeelazListener listener : listeners) { - listener.bestMoveNotification(bestMoves); - } + mHandicapWinrate = stats.maxWinrate; + togglePonder(); + undo(); + Lizzie.board.clear(); + } + + public static double mHandicapWinrate = 25; + + /** + * Convert winrate to handicap stones, by normalizing winrate by first move pass winrate (one + * stone handicap). + */ + public static double winrateToHandicap(double pWinrate) { + // we assume each additional handicap lowers winrate by fixed percentage. + // this is pretty accurate for human handicap games at least. + // also this kind of property is a requirement for handicaps to determined based on rank + // difference. + + // lets convert the 0%-50% range and 100%-50% from both the move and and pass into range of 0-1 + double moveWinrateSymmetric = 1 - Math.abs(1 - (pWinrate / 100) * 2); + double passWinrateSymmetric = 1 - Math.abs(1 - (mHandicapWinrate / 100) * 2); + + // convert the symmetric move winrate into correctly scaled log scale, so that winrate of + // passWinrate equals 1 handicap. + double handicapSymmetric = Math.log(moveWinrateSymmetric) / Math.log(passWinrateSymmetric); + + // make it negative if we had low winrate below 50. + return Math.signum(pWinrate - 50) * handicapSymmetric; + } + + public synchronized void addListener(LeelazListener listener) { + listeners.add(listener); + } + + // Beware, due to race conditions, bestMoveNotification can be called once even after item is + // removed + // with removeListener + public synchronized void removeListener(LeelazListener listener) { + listeners.remove(listener); + } + + private synchronized void notifyBestMoveListeners() { + for (LeelazListener listener : listeners) { + listener.bestMoveNotification(bestMoves); } + } - public boolean isLoaded() { - return isLoaded; - } + public boolean isLoaded() { + return isLoaded; + } } diff --git a/src/main/java/featurecat/lizzie/analysis/LeelazListener.java b/src/main/java/featurecat/lizzie/analysis/LeelazListener.java index 51e81a206..a07789c8b 100644 --- a/src/main/java/featurecat/lizzie/analysis/LeelazListener.java +++ b/src/main/java/featurecat/lizzie/analysis/LeelazListener.java @@ -3,5 +3,5 @@ import java.util.List; public interface LeelazListener { - void bestMoveNotification(List bestMoves); + void bestMoveNotification(List bestMoves); } diff --git a/src/main/java/featurecat/lizzie/analysis/MoveData.java b/src/main/java/featurecat/lizzie/analysis/MoveData.java index f66486dbb..988182925 100644 --- a/src/main/java/featurecat/lizzie/analysis/MoveData.java +++ b/src/main/java/featurecat/lizzie/analysis/MoveData.java @@ -6,73 +6,71 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * Holds the data from Leelaz's pondering mode - */ +/** Holds the data from Leelaz's pondering mode */ public class MoveData { - public String coordinate; - public int playouts; - public double winrate; - public List variation; + public String coordinate; + public int playouts; + public double winrate; + public List variation; - private MoveData() {} + private MoveData() {} - /** - * Parses a leelaz ponder output line. For example: - * - * info move R5 visits 38 winrate 5404 order 0 pv R5 Q5 R6 S4 Q10 C3 D3 C4 C6 C5 D5 - * - * @param line line of ponder output - */ - public static MoveData fromInfo(String line) throws ArrayIndexOutOfBoundsException { - MoveData result = new MoveData(); - String[] data = line.trim().split(" "); + /** + * Parses a leelaz ponder output line. For example: + * + *

info move R5 visits 38 winrate 5404 order 0 pv R5 Q5 R6 S4 Q10 C3 D3 C4 C6 C5 D5 + * + * @param line line of ponder output + */ + public static MoveData fromInfo(String line) throws ArrayIndexOutOfBoundsException { + MoveData result = new MoveData(); + String[] data = line.trim().split(" "); - // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed - for (int i=0; i(Arrays.asList(data)); - result.variation = result.variation.subList(i + 1, data.length); - break; - } else { - String value = data[++i]; - if (key.equals("move")) { - result.coordinate = value; - } - if (key.equals("visits")) { - result.playouts = Integer.parseInt(value); - } - if (key.equals("winrate")) { - result.winrate = Integer.parseInt(value) / 100.0; - } - } + // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed + for (int i = 0; i < data.length; i++) { + String key = data[i]; + if (key.equals("pv")) { + // Read variation to the end of line + result.variation = new ArrayList<>(Arrays.asList(data)); + result.variation = result.variation.subList(i + 1, data.length); + break; + } else { + String value = data[++i]; + if (key.equals("move")) { + result.coordinate = value; } - return result; + if (key.equals("visits")) { + result.playouts = Integer.parseInt(value); + } + if (key.equals("winrate")) { + result.winrate = Integer.parseInt(value) / 100.0; + } + } } + return result; + } - /** - * Parses a leelaz summary output line. For example: - * - * P16 -> 4 (V: 50.94%) (N: 5.79%) PV: P16 N18 R5 Q5 - * - * @param line line of summary output - */ - public static MoveData fromSummary(String summary) { - Matcher match = summaryPattern.matcher(summary.trim()); - if (!match.matches()) { - throw new IllegalArgumentException("Unexpected summary format: " + summary); - } else { - MoveData result = new MoveData(); - result.coordinate = match.group(1); - result.playouts = Integer.parseInt(match.group(2)); - result.winrate = Double.parseDouble(match.group(3)); - result.variation = Arrays.asList(match.group(4).split(" ")); - return result; - } + /** + * Parses a leelaz summary output line. For example: + * + *

P16 -> 4 (V: 50.94%) (N: 5.79%) PV: P16 N18 R5 Q5 + * + * @param line line of summary output + */ + public static MoveData fromSummary(String summary) { + Matcher match = summaryPattern.matcher(summary.trim()); + if (!match.matches()) { + throw new IllegalArgumentException("Unexpected summary format: " + summary); + } else { + MoveData result = new MoveData(); + result.coordinate = match.group(1); + result.playouts = Integer.parseInt(match.group(2)); + result.winrate = Double.parseDouble(match.group(3)); + result.variation = Arrays.asList(match.group(4).split(" ")); + return result; } + } - private static Pattern summaryPattern = Pattern.compile( - "^ *(\\w\\d*) -> *(\\d+) \\(V: ([^%)]+)%\\) \\([^\\)]+\\) PV: (.+).*$"); + private static Pattern summaryPattern = + Pattern.compile("^ *(\\w\\d*) -> *(\\d+) \\(V: ([^%)]+)%\\) \\([^\\)]+\\) PV: (.+).*$"); } diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 53b3895f7..dc54aad1c 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -1,8 +1,5 @@ package featurecat.lizzie.gui; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Branch; import featurecat.lizzie.analysis.MoveData; @@ -14,1070 +11,1215 @@ import java.awt.font.TextAttribute; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; -import java.awt.image.ImageObserver; -import java.util.HashMap; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; public class BoardRenderer { - private static final double MARGIN = 0.03; // percentage of the boardLength to offset before drawing black lines - private static final double MARGIN_WITH_COORDINATES = 0.06; - private static final double STARPOINT_DIAMETER = 0.015; - - private int x, y; - private int boardLength; - - private JSONObject uiConfig, uiPersist; - - private int scaledMargin, availableLength, squareLength, stoneRadius; - private Branch branch; - private List bestMoves; + private static final double MARGIN = + 0.03; // percentage of the boardLength to offset before drawing black lines + private static final double MARGIN_WITH_COORDINATES = 0.06; + private static final double STARPOINT_DIAMETER = 0.015; - private BufferedImage cachedBackgroundImage = null; - private boolean cachedBackgroundImageHasCoordinatesEnabled = false; - private int cachedX, cachedY; + private int x, y; + private int boardLength; - private BufferedImage cachedStonesImage = null; - private BufferedImage cachedBoardImage = null; - private BufferedImage cachedWallpaperImage = null; - private BufferedImage cachedStonesShadowImage = null; - private Zobrist cachedZhash = new Zobrist(); // defaults to an empty board + private JSONObject uiConfig, uiPersist; + private int scaledMargin, availableLength, squareLength, stoneRadius; + private Branch branch; + private List bestMoves; - private BufferedImage cachedBlackStoneImage = null; - private BufferedImage cachedWhiteStoneImage = null; + private BufferedImage cachedBackgroundImage = null; + private boolean cachedBackgroundImageHasCoordinatesEnabled = false; + private int cachedX, cachedY; - private BufferedImage branchStonesImage = null; - private BufferedImage branchStonesShadowImage = null; + private BufferedImage cachedStonesImage = null; + private BufferedImage cachedBoardImage = null; + private BufferedImage cachedWallpaperImage = null; + private BufferedImage cachedStonesShadowImage = null; + private Zobrist cachedZhash = new Zobrist(); // defaults to an empty board - private boolean lastInScoreMode = false; + private BufferedImage cachedBlackStoneImage = null; + private BufferedImage cachedWhiteStoneImage = null; - public List variation; + private BufferedImage branchStonesImage = null; + private BufferedImage branchStonesShadowImage = null; - // special values of displayedBranchLength - public static final int SHOW_RAW_BOARD = -1; - public static final int SHOW_NORMAL_BOARD = -2; + private boolean lastInScoreMode = false; - private int displayedBranchLength = SHOW_NORMAL_BOARD; - private int cachedDisplayedBranchLength = SHOW_RAW_BOARD; - private boolean showingBranch = false; - private boolean isMainBoard = false; + public List variation; - private int maxAlpha = 240; + // special values of displayedBranchLength + public static final int SHOW_RAW_BOARD = -1; + public static final int SHOW_NORMAL_BOARD = -2; - public BoardRenderer(boolean isMainBoard) { - uiConfig = Lizzie.config.config.getJSONObject("ui"); - uiPersist = Lizzie.config.persisted.getJSONObject("ui-persist"); - try { - maxAlpha = uiPersist.getInt("max-alpha"); - } catch (JSONException e) {} - this.isMainBoard = isMainBoard; - } - - /** - * Draw a go board - */ - public void draw(Graphics2D g) { - if (Lizzie.frame == null || Lizzie.board == null) - return; - - setupSizeParameters(); - -// Stopwatch timer = new Stopwatch(); - drawGoban(g); -// timer.lap("background"); - drawStones(); -// timer.lap("stones"); - if (Lizzie.board.inScoreMode() && isMainBoard) { - drawScore(g); - } else { - drawBranch(); - } -// timer.lap("branch"); - - renderImages(g); -// timer.lap("rendering images"); - - if (!isMainBoard) { - drawMoveNumbers(g); - return; - } - - if (!isShowingRawBoard()) { - drawMoveNumbers(g); -// timer.lap("movenumbers"); - if (!Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.config.showBestMoves) - drawLeelazSuggestions(g); - - if (Lizzie.config.showNextMoves) { - drawNextMoves(g); - } - } + private int displayedBranchLength = SHOW_NORMAL_BOARD; + private int cachedDisplayedBranchLength = SHOW_RAW_BOARD; + private boolean showingBranch = false; + private boolean isMainBoard = false; -// timer.lap("leelaz"); + private int maxAlpha = 240; -// timer.print(); + public BoardRenderer(boolean isMainBoard) { + uiConfig = Lizzie.config.config.getJSONObject("ui"); + uiPersist = Lizzie.config.persisted.getJSONObject("ui-persist"); + try { + maxAlpha = uiPersist.getInt("max-alpha"); + } catch (JSONException e) { } - - /** - * Return the best move of Leelaz's suggestions - * - * @return the coordinate name of the best move - */ - public String bestMoveCoordinateName() { - if (bestMoves == null || bestMoves.size() == 0) { - return null; - } else { - return bestMoves.get(0).coordinate; - } + this.isMainBoard = isMainBoard; + } + + /** Draw a go board */ + public void draw(Graphics2D g) { + if (Lizzie.frame == null || Lizzie.board == null) return; + + setupSizeParameters(); + + // Stopwatch timer = new Stopwatch(); + drawGoban(g); + // timer.lap("background"); + drawStones(); + // timer.lap("stones"); + if (Lizzie.board.inScoreMode() && isMainBoard) { + drawScore(g); + } else { + drawBranch(); } + // timer.lap("branch"); - /** - * Calculate good values for boardLength, scaledMargin, availableLength, and squareLength - */ - private void setupSizeParameters() { - int originalBoardLength = boardLength; + renderImages(g); + // timer.lap("rendering images"); - int[] calculatedPixelMargins = calculatePixelMargins(); - boardLength = calculatedPixelMargins[0]; - scaledMargin = calculatedPixelMargins[1]; - availableLength = calculatedPixelMargins[2]; + if (!isMainBoard) { + drawMoveNumbers(g); + return; + } - squareLength = calculateSquareLength(availableLength); - stoneRadius = squareLength / 2 - 1; + if (!isShowingRawBoard()) { + drawMoveNumbers(g); + // timer.lap("movenumbers"); + if (!Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.config.showBestMoves) + drawLeelazSuggestions(g); - // re-center board - setLocation(x + (originalBoardLength - boardLength) / 2, y + (originalBoardLength - boardLength) / 2); + if (Lizzie.config.showNextMoves) { + drawNextMoves(g); + } } - /** - * Draw the green background and go board with lines. We cache the image for a performance boost. - */ - private void drawGoban(Graphics2D g0) { - // draw the cached background image if frame size changes - if (cachedBackgroundImage == null || cachedBackgroundImage.getWidth() != Lizzie.frame.getWidth() || - cachedBackgroundImage.getHeight() != Lizzie.frame.getHeight() || - cachedX != x || cachedY != y || - cachedBackgroundImageHasCoordinatesEnabled != showCoordinates()) { - - cachedBackgroundImage = new BufferedImage(Lizzie.frame.getWidth(), Lizzie.frame.getHeight(), - BufferedImage.TYPE_INT_ARGB); - Graphics2D g = cachedBackgroundImage.createGraphics(); + // timer.lap("leelaz"); + + // timer.print(); + } + + /** + * Return the best move of Leelaz's suggestions + * + * @return the coordinate name of the best move + */ + public String bestMoveCoordinateName() { + if (bestMoves == null || bestMoves.size() == 0) { + return null; + } else { + return bestMoves.get(0).coordinate; + } + } + + /** Calculate good values for boardLength, scaledMargin, availableLength, and squareLength */ + private void setupSizeParameters() { + int originalBoardLength = boardLength; + + int[] calculatedPixelMargins = calculatePixelMargins(); + boardLength = calculatedPixelMargins[0]; + scaledMargin = calculatedPixelMargins[1]; + availableLength = calculatedPixelMargins[2]; + + squareLength = calculateSquareLength(availableLength); + stoneRadius = squareLength / 2 - 1; + + // re-center board + setLocation( + x + (originalBoardLength - boardLength) / 2, y + (originalBoardLength - boardLength) / 2); + } + + /** + * Draw the green background and go board with lines. We cache the image for a performance boost. + */ + private void drawGoban(Graphics2D g0) { + // draw the cached background image if frame size changes + if (cachedBackgroundImage == null + || cachedBackgroundImage.getWidth() != Lizzie.frame.getWidth() + || cachedBackgroundImage.getHeight() != Lizzie.frame.getHeight() + || cachedX != x + || cachedY != y + || cachedBackgroundImageHasCoordinatesEnabled != showCoordinates()) { + + cachedBackgroundImage = + new BufferedImage( + Lizzie.frame.getWidth(), Lizzie.frame.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g = cachedBackgroundImage.createGraphics(); + + // draw the wooden background + drawWoodenBoard(g); + + // draw the lines + g.setColor(Color.BLACK); + for (int i = 0; i < Board.BOARD_SIZE; i++) { + g.drawLine( + x + scaledMargin, + y + scaledMargin + squareLength * i, + x + scaledMargin + availableLength - 1, + y + scaledMargin + squareLength * i); + } + for (int i = 0; i < Board.BOARD_SIZE; i++) { + g.drawLine( + x + scaledMargin + squareLength * i, + y + scaledMargin, + x + scaledMargin + squareLength * i, + y + scaledMargin + availableLength - 1); + } - // draw the wooden background - drawWoodenBoard(g); + // draw the star points + drawStarPoints(g); - // draw the lines - g.setColor(Color.BLACK); - for (int i = 0; i < Board.BOARD_SIZE; i++) { - g.drawLine(x + scaledMargin, y + scaledMargin + squareLength * i, - x + scaledMargin + availableLength - 1, y + scaledMargin + squareLength * i); - } - for (int i = 0; i < Board.BOARD_SIZE; i++) { - g.drawLine(x + scaledMargin + squareLength * i, y + scaledMargin, - x + scaledMargin + squareLength * i, y + scaledMargin + availableLength - 1); - } - - // draw the star points - drawStarPoints(g); - - // draw coordinates if enabled - if (showCoordinates()) { - g.setColor(Color.BLACK); - String alphabet = "ABCDEFGHJKLMNOPQRST"; - for (int i = 0; i < Board.BOARD_SIZE; i++) { - drawString(g, x + scaledMargin + squareLength * i, y + scaledMargin / 2, LizzieFrame.OpenSansRegularBase, "" + alphabet.charAt(i), stoneRadius * 4 / 5, stoneRadius); - drawString(g, x + scaledMargin + squareLength * i, y - scaledMargin / 2 + boardLength, LizzieFrame.OpenSansRegularBase, "" + alphabet.charAt(i), stoneRadius * 4 / 5, stoneRadius); - } - for (int i = 0; i < Board.BOARD_SIZE; i++) { - drawString(g, x + scaledMargin / 2, y + scaledMargin + squareLength * i, LizzieFrame.OpenSansRegularBase, "" + (Board.BOARD_SIZE - i), stoneRadius * 4 / 5, stoneRadius); - drawString(g, x - scaledMargin / 2 + +boardLength, y + scaledMargin + squareLength * i, LizzieFrame.OpenSansRegularBase, "" + (Board.BOARD_SIZE - i), stoneRadius * 4 / 5, stoneRadius); - } - } - cachedBackgroundImageHasCoordinatesEnabled = showCoordinates(); - g.dispose(); + // draw coordinates if enabled + if (showCoordinates()) { + g.setColor(Color.BLACK); + String alphabet = "ABCDEFGHJKLMNOPQRST"; + for (int i = 0; i < Board.BOARD_SIZE; i++) { + drawString( + g, + x + scaledMargin + squareLength * i, + y + scaledMargin / 2, + LizzieFrame.OpenSansRegularBase, + "" + alphabet.charAt(i), + stoneRadius * 4 / 5, + stoneRadius); + drawString( + g, + x + scaledMargin + squareLength * i, + y - scaledMargin / 2 + boardLength, + LizzieFrame.OpenSansRegularBase, + "" + alphabet.charAt(i), + stoneRadius * 4 / 5, + stoneRadius); } - - g0.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - g0.drawImage(cachedBackgroundImage, 0, 0, null); - cachedX = x; - cachedY = y; - } - - /** - * Draw the star points on the board, according to board size - * - * @param g graphics2d object to draw - */ - private void drawStarPoints(Graphics2D g) { - if (Board.BOARD_SIZE == 9) { - drawStarPoints9x9(g); - } else if (Board.BOARD_SIZE == 13) { - drawStarPoints13x13(g); - } else { - drawStarPoints19x19(g); + for (int i = 0; i < Board.BOARD_SIZE; i++) { + drawString( + g, + x + scaledMargin / 2, + y + scaledMargin + squareLength * i, + LizzieFrame.OpenSansRegularBase, + "" + (Board.BOARD_SIZE - i), + stoneRadius * 4 / 5, + stoneRadius); + drawString( + g, + x - scaledMargin / 2 + +boardLength, + y + scaledMargin + squareLength * i, + LizzieFrame.OpenSansRegularBase, + "" + (Board.BOARD_SIZE - i), + stoneRadius * 4 / 5, + stoneRadius); } + } + cachedBackgroundImageHasCoordinatesEnabled = showCoordinates(); + g.dispose(); } - private void drawStarPoints19x19(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 3; - final int STARPOINT_EDGE_OFFSET = 3; - final int STARPOINT_GRID_DISTANCE = 6; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); - fillCircle(g, centerX, centerY, starPointRadius); - } - } + g0.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g0.drawImage(cachedBackgroundImage, 0, 0, null); + cachedX = x; + cachedY = y; + } + + /** + * Draw the star points on the board, according to board size + * + * @param g graphics2d object to draw + */ + private void drawStarPoints(Graphics2D g) { + if (Board.BOARD_SIZE == 9) { + drawStarPoints9x9(g); + } else if (Board.BOARD_SIZE == 13) { + drawStarPoints13x13(g); + } else { + drawStarPoints19x19(g); } - - private void drawStarPoints13x13(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 2; - final int STARPOINT_EDGE_OFFSET = 3; - final int STARPOINT_GRID_DISTANCE = 6; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); - fillCircle(g, centerX, centerY, starPointRadius); - } - } - - // Draw center - int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + } + + private void drawStarPoints19x19(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; + final int NUM_STARPOINTS = 3; + final int STARPOINT_EDGE_OFFSET = 3; + final int STARPOINT_GRID_DISTANCE = 6; + for (int i = 0; i < NUM_STARPOINTS; i++) { + for (int j = 0; j < NUM_STARPOINTS; j++) { + int centerX = + x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); + int centerY = + y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); fillCircle(g, centerX, centerY, starPointRadius); + } } - - private void drawStarPoints9x9(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 2; - final int STARPOINT_EDGE_OFFSET = 2; - final int STARPOINT_GRID_DISTANCE = 4; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); - fillCircle(g, centerX, centerY, starPointRadius); - } - } - - // Draw center - int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + } + + private void drawStarPoints13x13(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; + final int NUM_STARPOINTS = 2; + final int STARPOINT_EDGE_OFFSET = 3; + final int STARPOINT_GRID_DISTANCE = 6; + for (int i = 0; i < NUM_STARPOINTS; i++) { + for (int j = 0; j < NUM_STARPOINTS; j++) { + int centerX = + x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); + int centerY = + y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); fillCircle(g, centerX, centerY, starPointRadius); + } } - /** - * Draw the stones. We cache the image for a performance boost. - */ - private void drawStones() { - // draw a new image if frame size changes or board state changes - if (cachedStonesImage == null || cachedStonesImage.getWidth() != boardLength || - cachedStonesImage.getHeight() != boardLength || - cachedDisplayedBranchLength != displayedBranchLength || - !cachedZhash.equals(Lizzie.board.getData().zobrist) - || Lizzie.board.inScoreMode() || lastInScoreMode) { - - cachedStonesImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - cachedStonesShadowImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = cachedStonesImage.createGraphics(); - Graphics2D gShadow = cachedStonesShadowImage.createGraphics(); - - // we need antialiasing to make the stones pretty. Java is a bit slow at antialiasing; that's why we want the cache - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - gShadow.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - int stoneX = scaledMargin + squareLength * i; - int stoneY = scaledMargin + squareLength * j; - drawStone(g, gShadow, stoneX, stoneY, Lizzie.board.getStones()[Board.getIndex(i, j)], i, j); - } - } + // Draw center + int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + fillCircle(g, centerX, centerY, starPointRadius); + } + + private void drawStarPoints9x9(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; + final int NUM_STARPOINTS = 2; + final int STARPOINT_EDGE_OFFSET = 2; + final int STARPOINT_GRID_DISTANCE = 4; + for (int i = 0; i < NUM_STARPOINTS; i++) { + for (int j = 0; j < NUM_STARPOINTS; j++) { + int centerX = + x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); + int centerY = + y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); + fillCircle(g, centerX, centerY, starPointRadius); + } + } - cachedZhash = Lizzie.board.getData().zobrist.clone(); - cachedDisplayedBranchLength = displayedBranchLength; - g.dispose(); - gShadow.dispose(); - lastInScoreMode = false; + // Draw center + int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + fillCircle(g, centerX, centerY, starPointRadius); + } + + /** Draw the stones. We cache the image for a performance boost. */ + private void drawStones() { + // draw a new image if frame size changes or board state changes + if (cachedStonesImage == null + || cachedStonesImage.getWidth() != boardLength + || cachedStonesImage.getHeight() != boardLength + || cachedDisplayedBranchLength != displayedBranchLength + || !cachedZhash.equals(Lizzie.board.getData().zobrist) + || Lizzie.board.inScoreMode() + || lastInScoreMode) { + + cachedStonesImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); + cachedStonesShadowImage = + new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = cachedStonesImage.createGraphics(); + Graphics2D gShadow = cachedStonesShadowImage.createGraphics(); + + // we need antialiasing to make the stones pretty. Java is a bit slow at antialiasing; that's + // why we want the cache + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + gShadow.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + int stoneX = scaledMargin + squareLength * i; + int stoneY = scaledMargin + squareLength * j; + drawStone( + g, gShadow, stoneX, stoneY, Lizzie.board.getStones()[Board.getIndex(i, j)], i, j); } - if (Lizzie.board.inScoreMode()) lastInScoreMode = true; + } + cachedZhash = Lizzie.board.getData().zobrist.clone(); + cachedDisplayedBranchLength = displayedBranchLength; + g.dispose(); + gShadow.dispose(); + lastInScoreMode = false; } - - /* - * Draw a white/black dot on territory and captured stones. Dame is drawn as red dot. - */ - private void drawScore(Graphics2D go) { - Graphics2D g = cachedStonesImage.createGraphics(); - Stone scorestones[] = Lizzie.board.scoreStones(); - int scoreRadius = stoneRadius / 4; - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - int stoneX = scaledMargin + squareLength * i; - int stoneY = scaledMargin + squareLength * j; - switch (scorestones[Board.getIndex(i, j)]) { - case WHITE_POINT: - case BLACK_CAPTURED: - g.setColor(Color.white); - fillCircle(g, stoneX, stoneY, scoreRadius); - break; - case BLACK_POINT: - case WHITE_CAPTURED: - g.setColor(Color.black); - fillCircle(g, stoneX, stoneY, scoreRadius); - break; - case DAME: - g.setColor(Color.red); - fillCircle(g, stoneX, stoneY, scoreRadius); - break; - } - } + if (Lizzie.board.inScoreMode()) lastInScoreMode = true; + } + + /* + * Draw a white/black dot on territory and captured stones. Dame is drawn as red dot. + */ + private void drawScore(Graphics2D go) { + Graphics2D g = cachedStonesImage.createGraphics(); + Stone scorestones[] = Lizzie.board.scoreStones(); + int scoreRadius = stoneRadius / 4; + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + int stoneX = scaledMargin + squareLength * i; + int stoneY = scaledMargin + squareLength * j; + switch (scorestones[Board.getIndex(i, j)]) { + case WHITE_POINT: + case BLACK_CAPTURED: + g.setColor(Color.white); + fillCircle(g, stoneX, stoneY, scoreRadius); + break; + case BLACK_POINT: + case WHITE_CAPTURED: + g.setColor(Color.black); + fillCircle(g, stoneX, stoneY, scoreRadius); + break; + case DAME: + g.setColor(Color.red); + fillCircle(g, stoneX, stoneY, scoreRadius); + break; } - g.dispose(); + } } + g.dispose(); + } + + /** Draw the 'ghost stones' which show a variation Leelaz is thinking about */ + private void drawBranch() { + showingBranch = false; + branchStonesImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); + branchStonesShadowImage = + new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); + branch = null; + + if (Lizzie.frame.isPlayingAgainstLeelaz || Lizzie.leelaz == null) { + return; + } + // calculate best moves and branch + bestMoves = Lizzie.leelaz.getBestMoves(); + branch = null; + variation = null; - /** - * Draw the 'ghost stones' which show a variation Leelaz is thinking about - */ - private void drawBranch() { - showingBranch = false; - branchStonesImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - branchStonesShadowImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - branch = null; - - if (Lizzie.frame.isPlayingAgainstLeelaz || Lizzie.leelaz == null) { - return; - } - // calculate best moves and branch - bestMoves = Lizzie.leelaz.getBestMoves(); - branch = null; - variation = null; - - if (isMainBoard && (isShowingRawBoard() || !Lizzie.config.showBranch)) { - return; - } + if (isMainBoard && (isShowingRawBoard() || !Lizzie.config.showBranch)) { + return; + } - Graphics2D g = (Graphics2D) branchStonesImage.getGraphics(); - Graphics2D gShadow = (Graphics2D) branchStonesShadowImage.getGraphics(); + Graphics2D g = (Graphics2D) branchStonesImage.getGraphics(); + Graphics2D gShadow = (Graphics2D) branchStonesShadowImage.getGraphics(); - MoveData suggestedMove = (isMainBoard ? mouseHoveredMove() : getBestMove()); - if (suggestedMove == null) - return; - variation = suggestedMove.variation; - branch = new Branch(Lizzie.board, variation); + MoveData suggestedMove = (isMainBoard ? mouseHoveredMove() : getBestMove()); + if (suggestedMove == null) return; + variation = suggestedMove.variation; + branch = new Branch(Lizzie.board, variation); - if (branch == null) - return; - showingBranch = true; + if (branch == null) return; + showingBranch = true; - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - if (Lizzie.board.getData().stones[Board.getIndex(i,j)] != Stone.EMPTY) - continue; - if (branch.data.moveNumberList[Board.getIndex(i,j)] > maxBranchMoves()) - continue; + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + if (Lizzie.board.getData().stones[Board.getIndex(i, j)] != Stone.EMPTY) continue; + if (branch.data.moveNumberList[Board.getIndex(i, j)] > maxBranchMoves()) continue; - int stoneX = scaledMargin + squareLength * i; - int stoneY = scaledMargin + squareLength * j; + int stoneX = scaledMargin + squareLength * i; + int stoneY = scaledMargin + squareLength * j; - drawStone(g, gShadow, stoneX, stoneY, branch.data.stones[Board.getIndex(i, j)].unGhosted(), i, j); + drawStone( + g, gShadow, stoneX, stoneY, branch.data.stones[Board.getIndex(i, j)].unGhosted(), i, j); + } + } - } + g.dispose(); + gShadow.dispose(); + } + + private MoveData mouseHoveredMove() { + if (Lizzie.frame.mouseHoverCoordinate != null) { + for (int i = 0; i < bestMoves.size(); i++) { + MoveData move = bestMoves.get(i); + int[] coord = Board.convertNameToCoordinates(move.coordinate); + if (coord == null) { + continue; } - g.dispose(); - gShadow.dispose(); - } - - private MoveData mouseHoveredMove() { - if (Lizzie.frame.mouseHoverCoordinate != null) { - for (int i = 0; i < bestMoves.size(); i++) { - MoveData move = bestMoves.get(i); - int[] coord = Board.convertNameToCoordinates(move.coordinate); - if (coord == null) { - continue; - } - - if (coord[0] == Lizzie.frame.mouseHoverCoordinate[0] && coord[1] == Lizzie.frame.mouseHoverCoordinate[1]) { - return move; - } - } + if (coord[0] == Lizzie.frame.mouseHoverCoordinate[0] + && coord[1] == Lizzie.frame.mouseHoverCoordinate[1]) { + return move; } - return null; + } } - - private MoveData getBestMove() { - return bestMoves.isEmpty() ? null : bestMoves.get(0); + return null; + } + + private MoveData getBestMove() { + return bestMoves.isEmpty() ? null : bestMoves.get(0); + } + + /** render the shadows and stones in correct background-foreground order */ + private void renderImages(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g.drawImage(cachedStonesShadowImage, x, y, null); + if (Lizzie.config.showBranch) { + g.drawImage(branchStonesShadowImage, x, y, null); } + g.drawImage(cachedStonesImage, x, y, null); + if (Lizzie.config.showBranch) { + g.drawImage(branchStonesImage, x, y, null); + } + } + + /** Draw move numbers and/or mark the last played move */ + private void drawMoveNumbers(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int[] lastMove = branch == null ? Lizzie.board.getLastMove() : branch.data.lastMove; + if (!Lizzie.config.showMoveNumber && branch == null) { + if (lastMove != null) { + // mark the last coordinate + int lastMoveMarkerRadius = stoneRadius / 2; + int stoneX = x + scaledMargin + squareLength * lastMove[0]; + int stoneY = y + scaledMargin + squareLength * lastMove[1]; + + // set color to the opposite color of whatever is on the board + g.setColor( + Lizzie.board.getStones()[Board.getIndex(lastMove[0], lastMove[1])].isWhite() + ? Color.BLACK + : Color.WHITE); + drawCircle(g, stoneX, stoneY, lastMoveMarkerRadius); + } else if (lastMove == null + && Lizzie.board.getData().moveNumber != 0 + && !Lizzie.board.inScoreMode()) { + g.setColor( + Lizzie.board.getData().blackToPlay + ? new Color(255, 255, 255, 150) + : new Color(0, 0, 0, 150)); + g.fillOval( + x + boardLength / 2 - 4 * stoneRadius, + y + boardLength / 2 - 4 * stoneRadius, + stoneRadius * 8, + stoneRadius * 8); + g.setColor( + Lizzie.board.getData().blackToPlay + ? new Color(0, 0, 0, 255) + : new Color(255, 255, 255, 255)); + drawString( + g, + x + boardLength / 2, + y + boardLength / 2, + LizzieFrame.OpenSansRegularBase, + "pass", + stoneRadius * 4, + stoneRadius * 6); + } - /** - * render the shadows and stones in correct background-foreground order - */ - private void renderImages(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - g.drawImage(cachedStonesShadowImage, x, y, null); - if (Lizzie.config.showBranch) { - g.drawImage(branchStonesShadowImage, x, y, null); - } - g.drawImage(cachedStonesImage, x, y, null); - if (Lizzie.config.showBranch) { - g.drawImage(branchStonesImage, x, y, null); - } + return; } - /** - * Draw move numbers and/or mark the last played move - */ - private void drawMoveNumbers(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - int[] lastMove = branch == null ? Lizzie.board.getLastMove() : branch.data.lastMove; - if (!Lizzie.config.showMoveNumber && branch == null) { - if (lastMove != null) { - // mark the last coordinate - int lastMoveMarkerRadius = stoneRadius / 2; - int stoneX = x + scaledMargin + squareLength * lastMove[0]; - int stoneY = y + scaledMargin + squareLength * lastMove[1]; - - // set color to the opposite color of whatever is on the board - g.setColor(Lizzie.board.getStones()[Board.getIndex(lastMove[0], lastMove[1])].isWhite() ? - Color.BLACK : Color.WHITE); - drawCircle(g, stoneX, stoneY, lastMoveMarkerRadius); - } else if (lastMove == null && Lizzie.board.getData().moveNumber != 0 && !Lizzie.board.inScoreMode()) { - g.setColor(Lizzie.board.getData().blackToPlay ? new Color(255, 255, 255, 150) : new Color(0, 0, 0, 150)); - g.fillOval(x + boardLength / 2 - 4 * stoneRadius, y + boardLength / 2 - 4 * stoneRadius, stoneRadius * 8, stoneRadius * 8); - g.setColor(Lizzie.board.getData().blackToPlay ? new Color(0, 0, 0, 255) : new Color(255, 255, 255, 255)); - drawString(g, x + boardLength / 2, y + boardLength / 2, LizzieFrame.OpenSansRegularBase, "pass", stoneRadius * 4, stoneRadius * 6); - } + int[] moveNumberList = + branch == null ? Lizzie.board.getMoveNumberList() : branch.data.moveNumberList; - return; - } + // Allow to display only last move number + int lastMoveNumber = + branch == null ? Lizzie.board.getData().moveNumber : branch.data.moveNumber; + int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); - int[] moveNumberList = branch == null ? Lizzie.board.getMoveNumberList() : branch.data.moveNumberList; + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + int stoneX = x + scaledMargin + squareLength * i; + int stoneY = y + scaledMargin + squareLength * j; // Allow to display only last move number - int lastMoveNumber = branch == null ? Lizzie.board.getData().moveNumber : branch.data.moveNumber; - int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); - - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - int stoneX = x + scaledMargin + squareLength * i; - int stoneY = y + scaledMargin + squareLength * j; - - // Allow to display only last move number - if (lastMoveNumber - moveNumberList[Board.getIndex(i, j)] >= onlyLastMoveNumber) { - continue; - } - - Stone stoneAtThisPoint = branch == null ? Lizzie.board.getStones()[Board.getIndex(i, j)] : - branch.data.stones[Board.getIndex(i, j)]; - - // don't write the move number if either: the move number is 0, or there will already be playout information written - if (moveNumberList[Board.getIndex(i, j)] > 0 && !(branch != null && Lizzie.frame.mouseHoverCoordinate != null && i == Lizzie.frame.mouseHoverCoordinate[0] && j == Lizzie.frame.mouseHoverCoordinate[1])) { - if (lastMove != null && i == lastMove[0] && j == lastMove[1]) - g.setColor(Color.RED.brighter());//stoneAtThisPoint.isBlack() ? Color.RED.brighter() : Color.BLUE.brighter()); - else { - // Draw white letters on black stones nomally. - // But use black letters for showing black moves without stones. - boolean reverse = (moveNumberList[Board.getIndex(i, j)] > maxBranchMoves()); - g.setColor(stoneAtThisPoint.isBlack() ^ reverse ? Color.WHITE : Color.BLACK); - } - - String moveNumberString = moveNumberList[Board.getIndex(i, j)] + ""; - drawString(g, stoneX, stoneY, LizzieFrame.OpenSansRegularBase, moveNumberString, (float) (stoneRadius * 1.4), (int) (stoneRadius * 1.4)); - } - } + if (lastMoveNumber - moveNumberList[Board.getIndex(i, j)] >= onlyLastMoveNumber) { + continue; } - } - - /** - * Draw all of Leelaz's suggestions as colored stones with winrate/playout statistics overlayed - */ - private void drawLeelazSuggestions(Graphics2D g) { - if (Lizzie.leelaz == null) - return; - - final int MIN_ALPHA = 32; - final double HUE_SCALING_FACTOR = 3.0; - final double ALPHA_SCALING_FACTOR = 5.0; - final float GREEN_HUE = Color.RGBtoHSB(0, 1, 0, null)[0]; - final float CYAN_HUE = Color.RGBtoHSB(0, 1, 1, null)[0]; - - if (!bestMoves.isEmpty()) { - - int maxPlayouts = 0; - double maxWinrate = 0; - for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) - maxPlayouts = move.playouts; - if (move.winrate > maxWinrate) - maxWinrate = move.winrate; - } - - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - MoveData move = null; - - // this is inefficient but it looks better with shadows - for (MoveData m : bestMoves) { - int[] coord = Board.convertNameToCoordinates(m.coordinate); - // Handle passes - if (coord == null) { - continue; - } - if (coord[0] == i && coord[1] == j) { - move = m; - break; - } - } - - if (move == null) - continue; - - boolean isBestMove = bestMoves.get(0) == move; - boolean hasMaxWinrate = move.winrate == maxWinrate; - - if (move.playouts == 0) // this actually can happen - continue; - - double percentPlayouts = (double) move.playouts / maxPlayouts; - - int[] coordinates = Board.convertNameToCoordinates(move.coordinate); - int suggestionX = x + scaledMargin + squareLength * coordinates[0]; - int suggestionY = y + scaledMargin + squareLength * coordinates[1]; - - - // 0 = Reddest hue - float hue = isBestMove ? CYAN_HUE : (float) (-GREEN_HUE * Math.max(0, Math.log(percentPlayouts) / HUE_SCALING_FACTOR + 1)); - float saturation = 0.75f; //saturation - float brightness = 0.85f; //brightness - int alpha = (int) (MIN_ALPHA + (maxAlpha - MIN_ALPHA) * Math.max(0, Math.log(percentPlayouts) / - ALPHA_SCALING_FACTOR + 1)); -// if (uiConfig.getBoolean("shadows-enabled")) -// alpha = 255; - - Color hsbColor = Color.getHSBColor(hue, saturation, brightness); - Color color = new Color(hsbColor.getRed(), hsbColor.getBlue(), hsbColor.getGreen(), alpha); - - if (branch == null) { - drawShadow(g, suggestionX, suggestionY, true, (float) alpha / 255); - g.setColor(color); - fillCircle(g, suggestionX, suggestionY, stoneRadius); - } - - if (branch == null || (isBestMove && Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { - int strokeWidth = 1; - if (isBestMove != hasMaxWinrate) { - strokeWidth = 2; - g.setColor(isBestMove ? Color.RED : Color.BLUE); - g.setStroke(new BasicStroke(strokeWidth)); - } else { - g.setColor(color.darker()); - } - drawCircle(g, suggestionX, suggestionY, stoneRadius - strokeWidth / 2); - g.setStroke(new BasicStroke(1)); - } - - - if ((branch == null && (hasMaxWinrate || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats"))) || - (Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { - double roundedWinrate = Math.round(move.winrate * 10) / 10.0; - if (uiConfig.getBoolean("win-rate-always-black") && !Lizzie.board.getData().blackToPlay) { - roundedWinrate = 100.0 - roundedWinrate; - } - g.setColor(Color.BLACK); - if (branch != null && Lizzie.board.getData().blackToPlay) - g.setColor(Color.WHITE); - - String text; - if (Lizzie.config.handicapInsteadOfWinrate) { - text=String.format("%.2f", Lizzie.leelaz.winrateToHandicap(move.winrate)); - } else { - text=String.format("%.1f", roundedWinrate); - } - - drawString(g, suggestionX, suggestionY, LizzieFrame.OpenSansSemiboldBase, Font.PLAIN, text, stoneRadius, stoneRadius * 1.5, 1); - drawString(g, suggestionX, suggestionY + stoneRadius * 2 / 5, LizzieFrame.OpenSansRegularBase, getPlayoutsString(move.playouts), (float) (stoneRadius * 0.8), stoneRadius * 1.4); - } - } - } - + Stone stoneAtThisPoint = + branch == null + ? Lizzie.board.getStones()[Board.getIndex(i, j)] + : branch.data.stones[Board.getIndex(i, j)]; + + // don't write the move number if either: the move number is 0, or there will already be + // playout information written + if (moveNumberList[Board.getIndex(i, j)] > 0 + && !(branch != null + && Lizzie.frame.mouseHoverCoordinate != null + && i == Lizzie.frame.mouseHoverCoordinate[0] + && j == Lizzie.frame.mouseHoverCoordinate[1])) { + if (lastMove != null && i == lastMove[0] && j == lastMove[1]) + g.setColor( + Color.RED + .brighter()); // stoneAtThisPoint.isBlack() ? Color.RED.brighter() : + // Color.BLUE.brighter()); + else { + // Draw white letters on black stones nomally. + // But use black letters for showing black moves without stones. + boolean reverse = (moveNumberList[Board.getIndex(i, j)] > maxBranchMoves()); + g.setColor(stoneAtThisPoint.isBlack() ^ reverse ? Color.WHITE : Color.BLACK); + } + + String moveNumberString = moveNumberList[Board.getIndex(i, j)] + ""; + drawString( + g, + stoneX, + stoneY, + LizzieFrame.OpenSansRegularBase, + moveNumberString, + (float) (stoneRadius * 1.4), + (int) (stoneRadius * 1.4)); } + } } + } + + /** + * Draw all of Leelaz's suggestions as colored stones with winrate/playout statistics overlayed + */ + private void drawLeelazSuggestions(Graphics2D g) { + if (Lizzie.leelaz == null) return; + + final int MIN_ALPHA = 32; + final double HUE_SCALING_FACTOR = 3.0; + final double ALPHA_SCALING_FACTOR = 5.0; + final float GREEN_HUE = Color.RGBtoHSB(0, 1, 0, null)[0]; + final float CYAN_HUE = Color.RGBtoHSB(0, 1, 1, null)[0]; + + if (!bestMoves.isEmpty()) { + + int maxPlayouts = 0; + double maxWinrate = 0; + for (MoveData move : bestMoves) { + if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.winrate > maxWinrate) maxWinrate = move.winrate; + } - private void drawNextMoves(Graphics2D g) { - - List nexts = Lizzie.board.getHistory().getNexts(); + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + MoveData move = null; - for (int i = 0; i < nexts.size(); i++) { - int[] nextMove = nexts.get(i).getData().lastMove; - if (nextMove == null) continue; - if (Lizzie.board.getData().blackToPlay) { - g.setColor(Color.BLACK); - } else { - g.setColor(Color.WHITE); - } - int moveX = x + scaledMargin + squareLength * nextMove[0]; - int moveY = y + scaledMargin + squareLength * nextMove[1]; - if (i == 0) { - g.setStroke(new BasicStroke(3.0f)); + // this is inefficient but it looks better with shadows + for (MoveData m : bestMoves) { + int[] coord = Board.convertNameToCoordinates(m.coordinate); + // Handle passes + if (coord == null) { + continue; } - drawCircle(g, moveX, moveY, stoneRadius + 1); // slightly outside best move circle - if (i == 0) { - g.setStroke(new BasicStroke(1.0f)); + if (coord[0] == i && coord[1] == j) { + move = m; + break; } - } - } - - private void drawWoodenBoard(Graphics2D g) { - if (uiConfig.getBoolean("fancy-board")) { - if (cachedBoardImage == null) { - try { - cachedBoardImage = ImageIO.read(getClass().getResourceAsStream("/assets/board.png")); - } catch (IOException e) { - e.printStackTrace(); - } + } + + if (move == null) continue; + + boolean isBestMove = bestMoves.get(0) == move; + boolean hasMaxWinrate = move.winrate == maxWinrate; + + if (move.playouts == 0) // this actually can happen + continue; + + double percentPlayouts = (double) move.playouts / maxPlayouts; + + int[] coordinates = Board.convertNameToCoordinates(move.coordinate); + int suggestionX = x + scaledMargin + squareLength * coordinates[0]; + int suggestionY = y + scaledMargin + squareLength * coordinates[1]; + + // 0 = Reddest hue + float hue = + isBestMove + ? CYAN_HUE + : (float) + (-GREEN_HUE + * Math.max(0, Math.log(percentPlayouts) / HUE_SCALING_FACTOR + 1)); + float saturation = 0.75f; // saturation + float brightness = 0.85f; // brightness + int alpha = + (int) + (MIN_ALPHA + + (maxAlpha - MIN_ALPHA) + * Math.max(0, Math.log(percentPlayouts) / ALPHA_SCALING_FACTOR + 1)); + // if (uiConfig.getBoolean("shadows-enabled")) + // alpha = 255; + + Color hsbColor = Color.getHSBColor(hue, saturation, brightness); + Color color = + new Color(hsbColor.getRed(), hsbColor.getBlue(), hsbColor.getGreen(), alpha); + + if (branch == null) { + drawShadow(g, suggestionX, suggestionY, true, (float) alpha / 255); + g.setColor(color); + fillCircle(g, suggestionX, suggestionY, stoneRadius); + } + + if (branch == null + || (isBestMove + && Lizzie.frame.mouseHoverCoordinate != null + && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] + && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { + int strokeWidth = 1; + if (isBestMove != hasMaxWinrate) { + strokeWidth = 2; + g.setColor(isBestMove ? Color.RED : Color.BLUE); + g.setStroke(new BasicStroke(strokeWidth)); + } else { + g.setColor(color.darker()); } - - int shadowRadius = (int) (boardLength * MARGIN / 6); - drawTextureImage(g, cachedBoardImage, x - 2 * shadowRadius, y - 2 * shadowRadius, boardLength + 4 * shadowRadius, boardLength + 4 * shadowRadius); - - g.setStroke(new BasicStroke(shadowRadius * 2)); - // draw border - g.setColor(new Color(0, 0, 0, 50)); - g.drawRect(x - shadowRadius, y - shadowRadius, boardLength + 2 * shadowRadius, boardLength + 2 * shadowRadius); + drawCircle(g, suggestionX, suggestionY, stoneRadius - strokeWidth / 2); g.setStroke(new BasicStroke(1)); - - } else { - JSONArray boardColor = uiConfig.getJSONArray("board-color"); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - g.setColor(new Color(boardColor.getInt(0), boardColor.getInt(1), boardColor.getInt(2))); - g.fillRect(x, y, boardLength, boardLength); - } - } - - /** - * Calculates the lengths and pixel margins from a given boardLength. - * - * @param boardLength go board's length in pixels; must be boardLength >= BOARD_SIZE - 1 - * @return an array containing the three outputs: new boardLength, scaledMargin, availableLength - */ - private int[] calculatePixelMargins(int boardLength) { - //boardLength -= boardLength*MARGIN/3; // account for the shadows we will draw around the edge of the board -// if (boardLength < Board.BOARD_SIZE - 1) -// throw new IllegalArgumentException("boardLength may not be less than " + (Board.BOARD_SIZE - 1) + ", but was " + boardLength); - - int scaledMargin; - int availableLength; - - // decrease boardLength until the availableLength will result in square board intersections - double margin = showCoordinates() ? MARGIN_WITH_COORDINATES : MARGIN; - boardLength++; - do { - boardLength--; - scaledMargin = (int) (margin * boardLength); - availableLength = boardLength - 2 * scaledMargin; - } - while (!((availableLength - 1) % (Board.BOARD_SIZE - 1) == 0)); - // this will be true if BOARD_SIZE - 1 square intersections, plus one line, will fit - - return new int[]{boardLength, scaledMargin, availableLength}; - } - - private void drawShadow(Graphics2D g, int centerX, int centerY, boolean isGhost) { - drawShadow(g, centerX, centerY, isGhost, 1); - } - - private void drawShadow(Graphics2D g, int centerX, int centerY, boolean isGhost, float shadowStrength) { - if (!uiConfig.getBoolean("shadows-enabled")) - return; - - final int shadowSize = (int) (stoneRadius * 0.3 * uiConfig.getInt("shadow-size") / 100); - final int fartherShadowSize = (int) (stoneRadius * 0.17 * uiConfig.getInt("shadow-size") / 100); - - - final Paint TOP_GRADIENT_PAINT; - final Paint LOWER_RIGHT_GRADIENT_PAINT; - - if (isGhost) { - TOP_GRADIENT_PAINT = new RadialGradientPaint(new Point2D.Float(centerX, centerY), - stoneRadius + shadowSize, new float[]{((float) stoneRadius / (stoneRadius + shadowSize)) - 0.0001f, ((float) stoneRadius / (stoneRadius + shadowSize)), 1.0f}, new Color[]{ - new Color(0, 0, 0, 0), new Color(50, 50, 50, (int) (120 * shadowStrength)), new Color(0, 0, 0, 0) - }); - - LOWER_RIGHT_GRADIENT_PAINT = new RadialGradientPaint(new Point2D.Float(centerX + shadowSize * 2 / 3, centerY + shadowSize * 2 / 3), - stoneRadius + fartherShadowSize, new float[]{0.6f, 1.0f}, new Color[]{ - new Color(0, 0, 0, 180), new Color(0, 0, 0, 0) - }); - } else { - TOP_GRADIENT_PAINT = new RadialGradientPaint(new Point2D.Float(centerX, centerY), - stoneRadius + shadowSize, new float[]{0.3f, 1.0f}, new Color[]{ - new Color(50, 50, 50, 150), new Color(0, 0, 0, 0) - }); - LOWER_RIGHT_GRADIENT_PAINT = new RadialGradientPaint(new Point2D.Float(centerX + shadowSize, centerY + shadowSize), - stoneRadius + fartherShadowSize, new float[]{0.6f, 1.0f}, new Color[]{ - new Color(0, 0, 0, 140), new Color(0, 0, 0, 0) - }); - } - - final Paint originalPaint = g.getPaint(); - - g.setPaint(TOP_GRADIENT_PAINT); - fillCircle(g, centerX, centerY, stoneRadius + shadowSize); - if (!isGhost) { - g.setPaint(LOWER_RIGHT_GRADIENT_PAINT); - fillCircle(g, centerX + shadowSize, centerY + shadowSize, stoneRadius + fartherShadowSize); - } - g.setPaint(originalPaint); - } - - /** - * Draws a stone centered at (centerX, centerY) - */ - private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int centerY, Stone color, int x, int y) { -// g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, -// RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - // if no shadow graphics is supplied, just draw onto the same graphics - if (gShadow == null) - gShadow = g; - - if (color.isBlack() || color.isWhite()) { - boolean isBlack = color.isBlack(); - boolean isGhost = (color == Stone.BLACK_GHOST || color == Stone.WHITE_GHOST); - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, isGhost); - int size = stoneRadius * 2 + 1; - g.drawImage(getScaleStone(isBlack, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(isBlack ? (isGhost ? new Color(0, 0, 0) : Color.BLACK) : (isGhost ? new Color(255, 255, 255) : Color.WHITE)); - fillCircle(g, centerX, centerY, stoneRadius); - if (!isBlack) { - g.setColor(isGhost ? new Color(0, 0, 0) : Color.BLACK); - drawCircle(g, centerX, centerY, stoneRadius); - } + } + + if ((branch == null + && (hasMaxWinrate + || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats"))) + || (Lizzie.frame.mouseHoverCoordinate != null + && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] + && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { + double roundedWinrate = Math.round(move.winrate * 10) / 10.0; + if (uiConfig.getBoolean("win-rate-always-black") + && !Lizzie.board.getData().blackToPlay) { + roundedWinrate = 100.0 - roundedWinrate; } - } - } + g.setColor(Color.BLACK); + if (branch != null && Lizzie.board.getData().blackToPlay) g.setColor(Color.WHITE); - /** - * Get scaled stone, if cached then return cached - */ - private BufferedImage getScaleStone(boolean isBlack, int size) { - BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; - if (stone == null) { - stone = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - String imgPath = isBlack ? "/assets/black0.png" : "/assets/white0.png"; - Image img = null; - try { - img = ImageIO.read(getClass().getResourceAsStream(imgPath)); - } catch (IOException e) { - e.printStackTrace(); - } - Graphics2D g2 = stone.createGraphics(); - g2.drawImage(img.getScaledInstance(size, size, java.awt.Image.SCALE_SMOOTH), 0, 0, null); - g2.dispose(); - if (isBlack) { - cachedBlackStoneImage = stone; + String text; + if (Lizzie.config.handicapInsteadOfWinrate) { + text = String.format("%.2f", Lizzie.leelaz.winrateToHandicap(move.winrate)); } else { - cachedWhiteStoneImage = stone; + text = String.format("%.1f", roundedWinrate); } - } - return stone; - } - public BufferedImage getWallpaper() { - if (cachedWallpaperImage == null) { - try { - cachedWallpaperImage = ImageIO.read(getClass().getResourceAsStream("/assets/background.jpg")); - } catch (IOException e) { - e.printStackTrace(); + drawString( + g, + suggestionX, + suggestionY, + LizzieFrame.OpenSansSemiboldBase, + Font.PLAIN, + text, + stoneRadius, + stoneRadius * 1.5, + 1); + drawString( + g, + suggestionX, + suggestionY + stoneRadius * 2 / 5, + LizzieFrame.OpenSansRegularBase, + getPlayoutsString(move.playouts), + (float) (stoneRadius * 0.8), + stoneRadius * 1.4); + } } } - return cachedWallpaperImage; - } - - /** - * Draw scale smooth image, enhanced display quality (Not use, for future) - * This function use the traditional Image.getScaledInstance() method - * to provide the nice quality, but the performance is poor. - * Recommended for use in a few drawings - */ -// public void drawScaleSmoothImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height, ImageObserver observer) { -// BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); -// Graphics2D g2 = newstone.createGraphics(); -// g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, observer); -// g2.dispose(); -// g.drawImage(newstone, x, y, width, height, observer); -// } - - /** - * Draw scale smooth image, enhanced display quality (Not use, for future) - * This functions use a multi-step approach to prevent the information loss - * and produces a much higher quality that is close to the Image.getScaledInstance() - * and faster than Image.getScaledInstance() method. - */ -// public void drawScaleImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height, ImageObserver observer) { -// BufferedImage newstone = (BufferedImage)img; -// int w = img.getWidth(); -// int h = img.getHeight(); -// do { -// if (w > width) { -// w /= 2; -// if (w < width) { -// w = width; -// } -// } -// if (h > height) { -// h /= 2; -// if (h < height) { -// h = height; -// } -// } -// BufferedImage tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); -// Graphics2D g2 = tmp.createGraphics(); -// g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); -// g2.drawImage(newstone, 0, 0, w, h, null); -// g2.dispose(); -// newstone = tmp; -// } -// while (w != width || h != height); -// g.drawImage(newstone, x, y, width, height, observer); -// } - - /** - * Draw texture image - */ - public void drawTextureImage(Graphics2D g, BufferedImage img, int x, int y, int width, int height) { - TexturePaint paint = new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight())); - g.setPaint(paint); - g.fill(new Rectangle(x, y, width, height)); } + } - /** - * Fills in a circle centered at (centerX, centerY) with radius $radius$ - */ - private void fillCircle(Graphics2D g, int centerX, int centerY, int radius) { - g.fillOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1); - } + private void drawNextMoves(Graphics2D g) { - /** - * Draws the outline of a circle centered at (centerX, centerY) with radius $radius$ - */ - private void drawCircle(Graphics2D g, int centerX, int centerY, int radius) { - g.drawOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1); - } - - /** - * Draws a string centered at (x, y) of font $fontString$, whose contents are $string$. - * The maximum/default fontsize will be $maximumFontHeight$, and the length of the drawn string will be at most maximumFontWidth. - * The resulting actual size depends on the length of $string$. - * aboveOrBelow is a param that lets you set: - * aboveOrBelow = -1 -> y is the top of the string - * aboveOrBelow = 0 -> y is the vertical center of the string - * aboveOrBelow = 1 -> y is the bottom of the string - */ - private void drawString(Graphics2D g, int x, int y, Font fontBase, int style, String string, float maximumFontHeight, double maximumFontWidth, int aboveOrBelow) { - - Font font = makeFont(fontBase, style); - - // set maximum size of font - font = font.deriveFont((float) (font.getSize2D() * maximumFontWidth / g.getFontMetrics(font).stringWidth(string))); - font = font.deriveFont(Math.min(maximumFontHeight, font.getSize())); - g.setFont(font); - - FontMetrics metrics = g.getFontMetrics(font); - - int height = metrics.getAscent() - metrics.getDescent(); - int verticalOffset; - switch (aboveOrBelow) { - case -1: - verticalOffset = height / 2; - break; - - case 1: - verticalOffset = -height / 2; - break; - - default: - verticalOffset = 0; - } - // bounding box for debugging - // g.drawRect(x-(int)maximumFontWidth/2, y - height/2 + verticalOffset, (int)maximumFontWidth, height+verticalOffset ); - g.drawString(string, x - metrics.stringWidth(string) / 2, y + height / 2 + verticalOffset); - } - - private void drawString(Graphics2D g, int x, int y, Font fontBase, String string, float maximumFontHeight, double maximumFontWidth) { - drawString(g, x, y, fontBase, Font.PLAIN, string, maximumFontHeight, maximumFontWidth, 0); - } + List nexts = Lizzie.board.getHistory().getNexts(); - /** - * @return a font with kerning enabled - */ - private Font makeFont(Font fontBase, int style) { - Font font = fontBase.deriveFont(style, 100); - Map atts = new HashMap<>(); - atts.put(TextAttribute.KERNING, TextAttribute.KERNING_ON); - return font.deriveFont(atts); + for (int i = 0; i < nexts.size(); i++) { + int[] nextMove = nexts.get(i).getData().lastMove; + if (nextMove == null) continue; + if (Lizzie.board.getData().blackToPlay) { + g.setColor(Color.BLACK); + } else { + g.setColor(Color.WHITE); + } + int moveX = x + scaledMargin + squareLength * nextMove[0]; + int moveY = y + scaledMargin + squareLength * nextMove[1]; + if (i == 0) { + g.setStroke(new BasicStroke(3.0f)); + } + drawCircle(g, moveX, moveY, stoneRadius + 1); // slightly outside best move circle + if (i == 0) { + g.setStroke(new BasicStroke(1.0f)); + } } + } - - /** - * @return a shorter, rounded string version of playouts. e.g. 345 -> 345, 1265 -> 1.3k, 44556 -> 45k, 133523 -> 134k, 1234567 -> 1.2m - */ - private String getPlayoutsString(int playouts) { - if (playouts >= 1_000_000) { - double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567 - return Math.round(playoutsDouble) / 10.0 + "m"; - } else if (playouts >= 10_000) { - double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265 - return Math.round(playoutsDouble) + "k"; - } else if (playouts >= 1_000) { - double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65 - return Math.round(playoutsDouble) / 10.0 + "k"; - } else { - return String.valueOf(playouts); + private void drawWoodenBoard(Graphics2D g) { + if (uiConfig.getBoolean("fancy-board")) { + if (cachedBoardImage == null) { + try { + cachedBoardImage = ImageIO.read(getClass().getResourceAsStream("/assets/board.png")); + } catch (IOException e) { + e.printStackTrace(); } - } - - - private int[] calculatePixelMargins() { - return calculatePixelMargins(boardLength); - } + } - /** - * Set the location to render the board - * - * @param x x coordinate - * @param y y coordinate - */ - public void setLocation(int x, int y) { - this.x = x; - this.y = y; + int shadowRadius = (int) (boardLength * MARGIN / 6); + drawTextureImage( + g, + cachedBoardImage, + x - 2 * shadowRadius, + y - 2 * shadowRadius, + boardLength + 4 * shadowRadius, + boardLength + 4 * shadowRadius); + + g.setStroke(new BasicStroke(shadowRadius * 2)); + // draw border + g.setColor(new Color(0, 0, 0, 50)); + g.drawRect( + x - shadowRadius, + y - shadowRadius, + boardLength + 2 * shadowRadius, + boardLength + 2 * shadowRadius); + g.setStroke(new BasicStroke(1)); + + } else { + JSONArray boardColor = uiConfig.getJSONArray("board-color"); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g.setColor(new Color(boardColor.getInt(0), boardColor.getInt(1), boardColor.getInt(2))); + g.fillRect(x, y, boardLength, boardLength); } - - public Point getLocation() { - return new Point(x, y); + } + + /** + * Calculates the lengths and pixel margins from a given boardLength. + * + * @param boardLength go board's length in pixels; must be boardLength >= BOARD_SIZE - 1 + * @return an array containing the three outputs: new boardLength, scaledMargin, availableLength + */ + private int[] calculatePixelMargins(int boardLength) { + // boardLength -= boardLength*MARGIN/3; // account for the shadows we will draw around the edge + // of the board + // if (boardLength < Board.BOARD_SIZE - 1) + // throw new IllegalArgumentException("boardLength may not be less than " + + // (Board.BOARD_SIZE - 1) + ", but was " + boardLength); + + int scaledMargin; + int availableLength; + + // decrease boardLength until the availableLength will result in square board intersections + double margin = showCoordinates() ? MARGIN_WITH_COORDINATES : MARGIN; + boardLength++; + do { + boardLength--; + scaledMargin = (int) (margin * boardLength); + availableLength = boardLength - 2 * scaledMargin; + } while (!((availableLength - 1) % (Board.BOARD_SIZE - 1) == 0)); + // this will be true if BOARD_SIZE - 1 square intersections, plus one line, will fit + + return new int[] {boardLength, scaledMargin, availableLength}; + } + + private void drawShadow(Graphics2D g, int centerX, int centerY, boolean isGhost) { + drawShadow(g, centerX, centerY, isGhost, 1); + } + + private void drawShadow( + Graphics2D g, int centerX, int centerY, boolean isGhost, float shadowStrength) { + if (!uiConfig.getBoolean("shadows-enabled")) return; + + final int shadowSize = (int) (stoneRadius * 0.3 * uiConfig.getInt("shadow-size") / 100); + final int fartherShadowSize = (int) (stoneRadius * 0.17 * uiConfig.getInt("shadow-size") / 100); + + final Paint TOP_GRADIENT_PAINT; + final Paint LOWER_RIGHT_GRADIENT_PAINT; + + if (isGhost) { + TOP_GRADIENT_PAINT = + new RadialGradientPaint( + new Point2D.Float(centerX, centerY), + stoneRadius + shadowSize, + new float[] { + ((float) stoneRadius / (stoneRadius + shadowSize)) - 0.0001f, + ((float) stoneRadius / (stoneRadius + shadowSize)), + 1.0f + }, + new Color[] { + new Color(0, 0, 0, 0), + new Color(50, 50, 50, (int) (120 * shadowStrength)), + new Color(0, 0, 0, 0) + }); + + LOWER_RIGHT_GRADIENT_PAINT = + new RadialGradientPaint( + new Point2D.Float(centerX + shadowSize * 2 / 3, centerY + shadowSize * 2 / 3), + stoneRadius + fartherShadowSize, + new float[] {0.6f, 1.0f}, + new Color[] {new Color(0, 0, 0, 180), new Color(0, 0, 0, 0)}); + } else { + TOP_GRADIENT_PAINT = + new RadialGradientPaint( + new Point2D.Float(centerX, centerY), + stoneRadius + shadowSize, + new float[] {0.3f, 1.0f}, + new Color[] {new Color(50, 50, 50, 150), new Color(0, 0, 0, 0)}); + LOWER_RIGHT_GRADIENT_PAINT = + new RadialGradientPaint( + new Point2D.Float(centerX + shadowSize, centerY + shadowSize), + stoneRadius + fartherShadowSize, + new float[] {0.6f, 1.0f}, + new Color[] {new Color(0, 0, 0, 140), new Color(0, 0, 0, 0)}); } - /** - * Set the maximum boardLength to render the board - * - * @param boardLength the boardLength of the board - */ - public void setBoardLength(int boardLength) { - this.boardLength = boardLength; - } + final Paint originalPaint = g.getPaint(); - /** - * @return the actual board length, including the shadows drawn at the edge of the wooden board - */ - public int getActualBoardLength() { - return (int) (boardLength * (1 + MARGIN / 3)); + g.setPaint(TOP_GRADIENT_PAINT); + fillCircle(g, centerX, centerY, stoneRadius + shadowSize); + if (!isGhost) { + g.setPaint(LOWER_RIGHT_GRADIENT_PAINT); + fillCircle(g, centerX + shadowSize, centerY + shadowSize, stoneRadius + fartherShadowSize); } - - /** - * Converts a location on the screen to a location on the board - * - * @param x x pixel coordinate - * @param y y pixel coordinate - * @return if there is a valid coordinate, an array (x, y) where x and y are between 0 and BOARD_SIZE - 1. Otherwise, returns null - */ - public int[] convertScreenToCoordinates(int x, int y) { - int marginLength; // the pixel width of the margins - int boardLengthWithoutMargins; // the pixel width of the game board without margins - - // calculate a good set of boardLength, scaledMargin, and boardLengthWithoutMargins to use - int[] calculatedPixelMargins = calculatePixelMargins(); - setBoardLength(calculatedPixelMargins[0]); - marginLength = calculatedPixelMargins[1]; - boardLengthWithoutMargins = calculatedPixelMargins[2]; - - int squareSize = calculateSquareLength(boardLengthWithoutMargins); - - // transform the pixel coordinates to board coordinates - x = (x - this.x - marginLength + squareSize / 2) / squareSize; - y = (y - this.y - marginLength + squareSize / 2) / squareSize; - - // return these values if they are valid board coordinates - if (Board.isValid(x, y)) - return new int[]{x, y}; - else - return null; + g.setPaint(originalPaint); + } + + /** Draws a stone centered at (centerX, centerY) */ + private void drawStone( + Graphics2D g, Graphics2D gShadow, int centerX, int centerY, Stone color, int x, int y) { + // g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, + // RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + g.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // if no shadow graphics is supplied, just draw onto the same graphics + if (gShadow == null) gShadow = g; + + if (color.isBlack() || color.isWhite()) { + boolean isBlack = color.isBlack(); + boolean isGhost = (color == Stone.BLACK_GHOST || color == Stone.WHITE_GHOST); + if (uiConfig.getBoolean("fancy-stones")) { + drawShadow(gShadow, centerX, centerY, isGhost); + int size = stoneRadius * 2 + 1; + g.drawImage( + getScaleStone(isBlack, size), + centerX - stoneRadius, + centerY - stoneRadius, + size, + size, + null); + } else { + drawShadow(gShadow, centerX, centerY, true); + g.setColor( + isBlack + ? (isGhost ? new Color(0, 0, 0) : Color.BLACK) + : (isGhost ? new Color(255, 255, 255) : Color.WHITE)); + fillCircle(g, centerX, centerY, stoneRadius); + if (!isBlack) { + g.setColor(isGhost ? new Color(0, 0, 0) : Color.BLACK); + drawCircle(g, centerX, centerY, stoneRadius); + } + } } - - /** - * Calculate the boardLength of each intersection square - * - * @param availableLength the pixel board length of the game board without margins - * @return the board length of each intersection square - */ - private int calculateSquareLength(int availableLength) { - return availableLength / (Board.BOARD_SIZE - 1); + } + + /** Get scaled stone, if cached then return cached */ + private BufferedImage getScaleStone(boolean isBlack, int size) { + BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; + if (stone == null) { + stone = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + String imgPath = isBlack ? "/assets/black0.png" : "/assets/white0.png"; + Image img = null; + try { + img = ImageIO.read(getClass().getResourceAsStream(imgPath)); + } catch (IOException e) { + e.printStackTrace(); + } + Graphics2D g2 = stone.createGraphics(); + g2.drawImage(img.getScaledInstance(size, size, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g2.dispose(); + if (isBlack) { + cachedBlackStoneImage = stone; + } else { + cachedWhiteStoneImage = stone; + } } - - private boolean isShowingRawBoard() { - return (displayedBranchLength == SHOW_RAW_BOARD || displayedBranchLength == 0); + return stone; + } + + public BufferedImage getWallpaper() { + if (cachedWallpaperImage == null) { + try { + cachedWallpaperImage = + ImageIO.read(getClass().getResourceAsStream("/assets/background.jpg")); + } catch (IOException e) { + e.printStackTrace(); + } } - - private int maxBranchMoves() { - switch (displayedBranchLength) { - case SHOW_NORMAL_BOARD: - return Integer.MAX_VALUE; - case SHOW_RAW_BOARD: - return -1; - default: - return displayedBranchLength; - } + return cachedWallpaperImage; + } + + /** + * Draw scale smooth image, enhanced display quality (Not use, for future) This function use the + * traditional Image.getScaledInstance() method to provide the nice quality, but the performance + * is poor. Recommended for use in a few drawings + */ + // public void drawScaleSmoothImage(Graphics2D g, BufferedImage img, int x, int y, int width, + // int height, ImageObserver observer) { + // BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + // Graphics2D g2 = newstone.createGraphics(); + // g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, + // observer); + // g2.dispose(); + // g.drawImage(newstone, x, y, width, height, observer); + // } + + /** + * Draw scale smooth image, enhanced display quality (Not use, for future) This functions use a + * multi-step approach to prevent the information loss and produces a much higher quality that is + * close to the Image.getScaledInstance() and faster than Image.getScaledInstance() method. + */ + // public void drawScaleImage(Graphics2D g, BufferedImage img, int x, int y, int width, int + // height, ImageObserver observer) { + // BufferedImage newstone = (BufferedImage)img; + // int w = img.getWidth(); + // int h = img.getHeight(); + // do { + // if (w > width) { + // w /= 2; + // if (w < width) { + // w = width; + // } + // } + // if (h > height) { + // h /= 2; + // if (h < height) { + // h = height; + // } + // } + // BufferedImage tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + // Graphics2D g2 = tmp.createGraphics(); + // g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + // RenderingHints.VALUE_INTERPOLATION_BICUBIC); + // g2.drawImage(newstone, 0, 0, w, h, null); + // g2.dispose(); + // newstone = tmp; + // } + // while (w != width || h != height); + // g.drawImage(newstone, x, y, width, height, observer); + // } + + /** Draw texture image */ + public void drawTextureImage( + Graphics2D g, BufferedImage img, int x, int y, int width, int height) { + TexturePaint paint = + new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight())); + g.setPaint(paint); + g.fill(new Rectangle(x, y, width, height)); + } + + /** Fills in a circle centered at (centerX, centerY) with radius $radius$ */ + private void fillCircle(Graphics2D g, int centerX, int centerY, int radius) { + g.fillOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1); + } + + /** Draws the outline of a circle centered at (centerX, centerY) with radius $radius$ */ + private void drawCircle(Graphics2D g, int centerX, int centerY, int radius) { + g.drawOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1); + } + + /** + * Draws a string centered at (x, y) of font $fontString$, whose contents are $string$. The + * maximum/default fontsize will be $maximumFontHeight$, and the length of the drawn string will + * be at most maximumFontWidth. The resulting actual size depends on the length of $string$. + * aboveOrBelow is a param that lets you set: aboveOrBelow = -1 -> y is the top of the string + * aboveOrBelow = 0 -> y is the vertical center of the string aboveOrBelow = 1 -> y is the bottom + * of the string + */ + private void drawString( + Graphics2D g, + int x, + int y, + Font fontBase, + int style, + String string, + float maximumFontHeight, + double maximumFontWidth, + int aboveOrBelow) { + + Font font = makeFont(fontBase, style); + + // set maximum size of font + font = + font.deriveFont( + (float) + (font.getSize2D() * maximumFontWidth / g.getFontMetrics(font).stringWidth(string))); + font = font.deriveFont(Math.min(maximumFontHeight, font.getSize())); + g.setFont(font); + + FontMetrics metrics = g.getFontMetrics(font); + + int height = metrics.getAscent() - metrics.getDescent(); + int verticalOffset; + switch (aboveOrBelow) { + case -1: + verticalOffset = height / 2; + break; + + case 1: + verticalOffset = -height / 2; + break; + + default: + verticalOffset = 0; } - - public boolean isShowingBranch() { - return showingBranch; + // bounding box for debugging + // g.drawRect(x-(int)maximumFontWidth/2, y - height/2 + verticalOffset, (int)maximumFontWidth, + // height+verticalOffset ); + g.drawString(string, x - metrics.stringWidth(string) / 2, y + height / 2 + verticalOffset); + } + + private void drawString( + Graphics2D g, + int x, + int y, + Font fontBase, + String string, + float maximumFontHeight, + double maximumFontWidth) { + drawString(g, x, y, fontBase, Font.PLAIN, string, maximumFontHeight, maximumFontWidth, 0); + } + + /** @return a font with kerning enabled */ + private Font makeFont(Font fontBase, int style) { + Font font = fontBase.deriveFont(style, 100); + Map atts = new HashMap<>(); + atts.put(TextAttribute.KERNING, TextAttribute.KERNING_ON); + return font.deriveFont(atts); + } + + /** + * @return a shorter, rounded string version of playouts. e.g. 345 -> 345, 1265 -> 1.3k, 44556 -> + * 45k, 133523 -> 134k, 1234567 -> 1.2m + */ + private String getPlayoutsString(int playouts) { + if (playouts >= 1_000_000) { + double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567 + return Math.round(playoutsDouble) / 10.0 + "m"; + } else if (playouts >= 10_000) { + double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265 + return Math.round(playoutsDouble) + "k"; + } else if (playouts >= 1_000) { + double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65 + return Math.round(playoutsDouble) / 10.0 + "k"; + } else { + return String.valueOf(playouts); } - - public void setDisplayedBranchLength(int n) { - displayedBranchLength = n; + } + + private int[] calculatePixelMargins() { + return calculatePixelMargins(boardLength); + } + + /** + * Set the location to render the board + * + * @param x x coordinate + * @param y y coordinate + */ + public void setLocation(int x, int y) { + this.x = x; + this.y = y; + } + + public Point getLocation() { + return new Point(x, y); + } + + /** + * Set the maximum boardLength to render the board + * + * @param boardLength the boardLength of the board + */ + public void setBoardLength(int boardLength) { + this.boardLength = boardLength; + } + + /** + * @return the actual board length, including the shadows drawn at the edge of the wooden board + */ + public int getActualBoardLength() { + return (int) (boardLength * (1 + MARGIN / 3)); + } + + /** + * Converts a location on the screen to a location on the board + * + * @param x x pixel coordinate + * @param y y pixel coordinate + * @return if there is a valid coordinate, an array (x, y) where x and y are between 0 and + * BOARD_SIZE - 1. Otherwise, returns null + */ + public int[] convertScreenToCoordinates(int x, int y) { + int marginLength; // the pixel width of the margins + int boardLengthWithoutMargins; // the pixel width of the game board without margins + + // calculate a good set of boardLength, scaledMargin, and boardLengthWithoutMargins to use + int[] calculatedPixelMargins = calculatePixelMargins(); + setBoardLength(calculatedPixelMargins[0]); + marginLength = calculatedPixelMargins[1]; + boardLengthWithoutMargins = calculatedPixelMargins[2]; + + int squareSize = calculateSquareLength(boardLengthWithoutMargins); + + // transform the pixel coordinates to board coordinates + x = (x - this.x - marginLength + squareSize / 2) / squareSize; + y = (y - this.y - marginLength + squareSize / 2) / squareSize; + + // return these values if they are valid board coordinates + if (Board.isValid(x, y)) return new int[] {x, y}; + else return null; + } + + /** + * Calculate the boardLength of each intersection square + * + * @param availableLength the pixel board length of the game board without margins + * @return the board length of each intersection square + */ + private int calculateSquareLength(int availableLength) { + return availableLength / (Board.BOARD_SIZE - 1); + } + + private boolean isShowingRawBoard() { + return (displayedBranchLength == SHOW_RAW_BOARD || displayedBranchLength == 0); + } + + private int maxBranchMoves() { + switch (displayedBranchLength) { + case SHOW_NORMAL_BOARD: + return Integer.MAX_VALUE; + case SHOW_RAW_BOARD: + return -1; + default: + return displayedBranchLength; } - - public boolean incrementDisplayedBranchLength(int n) { - switch (displayedBranchLength) { - case SHOW_NORMAL_BOARD: - case SHOW_RAW_BOARD: - return false; - default: - // force nonnegative - displayedBranchLength = Math.max(0, displayedBranchLength + n); - return true; - } + } + + public boolean isShowingBranch() { + return showingBranch; + } + + public void setDisplayedBranchLength(int n) { + displayedBranchLength = n; + } + + public boolean incrementDisplayedBranchLength(int n) { + switch (displayedBranchLength) { + case SHOW_NORMAL_BOARD: + case SHOW_RAW_BOARD: + return false; + default: + // force nonnegative + displayedBranchLength = Math.max(0, displayedBranchLength + n); + return true; } + } - public boolean isInside(int x1, int y1) { - return (x <= x1 && x1 < x + boardLength && y <= y1 && y1 < y + boardLength); - } + public boolean isInside(int x1, int y1) { + return (x <= x1 && x1 < x + boardLength && y <= y1 && y1 < y + boardLength); + } - private boolean showCoordinates() { - return isMainBoard && Lizzie.frame.showCoordinates; - } + private boolean showCoordinates() { + return isMainBoard && Lizzie.frame.showCoordinates; + } - public void increaseMaxAlpha(int k) { - maxAlpha = Math.min(maxAlpha + k, 255); - uiPersist.put("max-alpha", maxAlpha); - } + public void increaseMaxAlpha(int k) { + maxAlpha = Math.min(maxAlpha + k, 255); + uiPersist.put("max-alpha", maxAlpha); + } } diff --git a/src/main/java/featurecat/lizzie/gui/GameInfoDialog.java b/src/main/java/featurecat/lizzie/gui/GameInfoDialog.java index 3c5c1a655..871a4cfa5 100644 --- a/src/main/java/featurecat/lizzie/gui/GameInfoDialog.java +++ b/src/main/java/featurecat/lizzie/gui/GameInfoDialog.java @@ -5,127 +5,135 @@ package featurecat.lizzie.gui; import featurecat.lizzie.analysis.GameInfo; - import java.awt.*; import java.text.DecimalFormat; import javax.swing.*; import javax.swing.border.*; -/** - * @author unknown - */ +/** @author unknown */ public class GameInfoDialog extends JDialog { - // create formatters - public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); - public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); - - static { - FORMAT_HANDICAP.setMaximumIntegerDigits(1); - } - - private JPanel dialogPane = new JPanel(); - private JPanel contentPanel = new JPanel(); - private JPanel buttonBar = new JPanel(); - private JButton okButton = new JButton(); - - private JTextField textFieldBlack; - private JTextField textFieldWhite; - private JTextField textFieldKomi; - private JTextField textFieldHandicap; - - private GameInfo gameInfo; - - public GameInfoDialog() { - initComponents(); - } - - private void initComponents() { - setMinimumSize(new Dimension(100, 100)); - setResizable(false); - setTitle("Game Info"); - setModal(true); - - Container contentPane = getContentPane(); - contentPane.setLayout(new BorderLayout()); - - initDialogPane(contentPane); - - pack(); - setLocationRelativeTo(getOwner()); - } - - private void initDialogPane(Container contentPane) { - dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); - dialogPane.setLayout(new BorderLayout()); - - initContentPanel(); - initButtonBar(); - - contentPane.add(dialogPane, BorderLayout.CENTER); - } - - private void initContentPanel() { - GridLayout gridLayout = new GridLayout(4, 2, 4, 4); - contentPanel.setLayout(gridLayout); - - // editable - textFieldWhite = new JTextField(); - textFieldBlack = new JTextField(); - - // read-only - textFieldKomi = new JFormattedTextField(FORMAT_KOMI); - textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); - textFieldKomi.setEditable(false); - textFieldHandicap.setEditable(false); - - contentPanel.add(new JLabel("Black")); - contentPanel.add(textFieldBlack); - contentPanel.add(new JLabel("White")); - contentPanel.add(textFieldWhite); - contentPanel.add(new JLabel("Komi")); - contentPanel.add(textFieldKomi); - contentPanel.add(new JLabel("Handicap")); - contentPanel.add(textFieldHandicap); - - dialogPane.add(contentPanel, BorderLayout.CENTER); - } - - private void initButtonBar() { - buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); - buttonBar.setLayout(new GridBagLayout()); - ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 80}; - ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0}; - - //---- okButton ---- - okButton.setText("OK"); - okButton.addActionListener(e -> apply()); - - buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); - - dialogPane.add(buttonBar, BorderLayout.SOUTH); - } - - public void setGameInfo(GameInfo gameInfo) { - this.gameInfo = gameInfo; - - textFieldBlack.setText(gameInfo.getPlayerBlack()); - textFieldWhite.setText(gameInfo.getPlayerWhite()); - textFieldHandicap.setText(FORMAT_HANDICAP.format(gameInfo.getHandicap())); - textFieldKomi.setText(FORMAT_KOMI.format(gameInfo.getKomi())); - } - - public void apply() { - // validate data - String playerBlack = textFieldBlack.getText(); - String playerWhite = textFieldWhite.getText(); - - // apply new values - gameInfo.setPlayerBlack(playerBlack); - gameInfo.setPlayerWhite(playerWhite); - - // close window - setVisible(false); - } + // create formatters + public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); + public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); + + static { + FORMAT_HANDICAP.setMaximumIntegerDigits(1); + } + + private JPanel dialogPane = new JPanel(); + private JPanel contentPanel = new JPanel(); + private JPanel buttonBar = new JPanel(); + private JButton okButton = new JButton(); + + private JTextField textFieldBlack; + private JTextField textFieldWhite; + private JTextField textFieldKomi; + private JTextField textFieldHandicap; + + private GameInfo gameInfo; + + public GameInfoDialog() { + initComponents(); + } + + private void initComponents() { + setMinimumSize(new Dimension(100, 100)); + setResizable(false); + setTitle("Game Info"); + setModal(true); + + Container contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + initDialogPane(contentPane); + + pack(); + setLocationRelativeTo(getOwner()); + } + + private void initDialogPane(Container contentPane) { + dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); + dialogPane.setLayout(new BorderLayout()); + + initContentPanel(); + initButtonBar(); + + contentPane.add(dialogPane, BorderLayout.CENTER); + } + + private void initContentPanel() { + GridLayout gridLayout = new GridLayout(4, 2, 4, 4); + contentPanel.setLayout(gridLayout); + + // editable + textFieldWhite = new JTextField(); + textFieldBlack = new JTextField(); + + // read-only + textFieldKomi = new JFormattedTextField(FORMAT_KOMI); + textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); + textFieldKomi.setEditable(false); + textFieldHandicap.setEditable(false); + + contentPanel.add(new JLabel("Black")); + contentPanel.add(textFieldBlack); + contentPanel.add(new JLabel("White")); + contentPanel.add(textFieldWhite); + contentPanel.add(new JLabel("Komi")); + contentPanel.add(textFieldKomi); + contentPanel.add(new JLabel("Handicap")); + contentPanel.add(textFieldHandicap); + + dialogPane.add(contentPanel, BorderLayout.CENTER); + } + + private void initButtonBar() { + buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); + buttonBar.setLayout(new GridBagLayout()); + ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[] {0, 80}; + ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0}; + + // ---- okButton ---- + okButton.setText("OK"); + okButton.addActionListener(e -> apply()); + + buttonBar.add( + okButton, + new GridBagConstraints( + 1, + 0, + 1, + 1, + 0.0, + 0.0, + GridBagConstraints.CENTER, + GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), + 0, + 0)); + + dialogPane.add(buttonBar, BorderLayout.SOUTH); + } + + public void setGameInfo(GameInfo gameInfo) { + this.gameInfo = gameInfo; + + textFieldBlack.setText(gameInfo.getPlayerBlack()); + textFieldWhite.setText(gameInfo.getPlayerWhite()); + textFieldHandicap.setText(FORMAT_HANDICAP.format(gameInfo.getHandicap())); + textFieldKomi.setText(FORMAT_KOMI.format(gameInfo.getKomi())); + } + + public void apply() { + // validate data + String playerBlack = textFieldBlack.getText(); + String playerWhite = textFieldWhite.getText(); + + // apply new values + gameInfo.setPlayerBlack(playerBlack); + gameInfo.setPlayerWhite(playerWhite); + + // close window + setVisible(false); + } } diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index b7f765cd0..4bbcd035c 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -1,389 +1,387 @@ package featurecat.lizzie.gui; +import static java.awt.event.KeyEvent.*; + import featurecat.lizzie.Lizzie; import java.awt.event.*; -import static java.awt.event.KeyEvent.*; import javax.swing.*; public class Input implements MouseListener, KeyListener, MouseWheelListener, MouseMotionListener { - @Override - public void mouseClicked(MouseEvent e) {} - - @Override - public void mousePressed(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1) // left click - Lizzie.frame.onClicked(e.getX(), e.getY()); - else if (e.getButton() == MouseEvent.BUTTON3) // right click - undo(); + @Override + public void mouseClicked(MouseEvent e) {} + + @Override + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) // left click + Lizzie.frame.onClicked(e.getX(), e.getY()); + else if (e.getButton() == MouseEvent.BUTTON3) // right click + undo(); + } + + @Override + public void mouseReleased(MouseEvent e) {} + + @Override + public void mouseEntered(MouseEvent e) {} + + @Override + public void mouseExited(MouseEvent e) {} + + @Override + public void mouseDragged(MouseEvent e) { + Lizzie.frame.onMouseDragged(e.getX(), e.getY()); + } + + @Override + public void mouseMoved(MouseEvent e) { + Lizzie.frame.onMouseMoved(e.getX(), e.getY()); + } + + @Override + public void keyTyped(KeyEvent e) {} + + private void undo() { + undo(1); + } + + private void undo(int movesToAdvance) { + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } - - @Override - public void mouseReleased(MouseEvent e) {} - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - - @Override - public void mouseDragged(MouseEvent e) { - Lizzie.frame.onMouseDragged(e.getX(), e.getY()); + if (Lizzie.frame.incrementDisplayedBranchLength(-movesToAdvance)) { + return; } - @Override - public void mouseMoved(MouseEvent e) { - Lizzie.frame.onMouseMoved(e.getX(), e.getY()); + for (int i = 0; i < movesToAdvance; i++) Lizzie.board.previousMove(); + } + + private void undoToChildOfPreviousWithVariation() { + // Undo until the position just after the junction position. + // If we are already on such a position, we go to + // the junction position for convenience. + // Use cases: + // [Delete branch] Call this function and then deleteMove. + // [Go to junction] Call this function twice. + if (!Lizzie.board.undoToChildOfPreviousWithVariation()) Lizzie.board.previousMove(); + } + + private void redo() { + redo(1); + } + + private void redo(int movesToAdvance) { + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } - - @Override - public void keyTyped(KeyEvent e) {} - - private void undo() { - undo(1); + if (Lizzie.frame.incrementDisplayedBranchLength(movesToAdvance)) { + return; } - private void undo(int movesToAdvance) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; - } - if (Lizzie.frame.incrementDisplayedBranchLength(- movesToAdvance)) { - return; - } + for (int i = 0; i < movesToAdvance; i++) Lizzie.board.nextMove(); + } - for (int i = 0; i < movesToAdvance; i++) - Lizzie.board.previousMove(); + private void startRawBoard() { + if (!Lizzie.config.showRawBoard) { + Lizzie.frame.startRawBoard(); } - - private void undoToChildOfPreviousWithVariation() { - // Undo until the position just after the junction position. - // If we are already on such a position, we go to - // the junction position for convenience. - // Use cases: - // [Delete branch] Call this function and then deleteMove. - // [Go to junction] Call this function twice. - if (!Lizzie.board.undoToChildOfPreviousWithVariation()) - Lizzie.board.previousMove(); + Lizzie.config.showRawBoard = true; + } + + private void stopRawBoard() { + Lizzie.frame.stopRawBoard(); + Lizzie.config.showRawBoard = false; + } + + private void toggleHints() { + Lizzie.config.toggleShowBranch(); + Lizzie.config.showSubBoard = + Lizzie.config.showNextMoves = Lizzie.config.showBestMoves = Lizzie.config.showBranch; + } + + private void nextBranch() { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } + Lizzie.board.nextBranch(); + } - private void redo() { - redo(1); + private void previousBranch() { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } + Lizzie.board.previousBranch(); + } + + private void moveBranchUp() { + Lizzie.board.moveBranchUp(); + } + + private void moveBranchDown() { + Lizzie.board.moveBranchDown(); + } + + private void deleteMove() { + Lizzie.board.deleteMove(); + } + + private void deleteBranch() { + Lizzie.board.deleteBranch(); + } + + private boolean controlIsPressed(KeyEvent e) { + boolean mac = System.getProperty("os.name", "").toUpperCase().startsWith("MAC"); + return e.isControlDown() || (mac && e.isMetaDown()); + } + + private void toggleShowDynamicKomi() { + Lizzie.config.showDynamicKomi = !Lizzie.config.showDynamicKomi; + } + + @Override + public void keyPressed(KeyEvent e) { + // If any controls key is pressed, let's disable analysis mode. + // This is probably the user attempting to exit analysis mode. + boolean shouldDisableAnalysis = true; + + switch (e.getKeyCode()) { + case VK_RIGHT: + if (e.isShiftDown()) { + moveBranchDown(); + } else { + nextBranch(); + } + break; - private void redo(int movesToAdvance) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; + case VK_LEFT: + if (e.isShiftDown()) { + moveBranchUp(); + } else { + previousBranch(); } - if (Lizzie.frame.incrementDisplayedBranchLength(movesToAdvance)) { - return; + break; + + case VK_UP: + if (e.isShiftDown()) { + undoToChildOfPreviousWithVariation(); + } else if (controlIsPressed(e)) { + undo(10); + } else { + undo(); } + break; - for (int i = 0; i < movesToAdvance; i++) - Lizzie.board.nextMove(); - } - - private void startRawBoard() { - if (!Lizzie.config.showRawBoard) { - Lizzie.frame.startRawBoard(); + case VK_PAGE_DOWN: + if (controlIsPressed(e) && e.isShiftDown()) { + Lizzie.frame.increaseMaxAlpha(-5); + } else { + redo(10); } - Lizzie.config.showRawBoard = true; - } + break; - private void stopRawBoard() { - Lizzie.frame.stopRawBoard(); - Lizzie.config.showRawBoard = false; - } - - private void toggleHints() { - Lizzie.config.toggleShowBranch(); - Lizzie.config.showSubBoard = Lizzie.config.showNextMoves = Lizzie.config.showBestMoves - = Lizzie.config.showBranch; - } - - private void nextBranch() { - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; + case VK_DOWN: + if (controlIsPressed(e)) { + redo(10); + } else { + redo(); } - Lizzie.board.nextBranch(); - } - - private void previousBranch() { + break; + + case VK_N: + // stop the ponder + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + LizzieFrame.startNewGame(); + break; + case VK_SPACE: if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; + Lizzie.frame.isPlayingAgainstLeelaz = false; + Lizzie.leelaz.togglePonder(); // we must toggle twice for it to restart pondering + Lizzie.leelaz.isThinking = false; } - Lizzie.board.previousBranch(); - } + Lizzie.leelaz.togglePonder(); + break; + + case VK_P: + Lizzie.board.pass(); + break; + + case VK_COMMA: + if (!Lizzie.frame.playCurrentVariation()) Lizzie.frame.playBestMove(); + break; + + case VK_M: + Lizzie.config.toggleShowMoveNumber(); + break; + + case VK_F: + Lizzie.config.toggleShowNextMoves(); + break; + + case VK_H: + Lizzie.config.toggleHandicapInsteadOfWinrate(); + break; + + case VK_PAGE_UP: + if (controlIsPressed(e) && e.isShiftDown()) { + Lizzie.frame.increaseMaxAlpha(5); + } else { + undo(10); + } + break; + + case VK_I: + // stop the ponder + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + Lizzie.frame.editGameInfo(); + break; + case VK_S: + // stop the ponder + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + LizzieFrame.saveFile(); + break; + + case VK_O: + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + LizzieFrame.openFile(); + break; + + case VK_V: + if (controlIsPressed(e)) { + Lizzie.frame.pasteSgf(); + } else { + Lizzie.config.toggleShowBranch(); + } + break; + + case VK_HOME: + while (Lizzie.board.previousMove()) ; + break; + + case VK_END: + while (Lizzie.board.nextMove()) ; + break; + + case VK_X: + if (!Lizzie.frame.showControls) { + if (Lizzie.leelaz.isPondering()) { + wasPonderingWhenControlsShown = true; + Lizzie.leelaz.togglePonder(); + } else { + wasPonderingWhenControlsShown = false; + } + Lizzie.frame.drawControls(); + } + Lizzie.frame.showControls = true; + break; + + case VK_W: + Lizzie.config.toggleShowWinrate(); + break; + + case VK_G: + Lizzie.config.toggleShowVariationGraph(); + break; + + case VK_T: + Lizzie.config.toggleShowComment(); + break; + + case VK_C: + if (controlIsPressed(e)) { + Lizzie.frame.copySgf(); + } else { + Lizzie.frame.toggleCoordinates(); + } + break; + + case VK_ENTER: + if (!Lizzie.leelaz.isThinking) { + Lizzie.leelaz.sendCommand( + "time_settings 0 " + + Lizzie.config + .config + .getJSONObject("leelaz") + .getInt("max-game-thinking-time-seconds") + + " 1"); + Lizzie.frame.playerIsBlack = !Lizzie.board.getData().blackToPlay; + Lizzie.frame.isPlayingAgainstLeelaz = true; + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "B" : "W")); + } + break; + + case VK_DELETE: + case VK_BACK_SPACE: + if (e.isShiftDown()) { + deleteBranch(); + } else { + deleteMove(); + } + break; - private void moveBranchUp() { - Lizzie.board.moveBranchUp(); - } + case VK_Z: + if (e.isShiftDown()) { + toggleHints(); + } else { + startRawBoard(); + } + break; - private void moveBranchDown() { - Lizzie.board.moveBranchDown(); - } + case VK_A: + shouldDisableAnalysis = false; + Lizzie.board.toggleAnalysis(); + break; - private void deleteMove() { Lizzie.board.deleteMove(); } + case VK_PERIOD: + if (Lizzie.board.getHistory().getNext() == null) { + Lizzie.board.setScoreMode(!Lizzie.board.inScoreMode()); + } + break; - private void deleteBranch() { Lizzie.board.deleteBranch(); } + case VK_D: + toggleShowDynamicKomi(); + break; - private boolean controlIsPressed(KeyEvent e) { - boolean mac = System.getProperty("os.name", "").toUpperCase().startsWith("MAC"); - return e.isControlDown() || (mac && e.isMetaDown()); + default: + shouldDisableAnalysis = false; } - private void toggleShowDynamicKomi() { - Lizzie.config.showDynamicKomi = !Lizzie.config.showDynamicKomi; - } + if (shouldDisableAnalysis && Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); - @Override - public void keyPressed(KeyEvent e) { - // If any controls key is pressed, let's disable analysis mode. - // This is probably the user attempting to exit analysis mode. - boolean shouldDisableAnalysis = true; - - switch (e.getKeyCode()) { - case VK_RIGHT: - if (e.isShiftDown()) { - moveBranchDown(); - } else { - nextBranch(); - } - break; - - case VK_LEFT: - if (e.isShiftDown()) { - moveBranchUp(); - } else { - previousBranch(); - } - break; - - case VK_UP: - if (e.isShiftDown()) { - undoToChildOfPreviousWithVariation(); - } else if (controlIsPressed(e)) { - undo(10); - } else { - undo(); - } - break; - - case VK_PAGE_DOWN: - if (controlIsPressed(e) && e.isShiftDown()) { - Lizzie.frame.increaseMaxAlpha(-5); - } else { - redo(10); - } - break; - - case VK_DOWN: - if (controlIsPressed(e)) { - redo(10); - } else { - redo(); - } - break; - - case VK_N: - // stop the ponder - if (Lizzie.leelaz.isPondering()) - Lizzie.leelaz.togglePonder(); - LizzieFrame.startNewGame(); - break; - case VK_SPACE: - 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(); - break; - - case VK_P: - Lizzie.board.pass(); - break; - - case VK_COMMA: - if (!Lizzie.frame.playCurrentVariation()) - Lizzie.frame.playBestMove(); - break; - - case VK_M: - Lizzie.config.toggleShowMoveNumber(); - break; - - case VK_F: - Lizzie.config.toggleShowNextMoves(); - break; - - case VK_H: - Lizzie.config.toggleHandicapInsteadOfWinrate(); - break; - - case VK_PAGE_UP: - if (controlIsPressed(e) && e.isShiftDown()) { - Lizzie.frame.increaseMaxAlpha(5); - } else { - undo(10); - } - break; - - case VK_I: - // stop the ponder - if (Lizzie.leelaz.isPondering()) - Lizzie.leelaz.togglePonder(); - Lizzie.frame.editGameInfo(); - break; - case VK_S: - // stop the ponder - if (Lizzie.leelaz.isPondering()) - Lizzie.leelaz.togglePonder(); - LizzieFrame.saveFile(); - break; - - case VK_O: - if (Lizzie.leelaz.isPondering()) - Lizzie.leelaz.togglePonder(); - LizzieFrame.openFile(); - break; - - case VK_V: - if (controlIsPressed(e)) { - Lizzie.frame.pasteSgf(); - } else { - Lizzie.config.toggleShowBranch(); - } - break; - - case VK_HOME: - while (Lizzie.board.previousMove()) ; - break; - - case VK_END: - while (Lizzie.board.nextMove()) ; - break; - - case VK_X: - if (!Lizzie.frame.showControls) { - if (Lizzie.leelaz.isPondering()) { - wasPonderingWhenControlsShown = true; - Lizzie.leelaz.togglePonder(); - } else { - wasPonderingWhenControlsShown = false; - } - Lizzie.frame.drawControls(); - } - Lizzie.frame.showControls = true; - break; - - case VK_W: - Lizzie.config.toggleShowWinrate(); - break; - - case VK_G: - Lizzie.config.toggleShowVariationGraph(); - break; - - case VK_T: - Lizzie.config.toggleShowComment(); - break; - - case VK_C: - if (controlIsPressed(e)) { - Lizzie.frame.copySgf(); - } else { - Lizzie.frame.toggleCoordinates(); - } - break; - - case VK_ENTER: - if (!Lizzie.leelaz.isThinking) { - Lizzie.leelaz.sendCommand("time_settings 0 " + Lizzie.config.config.getJSONObject("leelaz").getInt("max-game-thinking-time-seconds") + " 1"); - Lizzie.frame.playerIsBlack = !Lizzie.board.getData().blackToPlay; - Lizzie.frame.isPlayingAgainstLeelaz = true; - Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "B" : "W")); - } - break; - - case VK_DELETE: - case VK_BACK_SPACE: - if (e.isShiftDown()) { - deleteBranch(); - } else { - deleteMove(); - } - break; - - case VK_Z: - if (e.isShiftDown()) { - toggleHints(); - } else { - startRawBoard(); - } - break; - - case VK_A: - shouldDisableAnalysis = false; - Lizzie.board.toggleAnalysis(); - break; - - case VK_PERIOD: - if (Lizzie.board.getHistory().getNext() == null) - { - Lizzie.board.setScoreMode(!Lizzie.board.inScoreMode()); - } - break; - - case VK_D: - toggleShowDynamicKomi(); - break; - - default: - shouldDisableAnalysis = false; - } + Lizzie.frame.repaint(); + } - if (shouldDisableAnalysis && Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); + private boolean wasPonderingWhenControlsShown = false; + @Override + public void keyReleased(KeyEvent e) { + switch (e.getKeyCode()) { + case VK_X: + if (wasPonderingWhenControlsShown) Lizzie.leelaz.togglePonder(); + Lizzie.frame.showControls = false; Lizzie.frame.repaint(); - } + break; - private boolean wasPonderingWhenControlsShown = false; - @Override - public void keyReleased(KeyEvent e) { - switch (e.getKeyCode()) { - case VK_X: - if (wasPonderingWhenControlsShown) - Lizzie.leelaz.togglePonder(); - Lizzie.frame.showControls = false; - Lizzie.frame.repaint(); - break; - - case VK_Z: - stopRawBoard(); - Lizzie.frame.repaint(); - break; - - default: - } + case VK_Z: + stopRawBoard(); + Lizzie.frame.repaint(); + break; + + default: } + } - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - if (Lizzie.frame.processCommentMouseWheelMoved(e)) { - return; - } - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (e.getWheelRotation() > 0) { - redo(); - } else if (e.getWheelRotation() < 0) { - undo(); - } - Lizzie.frame.repaint(); + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (Lizzie.frame.processCommentMouseWheelMoved(e)) { + return; + } + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (e.getWheelRotation() > 0) { + redo(); + } else if (e.getWheelRotation() < 0) { + undo(); } + Lizzie.frame.repaint(); + } } diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 526b2bfeb..fe8df0ca1 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -1,15 +1,5 @@ package featurecat.lizzie.gui; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Font; -import java.awt.FontFormatException; -import java.awt.FontMetrics; -import java.awt.Frame; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; - import com.jhlabs.image.GaussianFilter; import featurecat.lizzie.Lizzie; import featurecat.lizzie.Util; @@ -19,17 +9,20 @@ import featurecat.lizzie.rules.BoardData; import featurecat.lizzie.rules.GIBParser; import featurecat.lizzie.rules.SGFParser; -import org.json.JSONObject; -import org.json.JSONArray; - -import javax.swing.*; -import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; - import java.awt.event.MouseWheelEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; @@ -40,1008 +33,1108 @@ import java.util.Arrays; import java.util.List; import java.util.ResourceBundle; +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.json.JSONArray; +import org.json.JSONObject; -/** - * The window used to display the game. - */ +/** The window used to display the game. */ public class LizzieFrame extends JFrame { - private static final ResourceBundle resourceBundle = ResourceBundle.getBundle("l10n.DisplayStrings"); - - private static final String[] commands = { - resourceBundle.getString("LizzieFrame.commands.keyN"), - resourceBundle.getString("LizzieFrame.commands.keyEnter"), - resourceBundle.getString("LizzieFrame.commands.keySpace"), - resourceBundle.getString("LizzieFrame.commands.keyUpArrow"), - resourceBundle.getString("LizzieFrame.commands.keyDownArrow"), - resourceBundle.getString("LizzieFrame.commands.rightClick"), - resourceBundle.getString("LizzieFrame.commands.mouseWheelScroll"), - resourceBundle.getString("LizzieFrame.commands.keyC"), - resourceBundle.getString("LizzieFrame.commands.keyP"), - resourceBundle.getString("LizzieFrame.commands.keyPeriod"), - resourceBundle.getString("LizzieFrame.commands.keyA"), - resourceBundle.getString("LizzieFrame.commands.keyM"), - resourceBundle.getString("LizzieFrame.commands.keyI"), - resourceBundle.getString("LizzieFrame.commands.keyO"), - resourceBundle.getString("LizzieFrame.commands.keyS"), - resourceBundle.getString("LizzieFrame.commands.keyAltC"), - resourceBundle.getString("LizzieFrame.commands.keyAltV"), - resourceBundle.getString("LizzieFrame.commands.keyF"), - resourceBundle.getString("LizzieFrame.commands.keyV"), - resourceBundle.getString("LizzieFrame.commands.keyW"), - resourceBundle.getString("LizzieFrame.commands.keyG"), - resourceBundle.getString("LizzieFrame.commands.keyT"), - resourceBundle.getString("LizzieFrame.commands.keyHome"), - resourceBundle.getString("LizzieFrame.commands.keyEnd"), - resourceBundle.getString("LizzieFrame.commands.keyControl"), - }; - private static final String DEFAULT_TITLE = "Lizzie - Leela Zero Interface"; - private static BoardRenderer boardRenderer; - private static BoardRenderer subBoardRenderer; - private static VariationTree variationTree; - private static WinrateGraph winrateGraph; - - public static Font OpenSansRegularBase; - public static Font OpenSansSemiboldBase; - - private final BufferStrategy bs; - - public int[] mouseHoverCoordinate; - public boolean showControls = false; - public boolean showCoordinates = false; - public boolean isPlayingAgainstLeelaz = false; - public boolean playerIsBlack = true; - public int winRateGridLines = 3; - - // Get the font name in current system locale - private String systemDefaultFontName = new JLabel().getFont().getFontName(); - - private long lastAutosaveTime = System.currentTimeMillis(); - - // Display Comment - private JScrollPane scrollPane = null; - private JTextPane commentPane = null; - private BufferedImage commentImage = null; - private String cachedComment = null; - private Rectangle commentRect = null; - - static { - // load fonts - try { - OpenSansRegularBase = Font.createFont(Font.TRUETYPE_FONT, Thread.currentThread().getContextClassLoader().getResourceAsStream("fonts/OpenSans-Regular.ttf")); - OpenSansSemiboldBase = Font.createFont(Font.TRUETYPE_FONT, Thread.currentThread().getContextClassLoader().getResourceAsStream("fonts/OpenSans-Semibold.ttf")); - } catch (IOException | FontFormatException e) { - e.printStackTrace(); - } + private static final ResourceBundle resourceBundle = + ResourceBundle.getBundle("l10n.DisplayStrings"); + + private static final String[] commands = { + resourceBundle.getString("LizzieFrame.commands.keyN"), + resourceBundle.getString("LizzieFrame.commands.keyEnter"), + resourceBundle.getString("LizzieFrame.commands.keySpace"), + resourceBundle.getString("LizzieFrame.commands.keyUpArrow"), + resourceBundle.getString("LizzieFrame.commands.keyDownArrow"), + resourceBundle.getString("LizzieFrame.commands.rightClick"), + resourceBundle.getString("LizzieFrame.commands.mouseWheelScroll"), + resourceBundle.getString("LizzieFrame.commands.keyC"), + resourceBundle.getString("LizzieFrame.commands.keyP"), + resourceBundle.getString("LizzieFrame.commands.keyPeriod"), + resourceBundle.getString("LizzieFrame.commands.keyA"), + resourceBundle.getString("LizzieFrame.commands.keyM"), + resourceBundle.getString("LizzieFrame.commands.keyI"), + resourceBundle.getString("LizzieFrame.commands.keyO"), + resourceBundle.getString("LizzieFrame.commands.keyS"), + resourceBundle.getString("LizzieFrame.commands.keyAltC"), + resourceBundle.getString("LizzieFrame.commands.keyAltV"), + resourceBundle.getString("LizzieFrame.commands.keyF"), + resourceBundle.getString("LizzieFrame.commands.keyV"), + resourceBundle.getString("LizzieFrame.commands.keyW"), + resourceBundle.getString("LizzieFrame.commands.keyG"), + resourceBundle.getString("LizzieFrame.commands.keyT"), + resourceBundle.getString("LizzieFrame.commands.keyHome"), + resourceBundle.getString("LizzieFrame.commands.keyEnd"), + resourceBundle.getString("LizzieFrame.commands.keyControl"), + }; + private static final String DEFAULT_TITLE = "Lizzie - Leela Zero Interface"; + private static BoardRenderer boardRenderer; + private static BoardRenderer subBoardRenderer; + private static VariationTree variationTree; + private static WinrateGraph winrateGraph; + + public static Font OpenSansRegularBase; + public static Font OpenSansSemiboldBase; + + private final BufferStrategy bs; + + public int[] mouseHoverCoordinate; + public boolean showControls = false; + public boolean showCoordinates = false; + public boolean isPlayingAgainstLeelaz = false; + public boolean playerIsBlack = true; + public int winRateGridLines = 3; + + // Get the font name in current system locale + private String systemDefaultFontName = new JLabel().getFont().getFontName(); + + private long lastAutosaveTime = System.currentTimeMillis(); + + // Display Comment + private JScrollPane scrollPane = null; + private JTextPane commentPane = null; + private BufferedImage commentImage = null; + private String cachedComment = null; + private Rectangle commentRect = null; + + static { + // load fonts + try { + OpenSansRegularBase = + Font.createFont( + Font.TRUETYPE_FONT, + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("fonts/OpenSans-Regular.ttf")); + OpenSansSemiboldBase = + Font.createFont( + Font.TRUETYPE_FONT, + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("fonts/OpenSans-Semibold.ttf")); + } catch (IOException | FontFormatException e) { + e.printStackTrace(); } + } - /** - * Creates a window - */ - public LizzieFrame() { - super(DEFAULT_TITLE); - - boardRenderer = new BoardRenderer(true); - subBoardRenderer = new BoardRenderer(false); - variationTree = new VariationTree(); - winrateGraph = new WinrateGraph(); - - setMinimumSize( new Dimension(640,480) ); - setLocationRelativeTo(null); // start centered - JSONArray windowSize = Lizzie.config.uiConfig.getJSONArray("window-size"); - setSize(windowSize.getInt(0), windowSize.getInt(1)); // use config file window size - - if (Lizzie.config.startMaximized) { - setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized - } - - // Comment Pane - commentPane = new JTextPane(); - commentPane.setEditable(false); - commentPane.setMargin(new Insets(5, 5, 5, 5)); - commentPane.setBackground(new Color(0, 0, 0, 200)); - commentPane.setForeground(Color.WHITE); - scrollPane = new JScrollPane(); - scrollPane.setViewportView(commentPane); - scrollPane.setBorder(null); - scrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); - - setVisible(true); - - createBufferStrategy(2); - bs = getBufferStrategy(); + /** Creates a window */ + public LizzieFrame() { + super(DEFAULT_TITLE); - Input input = new Input(); + boardRenderer = new BoardRenderer(true); + subBoardRenderer = new BoardRenderer(false); + variationTree = new VariationTree(); + winrateGraph = new WinrateGraph(); - this.addMouseListener(input); - this.addKeyListener(input); - this.addMouseWheelListener(input); - this.addMouseMotionListener(input); + setMinimumSize(new Dimension(640, 480)); + setLocationRelativeTo(null); // start centered + JSONArray windowSize = Lizzie.config.uiConfig.getJSONArray("window-size"); + setSize(windowSize.getInt(0), windowSize.getInt(1)); // use config file window size - // necessary for Windows users - otherwise Lizzie shows a blank white screen on startup until updates occur. - repaint(); - - // when the window is closed: save the SGF file, then run shutdown() - this.addWindowListener(new WindowAdapter() { - public void windowClosing(WindowEvent e) { - Lizzie.shutdown(); - } - }); - - } - - public static void startNewGame() { - GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); - - NewGameDialog newGameDialog = new NewGameDialog(); - newGameDialog.setGameInfo(gameInfo); - newGameDialog.setVisible(true); - boolean playerIsBlack = newGameDialog.playerIsBlack(); - newGameDialog.dispose(); - if (newGameDialog.isCancelled()) return; - - Lizzie.board.clear(); - Lizzie.leelaz.sendCommand("komi " + gameInfo.getKomi()); - - Lizzie.leelaz.sendCommand("time_settings 0 " + Lizzie.config.config.getJSONObject("leelaz").getInt("max-game-thinking-time-seconds") + " 1"); - Lizzie.frame.playerIsBlack = playerIsBlack; - Lizzie.frame.isPlayingAgainstLeelaz = true; - - boolean isHandicapGame = gameInfo.getHandicap() != 0; - if (isHandicapGame) { - Lizzie.board.getHistory().getData().blackToPlay = false; - Lizzie.leelaz.sendCommand("fixed_handicap " + gameInfo.getHandicap()); - if (playerIsBlack) Lizzie.leelaz.genmove("W"); - } else if (!playerIsBlack) { - Lizzie.leelaz.genmove("B"); - } + if (Lizzie.config.startMaximized) { + setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized } - public static void editGameInfo() { - GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); - - GameInfoDialog gameInfoDialog = new GameInfoDialog(); - gameInfoDialog.setGameInfo(gameInfo); - gameInfoDialog.setVisible(true); - - gameInfoDialog.dispose(); + // Comment Pane + commentPane = new JTextPane(); + commentPane.setEditable(false); + commentPane.setMargin(new Insets(5, 5, 5, 5)); + commentPane.setBackground(new Color(0, 0, 0, 200)); + commentPane.setForeground(Color.WHITE); + scrollPane = new JScrollPane(); + scrollPane.setViewportView(commentPane); + scrollPane.setBorder(null); + scrollPane.setVerticalScrollBarPolicy( + javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + + setVisible(true); + + createBufferStrategy(2); + bs = getBufferStrategy(); + + Input input = new Input(); + + this.addMouseListener(input); + this.addKeyListener(input); + this.addMouseWheelListener(input); + this.addMouseMotionListener(input); + + // necessary for Windows users - otherwise Lizzie shows a blank white screen on startup until + // updates occur. + repaint(); + + // when the window is closed: save the SGF file, then run shutdown() + this.addWindowListener( + new WindowAdapter() { + public void windowClosing(WindowEvent e) { + Lizzie.shutdown(); + } + }); + } + + public static void startNewGame() { + GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); + + NewGameDialog newGameDialog = new NewGameDialog(); + newGameDialog.setGameInfo(gameInfo); + newGameDialog.setVisible(true); + boolean playerIsBlack = newGameDialog.playerIsBlack(); + newGameDialog.dispose(); + if (newGameDialog.isCancelled()) return; + + Lizzie.board.clear(); + Lizzie.leelaz.sendCommand("komi " + gameInfo.getKomi()); + + Lizzie.leelaz.sendCommand( + "time_settings 0 " + + Lizzie.config.config.getJSONObject("leelaz").getInt("max-game-thinking-time-seconds") + + " 1"); + Lizzie.frame.playerIsBlack = playerIsBlack; + Lizzie.frame.isPlayingAgainstLeelaz = true; + + boolean isHandicapGame = gameInfo.getHandicap() != 0; + if (isHandicapGame) { + Lizzie.board.getHistory().getData().blackToPlay = false; + Lizzie.leelaz.sendCommand("fixed_handicap " + gameInfo.getHandicap()); + if (playerIsBlack) Lizzie.leelaz.genmove("W"); + } else if (!playerIsBlack) { + Lizzie.leelaz.genmove("B"); } - - public static void saveFile() { - FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf", "SGF"); - JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); - JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); - chooser.setFileFilter(filter); - chooser.setMultiSelectionEnabled(false); - int result = chooser.showSaveDialog(null); - if (result == JFileChooser.APPROVE_OPTION) { - File file = chooser.getSelectedFile(); - if (file.exists()) { - int ret = JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.sgfExists"), "Warning", JOptionPane.OK_CANCEL_OPTION); - if (ret == JOptionPane.CANCEL_OPTION) { - return; - } - } - if (!file.getPath().endsWith(".sgf")) { - file = new File(file.getPath() + ".sgf"); - } - try { - SGFParser.save(Lizzie.board, file.getPath()); - filesystem.put("last-folder", file.getParent()); - } catch (IOException err) { - JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedTosaveFile"), "Error", JOptionPane.ERROR); - } + } + + public static void editGameInfo() { + GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); + + GameInfoDialog gameInfoDialog = new GameInfoDialog(); + gameInfoDialog.setGameInfo(gameInfo); + gameInfoDialog.setVisible(true); + + gameInfoDialog.dispose(); + } + + public static void saveFile() { + FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf", "SGF"); + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); + chooser.setFileFilter(filter); + chooser.setMultiSelectionEnabled(false); + int result = chooser.showSaveDialog(null); + if (result == JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + if (file.exists()) { + int ret = + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.sgfExists"), + "Warning", + JOptionPane.OK_CANCEL_OPTION); + if (ret == JOptionPane.CANCEL_OPTION) { + return; } - } - - public static void openFile() { - FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf or *.gib", "SGF", "GIB"); - JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); - JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); - - chooser.setFileFilter(filter); - chooser.setMultiSelectionEnabled(false); - int result = chooser.showOpenDialog(null); - if (result == JFileChooser.APPROVE_OPTION) - loadFile(chooser.getSelectedFile()); - } - - public static void loadFile(File file) { - JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); - if (!(file.getPath().endsWith(".sgf") || file.getPath().endsWith(".gib"))) { - file = new File(file.getPath() + ".sgf"); + } + if (!file.getPath().endsWith(".sgf")) { + file = new File(file.getPath() + ".sgf"); } try { - System.out.println(file.getPath()); - if (file.getPath().endsWith(".sgf")) { - SGFParser.load(file.getPath()); - } else { - GIBParser.load(file.getPath()); - } - filesystem.put("last-folder", file.getParent()); + SGFParser.save(Lizzie.board, file.getPath()); + filesystem.put("last-folder", file.getParent()); } catch (IOException err) { - JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToOpenFile"), "Error", JOptionPane.ERROR); + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.failedTosaveFile"), + "Error", + JOptionPane.ERROR); } } - - private BufferedImage cachedImage = null; - - private BufferedImage cachedBackground = null; - private int cachedBackgroundWidth = 0, cachedBackgroundHeight = 0; - private boolean cachedBackgroundShowControls = false; - private boolean cachedShowWinrate = true; - private boolean cachedShowVariationGraph = true; - private boolean redrawBackgroundAnyway = false; - - /** - * Draws the game board and interface - * - * @param g0 not used - */ - public void paint(Graphics g0) { - autosaveMaybe(); - if (bs == null) - return; - - Graphics2D backgroundG; - if (cachedBackgroundWidth != getWidth() || cachedBackgroundHeight != getHeight() || cachedBackgroundShowControls != showControls || cachedShowWinrate != Lizzie.config.showWinrate || cachedShowVariationGraph != Lizzie.config.showVariationGraph || redrawBackgroundAnyway) - backgroundG = createBackground(); - else - backgroundG = null; - - if (!showControls) { - // layout parameters - - int topInset = this.getInsets().top; - - // board - int maxSize = (int) (Math.min(getWidth(), getHeight() - topInset) * 0.98); - maxSize = Math.max(maxSize, Board.BOARD_SIZE + 5); // don't let maxWidth become too small - int boardX = (getWidth() - maxSize) / 2; - int boardY = topInset + (getHeight() - topInset - maxSize) / 2 + 3; - - int panelMargin = (int) (maxSize * 0.05); - - // move statistics (winrate bar) - // boardX equals width of space on each side - int statx = 0; - int staty = boardY + maxSize / 8; - int statw = boardX - statx - panelMargin; - int stath = maxSize / 10; - - // winrate graph - int grx = statx; - int gry = staty + stath; - int grw = statw; - int grh = statw; - - // graph container - int contx = statx; - int conty = staty; - int contw = statw; - int conth = stath; - - // captured stones - int capx = 0; - int capy = this.getInsets().top; - int capw = boardX - (int)(maxSize*0.05); - int caph = boardY+ maxSize/8 - this.getInsets().top; - - // variation tree container - int vx = boardX + maxSize + panelMargin; - int vy = 0; - int vw = getWidth() - vx; - int vh = getHeight(); - - // variation tree - int treex = vx; - int treey = vy; - int treew = vw + 1; - int treeh = vh; - - // pondering message - int ponderingX = this.getInsets().left; - int ponderingY = boardY + (int) (maxSize*0.93); - double ponderingSize = .02; - - // dynamic komi - int dynamicKomiLabelX = this.getInsets().left; - int dynamicKomiLabelY = boardY + (int) (maxSize*0.86); - - int dynamicKomiX = this.getInsets().left; - int dynamicKomiY = boardY + (int) (maxSize*0.89); - double dynamicKomiSize = .02; - - - // loading message - int loadingX = ponderingX; - int loadingY = ponderingY; - double loadingSize = 0.03; - - // subboard - int subBoardX = 0; - int subBoardY = gry + grh; - int subBoardWidth = grw; - int subBoardHeight = ponderingY - subBoardY; - int subBoardLength = Math.min(subBoardWidth, subBoardHeight); - - if (Lizzie.config.showLargeSubBoard()) { - boardX = getWidth() - maxSize - panelMargin; - int spaceW = boardX - panelMargin; - int spaceH = getHeight() - topInset; - int panelW = spaceW / 2; - int panelH = spaceH / 4; - capx = 0; - capy = topInset; - capw = panelW; - caph = (int) (panelH * 0.2); - statx = 0; - staty = capy + caph; - statw = panelW; - stath = (int) (panelH * 0.4); - grx = statx; - gry = staty + stath; - grw = statw; - grh = panelH - caph - stath; - contx = statx; - conty = staty; - contw = statw; - conth = stath + grh; - vx = panelW; - vy = 0; - vw = panelW; - vh = topInset + panelH; - treex = vx; - treey = vy; - treew = vw + 1; - treeh = vh; - subBoardX = 0; - subBoardY = topInset + panelH; - subBoardWidth = spaceW; - subBoardHeight = ponderingY - subBoardY; - subBoardLength = Math.min(subBoardWidth, subBoardHeight); - } - - // initialize - - cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g = (Graphics2D) cachedImage.getGraphics(); - - if (Lizzie.config.showStatus) - drawCommandString(g); - - boardRenderer.setLocation(boardX, boardY); - boardRenderer.setBoardLength(maxSize); - boardRenderer.draw(g); - - if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { - if (Lizzie.config.showStatus) { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.pondering") + - (Lizzie.leelaz.isPondering()?resourceBundle.getString("LizzieFrame.display.on"):resourceBundle.getString("LizzieFrame.display.off")), - ponderingX, ponderingY, ponderingSize); - } - - if (Lizzie.config.showDynamicKomi && Lizzie.leelaz.getDynamicKomi() != null) { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.dynamic-komi"), dynamicKomiLabelX, dynamicKomiLabelY, dynamicKomiSize); - drawPonderingState(g, Lizzie.leelaz.getDynamicKomi(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); - } - - // Todo: Make board move over when there is no space beside the board - if (Lizzie.config.showWinrate) { - drawWinrateGraphContainer(backgroundG, contx, conty, contw, conth); - drawMoveStatistics(g, statx, staty, statw, stath); - winrateGraph.draw(g, grx, gry, grw, grh); - } - - if (Lizzie.config.showVariationGraph) { - drawVariationTreeContainer(backgroundG, vx, vy, vw, vh); - int cHeight = 0; - if (Lizzie.config.showComment) { - // Draw the Comment of the Sgf - cHeight = drawComment(g, vx, vy, vw, vh, false); - } - variationTree.draw(g, treex, treey, treew, treeh - cHeight); - } else { - if (Lizzie.config.showComment) { - // Draw the Comment of the Sgf - drawComment(g, vx, topInset, vw, vh - topInset + vy, true); - } - } - if (Lizzie.config.showSubBoard) { - try { - subBoardRenderer.setLocation(subBoardX, subBoardY); - subBoardRenderer.setBoardLength(subBoardLength); - subBoardRenderer.draw(g); - } catch (Exception e) { - // This can happen when no space is left for subboard. - } - } - } else if (Lizzie.config.showStatus) { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.loading"), loadingX, loadingY, loadingSize); - } - - if (Lizzie.config.showCaptured) - drawCaptured(g, capx, capy, capw, caph); - - // cleanup - g.dispose(); - } - - // draw the image - Graphics2D bsGraphics = (Graphics2D) bs.getDrawGraphics(); - bsGraphics.drawImage(cachedBackground, 0, 0, null); - bsGraphics.drawImage(cachedImage, 0, 0, null); - - // cleanup - bsGraphics.dispose(); - bs.show(); - } - - /** - * temporary measure to refresh background. ideally we shouldn't need this - * (but we want to release Lizzie 0.5 today, not tomorrow!). Refactor me out please! (you need to get blurring to - * work properly on startup). - */ - public void refreshBackground() { - redrawBackgroundAnyway = true; - } - - private Graphics2D createBackground() { - cachedBackground = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); - cachedBackgroundWidth = cachedBackground.getWidth(); - cachedBackgroundHeight = cachedBackground.getHeight(); - cachedBackgroundShowControls = showControls; - cachedShowWinrate = Lizzie.config.showWinrate; - cachedShowVariationGraph = Lizzie.config.showVariationGraph; - - redrawBackgroundAnyway = false; - - Graphics2D g = cachedBackground.createGraphics(); - - BufferedImage wallpaper = boardRenderer.getWallpaper(); - int drawWidth = Math.max(wallpaper.getWidth(), getWidth()); - int drawHeight = Math.max(wallpaper.getHeight(), getHeight()); - // Support seamless texture - boardRenderer.drawTextureImage(g, wallpaper, 0, 0, drawWidth, drawHeight); - - return g; - } - - private void drawVariationTreeContainer(Graphics2D g, int vx, int vy, int vw, int vh) { - vw = cachedBackground.getWidth() - vx; - - if (g == null || vw <= 0 || vh <= 0) - return; - - BufferedImage result = new BufferedImage(vw, vh, BufferedImage.TYPE_INT_ARGB); - filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); - g.drawImage(result, vx, vy, null); - } - - private void drawPonderingState(Graphics2D g, String text, int x, int y, double size) { - Font font = new Font(systemDefaultFontName, Font.PLAIN, (int)(Math.max(getWidth(), getHeight()) * size)); - FontMetrics fm = g.getFontMetrics(font); - int stringWidth = fm.stringWidth(text); - int stringHeight = fm.getAscent() - fm.getDescent(); - int width = stringWidth; - int height = (int)(stringHeight * 1.2); - - BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - // commenting this out for now... always causing an exception on startup. will fix in the upcoming refactoring -// filter20.filter(cachedBackground.getSubimage(x, y, result.getWidth(), result.getHeight()), result); - g.drawImage(result, x, y, null); - - g.setColor(new Color(0,0,0,130)); - g.fillRect(x, y, width, height); - g.drawRect(x, y, width, height); - - g.setColor(Color.white); - g.setFont(font); - g.drawString(text, x + (width - stringWidth)/2, y + stringHeight + (height - stringHeight)/2); + } + + public static void openFile() { + FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf or *.gib", "SGF", "GIB"); + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); + + chooser.setFileFilter(filter); + chooser.setMultiSelectionEnabled(false); + int result = chooser.showOpenDialog(null); + if (result == JFileChooser.APPROVE_OPTION) loadFile(chooser.getSelectedFile()); + } + + public static void loadFile(File file) { + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + if (!(file.getPath().endsWith(".sgf") || file.getPath().endsWith(".gib"))) { + file = new File(file.getPath() + ".sgf"); } - - private void drawWinrateGraphContainer(Graphics g, int statx, int staty, int statw, int stath) { - if (g == null || statw <= 0 || stath <= 0) - return; - - BufferedImage result = new BufferedImage(statw, stath + statw, BufferedImage.TYPE_INT_ARGB); - filter20.filter(cachedBackground.getSubimage(statx, staty, result.getWidth(), result.getHeight()), result); - g.drawImage(result, statx, staty, null); + try { + System.out.println(file.getPath()); + if (file.getPath().endsWith(".sgf")) { + SGFParser.load(file.getPath()); + } else { + GIBParser.load(file.getPath()); + } + filesystem.put("last-folder", file.getParent()); + } catch (IOException err) { + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.failedToOpenFile"), + "Error", + JOptionPane.ERROR); } + } + + private BufferedImage cachedImage = null; + + private BufferedImage cachedBackground = null; + private int cachedBackgroundWidth = 0, cachedBackgroundHeight = 0; + private boolean cachedBackgroundShowControls = false; + private boolean cachedShowWinrate = true; + private boolean cachedShowVariationGraph = true; + private boolean redrawBackgroundAnyway = false; + + /** + * Draws the game board and interface + * + * @param g0 not used + */ + public void paint(Graphics g0) { + autosaveMaybe(); + if (bs == null) return; + + Graphics2D backgroundG; + if (cachedBackgroundWidth != getWidth() + || cachedBackgroundHeight != getHeight() + || cachedBackgroundShowControls != showControls + || cachedShowWinrate != Lizzie.config.showWinrate + || cachedShowVariationGraph != Lizzie.config.showVariationGraph + || redrawBackgroundAnyway) backgroundG = createBackground(); + else backgroundG = null; + + if (!showControls) { + // layout parameters + + int topInset = this.getInsets().top; + + // board + int maxSize = (int) (Math.min(getWidth(), getHeight() - topInset) * 0.98); + maxSize = Math.max(maxSize, Board.BOARD_SIZE + 5); // don't let maxWidth become too small + int boardX = (getWidth() - maxSize) / 2; + int boardY = topInset + (getHeight() - topInset - maxSize) / 2 + 3; + + int panelMargin = (int) (maxSize * 0.05); + + // move statistics (winrate bar) + // boardX equals width of space on each side + int statx = 0; + int staty = boardY + maxSize / 8; + int statw = boardX - statx - panelMargin; + int stath = maxSize / 10; + + // winrate graph + int grx = statx; + int gry = staty + stath; + int grw = statw; + int grh = statw; + + // graph container + int contx = statx; + int conty = staty; + int contw = statw; + int conth = stath; + + // captured stones + int capx = 0; + int capy = this.getInsets().top; + int capw = boardX - (int) (maxSize * 0.05); + int caph = boardY + maxSize / 8 - this.getInsets().top; + + // variation tree container + int vx = boardX + maxSize + panelMargin; + int vy = 0; + int vw = getWidth() - vx; + int vh = getHeight(); + + // variation tree + int treex = vx; + int treey = vy; + int treew = vw + 1; + int treeh = vh; + + // pondering message + int ponderingX = this.getInsets().left; + int ponderingY = boardY + (int) (maxSize * 0.93); + double ponderingSize = .02; + + // dynamic komi + int dynamicKomiLabelX = this.getInsets().left; + int dynamicKomiLabelY = boardY + (int) (maxSize * 0.86); + + int dynamicKomiX = this.getInsets().left; + int dynamicKomiY = boardY + (int) (maxSize * 0.89); + double dynamicKomiSize = .02; + + // loading message + int loadingX = ponderingX; + int loadingY = ponderingY; + double loadingSize = 0.03; + + // subboard + int subBoardX = 0; + int subBoardY = gry + grh; + int subBoardWidth = grw; + int subBoardHeight = ponderingY - subBoardY; + int subBoardLength = Math.min(subBoardWidth, subBoardHeight); + + if (Lizzie.config.showLargeSubBoard()) { + boardX = getWidth() - maxSize - panelMargin; + int spaceW = boardX - panelMargin; + int spaceH = getHeight() - topInset; + int panelW = spaceW / 2; + int panelH = spaceH / 4; + capx = 0; + capy = topInset; + capw = panelW; + caph = (int) (panelH * 0.2); + statx = 0; + staty = capy + caph; + statw = panelW; + stath = (int) (panelH * 0.4); + grx = statx; + gry = staty + stath; + grw = statw; + grh = panelH - caph - stath; + contx = statx; + conty = staty; + contw = statw; + conth = stath + grh; + vx = panelW; + vy = 0; + vw = panelW; + vh = topInset + panelH; + treex = vx; + treey = vy; + treew = vw + 1; + treeh = vh; + subBoardX = 0; + subBoardY = topInset + panelH; + subBoardWidth = spaceW; + subBoardHeight = ponderingY - subBoardY; + subBoardLength = Math.min(subBoardWidth, subBoardHeight); + } - private GaussianFilter filter20 = new GaussianFilter(20); - private GaussianFilter filter10 = new GaussianFilter(10); + // initialize - /** - * Display the controls - */ - void drawControls() { - userAlreadyKnowsAboutCommandString = true; + cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) cachedImage.getGraphics(); - cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + if (Lizzie.config.showStatus) drawCommandString(g); - // redraw background - createBackground(); + boardRenderer.setLocation(boardX, boardY); + boardRenderer.setBoardLength(maxSize); + boardRenderer.draw(g); - List commandsToShow = new ArrayList<>(Arrays.asList(commands)); - if (Lizzie.leelaz.getDynamicKomi() != null) { - commandsToShow.add(resourceBundle.getString("LizzieFrame.commands.keyD")); + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showStatus) { + drawPonderingState( + g, + resourceBundle.getString("LizzieFrame.display.pondering") + + (Lizzie.leelaz.isPondering() + ? resourceBundle.getString("LizzieFrame.display.on") + : resourceBundle.getString("LizzieFrame.display.off")), + ponderingX, + ponderingY, + ponderingSize); } - Graphics2D g = cachedImage.createGraphics(); - - int maxSize = Math.min(getWidth(), getHeight()); - Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.034)); - g.setFont(font); - FontMetrics metrics = g.getFontMetrics(font); - int maxCommandWidth = commandsToShow.stream().reduce(0, (Integer i, String command) -> Math.max(i, metrics.stringWidth(command)), (Integer a, Integer b) -> Math.max(a, b)); - int lineHeight = (int) (font.getSize() * 1.15); - - int boxWidth = Util.clamp((int) (maxCommandWidth * 1.4), 0, getWidth()); - int boxHeight = Util.clamp(commandsToShow.size() * lineHeight, 0, getHeight()); - - int commandsX = Util.clamp(getWidth() / 2 - boxWidth / 2, 0, getWidth()); - int commandsY = Util.clamp(getHeight() / 2 - boxHeight / 2, 0, getHeight()); - - BufferedImage result = new BufferedImage(boxWidth, boxHeight, BufferedImage.TYPE_INT_ARGB); - filter10.filter(cachedBackground.getSubimage(commandsX, commandsY, boxWidth, boxHeight), result); - g.drawImage(result, commandsX, commandsY, null); - - g.setColor(new Color(0, 0, 0, 130)); - g.fillRect(commandsX, commandsY, boxWidth, boxHeight); - int strokeRadius = 2; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.setColor(new Color(0, 0, 0, 60)); - g.drawRect(commandsX + strokeRadius, commandsY + strokeRadius, boxWidth - 2 * strokeRadius, boxHeight - 2 * strokeRadius); - - int verticalLineX = (int) (commandsX + boxWidth * 0.3); - g.setColor(new Color(0, 0, 0, 60)); - g.drawLine(verticalLineX, commandsY + 2 * strokeRadius, verticalLineX, commandsY + boxHeight - 2 * strokeRadius); - - - g.setStroke(new BasicStroke(1)); - - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - g.setColor(Color.WHITE); - int lineOffset = commandsY; - for (String command : commandsToShow) { - String[] split = command.split("\\|"); - g.drawString(split[0], verticalLineX - metrics.stringWidth(split[0]) - strokeRadius * 4, font.getSize() + lineOffset); - g.drawString(split[1], verticalLineX + strokeRadius * 4, font.getSize() + lineOffset); - lineOffset += lineHeight; + if (Lizzie.config.showDynamicKomi && Lizzie.leelaz.getDynamicKomi() != null) { + drawPonderingState( + g, + resourceBundle.getString("LizzieFrame.display.dynamic-komi"), + dynamicKomiLabelX, + dynamicKomiLabelY, + dynamicKomiSize); + drawPonderingState( + g, Lizzie.leelaz.getDynamicKomi(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); } - refreshBackground(); - } - - private boolean userAlreadyKnowsAboutCommandString = false; - - private void drawCommandString(Graphics2D g) { - if (userAlreadyKnowsAboutCommandString) - return; - - int maxSize = (int) (Math.min(getWidth(), getHeight()) * 0.98); - - Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.03)); - String commandString = resourceBundle.getString("LizzieFrame.prompt.showControlsHint"); - int strokeRadius = 2; - - int showCommandsHeight = (int) (font.getSize() * 1.1); - int showCommandsWidth = g.getFontMetrics(font).stringWidth(commandString) + 4 * strokeRadius; - int showCommandsX = this.getInsets().left; - int showCommandsY = getHeight() - showCommandsHeight - this.getInsets().bottom; - g.setColor(new Color(0, 0, 0, 130)); - g.fillRect(showCommandsX, showCommandsY, showCommandsWidth, showCommandsHeight); - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.setColor(new Color(0, 0, 0, 60)); - g.drawRect(showCommandsX + strokeRadius, showCommandsY + strokeRadius, showCommandsWidth - 2 * strokeRadius, showCommandsHeight - 2 * strokeRadius); - g.setStroke(new BasicStroke(1)); - - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setColor(Color.WHITE); - g.setFont(font); - g.drawString(commandString, showCommandsX + 2 * strokeRadius, showCommandsY + font.getSize()); - } - - private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int height) { - if (width < 0 || height < 0) - return; // we don't have enough space - - double lastWR = 50; // winrate the previous move - boolean validLastWinrate = false; // whether it was actually calculated - BoardData lastNode = Lizzie.board.getHistory().getPrevious(); - if (lastNode != null && lastNode.playouts > 0) { - lastWR = lastNode.winrate; - validLastWinrate = true; + // Todo: Make board move over when there is no space beside the board + if (Lizzie.config.showWinrate) { + drawWinrateGraphContainer(backgroundG, contx, conty, contw, conth); + drawMoveStatistics(g, statx, staty, statw, stath); + winrateGraph.draw(g, grx, gry, grw, grh); } - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - double curWR = stats.maxWinrate; // winrate on this move - boolean validWinrate = (stats.totalPlayouts > 0); // and whether it was actually calculated - if (isPlayingAgainstLeelaz && playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) - validWinrate = false; - - if (!validWinrate) { - curWR = 100 - lastWR; // display last move's winrate for now (with color difference) - } - double whiteWR, blackWR; - if (Lizzie.board.getData().blackToPlay) { - blackWR = curWR; - } else { - blackWR = 100 - curWR; - } - - whiteWR = 100 - blackWR; - - // Background rectangle - g.setColor(new Color(0, 0, 0, 130)); - g.fillRect(posX, posY, width, height); - - // border. does not include bottom edge - int strokeRadius = 3; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.drawLine(posX + strokeRadius, posY + strokeRadius, - posX - strokeRadius + width, posY + strokeRadius); - g.drawLine(posX + strokeRadius, posY + 3 * strokeRadius, - posX + strokeRadius, posY - strokeRadius + height); - g.drawLine(posX - strokeRadius + width, posY + 3 * strokeRadius, - posX - strokeRadius + width, posY - strokeRadius + height); - - // resize the box now so it's inside the border - posX += 2 * strokeRadius; - posY += 2 * strokeRadius; - width -= 4 * strokeRadius; - height -= 4 * strokeRadius; - - // Title - strokeRadius = 2; - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setColor(Color.WHITE); - setPanelFont(g, (int) (Math.min(width, height) * 0.2)); - - // Last move - if (validLastWinrate && validWinrate) { - String text; - if(Lizzie.config.handicapInsteadOfWinrate) { - text=String.format(": %.2f", Lizzie.leelaz.winrateToHandicap(100-curWR) - Lizzie.leelaz.winrateToHandicap(lastWR)); - } else { - text=String.format(": %.1f%%", 100 - lastWR - curWR); - } - - g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + - text, - posX + 2 * strokeRadius, - posY + height - 2 * strokeRadius); // - font.getSize()); + if (Lizzie.config.showVariationGraph) { + drawVariationTreeContainer(backgroundG, vx, vy, vw, vh); + int cHeight = 0; + if (Lizzie.config.showComment) { + // Draw the Comment of the Sgf + cHeight = drawComment(g, vx, vy, vw, vh, false); + } + variationTree.draw(g, treex, treey, treew, treeh - cHeight); } else { - // I think it's more elegant to just not display anything when we don't have - // valid data --dfannius - // g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + ": ?%", - // posX + 2 * strokeRadius, posY + height - 2 * strokeRadius); + if (Lizzie.config.showComment) { + // Draw the Comment of the Sgf + drawComment(g, vx, topInset, vw, vh - topInset + vy, true); + } } - - if (validWinrate || validLastWinrate) { - int maxBarwidth = (int) (width); - int barWidthB = (int) (blackWR * maxBarwidth / 100); - int barWidthW = (int) (whiteWR * maxBarwidth / 100); - int barPosY = posY + height / 3; - int barPosxB = (int) (posX); - int barPosxW = barPosxB + barWidthB; - int barHeight = height / 3; - - // Draw winrate bars - g.fillRect(barPosxW, barPosY, barWidthW, barHeight); - g.setColor(Color.BLACK); - g.fillRect(barPosxB, barPosY, barWidthB, barHeight); - - // Show percentage above bars - g.setColor(Color.WHITE); - g.drawString(String.format("%.1f%%", blackWR), - barPosxB + 2 * strokeRadius, - posY + barHeight - 2 * strokeRadius); - String winString = String.format("%.1f%%", whiteWR); - int sw = g.getFontMetrics().stringWidth(winString); - g.drawString(winString, - barPosxB + maxBarwidth - sw - 2 * strokeRadius, - posY + barHeight - 2 * strokeRadius); - - g.setColor(Color.GRAY); - Stroke oldstroke = g.getStroke(); - Stroke dashed = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, - new float[]{4}, 0); - g.setStroke(dashed); - - for (int i = 1; i <= winRateGridLines; i++) { - int x = barPosxB + (int) (i * (maxBarwidth / (winRateGridLines + 1))); - g.drawLine(x, barPosY, x, barPosY + barHeight); - } - g.setStroke(oldstroke); + if (Lizzie.config.showSubBoard) { + try { + subBoardRenderer.setLocation(subBoardX, subBoardY); + subBoardRenderer.setBoardLength(subBoardLength); + subBoardRenderer.draw(g); + } catch (Exception e) { + // This can happen when no space is left for subboard. + } } - } + } else if (Lizzie.config.showStatus) { + drawPonderingState( + g, + resourceBundle.getString("LizzieFrame.display.loading"), + loadingX, + loadingY, + loadingSize); + } - private void drawCaptured(Graphics2D g, int posX, int posY, int width, int height) { - // Draw border - g.setColor(new Color(0, 0, 0, 130)); - g.fillRect(posX, posY, width, height); - - // border. does not include bottom edge - int strokeRadius = 3; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.drawLine(posX + strokeRadius, posY + strokeRadius, - posX - strokeRadius + width, posY + strokeRadius); - g.drawLine(posX + strokeRadius, posY + 3 * strokeRadius, - posX + strokeRadius, posY - strokeRadius + height); - g.drawLine(posX - strokeRadius + width, posY + 3 * strokeRadius, - posX - strokeRadius + width, posY - strokeRadius + height); - - // Draw middle line - g.drawLine(posX - strokeRadius + width/2, posY + 3 * strokeRadius, - posX - strokeRadius + width/2, posY - strokeRadius + height); - g.setColor(Color.white); - - // Draw black and white "stone" - int diam = height / 3; - int smallDiam = diam / 2; - int bdiam = diam, wdiam = diam; - if (Lizzie.board.inScoreMode()) { - // do nothing - } else if (Lizzie.board.getHistory().isBlacksTurn()) { - wdiam = smallDiam; - } else { - bdiam = smallDiam; - } - g.setColor(Color.black); - g.fillOval(posX + width/4 - bdiam/2, posY + height*3/8 + (diam - bdiam)/2, bdiam, bdiam); - - g.setColor(Color.WHITE); - g.fillOval(posX + width*3/4 - wdiam/2, posY + height*3/8 + (diam - wdiam)/2, wdiam, wdiam); - - // Draw captures - String bval, wval; - setPanelFont(g, (float) (width * 0.06)); - if (Lizzie.board.inScoreMode()) - { - double score[] = Lizzie.board.getScore(Lizzie.board.scoreStones()); - bval = String.format("%.0f", score[0]); - wval = String.format("%.1f", score[1]); - } else { - bval = String.format("%d", Lizzie.board.getData().blackCaptures); - wval = String.format("%d", Lizzie.board.getData().whiteCaptures); - } + if (Lizzie.config.showCaptured) drawCaptured(g, capx, capy, capw, caph); - g.setColor(Color.WHITE); - int bw = g.getFontMetrics().stringWidth(bval); - int ww = g.getFontMetrics().stringWidth(wval); - boolean largeSubBoard = Lizzie.config.showLargeSubBoard(); - int bx = (largeSubBoard ? diam : - bw/2); - int wx = (largeSubBoard ? bx : - ww/2); - - g.drawString(bval, - posX + width/4 + bx, - posY + height*7/8); - g.drawString(wval, - posX + width*3/4 + wx, - posY + height*7/8); + // cleanup + g.dispose(); } - private void setPanelFont(Graphics2D g, float size) { - Font font = OpenSansRegularBase.deriveFont(Font.PLAIN, size); - g.setFont(font); + // draw the image + Graphics2D bsGraphics = (Graphics2D) bs.getDrawGraphics(); + bsGraphics.drawImage(cachedBackground, 0, 0, null); + bsGraphics.drawImage(cachedImage, 0, 0, null); + + // cleanup + bsGraphics.dispose(); + bs.show(); + } + + /** + * temporary measure to refresh background. ideally we shouldn't need this (but we want to release + * Lizzie 0.5 today, not tomorrow!). Refactor me out please! (you need to get blurring to work + * properly on startup). + */ + public void refreshBackground() { + redrawBackgroundAnyway = true; + } + + private Graphics2D createBackground() { + cachedBackground = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); + cachedBackgroundWidth = cachedBackground.getWidth(); + cachedBackgroundHeight = cachedBackground.getHeight(); + cachedBackgroundShowControls = showControls; + cachedShowWinrate = Lizzie.config.showWinrate; + cachedShowVariationGraph = Lizzie.config.showVariationGraph; + + redrawBackgroundAnyway = false; + + Graphics2D g = cachedBackground.createGraphics(); + + BufferedImage wallpaper = boardRenderer.getWallpaper(); + int drawWidth = Math.max(wallpaper.getWidth(), getWidth()); + int drawHeight = Math.max(wallpaper.getHeight(), getHeight()); + // Support seamless texture + boardRenderer.drawTextureImage(g, wallpaper, 0, 0, drawWidth, drawHeight); + + return g; + } + + private void drawVariationTreeContainer(Graphics2D g, int vx, int vy, int vw, int vh) { + vw = cachedBackground.getWidth() - vx; + + if (g == null || vw <= 0 || vh <= 0) return; + + BufferedImage result = new BufferedImage(vw, vh, BufferedImage.TYPE_INT_ARGB); + filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); + g.drawImage(result, vx, vy, null); + } + + private void drawPonderingState(Graphics2D g, String text, int x, int y, double size) { + Font font = + new Font( + systemDefaultFontName, Font.PLAIN, (int) (Math.max(getWidth(), getHeight()) * size)); + FontMetrics fm = g.getFontMetrics(font); + int stringWidth = fm.stringWidth(text); + int stringHeight = fm.getAscent() - fm.getDescent(); + int width = stringWidth; + int height = (int) (stringHeight * 1.2); + + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + // commenting this out for now... always causing an exception on startup. will fix in the + // upcoming refactoring + // filter20.filter(cachedBackground.getSubimage(x, y, result.getWidth(), + // result.getHeight()), result); + g.drawImage(result, x, y, null); + + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(x, y, width, height); + g.drawRect(x, y, width, height); + + g.setColor(Color.white); + g.setFont(font); + g.drawString( + text, x + (width - stringWidth) / 2, y + stringHeight + (height - stringHeight) / 2); + } + + private void drawWinrateGraphContainer(Graphics g, int statx, int staty, int statw, int stath) { + if (g == null || statw <= 0 || stath <= 0) return; + + BufferedImage result = new BufferedImage(statw, stath + statw, BufferedImage.TYPE_INT_ARGB); + filter20.filter( + cachedBackground.getSubimage(statx, staty, result.getWidth(), result.getHeight()), result); + g.drawImage(result, statx, staty, null); + } + + private GaussianFilter filter20 = new GaussianFilter(20); + private GaussianFilter filter10 = new GaussianFilter(10); + + /** Display the controls */ + void drawControls() { + userAlreadyKnowsAboutCommandString = true; + + cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + + // redraw background + createBackground(); + + List commandsToShow = new ArrayList<>(Arrays.asList(commands)); + if (Lizzie.leelaz.getDynamicKomi() != null) { + commandsToShow.add(resourceBundle.getString("LizzieFrame.commands.keyD")); } - /** - * Checks whether or not something was clicked and performs the appropriate action - * - * @param x x coordinate - * @param y y coordinate - */ - public void onClicked(int x, int y) { - // check for board click - int[] boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); - int moveNumber = winrateGraph.moveNumber(x, y); - - if (boardCoordinates != null) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (!isPlayingAgainstLeelaz || (playerIsBlack == Lizzie.board.getData().blackToPlay)) - Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); - } - if (Lizzie.config.showWinrate && moveNumber >= 0) { - isPlayingAgainstLeelaz = false; - Lizzie.board.goToMoveNumberBeyondBranch(moveNumber); - } - if (Lizzie.config.showSubBoard && subBoardRenderer.isInside(x, y)) { - Lizzie.config.toggleLargeSubBoard(); - } - repaint(); + Graphics2D g = cachedImage.createGraphics(); + + int maxSize = Math.min(getWidth(), getHeight()); + Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.034)); + g.setFont(font); + FontMetrics metrics = g.getFontMetrics(font); + int maxCommandWidth = + commandsToShow + .stream() + .reduce( + 0, + (Integer i, String command) -> Math.max(i, metrics.stringWidth(command)), + (Integer a, Integer b) -> Math.max(a, b)); + int lineHeight = (int) (font.getSize() * 1.15); + + int boxWidth = Util.clamp((int) (maxCommandWidth * 1.4), 0, getWidth()); + int boxHeight = Util.clamp(commandsToShow.size() * lineHeight, 0, getHeight()); + + int commandsX = Util.clamp(getWidth() / 2 - boxWidth / 2, 0, getWidth()); + int commandsY = Util.clamp(getHeight() / 2 - boxHeight / 2, 0, getHeight()); + + BufferedImage result = new BufferedImage(boxWidth, boxHeight, BufferedImage.TYPE_INT_ARGB); + filter10.filter( + cachedBackground.getSubimage(commandsX, commandsY, boxWidth, boxHeight), result); + g.drawImage(result, commandsX, commandsY, null); + + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(commandsX, commandsY, boxWidth, boxHeight); + int strokeRadius = 2; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.setColor(new Color(0, 0, 0, 60)); + g.drawRect( + commandsX + strokeRadius, + commandsY + strokeRadius, + boxWidth - 2 * strokeRadius, + boxHeight - 2 * strokeRadius); + + int verticalLineX = (int) (commandsX + boxWidth * 0.3); + g.setColor(new Color(0, 0, 0, 60)); + g.drawLine( + verticalLineX, + commandsY + 2 * strokeRadius, + verticalLineX, + commandsY + boxHeight - 2 * strokeRadius); + + g.setStroke(new BasicStroke(1)); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g.setColor(Color.WHITE); + int lineOffset = commandsY; + for (String command : commandsToShow) { + String[] split = command.split("\\|"); + g.drawString( + split[0], + verticalLineX - metrics.stringWidth(split[0]) - strokeRadius * 4, + font.getSize() + lineOffset); + g.drawString(split[1], verticalLineX + strokeRadius * 4, font.getSize() + lineOffset); + lineOffset += lineHeight; } - public boolean playCurrentVariation() { - List variation = boardRenderer.variation; - boolean onVariation = (variation != null); - if (onVariation) { - for (int i = 0; i < variation.size(); i++) { - int[] boardCoordinates = Board.convertNameToCoordinates(variation.get(i)); - if (boardCoordinates != null) - Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); - } - } - return onVariation; + refreshBackground(); + } + + private boolean userAlreadyKnowsAboutCommandString = false; + + private void drawCommandString(Graphics2D g) { + if (userAlreadyKnowsAboutCommandString) return; + + int maxSize = (int) (Math.min(getWidth(), getHeight()) * 0.98); + + Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.03)); + String commandString = resourceBundle.getString("LizzieFrame.prompt.showControlsHint"); + int strokeRadius = 2; + + int showCommandsHeight = (int) (font.getSize() * 1.1); + int showCommandsWidth = g.getFontMetrics(font).stringWidth(commandString) + 4 * strokeRadius; + int showCommandsX = this.getInsets().left; + int showCommandsY = getHeight() - showCommandsHeight - this.getInsets().bottom; + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(showCommandsX, showCommandsY, showCommandsWidth, showCommandsHeight); + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.setColor(new Color(0, 0, 0, 60)); + g.drawRect( + showCommandsX + strokeRadius, + showCommandsY + strokeRadius, + showCommandsWidth - 2 * strokeRadius, + showCommandsHeight - 2 * strokeRadius); + g.setStroke(new BasicStroke(1)); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(Color.WHITE); + g.setFont(font); + g.drawString(commandString, showCommandsX + 2 * strokeRadius, showCommandsY + font.getSize()); + } + + private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int height) { + if (width < 0 || height < 0) return; // we don't have enough space + + double lastWR = 50; // winrate the previous move + boolean validLastWinrate = false; // whether it was actually calculated + BoardData lastNode = Lizzie.board.getHistory().getPrevious(); + if (lastNode != null && lastNode.playouts > 0) { + lastWR = lastNode.winrate; + validLastWinrate = true; } - public void playBestMove() { - String bestCoordinateName = boardRenderer.bestMoveCoordinateName(); - if (bestCoordinateName == null) - return; - int[] boardCoordinates = Board.convertNameToCoordinates(bestCoordinateName); - if (boardCoordinates != null) { - Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); - } - } + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + double curWR = stats.maxWinrate; // winrate on this move + boolean validWinrate = (stats.totalPlayouts > 0); // and whether it was actually calculated + if (isPlayingAgainstLeelaz && playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) + validWinrate = false; - public void onMouseMoved(int x, int y) { - int[] newMouseHoverCoordinate = boardRenderer.convertScreenToCoordinates(x, y); - if (mouseHoverCoordinate != null && newMouseHoverCoordinate != null && (mouseHoverCoordinate[0] != newMouseHoverCoordinate[0] || mouseHoverCoordinate[1] != newMouseHoverCoordinate[1])) { - mouseHoverCoordinate = newMouseHoverCoordinate; - repaint(); - } else { - mouseHoverCoordinate = newMouseHoverCoordinate; - } + if (!validWinrate) { + curWR = 100 - lastWR; // display last move's winrate for now (with color difference) } - - public void onMouseDragged(int x, int y) { - int moveNumber = winrateGraph.moveNumber(x, y); - if (Lizzie.config.showWinrate && moveNumber >= 0) { - if (Lizzie.board.goToMoveNumberWithinBranch(moveNumber)) { - repaint(); - } - } + double whiteWR, blackWR; + if (Lizzie.board.getData().blackToPlay) { + blackWR = curWR; + } else { + blackWR = 100 - curWR; } - /** - * Process Comment Mouse Wheel Moved - * - * @return true when the scroll event was processed by this method - */ - public boolean processCommentMouseWheelMoved(MouseWheelEvent e) { - if (Lizzie.config.showComment && commentRect != null && commentRect.contains(e.getX(), e.getY())) { - scrollPane.dispatchEvent(e); - createCommentImage(true, 0, 0); - getGraphics().drawImage(commentImage, commentRect.x, commentRect.y, commentRect.width, commentRect.height, null); - return true; - } else { - return false; - } - } + whiteWR = 100 - blackWR; + + // Background rectangle + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(posX, posY, width, height); + + // border. does not include bottom edge + int strokeRadius = 3; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.drawLine( + posX + strokeRadius, posY + strokeRadius, + posX - strokeRadius + width, posY + strokeRadius); + g.drawLine( + posX + strokeRadius, posY + 3 * strokeRadius, + posX + strokeRadius, posY - strokeRadius + height); + g.drawLine( + posX - strokeRadius + width, posY + 3 * strokeRadius, + posX - strokeRadius + width, posY - strokeRadius + height); + + // resize the box now so it's inside the border + posX += 2 * strokeRadius; + posY += 2 * strokeRadius; + width -= 4 * strokeRadius; + height -= 4 * strokeRadius; + + // Title + strokeRadius = 2; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(Color.WHITE); + setPanelFont(g, (int) (Math.min(width, height) * 0.2)); + + // Last move + if (validLastWinrate && validWinrate) { + String text; + if (Lizzie.config.handicapInsteadOfWinrate) { + text = + String.format( + ": %.2f", + Lizzie.leelaz.winrateToHandicap(100 - curWR) + - Lizzie.leelaz.winrateToHandicap(lastWR)); + } else { + text = String.format(": %.1f%%", 100 - lastWR - curWR); + } - /** - * Create comment cached image - * - * @param forceRefresh - * @param w - * @param h - */ - public void createCommentImage(boolean forceRefresh, int w, int h) { - if (forceRefresh || commentImage == null || scrollPane.getWidth() != w || scrollPane.getHeight() != h) { - if (w > 0 && h > 0) { - scrollPane.setSize(w, h); - } - commentImage = new BufferedImage(scrollPane.getWidth(), scrollPane.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = commentImage.createGraphics(); - scrollPane.doLayout(); - scrollPane.addNotify(); - scrollPane.validate(); - scrollPane.printAll(g2); - g2.dispose(); - } + g.drawString( + resourceBundle.getString("LizzieFrame.display.lastMove") + text, + posX + 2 * strokeRadius, + posY + height - 2 * strokeRadius); // - font.getSize()); + } else { + // I think it's more elegant to just not display anything when we don't have + // valid data --dfannius + // g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + ": ?%", + // posX + 2 * strokeRadius, posY + height - 2 * strokeRadius); } - private void autosaveMaybe() { - int interval = Lizzie.config.config.getJSONObject("ui").getInt("autosave-interval-seconds") * 1000; - long currentTime = System.currentTimeMillis(); - if (interval > 0 && currentTime - lastAutosaveTime >= interval) { - Lizzie.board.autosave(); - lastAutosaveTime = currentTime; - } + if (validWinrate || validLastWinrate) { + int maxBarwidth = (int) (width); + int barWidthB = (int) (blackWR * maxBarwidth / 100); + int barWidthW = (int) (whiteWR * maxBarwidth / 100); + int barPosY = posY + height / 3; + int barPosxB = (int) (posX); + int barPosxW = barPosxB + barWidthB; + int barHeight = height / 3; + + // Draw winrate bars + g.fillRect(barPosxW, barPosY, barWidthW, barHeight); + g.setColor(Color.BLACK); + g.fillRect(barPosxB, barPosY, barWidthB, barHeight); + + // Show percentage above bars + g.setColor(Color.WHITE); + g.drawString( + String.format("%.1f%%", blackWR), + barPosxB + 2 * strokeRadius, + posY + barHeight - 2 * strokeRadius); + String winString = String.format("%.1f%%", whiteWR); + int sw = g.getFontMetrics().stringWidth(winString); + g.drawString( + winString, + barPosxB + maxBarwidth - sw - 2 * strokeRadius, + posY + barHeight - 2 * strokeRadius); + + g.setColor(Color.GRAY); + Stroke oldstroke = g.getStroke(); + Stroke dashed = + new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] {4}, 0); + g.setStroke(dashed); + + for (int i = 1; i <= winRateGridLines; i++) { + int x = barPosxB + (int) (i * (maxBarwidth / (winRateGridLines + 1))); + g.drawLine(x, barPosY, x, barPosY + barHeight); + } + g.setStroke(oldstroke); } - - public void toggleCoordinates() { - showCoordinates = !showCoordinates; + } + + private void drawCaptured(Graphics2D g, int posX, int posY, int width, int height) { + // Draw border + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(posX, posY, width, height); + + // border. does not include bottom edge + int strokeRadius = 3; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.drawLine( + posX + strokeRadius, posY + strokeRadius, posX - strokeRadius + width, posY + strokeRadius); + g.drawLine( + posX + strokeRadius, + posY + 3 * strokeRadius, + posX + strokeRadius, + posY - strokeRadius + height); + g.drawLine( + posX - strokeRadius + width, + posY + 3 * strokeRadius, + posX - strokeRadius + width, + posY - strokeRadius + height); + + // Draw middle line + g.drawLine( + posX - strokeRadius + width / 2, + posY + 3 * strokeRadius, + posX - strokeRadius + width / 2, + posY - strokeRadius + height); + g.setColor(Color.white); + + // Draw black and white "stone" + int diam = height / 3; + int smallDiam = diam / 2; + int bdiam = diam, wdiam = diam; + if (Lizzie.board.inScoreMode()) { + // do nothing + } else if (Lizzie.board.getHistory().isBlacksTurn()) { + wdiam = smallDiam; + } else { + bdiam = smallDiam; } - - public void setPlayers(String whitePlayer, String blackPlayer) { - setTitle(String.format("%s (%s [W] vs %s [B])", DEFAULT_TITLE, - whitePlayer, blackPlayer)); + g.setColor(Color.black); + g.fillOval( + posX + width / 4 - bdiam / 2, posY + height * 3 / 8 + (diam - bdiam) / 2, bdiam, bdiam); + + g.setColor(Color.WHITE); + g.fillOval( + posX + width * 3 / 4 - wdiam / 2, posY + height * 3 / 8 + (diam - wdiam) / 2, wdiam, wdiam); + + // Draw captures + String bval, wval; + setPanelFont(g, (float) (width * 0.06)); + if (Lizzie.board.inScoreMode()) { + double score[] = Lizzie.board.getScore(Lizzie.board.scoreStones()); + bval = String.format("%.0f", score[0]); + wval = String.format("%.1f", score[1]); + } else { + bval = String.format("%d", Lizzie.board.getData().blackCaptures); + wval = String.format("%d", Lizzie.board.getData().whiteCaptures); } - private void setDisplayedBranchLength(int n) { - boardRenderer.setDisplayedBranchLength(n); + g.setColor(Color.WHITE); + int bw = g.getFontMetrics().stringWidth(bval); + int ww = g.getFontMetrics().stringWidth(wval); + boolean largeSubBoard = Lizzie.config.showLargeSubBoard(); + int bx = (largeSubBoard ? diam : -bw / 2); + int wx = (largeSubBoard ? bx : -ww / 2); + + g.drawString(bval, posX + width / 4 + bx, posY + height * 7 / 8); + g.drawString(wval, posX + width * 3 / 4 + wx, posY + height * 7 / 8); + } + + private void setPanelFont(Graphics2D g, float size) { + Font font = OpenSansRegularBase.deriveFont(Font.PLAIN, size); + g.setFont(font); + } + + /** + * Checks whether or not something was clicked and performs the appropriate action + * + * @param x x coordinate + * @param y y coordinate + */ + public void onClicked(int x, int y) { + // check for board click + int[] boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); + int moveNumber = winrateGraph.moveNumber(x, y); + + if (boardCoordinates != null) { + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (!isPlayingAgainstLeelaz || (playerIsBlack == Lizzie.board.getData().blackToPlay)) + Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); } - - public void startRawBoard() { - boolean onBranch = boardRenderer.isShowingBranch(); - int n = (onBranch ? 1 : BoardRenderer.SHOW_RAW_BOARD); - boardRenderer.setDisplayedBranchLength(n); + if (Lizzie.config.showWinrate && moveNumber >= 0) { + isPlayingAgainstLeelaz = false; + Lizzie.board.goToMoveNumberBeyondBranch(moveNumber); } - - public void stopRawBoard() { - boardRenderer.setDisplayedBranchLength(BoardRenderer.SHOW_NORMAL_BOARD); + if (Lizzie.config.showSubBoard && subBoardRenderer.isInside(x, y)) { + Lizzie.config.toggleLargeSubBoard(); } - - public boolean incrementDisplayedBranchLength(int n) { - return boardRenderer.incrementDisplayedBranchLength(n); + repaint(); + } + + public boolean playCurrentVariation() { + List variation = boardRenderer.variation; + boolean onVariation = (variation != null); + if (onVariation) { + for (int i = 0; i < variation.size(); i++) { + int[] boardCoordinates = Board.convertNameToCoordinates(variation.get(i)); + if (boardCoordinates != null) Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); + } } - - public void resetTitle() { - setTitle(DEFAULT_TITLE); + return onVariation; + } + + public void playBestMove() { + String bestCoordinateName = boardRenderer.bestMoveCoordinateName(); + if (bestCoordinateName == null) return; + int[] boardCoordinates = Board.convertNameToCoordinates(bestCoordinateName); + if (boardCoordinates != null) { + Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); } - - public void copySgf() { - try { - // Get sgf content from game - String sgfContent = SGFParser.saveToString(); - - // Save to clipboard - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - Transferable transferableString = new StringSelection(sgfContent); - clipboard.setContents(transferableString, null); - } catch (Exception e) { - e.printStackTrace(); - } + } + + public void onMouseMoved(int x, int y) { + int[] newMouseHoverCoordinate = boardRenderer.convertScreenToCoordinates(x, y); + if (mouseHoverCoordinate != null + && newMouseHoverCoordinate != null + && (mouseHoverCoordinate[0] != newMouseHoverCoordinate[0] + || mouseHoverCoordinate[1] != newMouseHoverCoordinate[1])) { + mouseHoverCoordinate = newMouseHoverCoordinate; + repaint(); + } else { + mouseHoverCoordinate = newMouseHoverCoordinate; } + } - public void pasteSgf() { - try { - String sgfContent = null; - // Get string from clipboard - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - Transferable clipboardContents = clipboard.getContents(null); - if (clipboardContents != null) { - if (clipboardContents.isDataFlavorSupported(DataFlavor.stringFlavor)) { - sgfContent = (String) clipboardContents.getTransferData(DataFlavor.stringFlavor); - } - } - - // load game contents from sgf string - if (sgfContent != null && !sgfContent.isEmpty()) { - SGFParser.loadFromString(sgfContent); - } - } catch (Exception e) { - e.printStackTrace(); - } + public void onMouseDragged(int x, int y) { + int moveNumber = winrateGraph.moveNumber(x, y); + if (Lizzie.config.showWinrate && moveNumber >= 0) { + if (Lizzie.board.goToMoveNumberWithinBranch(moveNumber)) { + repaint(); + } } - - public void increaseMaxAlpha(int k) { - boardRenderer.increaseMaxAlpha(k); + } + + /** + * Process Comment Mouse Wheel Moved + * + * @return true when the scroll event was processed by this method + */ + public boolean processCommentMouseWheelMoved(MouseWheelEvent e) { + if (Lizzie.config.showComment + && commentRect != null + && commentRect.contains(e.getX(), e.getY())) { + scrollPane.dispatchEvent(e); + createCommentImage(true, 0, 0); + getGraphics() + .drawImage( + commentImage, + commentRect.x, + commentRect.y, + commentRect.width, + commentRect.height, + null); + return true; + } else { + return false; } - - /** - * Draw the Comment of the Sgf file - * - * @param g - * @param x - * @param y - * @param w - * @param h - * @param full - * @return - */ - private int drawComment(Graphics2D g, int x, int y, int w, int h, boolean full) { - String comment = (Lizzie.board.getHistory().getData() != null && Lizzie.board.getHistory().getData().comment != null) ? Lizzie.board.getHistory().getData().comment : ""; - int cHeight = full ? h : (int)(h*0.5); - int fontSize = (int)(Math.min(getWidth(), getHeight()) * 0.0294); - if (Lizzie.config.commentFontSize > 0) { - fontSize = Lizzie.config.commentFontSize; - } else if (fontSize < 16) { - fontSize = 16; + } + + /** + * Create comment cached image + * + * @param forceRefresh + * @param w + * @param h + */ + public void createCommentImage(boolean forceRefresh, int w, int h) { + if (forceRefresh + || commentImage == null + || scrollPane.getWidth() != w + || scrollPane.getHeight() != h) { + if (w > 0 && h > 0) { + scrollPane.setSize(w, h); + } + commentImage = + new BufferedImage( + scrollPane.getWidth(), scrollPane.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = commentImage.createGraphics(); + scrollPane.doLayout(); + scrollPane.addNotify(); + scrollPane.validate(); + scrollPane.printAll(g2); + g2.dispose(); + } + } + + private void autosaveMaybe() { + int interval = + Lizzie.config.config.getJSONObject("ui").getInt("autosave-interval-seconds") * 1000; + long currentTime = System.currentTimeMillis(); + if (interval > 0 && currentTime - lastAutosaveTime >= interval) { + Lizzie.board.autosave(); + lastAutosaveTime = currentTime; + } + } + + public void toggleCoordinates() { + showCoordinates = !showCoordinates; + } + + public void setPlayers(String whitePlayer, String blackPlayer) { + setTitle(String.format("%s (%s [W] vs %s [B])", DEFAULT_TITLE, whitePlayer, blackPlayer)); + } + + private void setDisplayedBranchLength(int n) { + boardRenderer.setDisplayedBranchLength(n); + } + + public void startRawBoard() { + boolean onBranch = boardRenderer.isShowingBranch(); + int n = (onBranch ? 1 : BoardRenderer.SHOW_RAW_BOARD); + boardRenderer.setDisplayedBranchLength(n); + } + + public void stopRawBoard() { + boardRenderer.setDisplayedBranchLength(BoardRenderer.SHOW_NORMAL_BOARD); + } + + public boolean incrementDisplayedBranchLength(int n) { + return boardRenderer.incrementDisplayedBranchLength(n); + } + + public void resetTitle() { + setTitle(DEFAULT_TITLE); + } + + public void copySgf() { + try { + // Get sgf content from game + String sgfContent = SGFParser.saveToString(); + + // Save to clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable transferableString = new StringSelection(sgfContent); + clipboard.setContents(transferableString, null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void pasteSgf() { + try { + String sgfContent = null; + // Get string from clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable clipboardContents = clipboard.getContents(null); + if (clipboardContents != null) { + if (clipboardContents.isDataFlavorSupported(DataFlavor.stringFlavor)) { + sgfContent = (String) clipboardContents.getTransferData(DataFlavor.stringFlavor); } - Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); - commentPane.setFont(font); - commentPane.setText(comment); - commentPane.setSize(w, cHeight); - createCommentImage(comment != null && !comment.equals(this.cachedComment), w, cHeight); - commentRect = new Rectangle(x, y + (h - cHeight), scrollPane.getWidth(), scrollPane.getHeight()); - g.drawImage(commentImage, commentRect.x, commentRect.y, commentRect.width, commentRect.height, null); - cachedComment = comment; - return cHeight; + } + + // load game contents from sgf string + if (sgfContent != null && !sgfContent.isEmpty()) { + SGFParser.loadFromString(sgfContent); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void increaseMaxAlpha(int k) { + boardRenderer.increaseMaxAlpha(k); + } + + /** + * Draw the Comment of the Sgf file + * + * @param g + * @param x + * @param y + * @param w + * @param h + * @param full + * @return + */ + private int drawComment(Graphics2D g, int x, int y, int w, int h, boolean full) { + String comment = + (Lizzie.board.getHistory().getData() != null + && Lizzie.board.getHistory().getData().comment != null) + ? Lizzie.board.getHistory().getData().comment + : ""; + int cHeight = full ? h : (int) (h * 0.5); + int fontSize = (int) (Math.min(getWidth(), getHeight()) * 0.0294); + if (Lizzie.config.commentFontSize > 0) { + fontSize = Lizzie.config.commentFontSize; + } else if (fontSize < 16) { + fontSize = 16; } + Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); + commentPane.setFont(font); + commentPane.setText(comment); + commentPane.setSize(w, cHeight); + createCommentImage(comment != null && !comment.equals(this.cachedComment), w, cHeight); + commentRect = + new Rectangle(x, y + (h - cHeight), scrollPane.getWidth(), scrollPane.getHeight()); + g.drawImage( + commentImage, commentRect.x, commentRect.y, commentRect.width, commentRect.height, null); + cachedComment = comment; + return cHeight; + } } diff --git a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java index 1d3a8389a..585676d10 100644 --- a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java +++ b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java @@ -5,185 +5,194 @@ package featurecat.lizzie.gui; import featurecat.lizzie.analysis.GameInfo; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; import java.awt.*; import java.text.DecimalFormat; import java.text.ParseException; +import javax.swing.*; +import javax.swing.border.EmptyBorder; -/** - * @author unknown - */ +/** @author unknown */ public class NewGameDialog extends JDialog { - // create formatters - public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); - public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); - public static final JLabel PLACEHOLDER = new JLabel(""); - - static { - FORMAT_HANDICAP.setMaximumIntegerDigits(1); + // create formatters + public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); + public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); + public static final JLabel PLACEHOLDER = new JLabel(""); + + static { + FORMAT_HANDICAP.setMaximumIntegerDigits(1); + } + + private JPanel dialogPane = new JPanel(); + private JPanel contentPanel = new JPanel(); + private JPanel buttonBar = new JPanel(); + private JButton okButton = new JButton(); + + private JCheckBox checkBoxPlayerIsBlack; + private JTextField textFieldBlack; + private JTextField textFieldWhite; + private JTextField textFieldKomi; + private JTextField textFieldHandicap; + + private boolean cancelled = true; + private GameInfo gameInfo; + + public NewGameDialog() { + initComponents(); + } + + private void initComponents() { + setMinimumSize(new Dimension(100, 100)); + setResizable(false); + setTitle("New Game"); + setModal(true); + + Container contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + initDialogPane(contentPane); + + pack(); + setLocationRelativeTo(getOwner()); + } + + private void initDialogPane(Container contentPane) { + dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); + dialogPane.setLayout(new BorderLayout()); + + initContentPanel(); + initButtonBar(); + + contentPane.add(dialogPane, BorderLayout.CENTER); + } + + private void initContentPanel() { + GridLayout gridLayout = new GridLayout(5, 2, 4, 4); + contentPanel.setLayout(gridLayout); + + checkBoxPlayerIsBlack = new JCheckBox("Play black?", true); + checkBoxPlayerIsBlack.addChangeListener(evt -> togglePlayerIsBlack()); + textFieldWhite = new JTextField(); + textFieldBlack = new JTextField(); + textFieldKomi = new JFormattedTextField(FORMAT_KOMI); + textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); + textFieldHandicap.addPropertyChangeListener(evt -> modifyHandicap()); + + contentPanel.add(checkBoxPlayerIsBlack); + contentPanel.add(PLACEHOLDER); + contentPanel.add(new JLabel("Black")); + contentPanel.add(textFieldBlack); + contentPanel.add(new JLabel("White")); + contentPanel.add(textFieldWhite); + contentPanel.add(new JLabel("Komi")); + contentPanel.add(textFieldKomi); + contentPanel.add(new JLabel("Handicap")); + contentPanel.add(textFieldHandicap); + + textFieldKomi.setEnabled(false); + + dialogPane.add(contentPanel, BorderLayout.CENTER); + } + + private void togglePlayerIsBlack() { + JTextField humanTextField = playerIsBlack() ? textFieldBlack : textFieldWhite; + JTextField computerTextField = playerIsBlack() ? textFieldWhite : textFieldBlack; + + humanTextField.setEnabled(true); + humanTextField.setText(GameInfo.DEFAULT_NAME_HUMAN_PLAYER); + computerTextField.setEnabled(false); + computerTextField.setText(GameInfo.DEFAULT_NAME_CPU_PLAYER); + } + + private void modifyHandicap() { + try { + int handicap = FORMAT_HANDICAP.parse(textFieldHandicap.getText()).intValue(); + if (handicap < 0) throw new IllegalArgumentException(); + + textFieldKomi.setText(FORMAT_KOMI.format(GameInfo.DEFAULT_KOMI)); + } catch (ParseException | RuntimeException e) { + // do not correct user mistakes } - - private JPanel dialogPane = new JPanel(); - private JPanel contentPanel = new JPanel(); - private JPanel buttonBar = new JPanel(); - private JButton okButton = new JButton(); - - private JCheckBox checkBoxPlayerIsBlack; - private JTextField textFieldBlack; - private JTextField textFieldWhite; - private JTextField textFieldKomi; - private JTextField textFieldHandicap; - - private boolean cancelled = true; - private GameInfo gameInfo; - - public NewGameDialog() { - initComponents(); + } + + private void initButtonBar() { + buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); + buttonBar.setLayout(new GridBagLayout()); + ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[] {0, 80}; + ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0}; + + // ---- okButton ---- + okButton.setText("OK"); + okButton.addActionListener(e -> apply()); + + buttonBar.add( + okButton, + new GridBagConstraints( + 1, + 0, + 1, + 1, + 0.0, + 0.0, + GridBagConstraints.CENTER, + GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), + 0, + 0)); + + dialogPane.add(buttonBar, BorderLayout.SOUTH); + } + + public void apply() { + try { + // validate data + String playerBlack = textFieldBlack.getText(); + String playerWhite = textFieldWhite.getText(); + double komi = FORMAT_KOMI.parse(textFieldKomi.getText()).doubleValue(); + int handicap = FORMAT_HANDICAP.parse(textFieldHandicap.getText()).intValue(); + + // apply new values + gameInfo.setPlayerBlack(playerBlack); + gameInfo.setPlayerWhite(playerWhite); + gameInfo.setKomi(komi); + gameInfo.setHandicap(handicap); + + // close window + cancelled = false; + setVisible(false); + } catch (ParseException e) { + // hide input mistakes. } - - private void initComponents() { - setMinimumSize(new Dimension(100, 100)); - setResizable(false); - setTitle("New Game"); - setModal(true); - - Container contentPane = getContentPane(); - contentPane.setLayout(new BorderLayout()); - - initDialogPane(contentPane); - - pack(); - setLocationRelativeTo(getOwner()); - } - - private void initDialogPane(Container contentPane) { - dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); - dialogPane.setLayout(new BorderLayout()); - - initContentPanel(); - initButtonBar(); - - contentPane.add(dialogPane, BorderLayout.CENTER); - } - - private void initContentPanel() { - GridLayout gridLayout = new GridLayout(5, 2, 4, 4); - contentPanel.setLayout(gridLayout); - - checkBoxPlayerIsBlack = new JCheckBox("Play black?", true); - checkBoxPlayerIsBlack.addChangeListener(evt -> togglePlayerIsBlack()); - textFieldWhite = new JTextField(); - textFieldBlack = new JTextField(); - textFieldKomi = new JFormattedTextField(FORMAT_KOMI); - textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); - textFieldHandicap.addPropertyChangeListener(evt -> modifyHandicap()); - - contentPanel.add(checkBoxPlayerIsBlack); - contentPanel.add(PLACEHOLDER); - contentPanel.add(new JLabel("Black")); - contentPanel.add(textFieldBlack); - contentPanel.add(new JLabel("White")); - contentPanel.add(textFieldWhite); - contentPanel.add(new JLabel("Komi")); - contentPanel.add(textFieldKomi); - contentPanel.add(new JLabel("Handicap")); - contentPanel.add(textFieldHandicap); - - textFieldKomi.setEnabled(false); - - dialogPane.add(contentPanel, BorderLayout.CENTER); - } - - private void togglePlayerIsBlack() { - JTextField humanTextField = playerIsBlack() ? textFieldBlack : textFieldWhite; - JTextField computerTextField = playerIsBlack() ? textFieldWhite : textFieldBlack; - - humanTextField.setEnabled(true); - humanTextField.setText(GameInfo.DEFAULT_NAME_HUMAN_PLAYER); - computerTextField.setEnabled(false); - computerTextField.setText(GameInfo.DEFAULT_NAME_CPU_PLAYER); - } - - private void modifyHandicap() { - try { - int handicap = FORMAT_HANDICAP.parse(textFieldHandicap.getText()).intValue(); - if (handicap < 0) throw new IllegalArgumentException(); - - textFieldKomi.setText(FORMAT_KOMI.format(GameInfo.DEFAULT_KOMI)); - } catch (ParseException | RuntimeException e) { - // do not correct user mistakes - } - } - - private void initButtonBar() { - buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); - buttonBar.setLayout(new GridBagLayout()); - ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 80}; - ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0}; - - //---- okButton ---- - okButton.setText("OK"); - okButton.addActionListener(e -> apply()); - - buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); - - dialogPane.add(buttonBar, BorderLayout.SOUTH); - } - - public void apply() { - try { - // validate data - String playerBlack = textFieldBlack.getText(); - String playerWhite = textFieldWhite.getText(); - double komi = FORMAT_KOMI.parse(textFieldKomi.getText()).doubleValue(); - int handicap = FORMAT_HANDICAP.parse(textFieldHandicap.getText()).intValue(); - - // apply new values - gameInfo.setPlayerBlack(playerBlack); - gameInfo.setPlayerWhite(playerWhite); - gameInfo.setKomi(komi); - gameInfo.setHandicap(handicap); - - // close window - cancelled = false; - setVisible(false); - } catch (ParseException e) { - // hide input mistakes. - } - } - - public void setGameInfo(GameInfo gameInfo) { - this.gameInfo = gameInfo; - - textFieldBlack.setText(gameInfo.getPlayerBlack()); - textFieldWhite.setText(gameInfo.getPlayerWhite()); - textFieldHandicap.setText(FORMAT_HANDICAP.format(gameInfo.getHandicap())); - textFieldKomi.setText(FORMAT_KOMI.format(gameInfo.getKomi())); - - // update player names - togglePlayerIsBlack(); - } - - public boolean playerIsBlack() { - return checkBoxPlayerIsBlack.isSelected(); - } - - public boolean isCancelled() { - return cancelled; - } - - public static void main(String[] args) { - EventQueue.invokeLater(() -> { - try { - NewGameDialog window = new NewGameDialog(); - window.setVisible(true); - } catch (Exception e) { - e.printStackTrace(); - } + } + + public void setGameInfo(GameInfo gameInfo) { + this.gameInfo = gameInfo; + + textFieldBlack.setText(gameInfo.getPlayerBlack()); + textFieldWhite.setText(gameInfo.getPlayerWhite()); + textFieldHandicap.setText(FORMAT_HANDICAP.format(gameInfo.getHandicap())); + textFieldKomi.setText(FORMAT_KOMI.format(gameInfo.getKomi())); + + // update player names + togglePlayerIsBlack(); + } + + public boolean playerIsBlack() { + return checkBoxPlayerIsBlack.isSelected(); + } + + public boolean isCancelled() { + return cancelled; + } + + public static void main(String[] args) { + EventQueue.invokeLater( + () -> { + try { + NewGameDialog window = new NewGameDialog(); + window.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } }); - } + } } diff --git a/src/main/java/featurecat/lizzie/gui/VariationTree.java b/src/main/java/featurecat/lizzie/gui/VariationTree.java index 6c873dc0e..72f072bf0 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -3,143 +3,164 @@ import featurecat.lizzie.Lizzie; import featurecat.lizzie.rules.BoardHistoryList; import featurecat.lizzie.rules.BoardHistoryNode; - import java.awt.*; import java.util.ArrayList; public class VariationTree { - private int YSPACING; - private int XSPACING; - private int DOT_DIAM = 11; // Should be odd number - - private ArrayList laneUsageList; - private BoardHistoryNode curMove; - - public VariationTree() - { - laneUsageList = new ArrayList(); + private int YSPACING; + private int XSPACING; + private int DOT_DIAM = 11; // Should be odd number + + private ArrayList laneUsageList; + private BoardHistoryNode curMove; + + public VariationTree() { + laneUsageList = new ArrayList(); + } + + public void drawTree( + Graphics2D g, + int posx, + int posy, + int startLane, + int maxposy, + BoardHistoryNode startNode, + int variationNumber, + boolean isMain) { + if (isMain) g.setColor(Color.white); + else g.setColor(Color.gray.brighter()); + + // Finds depth on leftmost variation of this tree + int depth = BoardHistoryList.getDepth(startNode) + 1; + int lane = startLane; + // Figures out how far out too the right (which lane) we have to go not to collide with other + // variations + while (lane < laneUsageList.size() + && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { + // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" + laneUsageList.set(lane, startNode.getData().moveNumber - 1); + lane++; } - - public void drawTree(Graphics2D g, int posx, int posy, int startLane, int maxposy, BoardHistoryNode startNode, int variationNumber, boolean isMain) - { - if (isMain) g.setColor(Color.white); - else g.setColor(Color.gray.brighter()); - - - // Finds depth on leftmost variation of this tree - int depth = BoardHistoryList.getDepth(startNode) + 1; - int lane = startLane; - // Figures out how far out too the right (which lane) we have to go not to collide with other variations - while (lane < laneUsageList.size() && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { - // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" - laneUsageList.set(lane, startNode.getData().moveNumber - 1); - lane++; - } - if (lane >= laneUsageList.size()) - { - laneUsageList.add(0); - } - if (variationNumber > 1) - laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); - laneUsageList.set(lane, startNode.getData().moveNumber); - - // At this point, lane contains the lane we should use (the main branch is in lane 0) - - BoardHistoryNode cur = startNode; - int curposx = posx + lane*XSPACING; - int dotoffset = DOT_DIAM/2; - - // Draw line back to main branch - if (lane > 0) { - if (lane - startLane > 0 || variationNumber > 1) { - // Need a horizontal and an angled line - g.drawLine(curposx + dotoffset, posy + dotoffset, curposx + dotoffset - XSPACING, posy + dotoffset - YSPACING); - g.drawLine(posx + (startLane - variationNumber )*XSPACING + 2*dotoffset, posy - YSPACING + dotoffset, curposx + dotoffset - XSPACING, posy + dotoffset - YSPACING); - } else { - // Just an angled line - g.drawLine(curposx + dotoffset, posy + dotoffset, curposx + 2*dotoffset - XSPACING, posy + 2*dotoffset - YSPACING); - } - } - - // Draw all the nodes and lines in this lane (not variations) - Color curcolor = g.getColor(); - if (startNode == curMove) { - g.setColor(Color.green.brighter().brighter()); - } - if (startNode.previous() != null) { - g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); - g.setColor(Color.BLACK); - g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); - } else { - g.fillRect(curposx, posy, DOT_DIAM, DOT_DIAM); - g.setColor(Color.BLACK); - g.drawRect(curposx, posy, DOT_DIAM, DOT_DIAM); - } - g.setColor(curcolor); - - // Draw main line - while (cur.next() != null && posy + YSPACING < maxposy) { - posy += YSPACING; - cur = cur.next(); - if (cur == curMove) { - g.setColor(Color.green.brighter().brighter()); - } - g.fillOval(curposx , posy, DOT_DIAM, DOT_DIAM); - g.setColor(Color.BLACK); - g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); - g.setColor(curcolor); - g.drawLine(curposx + dotoffset, posy-1, curposx + dotoffset , posy - YSPACING + 2*dotoffset+2); - } - // Now we have drawn all the nodes in this variation, and has reached the bottom of this variation - // Move back up, and for each, draw any variations we find - while (cur.previous() != null && cur != startNode) { - cur = cur.previous(); - int curwidth = lane; - // Draw each variation, uses recursion - for (int i = 1; i < cur.numberOfChildren(); i++) { - curwidth++; - // Recursion, depth of recursion will normally not be very deep (one recursion level for every variation that has a variation (sort of)) - drawTree(g, posx, posy, curwidth, maxposy, cur.getVariation(i), i,false); - } - posy -= YSPACING; - } + if (lane >= laneUsageList.size()) { + laneUsageList.add(0); + } + if (variationNumber > 1) laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); + laneUsageList.set(lane, startNode.getData().moveNumber); + + // At this point, lane contains the lane we should use (the main branch is in lane 0) + + BoardHistoryNode cur = startNode; + int curposx = posx + lane * XSPACING; + int dotoffset = DOT_DIAM / 2; + + // Draw line back to main branch + if (lane > 0) { + if (lane - startLane > 0 || variationNumber > 1) { + // Need a horizontal and an angled line + g.drawLine( + curposx + dotoffset, + posy + dotoffset, + curposx + dotoffset - XSPACING, + posy + dotoffset - YSPACING); + g.drawLine( + posx + (startLane - variationNumber) * XSPACING + 2 * dotoffset, + posy - YSPACING + dotoffset, + curposx + dotoffset - XSPACING, + posy + dotoffset - YSPACING); + } else { + // Just an angled line + g.drawLine( + curposx + dotoffset, + posy + dotoffset, + curposx + 2 * dotoffset - XSPACING, + posy + 2 * dotoffset - YSPACING); + } } - public void draw(Graphics2D g, int posx, int posy, int width, int height) { - if (width <= 0 || height <= 0) - return; // we don't have enough space - - // Use dense tree for saving space if large-subboard - YSPACING = (Lizzie.config.showLargeSubBoard() ? 20 : 30); - XSPACING = YSPACING; - - // Draw background - g.setColor(new Color(0, 0, 0, 60)); - g.fillRect(posx, posy, width, height); - - // draw edge of panel - int strokeRadius = 2; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.drawLine(posx+strokeRadius, posy+strokeRadius, posx+strokeRadius, posy-strokeRadius+height); - g.setStroke(new BasicStroke(1)); - - - int middleY = posy + height/2; - int xoffset = 30; - laneUsageList.clear(); - - curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); - - // Is current move a variation? If so, find top of variation - BoardHistoryNode top = BoardHistoryList.findTop(curMove); - int curposy = middleY - YSPACING*(curMove.getData().moveNumber - top.getData().moveNumber); - // Go to very top of tree (visible in assigned area) - BoardHistoryNode node = top; - while (curposy > posy + YSPACING && node.previous() != null) { - node = node.previous(); - curposy -= YSPACING; - } - drawTree(g, posx + xoffset, curposy, 0, posy + height, node, 0,true); + // Draw all the nodes and lines in this lane (not variations) + Color curcolor = g.getColor(); + if (startNode == curMove) { + g.setColor(Color.green.brighter().brighter()); + } + if (startNode.previous() != null) { + g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); + } else { + g.fillRect(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawRect(curposx, posy, DOT_DIAM, DOT_DIAM); + } + g.setColor(curcolor); + + // Draw main line + while (cur.next() != null && posy + YSPACING < maxposy) { + posy += YSPACING; + cur = cur.next(); + if (cur == curMove) { + g.setColor(Color.green.brighter().brighter()); + } + g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(curcolor); + g.drawLine( + curposx + dotoffset, posy - 1, curposx + dotoffset, posy - YSPACING + 2 * dotoffset + 2); + } + // Now we have drawn all the nodes in this variation, and has reached the bottom of this + // variation + // Move back up, and for each, draw any variations we find + while (cur.previous() != null && cur != startNode) { + cur = cur.previous(); + int curwidth = lane; + // Draw each variation, uses recursion + for (int i = 1; i < cur.numberOfChildren(); i++) { + curwidth++; + // Recursion, depth of recursion will normally not be very deep (one recursion level for + // every variation that has a variation (sort of)) + drawTree(g, posx, posy, curwidth, maxposy, cur.getVariation(i), i, false); + } + posy -= YSPACING; + } + } + + public void draw(Graphics2D g, int posx, int posy, int width, int height) { + if (width <= 0 || height <= 0) return; // we don't have enough space + + // Use dense tree for saving space if large-subboard + YSPACING = (Lizzie.config.showLargeSubBoard() ? 20 : 30); + XSPACING = YSPACING; + + // Draw background + g.setColor(new Color(0, 0, 0, 60)); + g.fillRect(posx, posy, width, height); + + // draw edge of panel + int strokeRadius = 2; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.drawLine( + posx + strokeRadius, + posy + strokeRadius, + posx + strokeRadius, + posy - strokeRadius + height); + g.setStroke(new BasicStroke(1)); + + int middleY = posy + height / 2; + int xoffset = 30; + laneUsageList.clear(); + + curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); + + // Is current move a variation? If so, find top of variation + BoardHistoryNode top = BoardHistoryList.findTop(curMove); + int curposy = middleY - YSPACING * (curMove.getData().moveNumber - top.getData().moveNumber); + // Go to very top of tree (visible in assigned area) + BoardHistoryNode node = top; + while (curposy > posy + YSPACING && node.previous() != null) { + node = node.previous(); + curposy -= YSPACING; } + drawTree(g, posx + xoffset, curposy, 0, posy + height, node, 0, true); + } } diff --git a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java index 8f6074a56..1b0de91c6 100644 --- a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java +++ b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java @@ -4,223 +4,227 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.rules.BoardHistoryList; import featurecat.lizzie.rules.BoardHistoryNode; - import java.awt.*; import java.awt.geom.Point2D; public class WinrateGraph { - private int DOT_RADIUS = 6; - private int[] origParams = {0, 0, 0, 0}; - private int[] params = {0, 0, 0, 0, 0}; - - public void draw(Graphics2D g, int posx, int posy, int width, int height) - { - BoardHistoryNode curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); - BoardHistoryNode node = curMove; - - // draw background rectangle - final Paint gradient = new GradientPaint(new Point2D.Float(posx, posy), new Color(0, 0, 0, 150), new Point2D.Float(posx, posy+height), new Color(255, 255, 255, 150)); - final Paint borderGradient = new GradientPaint(new Point2D.Float(posx, posy), new Color(0, 0, 0, 150), new Point2D.Float(posx, posy+height), new Color(255, 255, 255, 150)); - - Paint original = g.getPaint(); - g.setPaint(gradient); - - g.fillRect(posx, posy, width, height); - - // draw border - int strokeRadius = 3; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.setPaint(borderGradient); - g.drawRect(posx+ strokeRadius, posy + strokeRadius, width - 2 * strokeRadius, height- 2 * strokeRadius); - - g.setPaint(original); - - // record parameters (before resizing) for calculating moveNumber - origParams[0] = posx; - origParams[1] = posy; - origParams[2] = width; - origParams[3] = height; - - // resize the box now so it's inside the border - posx += 2*strokeRadius; - posy += 2*strokeRadius; - width -= 4*strokeRadius; - height -= 4*strokeRadius; - - // draw lines marking 50% 60% 70% etc. - Stroke dashed = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, - new float[]{4}, 0); - g.setStroke(dashed); - - g.setColor(Color.white); - int winRateGridLines = Lizzie.frame.winRateGridLines; - for (int i = 1; i <= winRateGridLines; i++) { - double percent = i * 100.0 / (winRateGridLines + 1); - int y = posy + height - (int) (height * convertWinrate(percent) / 100); - g.drawLine(posx, y, posx + width, y); - } + private int DOT_RADIUS = 6; + private int[] origParams = {0, 0, 0, 0}; + private int[] params = {0, 0, 0, 0, 0}; + + public void draw(Graphics2D g, int posx, int posy, int width, int height) { + BoardHistoryNode curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); + BoardHistoryNode node = curMove; + + // draw background rectangle + final Paint gradient = + new GradientPaint( + new Point2D.Float(posx, posy), + new Color(0, 0, 0, 150), + new Point2D.Float(posx, posy + height), + new Color(255, 255, 255, 150)); + final Paint borderGradient = + new GradientPaint( + new Point2D.Float(posx, posy), + new Color(0, 0, 0, 150), + new Point2D.Float(posx, posy + height), + new Color(255, 255, 255, 150)); + + Paint original = g.getPaint(); + g.setPaint(gradient); + + g.fillRect(posx, posy, width, height); + + // draw border + int strokeRadius = 3; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.setPaint(borderGradient); + g.drawRect( + posx + strokeRadius, + posy + strokeRadius, + width - 2 * strokeRadius, + height - 2 * strokeRadius); + + g.setPaint(original); + + // record parameters (before resizing) for calculating moveNumber + origParams[0] = posx; + origParams[1] = posy; + origParams[2] = width; + origParams[3] = height; + + // resize the box now so it's inside the border + posx += 2 * strokeRadius; + posy += 2 * strokeRadius; + width -= 4 * strokeRadius; + height -= 4 * strokeRadius; + + // draw lines marking 50% 60% 70% etc. + Stroke dashed = + new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] {4}, 0); + g.setStroke(dashed); + + g.setColor(Color.white); + int winRateGridLines = Lizzie.frame.winRateGridLines; + for (int i = 1; i <= winRateGridLines; i++) { + double percent = i * 100.0 / (winRateGridLines + 1); + int y = posy + height - (int) (height * convertWinrate(percent) / 100); + g.drawLine(posx, y, posx + width, y); + } + + g.setColor(Color.green); + g.setStroke(new BasicStroke(3)); + + BoardHistoryNode topOfVariation = null; + int numMoves = 0; + if (!BoardHistoryList.isMainTrunk(curMove)) { + // We're in a variation, need to draw both main trunk and variation + // Find top of variation + topOfVariation = BoardHistoryList.findTop(curMove); + // Find depth of main trunk, need this for plot scaling + numMoves = + BoardHistoryList.getDepth(topOfVariation) + topOfVariation.getData().moveNumber - 1; + g.setStroke(dashed); + } - g.setColor(Color.green); - g.setStroke(new BasicStroke(3)); + // Go to end of variation and work our way backwards to the root + while (node.next() != null) node = node.next(); + if (numMoves < node.getData().moveNumber - 1) { + numMoves = node.getData().moveNumber - 1; + } - BoardHistoryNode topOfVariation = null; - int numMoves = 0; - if (!BoardHistoryList.isMainTrunk(curMove)) + if (numMoves < 1) return; + + // Plot + width = (int) (width * 0.95); // Leave some space after last move + double lastWr = 50; + boolean lastNodeOk = false; + boolean inFirstPath = true; + int movenum = node.getData().moveNumber - 1; + int lastOkMove = -1; + + while (node.previous() != null) { + double wr = node.getData().winrate; + int playouts = node.getData().playouts; + if (node == curMove) { + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + double bwr = stats.maxWinrate; + if (bwr >= 0 && stats.totalPlayouts > playouts) { + wr = bwr; + playouts = stats.totalPlayouts; + } { - // We're in a variation, need to draw both main trunk and variation - // Find top of variation - topOfVariation = BoardHistoryList.findTop(curMove); - // Find depth of main trunk, need this for plot scaling - numMoves = BoardHistoryList.getDepth(topOfVariation) + topOfVariation.getData().moveNumber - 1; - g.setStroke(dashed); + // Draw a vertical line at the current move + Stroke previousStroke = g.getStroke(); + int x = posx + (movenum * width / numMoves); + g.setStroke(dashed); + g.setColor(Color.white); + g.drawLine(x, posy, x, posy + height); + // Show move number + String moveNumString = "" + node.getData().moveNumber; + int mw = g.getFontMetrics().stringWidth(moveNumString); + int margin = strokeRadius; + int mx = x - posx < width / 2 ? x + margin : x - mw - margin; + g.drawString(moveNumString, mx, posy + height - margin); + g.setStroke(previousStroke); } - - // Go to end of variation and work our way backwards to the root - while (node.next() != null) node = node.next(); - if (numMoves < node.getData().moveNumber-1) { - numMoves = node.getData().moveNumber - 1; + } + if (playouts > 0) { + if (wr < 0) { + wr = 100 - lastWr; + } else if (!node.getData().blackToPlay) { + wr = 100 - wr; + } + if (Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack == !node.getData().blackToPlay) { + wr = lastWr; } - if (numMoves < 1) return; - - // Plot - width = (int)(width*0.95); // Leave some space after last move - double lastWr = 50; - boolean lastNodeOk = false; - boolean inFirstPath = true; - int movenum = node.getData().moveNumber - 1; - int lastOkMove = -1; + if (lastNodeOk) g.setColor(Color.green); + else g.setColor(Color.blue.darker()); - while (node.previous() != null) - { - double wr = node.getData().winrate; - int playouts = node.getData().playouts; - if (node == curMove) - { - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - double bwr = stats.maxWinrate; - if (bwr >= 0 && stats.totalPlayouts > playouts) { - wr = bwr; - playouts = stats.totalPlayouts; - } - { - // Draw a vertical line at the current move - Stroke previousStroke = g.getStroke(); - int x = posx + (movenum*width/numMoves); - g.setStroke(dashed); - g.setColor(Color.white); - g.drawLine(x, posy, x, posy + height); - // Show move number - String moveNumString = "" + node.getData().moveNumber; - int mw = g.getFontMetrics().stringWidth(moveNumString); - int margin = strokeRadius; - int mx = x - posx < width / 2 ? x + margin : x - mw - margin; - g.drawString(moveNumString, mx, posy + height - margin); - g.setStroke(previousStroke); - } - } - if (playouts > 0) { - if (wr < 0) - { - wr = 100 - lastWr; - } - else if (!node.getData().blackToPlay) - { - wr = 100 - wr; - } - if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack == !node.getData().blackToPlay) { - wr = lastWr; - } - - if (lastNodeOk) - g.setColor(Color.green); - else - g.setColor(Color.blue.darker()); - - if (lastOkMove > 0) { - g.drawLine(posx + (lastOkMove * width / numMoves), - posy + height - (int) (convertWinrate(lastWr) * height / 100), - posx + (movenum * width / numMoves), - posy + height - (int) (convertWinrate(wr) * height / 100)); - } - - if (node == curMove) - { - g.setColor(Color.green); - g.fillOval(posx + (movenum*width/numMoves) - DOT_RADIUS, - posy + height - (int)(convertWinrate(wr)*height/100) - DOT_RADIUS, - DOT_RADIUS*2, - DOT_RADIUS*2); - } - lastWr = wr; - lastNodeOk = true; - // Check if we were in a variation and has reached the main trunk - if (node == topOfVariation) { - // Reached top of variation, go to end of main trunk before continuing - while (node.next() != null) node = node.next(); - movenum = node.getData().moveNumber - 1; - lastWr = node.getData().winrate; - if (!node.getData().blackToPlay) - lastWr = 100 - lastWr; - g.setStroke(new BasicStroke(3)); - topOfVariation = null; - if (node.getData().playouts == 0) { - lastNodeOk = false; - } - inFirstPath = false; - } - lastOkMove = lastNodeOk ? movenum : -1; - } else { - lastNodeOk = false; - } - - node = node.previous(); - movenum--; + if (lastOkMove > 0) { + g.drawLine( + posx + (lastOkMove * width / numMoves), + posy + height - (int) (convertWinrate(lastWr) * height / 100), + posx + (movenum * width / numMoves), + posy + height - (int) (convertWinrate(wr) * height / 100)); } - g.setStroke(new BasicStroke(1)); + if (node == curMove) { + g.setColor(Color.green); + g.fillOval( + posx + (movenum * width / numMoves) - DOT_RADIUS, + posy + height - (int) (convertWinrate(wr) * height / 100) - DOT_RADIUS, + DOT_RADIUS * 2, + DOT_RADIUS * 2); + } + lastWr = wr; + lastNodeOk = true; + // Check if we were in a variation and has reached the main trunk + if (node == topOfVariation) { + // Reached top of variation, go to end of main trunk before continuing + while (node.next() != null) node = node.next(); + movenum = node.getData().moveNumber - 1; + lastWr = node.getData().winrate; + if (!node.getData().blackToPlay) lastWr = 100 - lastWr; + g.setStroke(new BasicStroke(3)); + topOfVariation = null; + if (node.getData().playouts == 0) { + lastNodeOk = false; + } + inFirstPath = false; + } + lastOkMove = lastNodeOk ? movenum : -1; + } else { + lastNodeOk = false; + } - // record parameters for calculating moveNumber - params[0] = posx; - params[1] = posy; - params[2] = width; - params[3] = height; - params[4] = numMoves; + node = node.previous(); + movenum--; } - private double convertWinrate(double winrate) { - double maxHandicap = 10; - if (Lizzie.config.handicapInsteadOfWinrate) { - double handicap = Lizzie.leelaz.winrateToHandicap(winrate); - // handicap == + maxHandicap => r == 1.0 - // handicap == - maxHandicap => r == 0.0 - double r = 0.5 + handicap / (2 * maxHandicap); - return Math.max(0, Math.min(r, 1)) * 100; - } else { - return winrate; - } + g.setStroke(new BasicStroke(1)); + + // record parameters for calculating moveNumber + params[0] = posx; + params[1] = posy; + params[2] = width; + params[3] = height; + params[4] = numMoves; + } + + private double convertWinrate(double winrate) { + double maxHandicap = 10; + if (Lizzie.config.handicapInsteadOfWinrate) { + double handicap = Lizzie.leelaz.winrateToHandicap(winrate); + // handicap == + maxHandicap => r == 1.0 + // handicap == - maxHandicap => r == 0.0 + double r = 0.5 + handicap / (2 * maxHandicap); + return Math.max(0, Math.min(r, 1)) * 100; + } else { + return winrate; } - - public int moveNumber(int x, int y) - { - int origPosx = origParams[0]; - int origPosy = origParams[1]; - int origWidth = origParams[2]; - int origHeight = origParams[3]; - int posx = params[0]; - int posy = params[1]; - int width = params[2]; - int height = params[3]; - int numMoves = params[4]; - if (origPosx <= x && x < origPosx + origWidth && - origPosy <= y && y < origPosy + origHeight) { - // x == posx + (movenum * width / numMoves) ==> movenum = ... - int movenum = Math.round((x - posx) * numMoves / (float) width); - // movenum == moveNumber - 1 ==> moveNumber = ... - return movenum + 1; - } else { - return -1; - } + } + + public int moveNumber(int x, int y) { + int origPosx = origParams[0]; + int origPosy = origParams[1]; + int origWidth = origParams[2]; + int origHeight = origParams[3]; + int posx = params[0]; + int posy = params[1]; + int width = params[2]; + int height = params[3]; + int numMoves = params[4]; + if (origPosx <= x && x < origPosx + origWidth && origPosy <= y && y < origPosy + origHeight) { + // x == posx + (movenum * width / numMoves) ==> movenum = ... + int movenum = Math.round((x - posx) * numMoves / (float) width); + // movenum == moveNumber - 1 ==> moveNumber = ... + return movenum + 1; + } else { + return -1; } + } } diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 9fdede7ac..f70752d55 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -4,1096 +4,1136 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.analysis.LeelazListener; import featurecat.lizzie.analysis.MoveData; -import featurecat.lizzie.rules.SGFParser; - import java.io.IOException; -import javax.swing.*; import java.util.ArrayDeque; import java.util.Deque; -import java.util.Queue; import java.util.List; +import java.util.Queue; +import javax.swing.*; import org.json.JSONException; public class Board implements LeelazListener { - public static final int BOARD_SIZE = Lizzie.config.config.getJSONObject("ui").optInt("board-size", 19); - private final static String alphabet = "ABCDEFGHJKLMNOPQRST"; - - private BoardHistoryList history; - private Stone[] capturedStones; - - private boolean scoreMode; - - private boolean analysisMode = false; - private int playoutsAnalysis = 100; - - - public Board() { - initialize(); + public static final int BOARD_SIZE = + Lizzie.config.config.getJSONObject("ui").optInt("board-size", 19); + private static final String alphabet = "ABCDEFGHJKLMNOPQRST"; + + private BoardHistoryList history; + private Stone[] capturedStones; + + private boolean scoreMode; + + private boolean analysisMode = false; + private int playoutsAnalysis = 100; + + public Board() { + initialize(); + } + + /** Initialize the board completely */ + private void initialize() { + Stone[] stones = new Stone[BOARD_SIZE * BOARD_SIZE]; + for (int i = 0; i < stones.length; i++) stones[i] = Stone.EMPTY; + + boolean blackToPlay = true; + int[] lastMove = null; + + capturedStones = null; + scoreMode = false; + + history = + new BoardHistoryList( + new BoardData( + stones, + lastMove, + Stone.EMPTY, + blackToPlay, + new Zobrist(), + 0, + new int[BOARD_SIZE * BOARD_SIZE], + 0, + 0, + 50, + 0)); + } + + /** + * Calculates the array index of a stone stored at (x, y) + * + * @param x the x coordinate + * @param y the y coordinate + * @return the array index + */ + public static int getIndex(int x, int y) { + return x * Board.BOARD_SIZE + y; + } + + /** + * Converts a named coordinate eg C16, T5, K10, etc to an x and y coordinate + * + * @param namedCoordinate a capitalized version of the named coordinate. Must be a valid 19x19 Go + * coordinate, without I + * @return an array containing x, followed by y + */ + public static int[] convertNameToCoordinates(String namedCoordinate) { + namedCoordinate = namedCoordinate.trim(); + if (namedCoordinate.equalsIgnoreCase("pass")) { + return null; } - - /** - * Initialize the board completely - */ - private void initialize() { - Stone[] stones = new Stone[BOARD_SIZE * BOARD_SIZE]; - for (int i = 0; i < stones.length; i++) - stones[i] = Stone.EMPTY; - - boolean blackToPlay = true; - int[] lastMove = null; - - capturedStones = null; - scoreMode = false; - - history = new BoardHistoryList(new BoardData(stones, lastMove, Stone.EMPTY, blackToPlay, - new Zobrist(), 0, new int[BOARD_SIZE * BOARD_SIZE], 0, 0, 50, 0)); + // coordinates take the form C16 A19 Q5 K10 etc. I is not used. + int x = alphabet.indexOf(namedCoordinate.charAt(0)); + int y = BOARD_SIZE - Integer.parseInt(namedCoordinate.substring(1)); + return new int[] {x, y}; + } + + /** + * Converts a x and y coordinate to a named coordinate eg C16, T5, K10, etc + * + * @param x x coordinate -- must be valid + * @param y y coordinate -- must be valid + * @return a string representing the coordinate + */ + public static String convertCoordinatesToName(int x, int y) { + // coordinates take the form C16 A19 Q5 K10 etc. I is not used. + return alphabet.charAt(x) + "" + (BOARD_SIZE - y); + } + + /** + * Checks if a coordinate is valid + * + * @param x x coordinate + * @param y y coordinate + * @return whether or not this coordinate is part of the board + */ + public static boolean isValid(int x, int y) { + return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE; + } + + /** + * The comment. Thread safe + * + * @param comment the comment of stone + */ + public void comment(String comment) { + synchronized (this) { + if (history.getData() != null) { + history.getData().comment = comment; + } } - - /** - * Calculates the array index of a stone stored at (x, y) - * - * @param x the x coordinate - * @param y the y coordinate - * @return the array index - */ - public static int getIndex(int x, int y) { - return x * Board.BOARD_SIZE + y; + } + + /** + * The pass. Thread safe + * + * @param color the type of pass + */ + public void pass(Stone color) { + synchronized (this) { + + // check to see if this move is being replayed in history + BoardData next = history.getNext(); + if (next != null && next.lastMove == null) { + // this is the next move in history. Just increment history so that we don't erase the + // redo's + history.next(); + Lizzie.leelaz.playMove(color, "pass"); + if (Lizzie.frame.isPlayingAgainstLeelaz) + Lizzie.leelaz.genmove((history.isBlacksTurn() ? "B" : "W")); + + return; + } + + Stone[] stones = history.getStones().clone(); + Zobrist zobrist = history.getZobrist(); + int moveNumber = history.getMoveNumber() + 1; + int[] moveNumberList = history.getMoveNumberList().clone(); + + // build the new game state + BoardData newState = + new BoardData( + stones, + null, + color, + color.equals(Stone.WHITE), + zobrist, + moveNumber, + moveNumberList, + history.getData().blackCaptures, + history.getData().whiteCaptures, + 0, + 0); + + // update leelaz with pass + Lizzie.leelaz.playMove(color, "pass"); + if (Lizzie.frame.isPlayingAgainstLeelaz) + Lizzie.leelaz.genmove((history.isBlacksTurn() ? "W" : "B")); + + // update history with pass + history.addOrGoto(newState); + + Lizzie.frame.repaint(); } - - /** - * Converts a named coordinate eg C16, T5, K10, etc to an x and y coordinate - * - * @param namedCoordinate a capitalized version of the named coordinate. Must be a valid 19x19 Go coordinate, without I - * @return an array containing x, followed by y - */ - public static int[] convertNameToCoordinates(String namedCoordinate) { - namedCoordinate = namedCoordinate.trim(); - if (namedCoordinate.equalsIgnoreCase("pass")) { - return null; + } + + /** overloaded method for pass(), chooses color in an alternating pattern */ + public void pass() { + pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + } + + /** + * Places a stone onto the board representation. Thread safe + * + * @param x x coordinate + * @param y y coordinate + * @param color the type of stone to place + */ + public void place(int x, int y, Stone color) { + synchronized (this) { + if (scoreMode) { + // Mark clicked stone as dead + Stone[] stones = history.getStones(); + toggleLiveStatus(capturedStones, x, y); + return; + } + + if (!isValid(x, y) || history.getStones()[getIndex(x, y)] != Stone.EMPTY) return; + + // Update winrate + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + + if (stats.maxWinrate >= 0 && stats.totalPlayouts > history.getData().playouts) { + history.getData().winrate = stats.maxWinrate; + history.getData().playouts = stats.totalPlayouts; + } + double nextWinrate = -100; + if (history.getData().winrate >= 0) nextWinrate = 100 - history.getData().winrate; + + // check to see if this coordinate is being replayed in history + BoardData next = history.getNext(); + if (next != null && next.lastMove != null && next.lastMove[0] == x && next.lastMove[1] == y) { + // this is the next coordinate in history. Just increment history so that we don't erase the + // redo's + history.next(); + // should be opposite from the bottom case + if (Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack != getData().blackToPlay) { + Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); + } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); } - // coordinates take the form C16 A19 Q5 K10 etc. I is not used. - int x = alphabet.indexOf(namedCoordinate.charAt(0)); - int y = BOARD_SIZE - Integer.parseInt(namedCoordinate.substring(1)); - return new int[]{x, y}; + return; + } + + // load a copy of the data at the current node of history + Stone[] stones = history.getStones().clone(); + Zobrist zobrist = history.getZobrist(); + int[] lastMove = new int[] {x, y}; // keep track of the last played stone + int moveNumber = history.getMoveNumber() + 1; + int[] moveNumberList = history.getMoveNumberList().clone(); + + moveNumberList[Board.getIndex(x, y)] = moveNumber; + + // set the stone at (x, y) to color + stones[getIndex(x, y)] = color; + zobrist.toggleStone(x, y, color); + + // remove enemy stones + int capturedStones = 0; + capturedStones += removeDeadChain(x + 1, y, color.opposite(), stones, zobrist); + capturedStones += removeDeadChain(x, y + 1, color.opposite(), stones, zobrist); + capturedStones += removeDeadChain(x - 1, y, color.opposite(), stones, zobrist); + capturedStones += removeDeadChain(x, y - 1, color.opposite(), stones, zobrist); + + // check to see if the player made a suicidal coordinate + int isSuicidal = removeDeadChain(x, y, color, stones, zobrist); + + for (int i = 0; i < BOARD_SIZE * BOARD_SIZE; i++) { + if (stones[i].equals(Stone.EMPTY)) { + moveNumberList[i] = 0; + } + } + + int bc = history.getData().blackCaptures; + int wc = history.getData().whiteCaptures; + if (color.isBlack()) bc += capturedStones; + else wc += capturedStones; + BoardData newState = + new BoardData( + stones, + lastMove, + color, + color.equals(Stone.WHITE), + zobrist, + moveNumber, + moveNumberList, + bc, + wc, + nextWinrate, + 0); + + // don't make this coordinate if it is suicidal or violates superko + if (isSuicidal > 0 || history.violatesSuperko(newState)) return; + + // update leelaz with board position + if (Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack == getData().blackToPlay) { + Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); + } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); + } + + // update history with this coordinate + history.addOrGoto(newState); + + Lizzie.frame.repaint(); } - - /** - * Converts a x and y coordinate to a named coordinate eg C16, T5, K10, etc - * - * @param x x coordinate -- must be valid - * @param y y coordinate -- must be valid - * @return a string representing the coordinate - */ - public static String convertCoordinatesToName(int x, int y) { - // coordinates take the form C16 A19 Q5 K10 etc. I is not used. - return alphabet.charAt(x) + "" + (BOARD_SIZE - y); + } + + /** + * overloaded method for place(), chooses color in an alternating pattern + * + * @param x x coordinate + * @param y y coordinate + */ + public void place(int x, int y) { + place(x, y, history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + } + + /** + * overloaded method for place. To be used by the LeelaZ engine. Color is then assumed to be + * alternating + * + * @param namedCoordinate the coordinate to place a stone, + */ + public void place(String namedCoordinate) { + if (namedCoordinate.contains("pass")) { + pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + return; + } else if (namedCoordinate.contains("resign")) { + pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + return; } - /** - * Checks if a coordinate is valid - * - * @param x x coordinate - * @param y y coordinate - * @return whether or not this coordinate is part of the board - */ - public static boolean isValid(int x, int y) { - return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE; + int[] coordinates = convertNameToCoordinates(namedCoordinate); + + place(coordinates[0], coordinates[1]); + } + + /** for handicap */ + public void flatten() { + Stone[] stones = history.getStones(); + boolean blackToPlay = history.isBlacksTurn(); + Zobrist zobrist = history.getZobrist().clone(); + BoardHistoryList oldHistory = history; + history = + new BoardHistoryList( + new BoardData( + stones, + null, + Stone.EMPTY, + blackToPlay, + zobrist, + 0, + new int[BOARD_SIZE * BOARD_SIZE], + 0, + 0, + 0.0, + 0)); + history.setGameInfo(oldHistory.getGameInfo()); + } + + /** + * Removes a chain if it has no liberties + * + * @param x x coordinate -- needn't be valid + * @param y y coordinate -- needn't be valid + * @param color the color of the chain to remove + * @param stones the stones array to modify + * @param zobrist the zobrist object to modify + * @return number of removed stones + */ + private int removeDeadChain(int x, int y, Stone color, Stone[] stones, Zobrist zobrist) { + if (!isValid(x, y) || stones[getIndex(x, y)] != color) return 0; + + boolean hasLiberties = hasLibertiesHelper(x, y, color, stones); + + // either remove stones or reset what hasLibertiesHelper does to the board + return cleanupHasLibertiesHelper(x, y, color.recursed(), stones, zobrist, !hasLiberties); + } + + /** + * Recursively determines if a chain has liberties. Alters the state of stones, so it must be + * counteracted + * + * @param x x coordinate -- needn't be valid + * @param y y coordinate -- needn't be valid + * @param color the color of the chain to be investigated + * @param stones the stones array to modify + * @return whether or not this chain has liberties + */ + private boolean hasLibertiesHelper(int x, int y, Stone color, Stone[] stones) { + if (!isValid(x, y)) return false; + + if (stones[getIndex(x, y)] == Stone.EMPTY) return true; // a liberty was found + else if (stones[getIndex(x, y)] != color) + return false; // we are either neighboring an enemy stone, or one we've already recursed on + + // set this index to be the recursed color to keep track of where we've already searched + stones[getIndex(x, y)] = color.recursed(); + + // set removeDeadChain to true if any recursive calls return true. Recurse in all 4 directions + boolean hasLiberties = + hasLibertiesHelper(x + 1, y, color, stones) + || hasLibertiesHelper(x, y + 1, color, stones) + || hasLibertiesHelper(x - 1, y, color, stones) + || hasLibertiesHelper(x, y - 1, color, stones); + + return hasLiberties; + } + + /** + * cleans up what hasLibertyHelper does to the board state + * + * @param x x coordinate -- needn't be valid + * @param y y coordinate -- needn't be valid + * @param color color to clean up. Must be a recursed stone type + * @param stones the stones array to modify + * @param zobrist the zobrist object to modify + * @param removeStones if true, we will remove all these stones. otherwise, we will set them to + * their unrecursed version + * @return number of removed stones + */ + private int cleanupHasLibertiesHelper( + int x, int y, Stone color, Stone[] stones, Zobrist zobrist, boolean removeStones) { + int removed = 0; + if (!isValid(x, y) || stones[getIndex(x, y)] != color) return 0; + + stones[getIndex(x, y)] = removeStones ? Stone.EMPTY : color.unrecursed(); + if (removeStones) { + zobrist.toggleStone(x, y, color.unrecursed()); + removed = 1; } - /** - * The comment. Thread safe - * @param comment the comment of stone - */ - public void comment(String comment) { - synchronized (this) { - - if (history.getData() != null) { - history.getData().comment = comment; - } + // use the flood fill algorithm to replace all adjacent recursed stones + removed += cleanupHasLibertiesHelper(x + 1, y, color, stones, zobrist, removeStones); + removed += cleanupHasLibertiesHelper(x, y + 1, color, stones, zobrist, removeStones); + removed += cleanupHasLibertiesHelper(x - 1, y, color, stones, zobrist, removeStones); + removed += cleanupHasLibertiesHelper(x, y - 1, color, stones, zobrist, removeStones); + return removed; + } + + /** + * get current board state + * + * @return the stones array corresponding to the current board state + */ + public Stone[] getStones() { + return history.getStones(); + } + + /** + * shows where to mark the last coordinate + * + * @return the last played stone + */ + public int[] getLastMove() { + return history.getLastMove(); + } + + /** + * get the move played in this position + * + * @return the next move, if any + */ + public int[] getNextMove() { + return history.getNextMove(); + } + + /** + * get current board move number + * + * @return the int array corresponding to the current board move number + */ + public int[] getMoveNumberList() { + return history.getMoveNumberList(); + } + + /** Goes to the next coordinate, thread safe */ + public boolean nextMove() { + 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; + } + if (history.next() != null) { + // update leelaz board position, before updating to next node + if (history.getData().lastMove == null) { + Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); + } else { + Lizzie.leelaz.playMove( + history.getLastMoveColor(), + convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); } + Lizzie.frame.repaint(); + return true; + } + return false; } + } - /** - * The pass. Thread safe - * - * @param color the type of pass - */ - public void pass(Stone color) { - synchronized (this) { - - // check to see if this move is being replayed in history - BoardData next = history.getNext(); - if (next != null && next.lastMove == null) { - // this is the next move in history. Just increment history so that we don't erase the redo's - history.next(); - Lizzie.leelaz.playMove(color, "pass"); - if (Lizzie.frame.isPlayingAgainstLeelaz) - Lizzie.leelaz.genmove((history.isBlacksTurn()? "B" : "W")); - - return; - } - - Stone[] stones = history.getStones().clone(); - Zobrist zobrist = history.getZobrist(); - int moveNumber = history.getMoveNumber() + 1; - int[] moveNumberList = history.getMoveNumberList().clone(); - - // build the new game state - BoardData newState = new BoardData(stones, null, color, color.equals(Stone.WHITE), - zobrist, moveNumber, moveNumberList, history.getData().blackCaptures, history.getData().whiteCaptures, 0, 0); + public boolean goToMoveNumber(int moveNumber) { + return goToMoveNumberHelper(moveNumber, false); + } - // update leelaz with pass - Lizzie.leelaz.playMove(color, "pass"); - if (Lizzie.frame.isPlayingAgainstLeelaz) - Lizzie.leelaz.genmove((history.isBlacksTurn()? "W" : "B")); + public boolean goToMoveNumberWithinBranch(int moveNumber) { + return goToMoveNumberHelper(moveNumber, true); + } - // update history with pass - history.addOrGoto(newState); - - Lizzie.frame.repaint(); - } + public boolean goToMoveNumberBeyondBranch(int moveNumber) { + // Go to main trunk if current branch is shorter than moveNumber. + if (moveNumber > history.currentBranchLength() && moveNumber <= history.mainTrunkLength()) { + goToMoveNumber(0); } - - /** - * overloaded method for pass(), chooses color in an alternating pattern - */ - public void pass() { - pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); - } - - /** - * Places a stone onto the board representation. Thread safe - * - * @param x x coordinate - * @param y y coordinate - * @param color the type of stone to place - */ - public void place(int x, int y, Stone color) { - synchronized (this) { - if (scoreMode) { - // Mark clicked stone as dead - Stone[] stones = history.getStones(); - toggleLiveStatus(capturedStones, x, y); - return; - } - - if (!isValid(x, y) || history.getStones()[getIndex(x, y)] != Stone.EMPTY) - return; - - // Update winrate - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - - if (stats.maxWinrate >= 0 && stats.totalPlayouts > history.getData().playouts) - { - history.getData().winrate = stats.maxWinrate; - history.getData().playouts = stats.totalPlayouts; - } - double nextWinrate = -100; - if (history.getData().winrate >= 0) nextWinrate = 100 - history.getData().winrate; - - // check to see if this coordinate is being replayed in history - BoardData next = history.getNext(); - if (next != null && next.lastMove != null && next.lastMove[0] == x && next.lastMove[1] == y) { - // this is the next coordinate in history. Just increment history so that we don't erase the redo's - history.next(); - // should be opposite from the bottom case - if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack != getData().blackToPlay) { - Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); - } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - } - return; - } - - // load a copy of the data at the current node of history - Stone[] stones = history.getStones().clone(); - Zobrist zobrist = history.getZobrist(); - int[] lastMove = new int[]{x, y}; // keep track of the last played stone - int moveNumber = history.getMoveNumber() + 1; - int[] moveNumberList = history.getMoveNumberList().clone(); - - moveNumberList[Board.getIndex(x, y)] = moveNumber; - - // set the stone at (x, y) to color - stones[getIndex(x, y)] = color; - zobrist.toggleStone(x, y, color); - - // remove enemy stones - int capturedStones = 0; - capturedStones += removeDeadChain(x + 1, y, color.opposite(), stones, zobrist); - capturedStones += removeDeadChain(x, y + 1, color.opposite(), stones, zobrist); - capturedStones += removeDeadChain(x - 1, y, color.opposite(), stones, zobrist); - capturedStones += removeDeadChain(x, y - 1, color.opposite(), stones, zobrist); - - // check to see if the player made a suicidal coordinate - int isSuicidal = removeDeadChain(x, y, color, stones, zobrist); - - for (int i = 0; i < BOARD_SIZE * BOARD_SIZE; i++) { - if (stones[i].equals(Stone.EMPTY)) { - moveNumberList[i] = 0; - } - } - - int bc = history.getData().blackCaptures; - int wc = history.getData().whiteCaptures; - if (color.isBlack()) - bc += capturedStones; - else - wc += capturedStones; - BoardData newState = new BoardData(stones, lastMove, color, color.equals(Stone.WHITE), - zobrist, moveNumber, moveNumberList, bc, wc, nextWinrate, 0); - - // don't make this coordinate if it is suicidal or violates superko - if (isSuicidal > 0|| history.violatesSuperko(newState)) - return; - - // update leelaz with board position - if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack == getData().blackToPlay) { - Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); - } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - } - - // update history with this coordinate - history.addOrGoto(newState); - - Lizzie.frame.repaint(); + return goToMoveNumber(moveNumber); + } + + public boolean goToMoveNumberHelper(int moveNumber, boolean withinBranch) { + int delta = moveNumber - history.getMoveNumber(); + boolean moved = false; + for (int i = 0; i < Math.abs(delta); i++) { + if (withinBranch && delta < 0) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + if (!curNode.isFirstChild()) { + break; } + } + if (!(delta > 0 ? nextMove() : previousMove())) { + break; + } + moved = true; } - - /** - * overloaded method for place(), chooses color in an alternating pattern - * - * @param x x coordinate - * @param y y coordinate - */ - public void place(int x, int y) { - place(x, y, history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); - } - - /** - * overloaded method for place. To be used by the LeelaZ engine. Color is then assumed to be alternating - * - * @param namedCoordinate the coordinate to place a stone, - */ - public void place(String namedCoordinate) { - if (namedCoordinate.contains("pass")) { - pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); - return; - } else if (namedCoordinate.contains("resign")) { - pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); - return; + return moved; + } + + /** Goes to the next variation, thread safe */ + public boolean nextVariation(int idx) { + synchronized (this) { + // Don't update winrate here as this is usually called when jumping between variations + if (history.nextVariation(idx) != null) { + // update leelaz board position, before updating to next node + if (history.getData().lastMove == null) { + Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); + } else { + Lizzie.leelaz.playMove( + history.getLastMoveColor(), + convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); } - - int[] coordinates = convertNameToCoordinates(namedCoordinate); - - place(coordinates[0], coordinates[1]); - } - - /** - * for handicap - */ - public void flatten() { - Stone[] stones = history.getStones(); - boolean blackToPlay = history.isBlacksTurn(); - Zobrist zobrist = history.getZobrist().clone(); - BoardHistoryList oldHistory = history; - history = new BoardHistoryList(new BoardData(stones, null, Stone.EMPTY, blackToPlay, zobrist, - 0, new int[BOARD_SIZE * BOARD_SIZE], 0, 0, 0.0, 0)); - history.setGameInfo(oldHistory.getGameInfo()); - } - - /** - * Removes a chain if it has no liberties - * - * @param x x coordinate -- needn't be valid - * @param y y coordinate -- needn't be valid - * @param color the color of the chain to remove - * @param stones the stones array to modify - * @param zobrist the zobrist object to modify - * @return number of removed stones - */ - private int removeDeadChain(int x, int y, Stone color, Stone[] stones, Zobrist zobrist) { - if (!isValid(x, y) || stones[getIndex(x, y)] != color) - return 0; - - boolean hasLiberties = hasLibertiesHelper(x, y, color, stones); - - // either remove stones or reset what hasLibertiesHelper does to the board - return cleanupHasLibertiesHelper(x, y, color.recursed(), stones, zobrist, !hasLiberties); - } - - /** - * Recursively determines if a chain has liberties. Alters the state of stones, so it must be counteracted - * - * @param x x coordinate -- needn't be valid - * @param y y coordinate -- needn't be valid - * @param color the color of the chain to be investigated - * @param stones the stones array to modify - * @return whether or not this chain has liberties - */ - private boolean hasLibertiesHelper(int x, int y, Stone color, Stone[] stones) { - if (!isValid(x, y)) - return false; - - if (stones[getIndex(x, y)] == Stone.EMPTY) - return true; // a liberty was found - else if (stones[getIndex(x, y)] != color) - return false; // we are either neighboring an enemy stone, or one we've already recursed on - - // set this index to be the recursed color to keep track of where we've already searched - stones[getIndex(x, y)] = color.recursed(); - - // set removeDeadChain to true if any recursive calls return true. Recurse in all 4 directions - boolean hasLiberties = hasLibertiesHelper(x + 1, y, color, stones) || - hasLibertiesHelper(x, y + 1, color, stones) || - hasLibertiesHelper(x - 1, y, color, stones) || - hasLibertiesHelper(x, y - 1, color, stones); - - return hasLiberties; + Lizzie.frame.repaint(); + return true; + } + return false; } - - /** - * cleans up what hasLibertyHelper does to the board state - * - * @param x x coordinate -- needn't be valid - * @param y y coordinate -- needn't be valid - * @param color color to clean up. Must be a recursed stone type - * @param stones the stones array to modify - * @param zobrist the zobrist object to modify - * @param removeStones if true, we will remove all these stones. otherwise, we will set them to their unrecursed version - * @return number of removed stones - */ - private int cleanupHasLibertiesHelper(int x, int y, Stone color, Stone[] stones, Zobrist zobrist, boolean removeStones) { - int removed = 0; - if (!isValid(x, y) || stones[getIndex(x, y)] != color) - return 0; - - stones[getIndex(x, y)] = removeStones ? Stone.EMPTY : color.unrecursed(); - if (removeStones) { - zobrist.toggleStone(x, y, color.unrecursed()); - removed = 1; + } + + /* + * Moves to next variation (variation to the right) if possible + * To move to another variation, the variation must have a move with the same move number as the current move in it. + * Note: Will only look within variations that start at the same move on the main trunk/branch, and if on trunk + * only in the ones closest + */ + public boolean nextBranch() { + synchronized (this) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + int curMoveNum = curNode.getData().moveNumber; + // First check if there is a branch to move to, if not, stay in same place + // Requirement: variaton need to have a move number same as current + if (BoardHistoryList.isMainTrunk(curNode)) { + // Check if there is a variation tree to the right that is deep enough + BoardHistoryNode startVarNode = BoardHistoryList.findChildOfPreviousWithVariation(curNode); + if (startVarNode == null) return false; + startVarNode = startVarNode.previous(); + boolean isDeepEnough = false; + for (int i = 1; i < startVarNode.numberOfChildren(); i++) { + if (BoardHistoryList.hasDepth( + startVarNode.getVariation(i), curMoveNum - startVarNode.getData().moveNumber - 1)) { + isDeepEnough = true; + break; + } } - - // use the flood fill algorithm to replace all adjacent recursed stones - removed += cleanupHasLibertiesHelper(x + 1, y, color, stones, zobrist, removeStones); - removed += cleanupHasLibertiesHelper(x, y + 1, color, stones, zobrist, removeStones); - removed += cleanupHasLibertiesHelper(x - 1, y, color, stones, zobrist, removeStones); - removed += cleanupHasLibertiesHelper(x, y - 1, color, stones, zobrist, removeStones); - return removed; - } - - /** - * get current board state - * - * @return the stones array corresponding to the current board state - */ - public Stone[] getStones() { - return history.getStones(); - } - - /** - * shows where to mark the last coordinate - * - * @return the last played stone - */ - public int[] getLastMove() { - return history.getLastMove(); - } - - /** - * get the move played in this position - * - * @return the next move, if any - */ - public int[] getNextMove() { - return history.getNextMove(); - } - - /** - * get current board move number - * - * @return the int array corresponding to the current board move number - */ - public int[] getMoveNumberList() { - return history.getMoveNumberList(); - } - - /** - * Goes to the next coordinate, thread safe - */ - public boolean nextMove() { - 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; - } - if (history.next() != null) { - // update leelaz board position, before updating to next node - if (history.getData().lastMove == null) { - Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); - } else { - Lizzie.leelaz.playMove(history.getLastMoveColor(), convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); - } - Lizzie.frame.repaint(); - return true; + if (!isDeepEnough) return false; + } else { + // We are in a variation, is there some variation to the right? + BoardHistoryNode tmpNode = curNode; + while (tmpNode != null) { + // Try to move to the right + BoardHistoryNode prevBranch = BoardHistoryList.findChildOfPreviousWithVariation(tmpNode); + int idx = BoardHistoryList.findIndexOfNode(prevBranch.previous(), prevBranch); + // Check if there are branches to the right, that are deep enough + boolean isDeepEnough = false; + for (int i = idx + 1; i < prevBranch.previous().numberOfChildren(); i++) { + if (BoardHistoryList.hasDepth( + prevBranch.previous().getVariation(i), + curMoveNum - prevBranch.previous().getData().moveNumber - 1)) { + isDeepEnough = true; + break; } + } + if (isDeepEnough) break; + // Did not find a deep enough branch, move up unless we reached main trunk + if (BoardHistoryList.isMainTrunk(prevBranch.previous())) { + // No right hand side branch to move too return false; + } + tmpNode = prevBranch.previous(); } - } - - public boolean goToMoveNumber(int moveNumber) { - return goToMoveNumberHelper(moveNumber, false); - } - - public boolean goToMoveNumberWithinBranch(int moveNumber) { - return goToMoveNumberHelper(moveNumber, true); - } - - public boolean goToMoveNumberBeyondBranch(int moveNumber) { - // Go to main trunk if current branch is shorter than moveNumber. - if (moveNumber > history.currentBranchLength() && - moveNumber <= history.mainTrunkLength()) { - goToMoveNumber(0); - } - return goToMoveNumber(moveNumber); - } - - public boolean goToMoveNumberHelper(int moveNumber, boolean withinBranch) { - int delta = moveNumber - history.getMoveNumber(); - boolean moved = false; - for (int i = 0; i < Math.abs(delta); i++) { - if (withinBranch && delta < 0) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - if (!curNode.isFirstChild()) { - break; - } - } - if (!(delta > 0 ? nextMove() : previousMove())) { - break; + } + + // At this point, we know there is somewhere to move to... Move there, one step at the time + // (because of Leelaz) + // Start moving up the tree until we reach a moves with variations that are deep enough + BoardHistoryNode prevNode; + int startIdx = 0; + while (curNode.previous() != null) { + prevNode = curNode; + previousMove(); + curNode = history.getCurrentHistoryNode(); + startIdx = BoardHistoryList.findIndexOfNode(curNode, prevNode) + 1; + if (curNode.numberOfChildren() > 1 && startIdx <= curNode.numberOfChildren()) { + // Find the variation that is deep enough + boolean isDeepEnough = false; + for (int i = startIdx; i < curNode.numberOfChildren(); i++) { + if (BoardHistoryList.hasDepth( + curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { + isDeepEnough = true; + break; } - moved = true; + } + if (isDeepEnough) break; } - return moved; - } - - /** - * Goes to the next variation, thread safe - */ - public boolean nextVariation(int idx) { - synchronized (this) { - // Don't update winrate here as this is usually called when jumping between variations - if (history.nextVariation(idx) != null) { - // update leelaz board position, before updating to next node - if (history.getData().lastMove == null) { - Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); - } else { - Lizzie.leelaz.playMove(history.getLastMoveColor(), convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); - } - Lizzie.frame.repaint(); - return true; + } + // Now move forward in new branch + while (curNode.getData().moveNumber < curMoveNum) { + if (curNode.numberOfChildren() == 1) { + // One-way street, just move to next + if (!nextVariation(0)) { + // Not supposed to happen, fail-safe + break; + } + } else { + // Has several variations, need to find the closest one that is deep enough + for (int i = startIdx; i < curNode.numberOfChildren(); i++) { + if (BoardHistoryList.hasDepth( + curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { + nextVariation(i); + break; } - return false; + } } + startIdx = 0; + curNode = history.getCurrentHistoryNode(); + } + return true; } - - /* - * Moves to next variation (variation to the right) if possible - * To move to another variation, the variation must have a move with the same move number as the current move in it. - * Note: Will only look within variations that start at the same move on the main trunk/branch, and if on trunk - * only in the ones closest - */ - public boolean nextBranch() { - synchronized (this) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - int curMoveNum = curNode.getData().moveNumber; - // First check if there is a branch to move to, if not, stay in same place - // Requirement: variaton need to have a move number same as current - if (BoardHistoryList.isMainTrunk(curNode)) { - // Check if there is a variation tree to the right that is deep enough - BoardHistoryNode startVarNode = BoardHistoryList.findChildOfPreviousWithVariation(curNode); - if (startVarNode == null) return false; - startVarNode = startVarNode.previous(); - boolean isDeepEnough = false; - for (int i = 1; i < startVarNode.numberOfChildren(); i++) - { - if (BoardHistoryList.hasDepth(startVarNode.getVariation(i), curMoveNum - startVarNode.getData().moveNumber - 1)) { - isDeepEnough = true; - break; - } - } - if (!isDeepEnough) return false; - } else { - // We are in a variation, is there some variation to the right? - BoardHistoryNode tmpNode = curNode; - while (tmpNode != null) { - // Try to move to the right - BoardHistoryNode prevBranch = BoardHistoryList.findChildOfPreviousWithVariation(tmpNode); - int idx = BoardHistoryList.findIndexOfNode(prevBranch.previous(), prevBranch); - // Check if there are branches to the right, that are deep enough - boolean isDeepEnough = false; - for (int i = idx + 1; i < prevBranch.previous().numberOfChildren(); i++) { - if (BoardHistoryList.hasDepth(prevBranch.previous().getVariation(i), - curMoveNum - prevBranch.previous().getData().moveNumber - 1)) { - isDeepEnough = true; - break; - } - } - if (isDeepEnough) - break; - // Did not find a deep enough branch, move up unless we reached main trunk - if (BoardHistoryList.isMainTrunk(prevBranch.previous())) { - // No right hand side branch to move too - return false; - } - tmpNode = prevBranch.previous(); - - } - } - - // At this point, we know there is somewhere to move to... Move there, one step at the time (because of Leelaz) - // Start moving up the tree until we reach a moves with variations that are deep enough - BoardHistoryNode prevNode; - int startIdx = 0; - while (curNode.previous() != null) { - prevNode = curNode; - previousMove(); - curNode = history.getCurrentHistoryNode(); - startIdx = BoardHistoryList.findIndexOfNode(curNode, prevNode) + 1; - if (curNode.numberOfChildren() > 1 && startIdx <= curNode.numberOfChildren()) { - // Find the variation that is deep enough - boolean isDeepEnough = false; - for (int i = startIdx; i < curNode.numberOfChildren(); i++) { - if (BoardHistoryList.hasDepth(curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) - { - isDeepEnough = true; - break; - } - } - if (isDeepEnough) - break; - } - } - // Now move forward in new branch - while (curNode.getData().moveNumber < curMoveNum) { - if (curNode.numberOfChildren() == 1) { - // One-way street, just move to next - if (!nextVariation(0)) { - // Not supposed to happen, fail-safe - break; - } - } else { - // Has several variations, need to find the closest one that is deep enough - for (int i = startIdx; i < curNode.numberOfChildren(); i++) - { - if (BoardHistoryList.hasDepth(curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { - nextVariation(i); - break; - } - } - } - startIdx = 0; - curNode = history.getCurrentHistoryNode(); + } + + /* + * Moves to previous variation (variation to the left) if possible, or back to main trunk + * To move to another variation, the variation must have the same number of moves in it. + * If no variation with sufficient moves exist, move back to main trunk. + * Note: Will always move back to main trunk, even if variation has more moves than main trunk (if that + * is the case it will move to the last move in the trunk) + */ + public boolean previousBranch() { + synchronized (this) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + BoardHistoryNode prevNode; + int curMoveNum = curNode.getData().moveNumber; + + if (BoardHistoryList.isMainTrunk(curNode)) { + // Not possible to move further to the left, so just return + return false; + } + // We already know we can move back (back to main trunk if necessary), so just start moving + // Move backwards first + int depth = 0; + int startIdx = 0; + boolean foundBranch = false; + while (!BoardHistoryList.isMainTrunk(curNode)) { + prevNode = curNode; + // Move back + previousMove(); + curNode = history.getCurrentHistoryNode(); + depth++; + startIdx = BoardHistoryList.findIndexOfNode(curNode, prevNode); + // If current move has children, check if any of those are deep enough (starting at the one + // closest) + if (curNode.numberOfChildren() > 1 && startIdx != 0) { + foundBranch = false; + for (int i = startIdx - 1; i >= 0; i--) { + if (BoardHistoryList.hasDepth(curNode.getVariation(i), depth - 1)) { + foundBranch = true; + startIdx = i; + break; } - return true; + } + if (foundBranch) break; // Found a variation (or main trunk) and it is deep enough } - } - - /* - * Moves to previous variation (variation to the left) if possible, or back to main trunk - * To move to another variation, the variation must have the same number of moves in it. - * If no variation with sufficient moves exist, move back to main trunk. - * Note: Will always move back to main trunk, even if variation has more moves than main trunk (if that - * is the case it will move to the last move in the trunk) - */ - public boolean previousBranch() { - synchronized (this) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - BoardHistoryNode prevNode; - int curMoveNum = curNode.getData().moveNumber; - - if (BoardHistoryList.isMainTrunk(curNode)) { - // Not possible to move further to the left, so just return - return false; - } - // We already know we can move back (back to main trunk if necessary), so just start moving - // Move backwards first - int depth = 0; - int startIdx = 0; - boolean foundBranch = false; - while (!BoardHistoryList.isMainTrunk(curNode)) { - prevNode = curNode; - // Move back - previousMove(); - curNode = history.getCurrentHistoryNode(); - depth++; - startIdx = BoardHistoryList.findIndexOfNode(curNode, prevNode); - // If current move has children, check if any of those are deep enough (starting at the one closest) - if (curNode.numberOfChildren() > 1 && startIdx != 0) { - foundBranch = false; - for (int i = startIdx - 1; i >= 0; i--) { - if (BoardHistoryList.hasDepth(curNode.getVariation(i), depth-1)) { - foundBranch = true; - startIdx = i; - break; - } - } - if (foundBranch) break; // Found a variation (or main trunk) and it is deep enough - } - } - - if (!foundBranch) - { - // Back at main trunk, and it is not long enough, move forward until we reach the end.. - while (nextVariation(0)) { + } - }; - return true; - } + if (!foundBranch) { + // Back at main trunk, and it is not long enough, move forward until we reach the end.. + while (nextVariation(0)) {} - // At this point, we are either back at the main trunk, or on top of variation we know is long enough - // Move forward - while (curNode.getData().moveNumber < curMoveNum && curNode.next() != null) { - if (curNode.numberOfChildren() == 1) { - // One-way street, just move to next - if (!nextVariation(0)) { - // Not supposed to happen... - break; - } - } else { - foundBranch = false; - // Several variations to choose between, make sure we select the one closest that is deep enough (if any) - for (int i = startIdx; i >= 0; i--) - { - if (BoardHistoryList.hasDepth(curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { - nextVariation(i); - foundBranch = true; - break; - } - } - if (!foundBranch) { - // Not supposed to happen, fail-safe - nextVariation(0); - } - } - // We have now moved one step further down - curNode = history.getCurrentHistoryNode(); - startIdx = curNode.numberOfChildren() - 1; + ; + return true; + } + + // At this point, we are either back at the main trunk, or on top of variation we know is long + // enough + // Move forward + while (curNode.getData().moveNumber < curMoveNum && curNode.next() != null) { + if (curNode.numberOfChildren() == 1) { + // One-way street, just move to next + if (!nextVariation(0)) { + // Not supposed to happen... + break; + } + } else { + foundBranch = false; + // Several variations to choose between, make sure we select the one closest that is deep + // enough (if any) + for (int i = startIdx; i >= 0; i--) { + if (BoardHistoryList.hasDepth( + curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { + nextVariation(i); + foundBranch = true; + break; } - return true; + } + if (!foundBranch) { + // Not supposed to happen, fail-safe + nextVariation(0); + } } + // We have now moved one step further down + curNode = history.getCurrentHistoryNode(); + startIdx = curNode.numberOfChildren() - 1; + } + return true; } + } - public void moveBranchUp() { - synchronized (this) { - history.getCurrentHistoryNode().topOfBranch().moveUp(); - } + public void moveBranchUp() { + synchronized (this) { + history.getCurrentHistoryNode().topOfBranch().moveUp(); } + } - public void moveBranchDown() { - synchronized (this) { - history.getCurrentHistoryNode().topOfBranch().moveDown(); - } + public void moveBranchDown() { + synchronized (this) { + history.getCurrentHistoryNode().topOfBranch().moveDown(); } - - public void deleteMove() { - synchronized (this) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - if (curNode.next() != null) { - // Will delete more than one move, ask for confirmation - int ret = JOptionPane.showConfirmDialog(null, "This will delete all moves and branches after this move", "Delete", JOptionPane.OK_CANCEL_OPTION); - if (ret != JOptionPane.OK_OPTION) { - return; - } - - } - // Clear the board if we're at the top - if (curNode.previous() == null) { - clear(); - return; - } - previousMove(); - int idx = BoardHistoryList.findIndexOfNode(curNode.previous(), curNode); - curNode.previous().deleteChild(idx); - } - } - - public void deleteBranch() { - int originalMoveNumber = history.getMoveNumber(); - undoToChildOfPreviousWithVariation(); - int moveNumberBeforeOperation = history.getMoveNumber(); - deleteMove(); - boolean canceled = (history.getMoveNumber() == moveNumberBeforeOperation); - if (canceled) { - goToMoveNumber(originalMoveNumber); + } + + public void deleteMove() { + synchronized (this) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + if (curNode.next() != null) { + // Will delete more than one move, ask for confirmation + int ret = + JOptionPane.showConfirmDialog( + null, + "This will delete all moves and branches after this move", + "Delete", + JOptionPane.OK_CANCEL_OPTION); + if (ret != JOptionPane.OK_OPTION) { + return; } + } + // Clear the board if we're at the top + if (curNode.previous() == null) { + clear(); + return; + } + previousMove(); + int idx = BoardHistoryList.findIndexOfNode(curNode.previous(), curNode); + curNode.previous().deleteChild(idx); } - - public BoardData getData() { - return history.getData(); + } + + public void deleteBranch() { + int originalMoveNumber = history.getMoveNumber(); + undoToChildOfPreviousWithVariation(); + int moveNumberBeforeOperation = history.getMoveNumber(); + deleteMove(); + boolean canceled = (history.getMoveNumber() == moveNumberBeforeOperation); + if (canceled) { + goToMoveNumber(originalMoveNumber); } - - public BoardHistoryList getHistory() { - return history; + } + + public BoardData getData() { + return history.getData(); + } + + public BoardHistoryList getHistory() { + return history; + } + + /** Clears all history and starts over from empty board. */ + public void clear() { + Lizzie.leelaz.sendCommand("clear_board"); + Lizzie.frame.resetTitle(); + initialize(); + } + + /** Goes to the previous coordinate, thread safe */ + public boolean previousMove() { + synchronized (this) { + if (inScoreMode()) setScoreMode(false); + // 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; + } + if (history.previous() != null) { + Lizzie.leelaz.undo(); + Lizzie.frame.repaint(); + return true; + } + return false; } - - /** - * Clears all history and starts over from empty board. - */ - public void clear() { - Lizzie.leelaz.sendCommand("clear_board"); - Lizzie.frame.resetTitle(); - initialize(); + } + + public boolean undoToChildOfPreviousWithVariation() { + BoardHistoryNode start = history.getCurrentHistoryNode(); + BoardHistoryNode goal = history.findChildOfPreviousWithVariation(start); + if (start == goal) return false; + while ((history.getCurrentHistoryNode() != goal) && previousMove()) ; + return true; + } + + public void setScoreMode(boolean on) { + if (on) { + // load a copy of the data at the current node of history + capturedStones = history.getStones().clone(); + } else { + capturedStones = null; } - - /** - * Goes to the previous coordinate, thread safe - */ - public boolean previousMove() { - synchronized (this) { - if (inScoreMode()) setScoreMode(false); - // 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; - } - if (history.previous() != null) { - Lizzie.leelaz.undo(); - Lizzie.frame.repaint(); - return true; - } - return false; - } + scoreMode = on; + } + + /* + * Starting at position stonex, stoney, remove all stones with same color within an area bordered by stones + * of opposite color (AKA captured stones) + */ + private void toggleLiveStatus(Stone[] stones, int stonex, int stoney) { + Stone[] shdwstones = stones.clone(); + Stone toggle = stones[getIndex(stonex, stoney)]; + Stone toggleToo; + switch (toggle) { + case BLACK: + toggleToo = Stone.BLACK_CAPTURED; + break; + case BLACK_CAPTURED: + toggleToo = Stone.BLACK; + break; + case WHITE: + toggleToo = Stone.WHITE_CAPTURED; + break; + case WHITE_CAPTURED: + toggleToo = Stone.WHITE; + break; + default: + return; } - - public boolean undoToChildOfPreviousWithVariation() { - BoardHistoryNode start = history.getCurrentHistoryNode(); - BoardHistoryNode goal = history.findChildOfPreviousWithVariation(start); - if (start == goal) - return false; - while ((history.getCurrentHistoryNode() != goal) && previousMove()) ; - return true; - } - - public void setScoreMode(boolean on) { - if (on) { - // load a copy of the data at the current node of history - capturedStones = history.getStones().clone(); + boolean lastup, lastdown; + // This is using a flood fill algorithm that uses a Q instead of being recursive + Queue visitQ = new ArrayDeque<>(); + visitQ.add(new int[] {stonex, stoney}); + while (!visitQ.isEmpty()) { + int[] curpos = visitQ.remove(); + int x = curpos[0]; + int y = curpos[1]; + + // Move all the way left + while (x > 0 + && (stones[getIndex(x - 1, y)] == Stone.EMPTY || stones[getIndex(x - 1, y)] == toggle)) { + x--; + } + + lastup = lastdown = false; + // Find all stones within empty area line by line (new lines added to Q) + while (x < BOARD_SIZE) { + if (shdwstones[getIndex(x, y)] == Stone.EMPTY) { + shdwstones[getIndex(x, y)] = Stone.DAME; // Too mark that it has been visited + } else if (stones[getIndex(x, y)] == toggle) { + stones[getIndex(x, y)] = toggleToo; } else { - capturedStones = null; + break; } - scoreMode = on; - } - - /* - * Starting at position stonex, stoney, remove all stones with same color within an area bordered by stones - * of opposite color (AKA captured stones) - */ - private void toggleLiveStatus(Stone[] stones, int stonex, int stoney) - { - Stone[] shdwstones = stones.clone(); - Stone toggle = stones[getIndex(stonex, stoney)]; - Stone toggleToo; - switch (toggle) { - case BLACK: - toggleToo = Stone.BLACK_CAPTURED; - break; - case BLACK_CAPTURED: - toggleToo = Stone.BLACK; - break; - case WHITE: - toggleToo = Stone.WHITE_CAPTURED; - break; - case WHITE_CAPTURED: - toggleToo = Stone.WHITE; - break; - default: - return; + // Check above + if (y - 1 >= 0 + && (shdwstones[getIndex(x, y - 1)] == Stone.EMPTY + || stones[getIndex(x, y - 1)] == toggle)) { + if (!lastup) visitQ.add(new int[] {x, y - 1}); + lastup = true; + } else { + lastup = false; } - boolean lastup, lastdown; - // This is using a flood fill algorithm that uses a Q instead of being recursive - Queue visitQ = new ArrayDeque<>(); - visitQ.add(new int[]{stonex, stoney}); - while (!visitQ.isEmpty()) - { - int[] curpos = visitQ.remove(); - int x = curpos[0]; - int y = curpos[1]; - - // Move all the way left - while (x > 0 && (stones[getIndex(x-1, y)] == Stone.EMPTY || stones[getIndex(x-1, y)] == toggle)) - { - x--; - } - - lastup = lastdown = false; - // Find all stones within empty area line by line (new lines added to Q) - while (x < BOARD_SIZE ) { - if (shdwstones[getIndex(x, y)] == Stone.EMPTY) { - shdwstones[getIndex(x, y)] = Stone.DAME; // Too mark that it has been visited - } else if (stones[getIndex(x, y)] == toggle) { - stones[getIndex(x, y)] = toggleToo; - } else { - break; - } - // Check above - if (y - 1 >= 0 && (shdwstones[getIndex(x, y - 1)] == Stone.EMPTY || stones[getIndex(x, y - 1)] == toggle)) { - if (!lastup) - visitQ.add(new int[]{x, y - 1}); - lastup = true; - } else { - lastup = false; - } - // Check below - if (y + 1 < BOARD_SIZE && (shdwstones[getIndex(x, y + 1)] == Stone.EMPTY || stones[getIndex(x, y + 1)] == toggle)) { - if (!lastdown) - visitQ.add(new int[]{x, y + 1}); - lastdown = true; - } else { - lastdown = false; - } - x++; - } + // Check below + if (y + 1 < BOARD_SIZE + && (shdwstones[getIndex(x, y + 1)] == Stone.EMPTY + || stones[getIndex(x, y + 1)] == toggle)) { + if (!lastdown) visitQ.add(new int[] {x, y + 1}); + lastdown = true; + } else { + lastdown = false; } - Lizzie.frame.repaint(); + x++; + } } - - /* - * Check if a point on the board is empty or contains a captured stone - */ - private boolean emptyOrCaptured(Stone[] stones, int x, int y) { - int curidx = getIndex(x, y); - if (stones[curidx] == Stone.EMPTY || stones[curidx] == Stone.BLACK_CAPTURED || stones[curidx] == Stone.WHITE_CAPTURED) - return true; - return false; - } - - /* - * Starting from startx, starty, mark all empty points within area as either white, black or dame. - * If two stones of opposite color (neither marked as captured) is encountered, the area is dame. - * - * @return A stone with color white, black or dame - */ - private Stone markEmptyArea(Stone[] stones, int startx, int starty) { - Stone[] shdwstones = stones.clone(); - Stone found = Stone.EMPTY; // Found will either be black or white, or dame if both are found in area - boolean lastup, lastdown; - Queue visitQ = new ArrayDeque<>(); - visitQ.add(new int[]{startx, starty}); - Deque allPoints = new ArrayDeque<>(); - // Check one line at the time, new lines added to visitQ - while (!visitQ.isEmpty()) - { - int[] curpos = visitQ.remove(); - int x = curpos[0]; - int y = curpos[1]; - if (!emptyOrCaptured(shdwstones, x, y)) { - continue; - } - // Move all the way left - while (x > 0 && emptyOrCaptured(shdwstones, x - 1, y)) - { - x--; + Lizzie.frame.repaint(); + } + + /* + * Check if a point on the board is empty or contains a captured stone + */ + private boolean emptyOrCaptured(Stone[] stones, int x, int y) { + int curidx = getIndex(x, y); + if (stones[curidx] == Stone.EMPTY + || stones[curidx] == Stone.BLACK_CAPTURED + || stones[curidx] == Stone.WHITE_CAPTURED) return true; + return false; + } + + /* + * Starting from startx, starty, mark all empty points within area as either white, black or dame. + * If two stones of opposite color (neither marked as captured) is encountered, the area is dame. + * + * @return A stone with color white, black or dame + */ + private Stone markEmptyArea(Stone[] stones, int startx, int starty) { + Stone[] shdwstones = stones.clone(); + Stone found = + Stone.EMPTY; // Found will either be black or white, or dame if both are found in area + boolean lastup, lastdown; + Queue visitQ = new ArrayDeque<>(); + visitQ.add(new int[] {startx, starty}); + Deque allPoints = new ArrayDeque<>(); + // Check one line at the time, new lines added to visitQ + while (!visitQ.isEmpty()) { + int[] curpos = visitQ.remove(); + int x = curpos[0]; + int y = curpos[1]; + if (!emptyOrCaptured(shdwstones, x, y)) { + continue; + } + // Move all the way left + while (x > 0 && emptyOrCaptured(shdwstones, x - 1, y)) { + x--; + } + // Are we on the border, or do we have a stone to the left? + if (x > 0 && shdwstones[getIndex(x - 1, y)] != found) { + if (found == Stone.EMPTY) found = shdwstones[getIndex(x - 1, y)]; + else found = Stone.DAME; + } + + lastup = lastdown = false; + while (x < BOARD_SIZE && emptyOrCaptured(shdwstones, x, y)) { + // Check above + if (y - 1 >= 0 && shdwstones[getIndex(x, y - 1)] != Stone.DAME) { + if (emptyOrCaptured(shdwstones, x, y - 1)) { + if (!lastup) visitQ.add(new int[] {x, y - 1}); + lastup = true; + } else { + lastup = false; + if (found != shdwstones[getIndex(x, y - 1)]) { + if (found == Stone.EMPTY) { + found = shdwstones[getIndex(x, y - 1)]; + } else { + found = Stone.DAME; + } } - // Are we on the border, or do we have a stone to the left? - if (x > 0 && shdwstones[getIndex(x - 1, y)] != found) { - if (found == Stone.EMPTY) found = shdwstones[getIndex(x - 1, y)]; - else found = Stone.DAME; - } - - lastup = lastdown = false; - while (x < BOARD_SIZE && emptyOrCaptured(shdwstones, x, y)) { - // Check above - if (y - 1 >= 0 && shdwstones[getIndex(x, y - 1)] != Stone.DAME) { - if (emptyOrCaptured(shdwstones, x, y - 1)) { - if (!lastup) - visitQ.add(new int[]{x, y - 1}); - lastup = true; - } else { - lastup = false; - if (found != shdwstones[getIndex(x, y - 1)]) { - if (found == Stone.EMPTY) { - found = shdwstones[getIndex(x, y - 1)]; - } - else { - found = Stone.DAME; - } - } - } - } - // Check below - if (y + 1 < BOARD_SIZE && shdwstones[getIndex(x, y + 1)] != Stone.DAME) { - if (emptyOrCaptured(shdwstones, x, y + 1)) { - if (!lastdown) { - visitQ.add(new int[]{x, y + 1}); - } - lastdown = true; - } else { - lastdown = false; - if (found != shdwstones[getIndex(x, y + 1)]) { - if (found == Stone.EMPTY) { - found = shdwstones[getIndex(x, y + 1)]; - } - else { - found = Stone.DAME; - } - } - } - } - // Add current stone to empty area and mark as visited - if (shdwstones[getIndex(x,y)] == Stone.EMPTY) - allPoints.add(getIndex(x,y)); - - // Use dame stone to mark as visited - shdwstones[getIndex(x, y)] = Stone.DAME; - x++; + } + } + // Check below + if (y + 1 < BOARD_SIZE && shdwstones[getIndex(x, y + 1)] != Stone.DAME) { + if (emptyOrCaptured(shdwstones, x, y + 1)) { + if (!lastdown) { + visitQ.add(new int[] {x, y + 1}); } - // At this point x is at the edge of the board or on a stone - if (x < BOARD_SIZE && shdwstones[getIndex(x, y)] != found) { - if (found == Stone.EMPTY) found = shdwstones[getIndex(x, y )]; - else found = Stone.DAME; + lastdown = true; + } else { + lastdown = false; + if (found != shdwstones[getIndex(x, y + 1)]) { + if (found == Stone.EMPTY) { + found = shdwstones[getIndex(x, y + 1)]; + } else { + found = Stone.DAME; + } } + } } - // Finally mark all points as black or white captured if they were surronded by white or black - if (found == Stone.WHITE) found = Stone.WHITE_POINT; - else if (found == Stone.BLACK) found = Stone.BLACK_POINT; - // else found == DAME and will be set as this. - while (!allPoints.isEmpty()) { - int idx = allPoints.remove(); - stones[idx] = found; - } - return found; + // Add current stone to empty area and mark as visited + if (shdwstones[getIndex(x, y)] == Stone.EMPTY) allPoints.add(getIndex(x, y)); + + // Use dame stone to mark as visited + shdwstones[getIndex(x, y)] = Stone.DAME; + x++; + } + // At this point x is at the edge of the board or on a stone + if (x < BOARD_SIZE && shdwstones[getIndex(x, y)] != found) { + if (found == Stone.EMPTY) found = shdwstones[getIndex(x, y)]; + else found = Stone.DAME; + } + } + // Finally mark all points as black or white captured if they were surronded by white or black + if (found == Stone.WHITE) found = Stone.WHITE_POINT; + else if (found == Stone.BLACK) found = Stone.BLACK_POINT; + // else found == DAME and will be set as this. + while (!allPoints.isEmpty()) { + int idx = allPoints.remove(); + stones[idx] = found; } + return found; + } - /* - * Mark all empty points on board as black point, white point or dame - */ - public Stone[] scoreStones() { + /* + * Mark all empty points on board as black point, white point or dame + */ + public Stone[] scoreStones() { - Stone[] scoreStones = capturedStones.clone(); + Stone[] scoreStones = capturedStones.clone(); - for (int i = 0; i < BOARD_SIZE; i++) { - for (int j = 0; j < BOARD_SIZE; j++) { - if (scoreStones[getIndex(i, j)] == Stone.EMPTY) { - markEmptyArea(scoreStones, i, j); - } - } + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + if (scoreStones[getIndex(i, j)] == Stone.EMPTY) { + markEmptyArea(scoreStones, i, j); } - return scoreStones; + } } - - /* - * Count score for whole board, including komi and captured stones - */ - public double[] getScore(Stone[] scoreStones) { - double score[] = new double[] {getData().blackCaptures, getData().whiteCaptures + getHistory().getGameInfo().getKomi()}; - for (int i = 0; i < BOARD_SIZE; i++) { - for (int j = 0; j < BOARD_SIZE; j++) { - switch (scoreStones[getIndex(i, j)]) { - case BLACK_POINT: - score[0]++; - break; - case BLACK_CAPTURED: - score[1] += 2; - break; - - case WHITE_POINT: - score[1]++; - break; - case WHITE_CAPTURED: - score[0] += 2; - break; - - } - } + return scoreStones; + } + + /* + * Count score for whole board, including komi and captured stones + */ + public double[] getScore(Stone[] scoreStones) { + double score[] = + new double[] { + getData().blackCaptures, getData().whiteCaptures + getHistory().getGameInfo().getKomi() + }; + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + switch (scoreStones[getIndex(i, j)]) { + case BLACK_POINT: + score[0]++; + break; + case BLACK_CAPTURED: + score[1] += 2; + break; + + case WHITE_POINT: + score[1]++; + break; + case WHITE_CAPTURED: + score[0] += 2; + break; } - return score; - } - - public boolean inAnalysisMode() { - return analysisMode; + } } - - public boolean inScoreMode() { - return scoreMode; + return score; + } + + public boolean inAnalysisMode() { + return analysisMode; + } + + public boolean inScoreMode() { + return scoreMode; + } + + public void toggleAnalysis() { + if (analysisMode) { + Lizzie.leelaz.removeListener(this); + analysisMode = false; + } else { + if (getNextMove() == null) return; + String answer = + JOptionPane.showInputDialog( + "# playouts for analysis (e.g. 100 (fast) or 50000 (slow)): "); + if (answer == null) return; + try { + playoutsAnalysis = Integer.parseInt(answer); + } catch (NumberFormatException err) { + System.out.println("Not a valid number"); + return; + } + Lizzie.leelaz.addListener(this); + analysisMode = true; + if (!Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); } - - public void toggleAnalysis() { - if (analysisMode) { - Lizzie.leelaz.removeListener(this); - analysisMode = false; - } else { - if (getNextMove() == null) return; - String answer = JOptionPane.showInputDialog("# playouts for analysis (e.g. 100 (fast) or 50000 (slow)): "); - if (answer == null) return; - try { - playoutsAnalysis = Integer.parseInt(answer); - } catch (NumberFormatException err) { - System.out.println("Not a valid number"); - return; - } - Lizzie.leelaz.addListener(this); - analysisMode = true; - if (!Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + } + + public void bestMoveNotification(List bestMoves) { + if (analysisMode) { + boolean isSuccessivePass = + (history.getPrevious() != null + && history.getPrevious().lastMove == null + && getLastMove() == null); + // Note: We cannot replace this history.getNext() with getNextMove() + // because the latter returns null if the next move is "pass". + if (history.getNext() == null || isSuccessivePass) { + // Reached the end... + toggleAnalysis(); + } else if (bestMoves == null || bestMoves.size() == 0) { + // If we get empty list, something strange happened, ignore notification + } else { + // sum the playouts to proceed like leelaz's --visits option. + int sum = 0; + for (MoveData move : bestMoves) { + sum += move.playouts; } - } - - public void bestMoveNotification(List bestMoves) { - if (analysisMode) { - boolean isSuccessivePass = (history.getPrevious() != null && - history.getPrevious().lastMove == null && - getLastMove() == null); - // Note: We cannot replace this history.getNext() with getNextMove() - // because the latter returns null if the next move is "pass". - if (history.getNext() == null || isSuccessivePass) { - // Reached the end... - toggleAnalysis(); - } else if (bestMoves == null || bestMoves.size() == 0) { - // If we get empty list, something strange happened, ignore notification - } else { - // sum the playouts to proceed like leelaz's --visits option. - int sum = 0; - for (MoveData move : bestMoves) { - sum += move.playouts; - } - if (sum >= playoutsAnalysis) { - nextMove(); - } - } + if (sum >= playoutsAnalysis) { + nextMove(); } + } } - - public void autosave() { - if (autosaveToMemory()) { - try { - Lizzie.config.persist(); - } catch (IOException err) {} - } + } + + public void autosave() { + if (autosaveToMemory()) { + try { + Lizzie.config.persist(); + } catch (IOException err) { + } } + } - public boolean autosaveToMemory() { - try { - String sgf = SGFParser.saveToString(); - if (sgf.equals(Lizzie.config.persisted.getString("autosave"))) { - return false; - } - Lizzie.config.persisted.put("autosave", sgf); - } catch (Exception err) { // IOException or JSONException - return false; - } - return true; + public boolean autosaveToMemory() { + try { + String sgf = SGFParser.saveToString(); + if (sgf.equals(Lizzie.config.persisted.getString("autosave"))) { + return false; + } + Lizzie.config.persisted.put("autosave", sgf); + } catch (Exception err) { // IOException or JSONException + return false; } - - public void resumePreviousGame() { - try { - SGFParser.loadFromString(Lizzie.config.persisted.getString("autosave")); - while (nextMove()) ; - } catch (JSONException err) {} + return true; + } + + public void resumePreviousGame() { + try { + SGFParser.loadFromString(Lizzie.config.persisted.getString("autosave")); + while (nextMove()) ; + } catch (JSONException err) { } + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index be77d1619..ac91dd40a 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -1,40 +1,51 @@ package featurecat.lizzie.rules; public class BoardData { - public int moveNumber; - public int[] lastMove; - public int[] moveNumberList; - public boolean blackToPlay; - - public Stone lastMoveColor; - public Stone[] stones; - public Zobrist zobrist; - - public boolean verify; - - public double winrate; - public int playouts; - - public int blackCaptures; - public int whiteCaptures; - - // Comment in the Sgf move - public String comment; - - public BoardData(Stone[] stones, int[] lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, int moveNumber, int[] moveNumberList, int blackCaptures, int whiteCaptures, double winrate, int playouts) { - this.moveNumber = moveNumber; - this.lastMove = lastMove; - this.moveNumberList = moveNumberList; - this.blackToPlay = blackToPlay; - - this.lastMoveColor = lastMoveColor; - this.stones = stones; - this.zobrist = zobrist; - this.verify = false; - - this.winrate = winrate; - this.playouts = playouts; - this.blackCaptures = blackCaptures; - this.whiteCaptures = whiteCaptures; - } + public int moveNumber; + public int[] lastMove; + public int[] moveNumberList; + public boolean blackToPlay; + + public Stone lastMoveColor; + public Stone[] stones; + public Zobrist zobrist; + + public boolean verify; + + public double winrate; + public int playouts; + + public int blackCaptures; + public int whiteCaptures; + + // Comment in the Sgf move + public String comment; + + public BoardData( + Stone[] stones, + int[] lastMove, + Stone lastMoveColor, + boolean blackToPlay, + Zobrist zobrist, + int moveNumber, + int[] moveNumberList, + int blackCaptures, + int whiteCaptures, + double winrate, + int playouts) { + this.moveNumber = moveNumber; + this.lastMove = lastMove; + this.moveNumberList = moveNumberList; + this.blackToPlay = blackToPlay; + + this.lastMoveColor = lastMoveColor; + this.stones = stones; + this.zobrist = zobrist; + this.verify = false; + + this.winrate = winrate; + this.playouts = playouts; + this.blackCaptures = blackCaptures; + this.whiteCaptures = whiteCaptures; + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index 643079036..3d4027a16 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -1,352 +1,319 @@ package featurecat.lizzie.rules; -import java.util.List; - import featurecat.lizzie.analysis.GameInfo; +import java.util.List; -/** - * Linked list data structure to store board history - */ +/** Linked list data structure to store board history */ public class BoardHistoryList { - private GameInfo gameInfo; - private BoardHistoryNode head; - - /** - * Initialize a new board history list, whose first node is data - * - * @param data the data to be stored for the first entry - */ - public BoardHistoryList(BoardData data) { - head = new BoardHistoryNode(data); - gameInfo = new GameInfo(); - } - - public GameInfo getGameInfo() { - return gameInfo; - } - - public void setGameInfo(GameInfo gameInfo) { - this.gameInfo = gameInfo; - } - - public BoardHistoryList shallowCopy() { - BoardHistoryList copy = new BoardHistoryList(null); - copy.head = head; - copy.gameInfo = gameInfo; - return copy; - } - - /** - * Clear history. - */ - public void clear() { - head.clear(); - } - - /** - * Add new data after head. Overwrites any data that may have been stored after head. - * - * @param data the data to add - */ - public void add(BoardData data) { - head = head.add(new BoardHistoryNode(data)); - } - - public void addOrGoto(BoardData data) { - head = head.addOrGoto(data); - } - - /** - * moves the pointer to the left, returns the data stored there - * - * @return data of previous node, null if there is no previous node - */ - public BoardData previous() { - if (head.previous() == null) - return null; - else - head = head.previous(); - - return head.getData(); - } - - public void toStart() { - while (previous() != null); - } - - /** - * moves the pointer to the right, returns the data stored there - * - * @return the data of next node, null if there is no next node - */ - public BoardData next() { - if (head.next() == null) - return null; - else - head = head.next(); - - return head.getData(); - } - - /** - * moves the pointer to the variation number idx, returns the data stored there - * - * @return the data of next node, null if there is no variaton with index - */ - public BoardData nextVariation(int idx) { - if (head.getVariation(idx) == null) - return null; - else - head = head.getVariation(idx); - - return head.getData(); - } - - /** - * Does not change the pointer position - * - * @return the data stored at the next index. null if not present - */ - public BoardData getNext() { - if (head.next() == null) - return null; - else - return head.next().getData(); - } - - /** - * @return nexts for display - */ - public List getNexts() { - return head.getNexts(); - } - - /** - * Does not change the pointer position - * - * @return the data stored at the previous index. null if not present - */ - public BoardData getPrevious() { - if (head.previous() == null) - return null; - else - return head.previous().getData(); - } - - /** - * @return the data of the current node - */ - public BoardData getData() { - return head.getData(); - } - - public void setStone(int[] coordinates, Stone stone) { - int index = Board.getIndex(coordinates[0], coordinates[1]); - head.getData().stones[index] = stone; - head.getData().zobrist.toggleStone(coordinates[0], coordinates[1], stone); - } - - public Stone[] getStones() { - return head.getData().stones; - } - - public int[] getLastMove() { - return head.getData().lastMove; - } - - public int[] getNextMove() { - BoardData next = getNext(); - if (next == null) - return null; - else - return next.lastMove; - } - - public Stone getLastMoveColor() { - return head.getData().lastMoveColor; - } - - public boolean isBlacksTurn() { - return head.getData().blackToPlay; - } - - public Zobrist getZobrist() { - return head.getData().zobrist.clone(); - } - - public int getMoveNumber() { - return head.getData().moveNumber; - } - - public int[] getMoveNumberList() { - return head.getData().moveNumberList; - } - - public BoardHistoryNode getCurrentHistoryNode() { - return head; - } - - /** - * @param data the board position to check against superko - * @return whether or not the given position violates the superko rule at the head's state - */ - public boolean violatesSuperko(BoardData data) { - BoardHistoryNode head = this.head; - - // check to see if this position has occurred before - while (head.previous() != null) { - // if two zobrist hashes are equal, and it's the same player to coordinate, they are the same position - if (data.zobrist.equals(head.getData().zobrist) && data.blackToPlay == head.getData().blackToPlay) - return true; - - head = head.previous(); + private GameInfo gameInfo; + private BoardHistoryNode head; + + /** + * Initialize a new board history list, whose first node is data + * + * @param data the data to be stored for the first entry + */ + public BoardHistoryList(BoardData data) { + head = new BoardHistoryNode(data); + gameInfo = new GameInfo(); + } + + public GameInfo getGameInfo() { + return gameInfo; + } + + public void setGameInfo(GameInfo gameInfo) { + this.gameInfo = gameInfo; + } + + public BoardHistoryList shallowCopy() { + BoardHistoryList copy = new BoardHistoryList(null); + copy.head = head; + copy.gameInfo = gameInfo; + return copy; + } + + /** Clear history. */ + public void clear() { + head.clear(); + } + + /** + * Add new data after head. Overwrites any data that may have been stored after head. + * + * @param data the data to add + */ + public void add(BoardData data) { + head = head.add(new BoardHistoryNode(data)); + } + + public void addOrGoto(BoardData data) { + head = head.addOrGoto(data); + } + + /** + * moves the pointer to the left, returns the data stored there + * + * @return data of previous node, null if there is no previous node + */ + public BoardData previous() { + if (head.previous() == null) return null; + else head = head.previous(); + + return head.getData(); + } + + public void toStart() { + while (previous() != null) ; + } + + /** + * moves the pointer to the right, returns the data stored there + * + * @return the data of next node, null if there is no next node + */ + public BoardData next() { + if (head.next() == null) return null; + else head = head.next(); + + return head.getData(); + } + + /** + * moves the pointer to the variation number idx, returns the data stored there + * + * @return the data of next node, null if there is no variaton with index + */ + public BoardData nextVariation(int idx) { + if (head.getVariation(idx) == null) return null; + else head = head.getVariation(idx); + + return head.getData(); + } + + /** + * Does not change the pointer position + * + * @return the data stored at the next index. null if not present + */ + public BoardData getNext() { + if (head.next() == null) return null; + else return head.next().getData(); + } + + /** @return nexts for display */ + public List getNexts() { + return head.getNexts(); + } + + /** + * Does not change the pointer position + * + * @return the data stored at the previous index. null if not present + */ + public BoardData getPrevious() { + if (head.previous() == null) return null; + else return head.previous().getData(); + } + + /** @return the data of the current node */ + public BoardData getData() { + return head.getData(); + } + + public void setStone(int[] coordinates, Stone stone) { + int index = Board.getIndex(coordinates[0], coordinates[1]); + head.getData().stones[index] = stone; + head.getData().zobrist.toggleStone(coordinates[0], coordinates[1], stone); + } + + public Stone[] getStones() { + return head.getData().stones; + } + + public int[] getLastMove() { + return head.getData().lastMove; + } + + public int[] getNextMove() { + BoardData next = getNext(); + if (next == null) return null; + else return next.lastMove; + } + + public Stone getLastMoveColor() { + return head.getData().lastMoveColor; + } + + public boolean isBlacksTurn() { + return head.getData().blackToPlay; + } + + public Zobrist getZobrist() { + return head.getData().zobrist.clone(); + } + + public int getMoveNumber() { + return head.getData().moveNumber; + } + + public int[] getMoveNumberList() { + return head.getData().moveNumberList; + } + + public BoardHistoryNode getCurrentHistoryNode() { + return head; + } + + /** + * @param data the board position to check against superko + * @return whether or not the given position violates the superko rule at the head's state + */ + public boolean violatesSuperko(BoardData data) { + BoardHistoryNode head = this.head; + + // check to see if this position has occurred before + while (head.previous() != null) { + // if two zobrist hashes are equal, and it's the same player to coordinate, they are the same + // position + if (data.zobrist.equals(head.getData().zobrist) + && data.blackToPlay == head.getData().blackToPlay) return true; + + head = head.previous(); + } + + // no position matched this position, so it's valid + return false; + } + + /** + * Returns the root node + * + * @return root node + */ + public BoardHistoryNode root() { + BoardHistoryNode top = head; + while (top.previous() != null) { + top = top.previous(); + } + return top; + } + + /** + * Returns the length of current branch + * + * @return length of current branch + */ + public int currentBranchLength() { + return getMoveNumber() + BoardHistoryList.getDepth(head); + } + + /** + * Returns the length of main trunk + * + * @return length of main trunk + */ + public int mainTrunkLength() { + return BoardHistoryList.getDepth(root()); + } + + /* + * Static helper methods + */ + + /** + * Returns the number of moves in a tree (only the left-most (trunk) variation) + * + * @return number of moves in a tree + */ + public static int getDepth(BoardHistoryNode node) { + int c = 0; + while (node.next() != null) { + c++; + node = node.next(); + } + return c; + } + + /** + * Check if there is a branch that is at least depth deep (at least depth moves) + * + * @return true if it is deep enough, false otherwise + */ + public static boolean hasDepth(BoardHistoryNode node, int depth) { + int c = 0; + if (depth <= 0) return true; + while (node.next() != null) { + if (node.numberOfChildren() > 1) { + for (int i = 0; i < node.numberOfChildren(); i++) { + if (hasDepth(node.getVariation(i), depth - c - 1)) return true; } - - // no position matched this position, so it's valid return false; - } - - /** - * Returns the root node - * - * @return root node - */ - public BoardHistoryNode root() { - BoardHistoryNode top = head; - while (top.previous() != null) { - top = top.previous(); - } - return top; - } - - /** - * Returns the length of current branch - * - * @return length of current branch - */ - public int currentBranchLength() { - return getMoveNumber() + BoardHistoryList.getDepth(head); - } - - /** - * Returns the length of main trunk - * - * @return length of main trunk - */ - public int mainTrunkLength() { - return BoardHistoryList.getDepth(root()); - } - - /* - * Static helper methods - */ - - /** - * Returns the number of moves in a tree (only the left-most (trunk) variation) - * - * @return number of moves in a tree - */ - static public int getDepth(BoardHistoryNode node) - { - int c = 0; - while (node.next() != null) - { - c++; - node = node.next(); - } - return c; - } - - /** - * Check if there is a branch that is at least depth deep (at least depth moves) - * - * @return true if it is deep enough, false otherwise - */ - static public boolean hasDepth(BoardHistoryNode node, int depth) { - int c = 0; - if (depth <= 0) return true; - while (node.next() != null) { - if (node.numberOfChildren() > 1) { - for (int i = 0; i < node.numberOfChildren(); i++) { - if (hasDepth(node.getVariation(i), depth - c - 1)) - return true; - } - return false; - } else { - node = node.next(); - c++; - if (c >= depth) return true; - } - } + } else { + node = node.next(); + c++; + if (c >= depth) return true; + } + } + return false; + } + + /** + * Find top of variation (the first move that is on the main trunk) + * + * @return top of variaton, if on main trunk, return start move + */ + public static BoardHistoryNode findTop(BoardHistoryNode start) { + BoardHistoryNode top = start; + while (start.previous() != null) { + if (start.previous().next() != start) { + top = start.previous(); + } + start = start.previous(); + } + return top; + } + + /** + * Find first move with variations in tree above node + * + * @return The child (in the current variation) of the first node with variations + */ + public static BoardHistoryNode findChildOfPreviousWithVariation(BoardHistoryNode node) { + while (node.previous() != null) { + if (node.previous().numberOfChildren() > 1) { + return node; + } + node = node.previous(); + } + return null; + } + + /** + * Given a parent node and a child node, find the index of the child node + * + * @return index of child node, -1 if child node not a child of parent + */ + public static int findIndexOfNode(BoardHistoryNode parentNode, BoardHistoryNode childNode) { + if (parentNode.next() == null) return -1; + for (int i = 0; i < parentNode.numberOfChildren(); i++) { + if (parentNode.getVariation(i) == childNode) return i; + } + return -1; + } + + /** + * Check if node is part of the main trunk (rightmost branch) + * + * @return true if node is part of main trunk, false otherwise + */ + public static boolean isMainTrunk(BoardHistoryNode node) { + while (node.previous() != null) { + if (node.previous().next() != node) { return false; + } + node = node.previous(); } - - /** - * Find top of variation (the first move that is on the main trunk) - * - * @return top of variaton, if on main trunk, return start move - */ - static public BoardHistoryNode findTop(BoardHistoryNode start) - { - BoardHistoryNode top = start; - while (start.previous() != null) - { - if (start.previous().next() != start) - { - top = start.previous(); - } - start = start.previous(); - } - return top; - } - - /** - * Find first move with variations in tree above node - * - * @return The child (in the current variation) of the first node with variations - */ - static public BoardHistoryNode findChildOfPreviousWithVariation(BoardHistoryNode node) - { - while (node.previous() != null) - { - if (node.previous().numberOfChildren() > 1) - { - return node; - } - node = node.previous(); - } - return null; - } - - /** - * Given a parent node and a child node, find the index of the child node - * - * @return index of child node, -1 if child node not a child of parent - */ - static public int findIndexOfNode(BoardHistoryNode parentNode, BoardHistoryNode childNode) - { - if (parentNode.next() == null) return -1; - for (int i = 0; i < parentNode.numberOfChildren(); i++) - { - if (parentNode.getVariation(i) == childNode) return i; - } - return -1; - } - - /** - * Check if node is part of the main trunk (rightmost branch) - * - * @return true if node is part of main trunk, false otherwise - */ - static public boolean isMainTrunk(BoardHistoryNode node) - { - while (node.previous() != null) { - if (node.previous().next() != node) { - return false; - } - node = node.previous(); - } - return true; - } - + return true; + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java index 95cf436f0..32d86d4e4 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java @@ -1,176 +1,170 @@ package featurecat.lizzie.rules; + import java.util.ArrayList; import java.util.List; -/** - * Node structure for the board history / sgf tree - */ +/** Node structure for the board history / sgf tree */ public class BoardHistoryNode { - private BoardHistoryNode previous; - private ArrayList nexts; - - private BoardData data; - - /** - * Initializes a new list node - */ - public BoardHistoryNode(BoardData data) { - previous = null; - nexts = new ArrayList(); - this.data = data; - } - - /** - * Remove all subsequent nodes. - */ - public void clear() { - nexts.clear(); - } - - /** - * Sets up for a new node. Overwrites future history. - * - * @param node the node following this one - * @return the node that was just set - */ - public BoardHistoryNode add(BoardHistoryNode node) { - nexts.clear(); - nexts.add(node); - node.previous = this; - - return node; - } - - /** - * If we already have a next node with the same BoardData, move to it, - * otherwise add it and move to it. - * - * @param data the node following this one - * @return the node that was just set - */ - public BoardHistoryNode addOrGoto(BoardData data) { - // If you play a hand and immediately return it, it is most likely that you have made a mistake. Ask whether to delete the previous node. -// if (!nexts.isEmpty() && !nexts.get(0).data.zobrist.equals(data.zobrist)) { -// // You may just mark this hand, so it's not necessarily wrong. Answer when the first query is wrong or it will not ask whether the move is wrong. -// if (!nexts.get(0).data.verify) { -// int ret = JOptionPane.showConfirmDialog(null, "Do you want undo?", "Undo", JOptionPane.OK_CANCEL_OPTION); -// if (ret == JOptionPane.OK_OPTION) { -// nexts.remove(0); -// } else { -// nexts.get(0).data.verify = true; -// } -// } -// } - for (int i = 0; i < nexts.size(); i++) { - if (nexts.get(i).data.zobrist.equals(data.zobrist)) { -// if (i != 0) { -// // Swap selected next to foremost -// BoardHistoryNode currentNext = nexts.get(i); -// nexts.set(i, nexts.get(0)); -// nexts.set(0, currentNext); -// } - return nexts.get(i); - } - } - BoardHistoryNode node = new BoardHistoryNode(data); - // Add node - nexts.add(node); - node.previous = this; - - return node; - } - - /** - * @return data stored on this node - */ - public BoardData getData() { - return data; - } - - /** - * @return nexts for display - */ - public List getNexts() { - return nexts; - } - - public BoardHistoryNode previous() { - return previous; - } - - public BoardHistoryNode next() { - if (nexts.size() == 0) { - return null; - } else { - return nexts.get(0); - } - } - - public BoardHistoryNode topOfBranch() { - BoardHistoryNode top = this; - while (top.previous != null && top.previous.nexts.size() == 1) { - top = top.previous; - } - return top; - } - - public int numberOfChildren() { - if (nexts == null) { - return 0; - } else { - return nexts.size(); - } - } - - public boolean isFirstChild() { - return (previous != null) && previous.next() == this; - } - - public BoardHistoryNode getVariation(int idx) { - if (nexts.size() <= idx) { - return null; - } else { - return nexts.get(idx); - } - } - - public void moveUp() { - if (previous != null) { - previous.moveChildUp(this); - } - } - - public void moveDown() { - if (previous != null) { - previous.moveChildDown(this); - } - } - - public void moveChildUp(BoardHistoryNode child) { - for (int i = 1; i < nexts.size(); i++) { - if (nexts.get(i).data.zobrist.equals(child.data.zobrist)) { - BoardHistoryNode tmp = nexts.get(i-1); - nexts.set(i-1, child); - nexts.set(i, tmp); - return; - } - } - } - - public void moveChildDown(BoardHistoryNode child) { - for (int i = 0; i < nexts.size() - 1; i++) { - if (nexts.get(i).data.zobrist.equals(child.data.zobrist)) { - BoardHistoryNode tmp = nexts.get(i+1); - nexts.set(i+1, child); - nexts.set(i, tmp); - return; - } - } - } - - public void deleteChild(int idx) { - if (idx < numberOfChildren()) { - nexts.remove(idx); - } - } + private BoardHistoryNode previous; + private ArrayList nexts; + + private BoardData data; + + /** Initializes a new list node */ + public BoardHistoryNode(BoardData data) { + previous = null; + nexts = new ArrayList(); + this.data = data; + } + + /** Remove all subsequent nodes. */ + public void clear() { + nexts.clear(); + } + + /** + * Sets up for a new node. Overwrites future history. + * + * @param node the node following this one + * @return the node that was just set + */ + public BoardHistoryNode add(BoardHistoryNode node) { + nexts.clear(); + nexts.add(node); + node.previous = this; + + return node; + } + + /** + * If we already have a next node with the same BoardData, move to it, otherwise add it and move + * to it. + * + * @param data the node following this one + * @return the node that was just set + */ + public BoardHistoryNode addOrGoto(BoardData data) { + // If you play a hand and immediately return it, it is most likely that you have made a mistake. + // Ask whether to delete the previous node. + // if (!nexts.isEmpty() && !nexts.get(0).data.zobrist.equals(data.zobrist)) { + // // You may just mark this hand, so it's not necessarily wrong. Answer when the + // first query is wrong or it will not ask whether the move is wrong. + // if (!nexts.get(0).data.verify) { + // int ret = JOptionPane.showConfirmDialog(null, "Do you want undo?", "Undo", + // JOptionPane.OK_CANCEL_OPTION); + // if (ret == JOptionPane.OK_OPTION) { + // nexts.remove(0); + // } else { + // nexts.get(0).data.verify = true; + // } + // } + // } + for (int i = 0; i < nexts.size(); i++) { + if (nexts.get(i).data.zobrist.equals(data.zobrist)) { + // if (i != 0) { + // // Swap selected next to foremost + // BoardHistoryNode currentNext = nexts.get(i); + // nexts.set(i, nexts.get(0)); + // nexts.set(0, currentNext); + // } + return nexts.get(i); + } + } + BoardHistoryNode node = new BoardHistoryNode(data); + // Add node + nexts.add(node); + node.previous = this; + + return node; + } + + /** @return data stored on this node */ + public BoardData getData() { + return data; + } + + /** @return nexts for display */ + public List getNexts() { + return nexts; + } + + public BoardHistoryNode previous() { + return previous; + } + + public BoardHistoryNode next() { + if (nexts.size() == 0) { + return null; + } else { + return nexts.get(0); + } + } + + public BoardHistoryNode topOfBranch() { + BoardHistoryNode top = this; + while (top.previous != null && top.previous.nexts.size() == 1) { + top = top.previous; + } + return top; + } + + public int numberOfChildren() { + if (nexts == null) { + return 0; + } else { + return nexts.size(); + } + } + + public boolean isFirstChild() { + return (previous != null) && previous.next() == this; + } + + public BoardHistoryNode getVariation(int idx) { + if (nexts.size() <= idx) { + return null; + } else { + return nexts.get(idx); + } + } + + public void moveUp() { + if (previous != null) { + previous.moveChildUp(this); + } + } + + public void moveDown() { + if (previous != null) { + previous.moveChildDown(this); + } + } + + public void moveChildUp(BoardHistoryNode child) { + for (int i = 1; i < nexts.size(); i++) { + if (nexts.get(i).data.zobrist.equals(child.data.zobrist)) { + BoardHistoryNode tmp = nexts.get(i - 1); + nexts.set(i - 1, child); + nexts.set(i, tmp); + return; + } + } + } + + public void moveChildDown(BoardHistoryNode child) { + for (int i = 0; i < nexts.size() - 1; i++) { + if (nexts.get(i).data.zobrist.equals(child.data.zobrist)) { + BoardHistoryNode tmp = nexts.get(i + 1); + nexts.set(i + 1, child); + nexts.set(i, tmp); + return; + } + } + } + + public void deleteChild(int idx) { + if (idx < numberOfChildren()) { + nexts.remove(idx); + } + } } diff --git a/src/main/java/featurecat/lizzie/rules/GIBParser.java b/src/main/java/featurecat/lizzie/rules/GIBParser.java index 7f0c08aff..3f5e826ff 100644 --- a/src/main/java/featurecat/lizzie/rules/GIBParser.java +++ b/src/main/java/featurecat/lizzie/rules/GIBParser.java @@ -7,100 +7,100 @@ import java.io.InputStreamReader; public class GIBParser { - private static int[][] handicapPlacement = {{3, 15}, {15,3}, {15, 15}, {3, 3}, {3, 9}, - {15, 9}, {9, 3}, {9, 15}, {9, 9}}; + private static int[][] handicapPlacement = { + {3, 15}, {15, 3}, {15, 15}, {3, 3}, {3, 9}, {15, 9}, {9, 3}, {9, 15}, {9, 9} + }; - public static boolean load(String filename) throws IOException { - // Clear the board - Lizzie.board.clear(); + public static boolean load(String filename) throws IOException { + // Clear the board + Lizzie.board.clear(); - File file = new File(filename); - if (!file.exists() || !file.canRead()) { - return false; - } - - FileInputStream fp = new FileInputStream(file); - InputStreamReader reader = new InputStreamReader(fp); - StringBuilder builder = new StringBuilder(); - while (reader.ready()) { - builder.append((char) reader.read()); - } - reader.close(); - fp.close(); - String value = builder.toString(); - if (value.isEmpty()) { - return false; - } + File file = new File(filename); + if (!file.exists() || !file.canRead()) { + return false; + } - boolean returnValue = parse(value); - return returnValue; + FileInputStream fp = new FileInputStream(file); + InputStreamReader reader = new InputStreamReader(fp); + StringBuilder builder = new StringBuilder(); + while (reader.ready()) { + builder.append((char) reader.read()); + } + reader.close(); + fp.close(); + String value = builder.toString(); + if (value.isEmpty()) { + return false; } - private static void placeHandicap(int handi) { - if (handi > 9) { - System.out.println("More than 9 in handicap not supported!"); - handi = 9; - } - if (handi == 5 || handi == 7) { - Lizzie.board.place(9, 9, Stone.BLACK); - handi--; - } - for (int i = 0; i < handi; i++) { - Lizzie.board.place(handicapPlacement[i][0], handicapPlacement[i][1], Stone.BLACK); - } + boolean returnValue = parse(value); + return returnValue; + } + + private static void placeHandicap(int handi) { + if (handi > 9) { + System.out.println("More than 9 in handicap not supported!"); + handi = 9; + } + if (handi == 5 || handi == 7) { + Lizzie.board.place(9, 9, Stone.BLACK); + handi--; } + for (int i = 0; i < handi; i++) { + Lizzie.board.place(handicapPlacement[i][0], handicapPlacement[i][1], Stone.BLACK); + } + } - private static boolean parse(String value) { - String[] lines = value.trim().split("\n"); - String whitePlayer = "Player 1"; - String blackPlayer = "Player 2"; - double komi = 1.5; - int handicap = 0; + private static boolean parse(String value) { + String[] lines = value.trim().split("\n"); + String whitePlayer = "Player 1"; + String blackPlayer = "Player 2"; + double komi = 1.5; + int handicap = 0; - for (String line: lines) { - if (line.startsWith("\\[GAMEINFOMAIN=")) { - // See if komi is included - int sk = line.indexOf("GONGJE:") + 7; - int ek = line.indexOf(',', sk); + for (String line : lines) { + if (line.startsWith("\\[GAMEINFOMAIN=")) { + // See if komi is included + int sk = line.indexOf("GONGJE:") + 7; + int ek = line.indexOf(',', sk); - if (sk > 0) { - komi = Integer.parseInt(line.substring(sk, ek))/10.0; - } - } - // Players names - if (line.startsWith("\\[GAMEBLACKNAME=")) { - blackPlayer = line.substring(16, line.length() - 3); - } - if (line.startsWith("\\[GAMEWHITENAME=")) { - whitePlayer = line.substring(16, line.length()-3); - } - // Handicap info - if (line.startsWith("INI")) { - String[] fields = line.split(" "); - handicap = Integer.parseInt(fields[3]); - if (handicap >= 2) { - placeHandicap(handicap); - } - } - // Actual moves - if (line.startsWith("STO")) { - String[] fields = line.split(" "); - int x = Integer.parseInt(fields[4]); - int y = Integer.parseInt(fields[5]); - Stone s = fields[3].equals("1") ? Stone.BLACK : Stone.WHITE; - Lizzie.board.place(x, y, s); - } - // Pass - if (line.startsWith("SKI")) { - Lizzie.board.pass(); - } + if (sk > 0) { + komi = Integer.parseInt(line.substring(sk, ek)) / 10.0; } - Lizzie.board.getHistory().getGameInfo().setKomi(komi); - Lizzie.frame.setPlayers(whitePlayer, blackPlayer); - // Rewind to game start - while (Lizzie.board.previousMove()) ; - - return false; + } + // Players names + if (line.startsWith("\\[GAMEBLACKNAME=")) { + blackPlayer = line.substring(16, line.length() - 3); + } + if (line.startsWith("\\[GAMEWHITENAME=")) { + whitePlayer = line.substring(16, line.length() - 3); + } + // Handicap info + if (line.startsWith("INI")) { + String[] fields = line.split(" "); + handicap = Integer.parseInt(fields[3]); + if (handicap >= 2) { + placeHandicap(handicap); + } + } + // Actual moves + if (line.startsWith("STO")) { + String[] fields = line.split(" "); + int x = Integer.parseInt(fields[4]); + int y = Integer.parseInt(fields[5]); + Stone s = fields[3].equals("1") ? Stone.BLACK : Stone.WHITE; + Lizzie.board.place(x, y, s); + } + // Pass + if (line.startsWith("SKI")) { + Lizzie.board.pass(); + } } + Lizzie.board.getHistory().getGameInfo().setKomi(komi); + Lizzie.frame.setPlayers(whitePlayer, blackPlayer); + // Rewind to game start + while (Lizzie.board.previousMove()) ; + return false; + } } diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 52b4faf15..48bc2795c 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -1,373 +1,372 @@ package featurecat.lizzie.rules; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.GameInfo; import java.io.*; import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class SGFParser { - private static final SimpleDateFormat SGF_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); - - public static boolean load(String filename) throws IOException { - // Clear the board - Lizzie.board.clear(); + private static final SimpleDateFormat SGF_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); - File file = new File(filename); - if (!file.exists() || !file.canRead()) { - return false; - } + public static boolean load(String filename) throws IOException { + // Clear the board + Lizzie.board.clear(); - FileInputStream fp = new FileInputStream(file); - InputStreamReader reader = new InputStreamReader(fp); - StringBuilder builder = new StringBuilder(); - while (reader.ready()) { - builder.append((char) reader.read()); - } - reader.close(); - fp.close(); - String value = builder.toString(); - if (value.isEmpty()) { - return false; - } - - boolean returnValue = parse(value); - return returnValue; + File file = new File(filename); + if (!file.exists() || !file.canRead()) { + return false; } - public static boolean loadFromString(String sgfString) { - // Clear the board - Lizzie.board.clear(); - - return parse(sgfString); + FileInputStream fp = new FileInputStream(file); + InputStreamReader reader = new InputStreamReader(fp); + StringBuilder builder = new StringBuilder(); + while (reader.ready()) { + builder.append((char) reader.read()); } - - public static int[] convertSgfPosToCoord(String pos) { - if (pos.equals("tt") || pos.isEmpty()) - return null; - int[] ret = new int[2]; - ret[0] = (int) pos.charAt(0) - 'a'; - ret[1] = (int) pos.charAt(1) - 'a'; - return ret; + reader.close(); + fp.close(); + String value = builder.toString(); + if (value.isEmpty()) { + return false; } - private static boolean parse(String value) { - // Drop anything outside "(;...)" - final Pattern SGF_PATTERN = Pattern.compile("(?s).*?(\\(\\s*;.*\\)).*?"); - Matcher sgfMatcher = SGF_PATTERN.matcher(value); - if (sgfMatcher.matches()) { - value = sgfMatcher.group(1); - } else { - return false; - } - int subTreeDepth = 0; - // Save the variation step count - Map subTreeStepMap = new HashMap(); - // Comment of the AW/AB (Add White/Add Black) stone - String awabComment = null; - // Previous Tag - String prevTag = null; - boolean inTag = false, isMultiGo = false, escaping = false; - String tag = null; - StringBuilder tagBuilder = new StringBuilder(); - StringBuilder tagContentBuilder = new StringBuilder(); - // MultiGo 's branch: (Main Branch (Main Branch) (Branch) ) - // Other 's branch: (Main Branch (Branch) Main Branch) - if (value.matches("(?s).*\\)\\s*\\)")) { - isMultiGo = true; - } + boolean returnValue = parse(value); + return returnValue; + } + + public static boolean loadFromString(String sgfString) { + // Clear the board + Lizzie.board.clear(); + + return parse(sgfString); + } + + public static int[] convertSgfPosToCoord(String pos) { + if (pos.equals("tt") || pos.isEmpty()) return null; + int[] ret = new int[2]; + ret[0] = (int) pos.charAt(0) - 'a'; + ret[1] = (int) pos.charAt(1) - 'a'; + return ret; + } + + private static boolean parse(String value) { + // Drop anything outside "(;...)" + final Pattern SGF_PATTERN = Pattern.compile("(?s).*?(\\(\\s*;.*\\)).*?"); + Matcher sgfMatcher = SGF_PATTERN.matcher(value); + if (sgfMatcher.matches()) { + value = sgfMatcher.group(1); + } else { + return false; + } + int subTreeDepth = 0; + // Save the variation step count + Map subTreeStepMap = new HashMap(); + // Comment of the AW/AB (Add White/Add Black) stone + String awabComment = null; + // Previous Tag + String prevTag = null; + boolean inTag = false, isMultiGo = false, escaping = false; + String tag = null; + StringBuilder tagBuilder = new StringBuilder(); + StringBuilder tagContentBuilder = new StringBuilder(); + // MultiGo 's branch: (Main Branch (Main Branch) (Branch) ) + // Other 's branch: (Main Branch (Branch) Main Branch) + if (value.matches("(?s).*\\)\\s*\\)")) { + isMultiGo = true; + } - String blackPlayer = "", whitePlayer = ""; - - // Support unicode characters (UTF-8) - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - if (escaping) { - // Any char following "\" is inserted verbatim - // (ref) "3.2. Text" in https://www.red-bean.com/sgf/sgf4.html - tagContentBuilder.append(c); - escaping = false; - continue; + String blackPlayer = "", whitePlayer = ""; + + // Support unicode characters (UTF-8) + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (escaping) { + // Any char following "\" is inserted verbatim + // (ref) "3.2. Text" in https://www.red-bean.com/sgf/sgf4.html + tagContentBuilder.append(c); + escaping = false; + continue; + } + switch (c) { + case '(': + if (!inTag) { + subTreeDepth += 1; + // Initialize the step count + subTreeStepMap.put(subTreeDepth, 0); + } else { + if (i > 0) { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); } - switch (c) { - case '(': - if (!inTag) { - subTreeDepth += 1; - // Initialize the step count - subTreeStepMap.put(subTreeDepth, 0); - } else { - if (i > 0) { - // Allow the comment tag includes '(' - tagContentBuilder.append(c); - } - } - break; - case ')': - if (!inTag) { - if (isMultiGo) { - // Restore to the variation node - int varStep = subTreeStepMap.get(subTreeDepth); - for (int s = 0; s < varStep; s++) { - Lizzie.board.previousMove(); - } - } - subTreeDepth -= 1; - } else { - // Allow the comment tag includes '(' - tagContentBuilder.append(c); - } - break; - case '[': - if (subTreeDepth > 1 && !isMultiGo) { - break; - } - inTag = true; - String tagTemp = tagBuilder.toString(); - if (!tagTemp.isEmpty()) { - // Ignore small letters in tags for the long format Smart-Go file. - // (ex) "PlayerBlack" ==> "PB" - // It is the default format of mgt, an old SGF tool. - // (Mgt is still supported in Debian and Ubuntu.) - tag = tagTemp.replaceAll("[a-z]", ""); - } - tagContentBuilder = new StringBuilder(); - break; - case ']': - if (subTreeDepth > 1 && !isMultiGo) { - break; - } - inTag = false; - tagBuilder = new StringBuilder(); - String tagContent = tagContentBuilder.toString(); - // We got tag, we can parse this tag now. - if (tag.equals("B")) { - int[] move = convertSgfPosToCoord(tagContent); - if (move == null) { - Lizzie.board.pass(Stone.BLACK); - } else { - // Save the step count - subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); - Lizzie.board.place(move[0], move[1], Stone.BLACK); - } - } else if (tag.equals("W")) { - int[] move = convertSgfPosToCoord(tagContent); - if (move == null) { - Lizzie.board.pass(Stone.WHITE); - } else { - // Save the step count - subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); - Lizzie.board.place(move[0], move[1], Stone.WHITE); - } - } else if (tag.equals("C")) { - // Support comment - if ("AW".equals(prevTag) || "AB".equals(prevTag)) { - awabComment = tagContent; - } else { - Lizzie.board.comment(tagContent); - } - } else if (tag.equals("AB")) { - int[] move = convertSgfPosToCoord(tagContent); - if (move == null) { - Lizzie.board.pass(Stone.BLACK); - } else { - Lizzie.board.place(move[0], move[1], Stone.BLACK); - } - Lizzie.board.flatten(); - } else if (tag.equals("AW")) { - int[] move = convertSgfPosToCoord(tagContent); - if (move == null) { - Lizzie.board.pass(Stone.WHITE); - } else { - Lizzie.board.place(move[0], move[1], Stone.WHITE); - } - Lizzie.board.flatten(); - } else if (tag.equals("PB")) { - blackPlayer = tagContent; - } else if (tag.equals("PW")) { - whitePlayer = tagContent; - } else if (tag.equals("KM")) { - try { - if (tagContent.trim().isEmpty()) { - tagContent = "0.0"; - } - Lizzie.board.getHistory().getGameInfo().setKomi(Double.parseDouble(tagContent)); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } - prevTag = tag; - break; - case ';': - break; - default: - if (subTreeDepth > 1 && !isMultiGo) { - break; - } - if (inTag) { - if (c == '\\') { - escaping = true; - continue; - } - tagContentBuilder.append(c); - } else { - if (c != '\n' && c != '\r' && c != '\t' && c != ' ') { - tagBuilder.append(c); - } - } + } + break; + case ')': + if (!inTag) { + if (isMultiGo) { + // Restore to the variation node + int varStep = subTreeStepMap.get(subTreeDepth); + for (int s = 0; s < varStep; s++) { + Lizzie.board.previousMove(); + } } - } - - Lizzie.frame.setPlayers(whitePlayer, blackPlayer); + subTreeDepth -= 1; + } else { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); + } + break; + case '[': + if (subTreeDepth > 1 && !isMultiGo) { + break; + } + inTag = true; + String tagTemp = tagBuilder.toString(); + if (!tagTemp.isEmpty()) { + // Ignore small letters in tags for the long format Smart-Go file. + // (ex) "PlayerBlack" ==> "PB" + // It is the default format of mgt, an old SGF tool. + // (Mgt is still supported in Debian and Ubuntu.) + tag = tagTemp.replaceAll("[a-z]", ""); + } + tagContentBuilder = new StringBuilder(); + break; + case ']': + if (subTreeDepth > 1 && !isMultiGo) { + break; + } + inTag = false; + tagBuilder = new StringBuilder(); + String tagContent = tagContentBuilder.toString(); + // We got tag, we can parse this tag now. + if (tag.equals("B")) { + int[] move = convertSgfPosToCoord(tagContent); + if (move == null) { + Lizzie.board.pass(Stone.BLACK); + } else { + // Save the step count + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); + Lizzie.board.place(move[0], move[1], Stone.BLACK); + } + } else if (tag.equals("W")) { + int[] move = convertSgfPosToCoord(tagContent); + if (move == null) { + Lizzie.board.pass(Stone.WHITE); + } else { + // Save the step count + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); + Lizzie.board.place(move[0], move[1], Stone.WHITE); + } + } else if (tag.equals("C")) { + // Support comment + if ("AW".equals(prevTag) || "AB".equals(prevTag)) { + awabComment = tagContent; + } else { + Lizzie.board.comment(tagContent); + } + } else if (tag.equals("AB")) { + int[] move = convertSgfPosToCoord(tagContent); + if (move == null) { + Lizzie.board.pass(Stone.BLACK); + } else { + Lizzie.board.place(move[0], move[1], Stone.BLACK); + } + Lizzie.board.flatten(); + } else if (tag.equals("AW")) { + int[] move = convertSgfPosToCoord(tagContent); + if (move == null) { + Lizzie.board.pass(Stone.WHITE); + } else { + Lizzie.board.place(move[0], move[1], Stone.WHITE); + } + Lizzie.board.flatten(); + } else if (tag.equals("PB")) { + blackPlayer = tagContent; + } else if (tag.equals("PW")) { + whitePlayer = tagContent; + } else if (tag.equals("KM")) { + try { + if (tagContent.trim().isEmpty()) { + tagContent = "0.0"; + } + Lizzie.board.getHistory().getGameInfo().setKomi(Double.parseDouble(tagContent)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + prevTag = tag; + break; + case ';': + break; + default: + if (subTreeDepth > 1 && !isMultiGo) { + break; + } + if (inTag) { + if (c == '\\') { + escaping = true; + continue; + } + tagContentBuilder.append(c); + } else { + if (c != '\n' && c != '\r' && c != '\t' && c != ' ') { + tagBuilder.append(c); + } + } + } + } - // Rewind to game start - while (Lizzie.board.previousMove()) ; + Lizzie.frame.setPlayers(whitePlayer, blackPlayer); - // Set AW/AB Comment - if (awabComment != null) { - Lizzie.board.comment(awabComment); - } + // Rewind to game start + while (Lizzie.board.previousMove()) ; - return true; + // Set AW/AB Comment + if (awabComment != null) { + Lizzie.board.comment(awabComment); } - public static String saveToString() throws IOException { - try (StringWriter writer = new StringWriter()) { - saveToStream(Lizzie.board, writer); - return writer.toString(); - } - } + return true; + } - public static void save(Board board, String filename) throws IOException { - try (Writer writer = new OutputStreamWriter(new FileOutputStream(filename))) { - saveToStream(board, writer); - } + public static String saveToString() throws IOException { + try (StringWriter writer = new StringWriter()) { + saveToStream(Lizzie.board, writer); + return writer.toString(); } + } - private static void saveToStream(Board board, Writer writer) throws IOException { - // collect game info - BoardHistoryList history = board.getHistory().shallowCopy(); - GameInfo gameInfo = history.getGameInfo(); - String playerBlack = gameInfo.getPlayerBlack(); - String playerWhite = gameInfo.getPlayerWhite(); - Double komi = gameInfo.getKomi(); - Integer handicap = gameInfo.getHandicap(); - String date = SGF_DATE_FORMAT.format(gameInfo.getDate()); - - // add SGF header - StringBuilder builder = new StringBuilder("(;"); - if (handicap != 0) builder.append(String.format("HA[%s]", handicap)); - builder.append(String.format("KM[%s]PW[%s]PB[%s]DT[%s]AP[Lizzie: %s]", - komi, playerWhite, playerBlack, date, Lizzie.lizzieVersion)); - - // move to the first move - history.toStart(); - - // add handicap stones to SGF - if (handicap != 0) { - builder.append("AB"); - Stone[] stones = history.getStones(); - for (int i = 0; i < stones.length; i++) { - Stone stone = stones[i]; - if (stone.isBlack()) { - // i = x * Board.BOARD_SIZE + y; - int corY = i % Board.BOARD_SIZE; - int corX = (i - corY) / Board.BOARD_SIZE; - - char x = (char) (corX + 'a'); - char y = (char) (corY + 'a'); - builder.append(String.format("[%c%c]", x, y)); - } - } - } else { - // Process the AW/AB stone - Stone[] stones = history.getStones(); - StringBuilder abStone = new StringBuilder(); - StringBuilder awStone = new StringBuilder(); - for (int i = 0; i < stones.length; i++) { - Stone stone = stones[i]; - if (stone.isBlack() || stone.isWhite()) { - // i = x * Board.BOARD_SIZE + y; - int corY = i % Board.BOARD_SIZE; - int corX = (i - corY) / Board.BOARD_SIZE; - - char x = (char) (corX + 'a'); - char y = (char) (corY + 'a'); - - if (stone.isBlack()) { - abStone.append(String.format("[%c%c]", x, y)); - } else { - awStone.append(String.format("[%c%c]", x, y)); - } - } - } - if (abStone.length() > 0) { - builder.append("AB").append(abStone); - } - if (awStone.length() > 0) { - builder.append("AW").append(awStone); - } + public static void save(Board board, String filename) throws IOException { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(filename))) { + saveToStream(board, writer); + } + } + + private static void saveToStream(Board board, Writer writer) throws IOException { + // collect game info + BoardHistoryList history = board.getHistory().shallowCopy(); + GameInfo gameInfo = history.getGameInfo(); + String playerBlack = gameInfo.getPlayerBlack(); + String playerWhite = gameInfo.getPlayerWhite(); + Double komi = gameInfo.getKomi(); + Integer handicap = gameInfo.getHandicap(); + String date = SGF_DATE_FORMAT.format(gameInfo.getDate()); + + // add SGF header + StringBuilder builder = new StringBuilder("(;"); + if (handicap != 0) builder.append(String.format("HA[%s]", handicap)); + builder.append( + String.format( + "KM[%s]PW[%s]PB[%s]DT[%s]AP[Lizzie: %s]", + komi, playerWhite, playerBlack, date, Lizzie.lizzieVersion)); + + // move to the first move + history.toStart(); + + // add handicap stones to SGF + if (handicap != 0) { + builder.append("AB"); + Stone[] stones = history.getStones(); + for (int i = 0; i < stones.length; i++) { + Stone stone = stones[i]; + if (stone.isBlack()) { + // i = x * Board.BOARD_SIZE + y; + int corY = i % Board.BOARD_SIZE; + int corX = (i - corY) / Board.BOARD_SIZE; + + char x = (char) (corX + 'a'); + char y = (char) (corY + 'a'); + builder.append(String.format("[%c%c]", x, y)); } - - // The AW/AB Comment - if (history.getData().comment != null) { - builder.append(String.format("C[%s]", history.getData().comment)); + } + } else { + // Process the AW/AB stone + Stone[] stones = history.getStones(); + StringBuilder abStone = new StringBuilder(); + StringBuilder awStone = new StringBuilder(); + for (int i = 0; i < stones.length; i++) { + Stone stone = stones[i]; + if (stone.isBlack() || stone.isWhite()) { + // i = x * Board.BOARD_SIZE + y; + int corY = i % Board.BOARD_SIZE; + int corX = (i - corY) / Board.BOARD_SIZE; + + char x = (char) (corX + 'a'); + char y = (char) (corY + 'a'); + + if (stone.isBlack()) { + abStone.append(String.format("[%c%c]", x, y)); + } else { + awStone.append(String.format("[%c%c]", x, y)); + } } + } + if (abStone.length() > 0) { + builder.append("AB").append(abStone); + } + if (awStone.length() > 0) { + builder.append("AW").append(awStone); + } + } - // replay moves, and convert them to tags. - // * format: ";B[xy]" or ";W[xy]" - // * with 'xy' = coordinates ; or 'tt' for pass. + // The AW/AB Comment + if (history.getData().comment != null) { + builder.append(String.format("C[%s]", history.getData().comment)); + } - // Write variation tree - builder.append(generateNode(board, history.getCurrentHistoryNode())); + // replay moves, and convert them to tags. + // * format: ";B[xy]" or ";W[xy]" + // * with 'xy' = coordinates ; or 'tt' for pass. - // close file - builder.append(')'); - writer.append(builder.toString()); - } + // Write variation tree + builder.append(generateNode(board, history.getCurrentHistoryNode())); - /** - * Generate node with variations - */ - private static String generateNode(Board board, BoardHistoryNode node) throws IOException { - StringBuilder builder = new StringBuilder(""); + // close file + builder.append(')'); + writer.append(builder.toString()); + } - if (node != null) { + /** Generate node with variations */ + private static String generateNode(Board board, BoardHistoryNode node) throws IOException { + StringBuilder builder = new StringBuilder(""); - BoardData data = node.getData(); - String stone = ""; - if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { + if (node != null) { - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + BoardData data = node.getData(); + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - builder.append(String.format(";%s[%c%c]", stone, x, y)); + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); - // Write the comment - if (data.comment != null) { - builder.append(String.format("C[%s]", data.comment)); - } - } + builder.append(String.format(";%s[%c%c]", stone, x, y)); - if (node.numberOfChildren() > 1) { - // Variation - for (BoardHistoryNode sub : node.getNexts()) { - builder.append("("); - builder.append(generateNode(board, sub)); - builder.append(")"); - } - } else if (node.numberOfChildren() == 1) { - builder.append(generateNode(board, node.next())); - } else { - return builder.toString(); - } + // Write the comment + if (data.comment != null) { + builder.append(String.format("C[%s]", data.comment)); } - + } + + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, sub)); + builder.append(")"); + } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, node.next())); + } else { return builder.toString(); + } } + + return builder.toString(); + } } diff --git a/src/main/java/featurecat/lizzie/rules/Stone.java b/src/main/java/featurecat/lizzie/rules/Stone.java index 585fd6c10..46e49d7fe 100644 --- a/src/main/java/featurecat/lizzie/rules/Stone.java +++ b/src/main/java/featurecat/lizzie/rules/Stone.java @@ -1,80 +1,87 @@ package featurecat.lizzie.rules; public enum Stone { - BLACK, WHITE, EMPTY, BLACK_RECURSED, WHITE_RECURSED, BLACK_GHOST, WHITE_GHOST, DAME, BLACK_POINT, WHITE_POINT, BLACK_CAPTURED, WHITE_CAPTURED; + BLACK, + WHITE, + EMPTY, + BLACK_RECURSED, + WHITE_RECURSED, + BLACK_GHOST, + WHITE_GHOST, + DAME, + BLACK_POINT, + WHITE_POINT, + BLACK_CAPTURED, + WHITE_CAPTURED; - /** - * used to find the opposite color stone - * - * @return the opposite stone type - */ - public Stone opposite() { - switch (this) { - case BLACK: - return WHITE; - case WHITE: - return BLACK; - default: - return this; - } + /** + * used to find the opposite color stone + * + * @return the opposite stone type + */ + public Stone opposite() { + switch (this) { + case BLACK: + return WHITE; + case WHITE: + return BLACK; + default: + return this; } + } - /** - * used to keep track of which stones were visited during removal of dead stones - * - * @return the recursed version of this stone color - */ - public Stone recursed() { - switch (this) { - case BLACK: - return BLACK_RECURSED; - case WHITE: - return WHITE_RECURSED; - default: - return this; - } + /** + * used to keep track of which stones were visited during removal of dead stones + * + * @return the recursed version of this stone color + */ + public Stone recursed() { + switch (this) { + case BLACK: + return BLACK_RECURSED; + case WHITE: + return WHITE_RECURSED; + default: + return this; } + } - /** - * used to keep track of which stones were visited during removal of dead stones - * - * @return the unrecursed version of this stone color - */ - public Stone unrecursed() { - switch (this) { - case BLACK_RECURSED: - return BLACK; - case WHITE_RECURSED: - return WHITE; - default: - return this; - } + /** + * used to keep track of which stones were visited during removal of dead stones + * + * @return the unrecursed version of this stone color + */ + public Stone unrecursed() { + switch (this) { + case BLACK_RECURSED: + return BLACK; + case WHITE_RECURSED: + return WHITE; + default: + return this; } + } - /** - * @return Whether or not this stone is of the black variants. - */ - public boolean isBlack() { - return this == BLACK || this == BLACK_RECURSED || this == BLACK_GHOST; - } + /** @return Whether or not this stone is of the black variants. */ + public boolean isBlack() { + return this == BLACK || this == BLACK_RECURSED || this == BLACK_GHOST; + } - /** - * @return Whether or not this stone is of the white variants. - */ - public boolean isWhite() { - return this != EMPTY && !this.isBlack(); - } + /** @return Whether or not this stone is of the white variants. */ + public boolean isWhite() { + return this != EMPTY && !this.isBlack(); + } - public Stone unGhosted() { - switch (this) { - case BLACK: - case BLACK_GHOST: - return BLACK; - case WHITE: - case WHITE_GHOST: - return WHITE; - default: - return EMPTY; - } + public Stone unGhosted() { + switch (this) { + case BLACK: + case BLACK_GHOST: + return BLACK; + case WHITE: + case WHITE_GHOST: + return WHITE; + default: + return EMPTY; } + } } diff --git a/src/main/java/featurecat/lizzie/rules/Zobrist.java b/src/main/java/featurecat/lizzie/rules/Zobrist.java index 781df4ca5..508755847 100644 --- a/src/main/java/featurecat/lizzie/rules/Zobrist.java +++ b/src/main/java/featurecat/lizzie/rules/Zobrist.java @@ -2,73 +2,69 @@ import java.util.Random; -/** - * Used to maintain zobrist hashes for ko detection - */ +/** Used to maintain zobrist hashes for ko detection */ public class Zobrist { - private static final long[] blackZobrist, whiteZobrist; + private static final long[] blackZobrist, whiteZobrist; - // initialize zobrist hashing - static { - Random random = new Random(); - blackZobrist = new long[Board.BOARD_SIZE * Board.BOARD_SIZE]; - whiteZobrist = new long[Board.BOARD_SIZE * Board.BOARD_SIZE]; + // initialize zobrist hashing + static { + Random random = new Random(); + blackZobrist = new long[Board.BOARD_SIZE * Board.BOARD_SIZE]; + whiteZobrist = new long[Board.BOARD_SIZE * Board.BOARD_SIZE]; - for (int i = 0; i < blackZobrist.length; i++) { - blackZobrist[i] = random.nextLong(); - whiteZobrist[i] = random.nextLong(); - } + for (int i = 0; i < blackZobrist.length; i++) { + blackZobrist[i] = random.nextLong(); + whiteZobrist[i] = random.nextLong(); } + } - // hash to be used to compare two board states - private long zhash; + // hash to be used to compare two board states + private long zhash; - public Zobrist() { - zhash = 0; - } + public Zobrist() { + zhash = 0; + } - public Zobrist(long zhash) { - this.zhash = zhash; - } + public Zobrist(long zhash) { + this.zhash = zhash; + } - /** - * @return a copy of this zobrist - */ - public Zobrist clone() { - return new Zobrist(zhash); - } + /** @return a copy of this zobrist */ + public Zobrist clone() { + return new Zobrist(zhash); + } - /** - * Call this method to alter the current zobrist hash for this stone - * - * @param x x coordinate -- must be valid - * @param y y coordinate -- must be valid - * @param color color of the stone to alter (for adding or removing a stone color) - */ - public void toggleStone(int x, int y, Stone color) { - switch (color) { - case BLACK: - zhash ^= blackZobrist[Board.getIndex(x, y)]; - break; - case WHITE: - zhash ^= whiteZobrist[Board.getIndex(x, y)]; - break; - default: - } + /** + * Call this method to alter the current zobrist hash for this stone + * + * @param x x coordinate -- must be valid + * @param y y coordinate -- must be valid + * @param color color of the stone to alter (for adding or removing a stone color) + */ + public void toggleStone(int x, int y, Stone color) { + switch (color) { + case BLACK: + zhash ^= blackZobrist[Board.getIndex(x, y)]; + break; + case WHITE: + zhash ^= whiteZobrist[Board.getIndex(x, y)]; + break; + default: } + } - @Override - public boolean equals(Object o) { - return o instanceof Zobrist && (((Zobrist) o).zhash == zhash); - } + @Override + public boolean equals(Object o) { + return o instanceof Zobrist && (((Zobrist) o).zhash == zhash); + } - @Override - public int hashCode() { - return (int) zhash; - } + @Override + public int hashCode() { + return (int) zhash; + } - @Override - public String toString() { - return "" + zhash; - } + @Override + public String toString() { + return "" + zhash; + } } diff --git a/src/test/java/common/Util.java b/src/test/java/common/Util.java index e201d57f3..2811152d5 100644 --- a/src/test/java/common/Util.java +++ b/src/test/java/common/Util.java @@ -1,149 +1,137 @@ package common; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import java.awt.Color; -import java.awt.Graphics2D; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.swing.UnsupportedLookAndFeelException; - -import org.json.JSONException; -import org.junit.Test; - -import featurecat.lizzie.Config; import featurecat.lizzie.Lizzie; -import featurecat.lizzie.analysis.Leelaz; -import featurecat.lizzie.analysis.MoveData; -import featurecat.lizzie.gui.LizzieFrame; import featurecat.lizzie.rules.Board; import featurecat.lizzie.rules.BoardData; import featurecat.lizzie.rules.BoardHistoryList; import featurecat.lizzie.rules.BoardHistoryNode; import featurecat.lizzie.rules.SGFParser; import featurecat.lizzie.rules.Stone; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class Util { - - private static ArrayList laneUsageList = new ArrayList(); - - /** - * Get Variation Tree as String List - * The logic is same as the function VariationTree.drawTree - * - * @param startLane - * @param startNode - * @param variationNumber - * @param isMain - */ - public static void getVariationTree(List moveList, int startLane, BoardHistoryNode startNode, int variationNumber, boolean isMain) { - // Finds depth on leftmost variation of this tree - int depth = BoardHistoryList.getDepth(startNode) + 1; - int lane = startLane; - // Figures out how far out too the right (which lane) we have to go not to collide with other variations - while (lane < laneUsageList.size() && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { - // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" - laneUsageList.set(lane, startNode.getData().moveNumber - 1); - lane++; - } - if (lane >= laneUsageList.size()) - { - laneUsageList.add(0); - } - if (variationNumber > 1) - laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); - laneUsageList.set(lane, startNode.getData().moveNumber); - // At this point, lane contains the lane we should use (the main branch is in lane 0) - BoardHistoryNode cur = startNode; + private static ArrayList laneUsageList = new ArrayList(); - // Draw main line - StringBuilder sb = new StringBuilder(); - sb.append(formatMove(cur.getData())); - while (cur.next() != null) { - cur = cur.next(); - sb.append(formatMove(cur.getData())); - } - moveList.add(sb.toString()); - // Now we have drawn all the nodes in this variation, and has reached the bottom of this variation - // Move back up, and for each, draw any variations we find - while (cur.previous() != null && cur != startNode) { - cur = cur.previous(); - int curwidth = lane; - // Draw each variation, uses recursion - for (int i = 1; i < cur.numberOfChildren(); i++) { - curwidth++; - // Recursion, depth of recursion will normally not be very deep (one recursion level for every variation that has a variation (sort of)) - getVariationTree(moveList, curwidth, cur.getVariation(i), i, false); - } - } + /** + * Get Variation Tree as String List The logic is same as the function VariationTree.drawTree + * + * @param startLane + * @param startNode + * @param variationNumber + * @param isMain + */ + public static void getVariationTree( + List moveList, + int startLane, + BoardHistoryNode startNode, + int variationNumber, + boolean isMain) { + // Finds depth on leftmost variation of this tree + int depth = BoardHistoryList.getDepth(startNode) + 1; + int lane = startLane; + // Figures out how far out too the right (which lane) we have to go not to collide with other + // variations + while (lane < laneUsageList.size() + && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { + // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" + laneUsageList.set(lane, startNode.getData().moveNumber - 1); + lane++; + } + if (lane >= laneUsageList.size()) { + laneUsageList.add(0); } - - private static String formatMove(BoardData data) { - String stone = ""; - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - else return stone; + if (variationNumber > 1) laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); + laneUsageList.set(lane, startNode.getData().moveNumber); - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + // At this point, lane contains the lane we should use (the main branch is in lane 0) + BoardHistoryNode cur = startNode; - String comment = ""; - if (data.comment != null && data.comment.trim().length() > 0) { - comment = String.format("C[%s]", data.comment); - } - return String.format(";%s[%c%c]%s", stone, x, y, comment); + // Draw main line + StringBuilder sb = new StringBuilder(); + sb.append(formatMove(cur.getData())); + while (cur.next() != null) { + cur = cur.next(); + sb.append(formatMove(cur.getData())); } + moveList.add(sb.toString()); + // Now we have drawn all the nodes in this variation, and has reached the bottom of this + // variation + // Move back up, and for each, draw any variations we find + while (cur.previous() != null && cur != startNode) { + cur = cur.previous(); + int curwidth = lane; + // Draw each variation, uses recursion + for (int i = 1; i < cur.numberOfChildren(); i++) { + curwidth++; + // Recursion, depth of recursion will normally not be very deep (one recursion level for + // every variation that has a variation (sort of)) + getVariationTree(moveList, curwidth, cur.getVariation(i), i, false); + } + } + } + + private static String formatMove(BoardData data) { + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + else return stone; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + String comment = ""; + if (data.comment != null && data.comment.trim().length() > 0) { + comment = String.format("C[%s]", data.comment); + } + return String.format(";%s[%c%c]%s", stone, x, y, comment); + } + + public static String trimGameInfo(String sgf) { + String gameInfo = String.format("(?s).*AP\\[Lizzie: %s\\]", Lizzie.lizzieVersion); + return sgf.replaceFirst(gameInfo, "("); + } - public static String trimGameInfo(String sgf) { - String gameInfo = String.format("(?s).*AP\\[Lizzie: %s\\]", - Lizzie.lizzieVersion); - return sgf.replaceFirst(gameInfo, "("); + public static String[] splitAwAbSgf(String sgf) { + String[] ret = new String[2]; + String regex = "(A[BW]{1}(\\[[a-z]{2}\\])+)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(sgf); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + sb.append(matcher.group(0)); } + ret[0] = sb.toString(); + ret[1] = sgf.replaceAll(regex, ""); + return ret; + } - public static String[] splitAwAbSgf(String sgf) { - String[] ret = new String[2]; - String regex = "(A[BW]{1}(\\[[a-z]{2}\\])+)"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(sgf); - StringBuilder sb = new StringBuilder(); - while (matcher.find()) { - sb.append(matcher.group(0)); - } - ret[0] = sb.toString(); - ret[1] = sgf.replaceAll(regex, ""); - return ret; + public static Stone[] convertStones(String awAb) { + Stone[] stones = new Stone[Board.BOARD_SIZE * Board.BOARD_SIZE]; + for (int i = 0; i < stones.length; i++) { + stones[i] = Stone.EMPTY; } - - public static Stone[] convertStones(String awAb) { - Stone[] stones = new Stone[Board.BOARD_SIZE * Board.BOARD_SIZE]; - for (int i = 0; i < stones.length; i++) { - stones[i] = Stone.EMPTY; - } - String regex = "(A[BW]{1})|(?<=\\[)([a-z]{2})(?=\\])"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(awAb); - StringBuilder sb = new StringBuilder(); - Stone stone = Stone.EMPTY; - while (matcher.find()) { - String str = matcher.group(0); - if("AB".equals(str)) { - stone = Stone.BLACK; - } else if("AW".equals(str)) { - stone = Stone.WHITE; - } else { - int[] move = SGFParser.convertSgfPosToCoord(str); - int index = Board.getIndex(move[0], move[1]); - stones[index] = stone; - } - } - return stones; + String regex = "(A[BW]{1})|(?<=\\[)([a-z]{2})(?=\\])"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(awAb); + StringBuilder sb = new StringBuilder(); + Stone stone = Stone.EMPTY; + while (matcher.find()) { + String str = matcher.group(0); + if ("AB".equals(str)) { + stone = Stone.BLACK; + } else if ("AW".equals(str)) { + stone = Stone.WHITE; + } else { + int[] move = SGFParser.convertSgfPosToCoord(str); + int index = Board.getIndex(move[0], move[1]); + stones[index] = stone; + } } + return stones; + } } diff --git a/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java index 1c71ec958..dbb5c5103 100644 --- a/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java +++ b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java @@ -1,52 +1,64 @@ package featurecat.lizzie.analysis; -import org.junit.Test; -import java.util.List; -import java.util.Arrays; import static org.junit.Assert.assertEquals; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + public class MoveDataTest { - @Test public void testFromInfoLine() { + @Test + public void testFromInfoLine() { String info = "move R5 visits 38 winrate 5404 order 0 pv R5 Q5 R6 S4 Q10 C3 D3 C4 C6 C5 D5"; MoveData moveData = MoveData.fromInfo(info); assertEquals(moveData.coordinate, "R5"); assertEquals(moveData.playouts, 38); assertEquals(moveData.winrate, 54.04, 0.01); - assertEquals(moveData.variation, Arrays.asList( - "R5", "Q5", "R6", "S4", "Q10", "C3", "D3", "C4", "C6", "C5", "D5")); + assertEquals( + moveData.variation, + Arrays.asList("R5", "Q5", "R6", "S4", "Q10", "C3", "D3", "C4", "C6", "C5", "D5")); } - private void testSummary(String summary, String coordinate, int playouts, double winrate, List variation) { - MoveData moveData = MoveData.fromSummary(summary); - assertEquals(moveData.coordinate, coordinate); - assertEquals(moveData.playouts, playouts); - assertEquals(moveData.winrate, winrate, 0.01); - assertEquals(moveData.variation, variation); + private void testSummary( + String summary, String coordinate, int playouts, double winrate, List variation) { + MoveData moveData = MoveData.fromSummary(summary); + assertEquals(moveData.coordinate, coordinate); + assertEquals(moveData.playouts, playouts); + assertEquals(moveData.winrate, winrate, 0.01); + assertEquals(moveData.variation, variation); } - @Test public void summaryLine1() { - testSummary(" P16 -> 4 (V: 50.94%) (N: 5.79%) PV: P16 N18 R5 Q5", - "P16", 4, 50.94, Arrays.asList("P16", "N18", "R5", "Q5")); + @Test + public void summaryLine1() { + testSummary( + " P16 -> 4 (V: 50.94%) (N: 5.79%) PV: P16 N18 R5 Q5", + "P16", 4, 50.94, Arrays.asList("P16", "N18", "R5", "Q5")); } - @Test public void summaryLine2() { - testSummary(" D9 -> 59 (V: 60.61%) (N: 52.59%) PV: D9 D12 E9 C13 C15 F17", - "D9", 59, 60.61, Arrays.asList("D9", "D12", "E9", "C13", "C15", "F17")); + @Test + public void summaryLine2() { + testSummary( + " D9 -> 59 (V: 60.61%) (N: 52.59%) PV: D9 D12 E9 C13 C15 F17", + "D9", 59, 60.61, Arrays.asList("D9", "D12", "E9", "C13", "C15", "F17")); } - @Test public void summaryLine3() { - testSummary(" B2 -> 1 (V: 46.52%) (N: 86.74%) PV: B2", - "B2", 1, 46.52, Arrays.asList("B2")); + @Test + public void summaryLine3() { + testSummary( + " B2 -> 1 (V: 46.52%) (N: 86.74%) PV: B2", "B2", 1, 46.52, Arrays.asList("B2")); } - @Test public void summaryLine4() { - testSummary(" D16 -> 33 (V: 53.63%) (N: 27.64%) PV: D16 D4 Q16 O4 C3 C4", - "D16", 33, 53.63, Arrays.asList("D16", "D4", "Q16", "O4", "C3", "C4")); + @Test + public void summaryLine4() { + testSummary( + " D16 -> 33 (V: 53.63%) (N: 27.64%) PV: D16 D4 Q16 O4 C3 C4", + "D16", 33, 53.63, Arrays.asList("D16", "D4", "Q16", "O4", "C3", "C4")); } - @Test public void summaryLine5() { - testSummary(" Q16 -> 0 (V: 0.00%) (N: 0.52%) PV: Q16\n", - "Q16", 0, 0.0, Arrays.asList("Q16")); + @Test + public void summaryLine5() { + testSummary( + " Q16 -> 0 (V: 0.00%) (N: 0.52%) PV: Q16\n", "Q16", 0, 0.0, Arrays.asList("Q16")); } } diff --git a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java index 893cb64e2..632e70c92 100644 --- a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java +++ b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java @@ -1,124 +1,125 @@ package featurecat.lizzie.rules; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertArrayEquals; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Test; import common.Util; - import featurecat.lizzie.Config; import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.gui.LizzieFrame; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; public class SGFParserTest { - private Lizzie lizzie = null; - - @Test - - public void run() throws IOException { - lizzie = new Lizzie(); - lizzie.config = new Config(); - lizzie.board = new Board(); - lizzie.frame = new LizzieFrame(); - // new Thread( () -> { - lizzie.leelaz = new Leelaz(); - // }).start(); - - testVariaionOnly1(); - testFull1(); - } - - public void testVariaionOnly1() throws IOException { - - String sgfString = "(;B[pd];W[dp];B[pp];W[dd];B[fq]" - + "(;W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd]" - + "(;B[gb]" - + "(;W[hc];B[nq])" - + "(;W[gc];B[ec];W[hc];B[hb];W[ic]))" - + "(;B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]))" - + "(;W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]))"; - - int variationNum = 4; - String mainBranch = ";B[pd];W[dp];B[pp];W[dd];B[fq];W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd];B[gb];W[hc];B[nq]"; - String variation1 = ";W[gc];B[ec];W[hc];B[hb];W[ic]"; - String variation2 = ";B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]"; - String variation3 = ";W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]"; - - // Load correctly - boolean loaded = SGFParser.loadFromString(sgfString); - assertTrue(loaded); - - // Variations - List moveList = new ArrayList(); - Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); - - assertTrue(moveList != null); - assertEquals(moveList.size(), variationNum); - - assertEquals(moveList.get(0), mainBranch); - assertEquals(moveList.get(1), variation1); - assertEquals(moveList.get(2), variation2); - assertEquals(moveList.get(3), variation3); - - // Save correctly - String saveSgf = SGFParser.saveToString(); - assertTrue(saveSgf != null && saveSgf.trim().length() > 0); - - assertEquals(sgfString, Util.trimGameInfo(saveSgf)); - } - - public void testFull1() throws IOException { - - String sgfInfo = "(;CA[utf8]AP[MultiGo:4.4.4]SZ[19]"; - String sgfAwAb = "AB[pe][pq][oq][nq][mq][cp][dq][eq][fp]AB[qd]AW[dc][cf][oc][qo][op][np][mp][ep][fq]"; - String sgfContent = ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?]" - + "(;B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.])" - + "(;B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]))"; - String sgfString = sgfInfo + sgfAwAb + sgfContent; - - int variationNum = 2; - String mainBranch = ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?];B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.]"; - String variation1 = ";B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]"; - - Stone[] expectStones = Util.convertStones(sgfAwAb); - - // Load correctly - boolean loaded = SGFParser.loadFromString(sgfString); - assertTrue(loaded); - - // Variations - List moveList = new ArrayList(); - Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); - - assertTrue(moveList != null); - assertEquals(moveList.size(), variationNum); - assertEquals(moveList.get(0), mainBranch); - assertEquals(moveList.get(1), variation1); - - // AW/AB - assertArrayEquals(expectStones, Lizzie.board.getHistory().getStones()); - - // Save correctly - String saveSgf = SGFParser.saveToString(); - assertTrue(saveSgf != null && saveSgf.trim().length() > 0); - - String sgf = Util.trimGameInfo(saveSgf); - String[] ret = Util.splitAwAbSgf(sgf); - Stone[] actualStones = Util.convertStones(ret[0]); - - // AW/AB - assertArrayEquals(expectStones, actualStones); - - // Content - assertEquals("(" + sgfContent, ret[1]); - } - + private Lizzie lizzie = null; + + @Test + public void run() throws IOException { + lizzie = new Lizzie(); + lizzie.config = new Config(); + lizzie.board = new Board(); + lizzie.frame = new LizzieFrame(); + // new Thread( () -> { + lizzie.leelaz = new Leelaz(); + // }).start(); + + testVariaionOnly1(); + testFull1(); + } + + public void testVariaionOnly1() throws IOException { + + String sgfString = + "(;B[pd];W[dp];B[pp];W[dd];B[fq]" + + "(;W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd]" + + "(;B[gb]" + + "(;W[hc];B[nq])" + + "(;W[gc];B[ec];W[hc];B[hb];W[ic]))" + + "(;B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]))" + + "(;W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]))"; + + int variationNum = 4; + String mainBranch = + ";B[pd];W[dp];B[pp];W[dd];B[fq];W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd];B[gb];W[hc];B[nq]"; + String variation1 = ";W[gc];B[ec];W[hc];B[hb];W[ic]"; + String variation2 = ";B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]"; + String variation3 = ";W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]"; + + // Load correctly + boolean loaded = SGFParser.loadFromString(sgfString); + assertTrue(loaded); + + // Variations + List moveList = new ArrayList(); + Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); + + assertTrue(moveList != null); + assertEquals(moveList.size(), variationNum); + + assertEquals(moveList.get(0), mainBranch); + assertEquals(moveList.get(1), variation1); + assertEquals(moveList.get(2), variation2); + assertEquals(moveList.get(3), variation3); + + // Save correctly + String saveSgf = SGFParser.saveToString(); + assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + + assertEquals(sgfString, Util.trimGameInfo(saveSgf)); + } + + public void testFull1() throws IOException { + + String sgfInfo = "(;CA[utf8]AP[MultiGo:4.4.4]SZ[19]"; + String sgfAwAb = + "AB[pe][pq][oq][nq][mq][cp][dq][eq][fp]AB[qd]AW[dc][cf][oc][qo][op][np][mp][ep][fq]"; + String sgfContent = + ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?]" + + "(;B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.])" + + "(;B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]))"; + String sgfString = sgfInfo + sgfAwAb + sgfContent; + + int variationNum = 2; + String mainBranch = + ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?];B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.]"; + String variation1 = + ";B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]"; + + Stone[] expectStones = Util.convertStones(sgfAwAb); + + // Load correctly + boolean loaded = SGFParser.loadFromString(sgfString); + assertTrue(loaded); + + // Variations + List moveList = new ArrayList(); + Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); + + assertTrue(moveList != null); + assertEquals(moveList.size(), variationNum); + assertEquals(moveList.get(0), mainBranch); + assertEquals(moveList.get(1), variation1); + + // AW/AB + assertArrayEquals(expectStones, Lizzie.board.getHistory().getStones()); + + // Save correctly + String saveSgf = SGFParser.saveToString(); + assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + + String sgf = Util.trimGameInfo(saveSgf); + String[] ret = Util.splitAwAbSgf(sgf); + Stone[] actualStones = Util.convertStones(ret[0]); + + // AW/AB + assertArrayEquals(expectStones, actualStones); + + // Content + assertEquals("(" + sgfContent, ret[1]); + } } From 43153836698eb0b3347a903e66dba533c5ce12d2 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sat, 6 Oct 2018 11:16:33 +0200 Subject: [PATCH 75/75] Refactorings to fit in the 100 line width --- src/main/java/featurecat/lizzie/Lizzie.java | 64 ++-- .../featurecat/lizzie/analysis/Leelaz.java | 22 +- .../featurecat/lizzie/gui/BoardRenderer.java | 310 +++++++----------- .../featurecat/lizzie/gui/LizzieFrame.java | 78 ++--- .../featurecat/lizzie/gui/NewGameDialog.java | 15 +- .../featurecat/lizzie/gui/VariationTree.java | 4 +- .../featurecat/lizzie/gui/WinrateGraph.java | 28 +- .../java/featurecat/lizzie/rules/Board.java | 25 +- .../featurecat/lizzie/rules/BoardData.java | 2 - .../featurecat/lizzie/rules/SGFParser.java | 10 +- src/test/java/common/Util.java | 1 - 11 files changed, 219 insertions(+), 340 deletions(-) diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index fb85a4873..285595d31 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -6,7 +6,6 @@ import java.io.File; import java.io.IOException; import javax.swing.*; -import org.json.JSONException; /** Main class. */ public class Lizzie { @@ -15,35 +14,48 @@ public class Lizzie { public static Board board; public static Config config; public static String lizzieVersion = "0.5"; + private static String[] args; /** Launches the game window, and runs the game. */ - public static void main(String[] args) - throws IOException, JSONException, ClassNotFoundException, UnsupportedLookAndFeelException, - InstantiationException, IllegalAccessException, InterruptedException { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + public static void main(String[] args) throws IOException { + setLookAndFeel(); + args = args; config = new Config(); board = new Board(); frame = new LizzieFrame(); + new Thread(Lizzie::run).start(); + } - new Thread( - () -> { - try { - leelaz = new Leelaz(); - if (config.handicapInsteadOfWinrate) { - leelaz.estimatePassWinrate(); - } - if (args.length == 1) { - frame.loadFile(new File(args[0])); - } else if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { - board.resumePreviousGame(); - } - leelaz.togglePonder(); - } catch (IOException e) { - e.printStackTrace(); - System.exit(-1); - } - }) - .start(); + public static void run() { + try { + leelaz = new Leelaz(); + if (config.handicapInsteadOfWinrate) { + leelaz.estimatePassWinrate(); + } + if (args.length == 1) { + frame.loadFile(new File(args[0])); + } else if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { + board.resumePreviousGame(); + } + leelaz.togglePonder(); + } catch (IOException e) { + e.printStackTrace(); + System.exit(-1); + } + } + + public static void setLookAndFeel() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (UnsupportedLookAndFeelException e) { + e.printStackTrace(); + } } public static void shutdown() { @@ -61,8 +73,8 @@ public static void shutdown() { try { config.persist(); - } catch (IOException err) { - // Failed to save config + } catch (IOException e) { + e.printStackTrace(); // Failed to save config } if (leelaz != null) leelaz.shutdown(); diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 5c2774437..8e8a2378a 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -424,9 +424,8 @@ private void ponder() { + Lizzie.config .config .getJSONObject("leelaz") - .getInt( - "analyze-update-interval-centisec")); // until it responds to this, incoming - // ponder results are obsolete + .getInt("analyze-update-interval-centisec")); // until it responds to this, incoming + // ponder results are obsolete } public void togglePonder() { @@ -484,23 +483,12 @@ public WinrateStats getWinrateStats() { final List moves = new ArrayList(bestMoves); // get the total number of playouts in moves - stats.totalPlayouts = - moves - .stream() - .reduce( - 0, - (Integer result, MoveData move) -> result + move.playouts, - (Integer a, Integer b) -> a + b); + int totalPlayouts = moves.stream().mapToInt(move -> move.playouts).sum(); + stats.totalPlayouts = totalPlayouts; // set maxWinrate to the weighted average winrate of moves stats.maxWinrate = - moves - .stream() - .reduce( - 0d, - (Double result, MoveData move) -> - result + move.winrate * move.playouts / stats.totalPlayouts, - (Double a, Double b) -> a + b); + moves.stream().mapToDouble(move -> move.winrate * move.playouts / totalPlayouts).sum(); } return stats; diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index dc54aad1c..efff7f5a5 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -1,5 +1,12 @@ package featurecat.lizzie.gui; +import static java.awt.RenderingHints.*; +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; +import static java.lang.Math.log; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.round; + import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Branch; import featurecat.lizzie.analysis.MoveData; @@ -21,8 +28,8 @@ import org.json.JSONObject; public class BoardRenderer { - private static final double MARGIN = - 0.03; // percentage of the boardLength to offset before drawing black lines + // Percentage of the boardLength to offset before drawing black lines + private static final double MARGIN = 0.03; private static final double MARGIN_WITH_COORDINATES = 0.06; private static final double STARPOINT_DIAMETER = 0.015; @@ -132,7 +139,7 @@ public String bestMoveCoordinateName() { /** Calculate good values for boardLength, scaledMargin, availableLength, and squareLength */ private void setupSizeParameters() { - int originalBoardLength = boardLength; + int boardLength0 = boardLength; int[] calculatedPixelMargins = calculatePixelMargins(); boardLength = calculatedPixelMargins[0]; @@ -143,25 +150,25 @@ private void setupSizeParameters() { stoneRadius = squareLength / 2 - 1; // re-center board - setLocation( - x + (originalBoardLength - boardLength) / 2, y + (originalBoardLength - boardLength) / 2); + setLocation(x + (boardLength0 - boardLength) / 2, y + (boardLength0 - boardLength) / 2); } /** * Draw the green background and go board with lines. We cache the image for a performance boost. */ private void drawGoban(Graphics2D g0) { + int width = Lizzie.frame.getWidth(); + int height = Lizzie.frame.getHeight(); + // draw the cached background image if frame size changes if (cachedBackgroundImage == null - || cachedBackgroundImage.getWidth() != Lizzie.frame.getWidth() - || cachedBackgroundImage.getHeight() != Lizzie.frame.getHeight() + || cachedBackgroundImage.getWidth() != width + || cachedBackgroundImage.getHeight() != height || cachedX != x || cachedY != y || cachedBackgroundImageHasCoordinatesEnabled != showCoordinates()) { - cachedBackgroundImage = - new BufferedImage( - Lizzie.frame.getWidth(), Lizzie.frame.getHeight(), BufferedImage.TYPE_INT_ARGB); + cachedBackgroundImage = new BufferedImage(width, height, TYPE_INT_ARGB); Graphics2D g = cachedBackgroundImage.createGraphics(); // draw the wooden background @@ -232,7 +239,7 @@ private void drawGoban(Graphics2D g0) { g.dispose(); } - g0.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g0.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); g0.drawImage(cachedBackgroundImage, 0, 0, null); cachedX = x; cachedY = y; @@ -245,73 +252,31 @@ private void drawGoban(Graphics2D g0) { */ private void drawStarPoints(Graphics2D g) { if (Board.BOARD_SIZE == 9) { - drawStarPoints9x9(g); + drawStarPoints0(2, 2, 4, true, g); } else if (Board.BOARD_SIZE == 13) { - drawStarPoints13x13(g); + drawStarPoints0(2, 3, 6, true, g); } else { - drawStarPoints19x19(g); - } - } - - private void drawStarPoints19x19(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 3; - final int STARPOINT_EDGE_OFFSET = 3; - final int STARPOINT_GRID_DISTANCE = 6; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = - x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = - y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); - fillCircle(g, centerX, centerY, starPointRadius); - } + drawStarPoints0(3, 3, 6, false, g); } } - private void drawStarPoints13x13(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + private void drawStarPoints0( + int nStarpoints, int edgeOffset, int gridDistance, boolean center, Graphics2D g) { + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 2; - final int STARPOINT_EDGE_OFFSET = 3; - final int STARPOINT_GRID_DISTANCE = 6; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = - x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = - y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); + for (int i = 0; i < nStarpoints; i++) { + for (int j = 0; j < nStarpoints; j++) { + int centerX = x + scaledMargin + squareLength * (edgeOffset + gridDistance * i); + int centerY = y + scaledMargin + squareLength * (edgeOffset + gridDistance * j); fillCircle(g, centerX, centerY, starPointRadius); } } - // Draw center - int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - fillCircle(g, centerX, centerY, starPointRadius); - } - - private void drawStarPoints9x9(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 2; - final int STARPOINT_EDGE_OFFSET = 2; - final int STARPOINT_GRID_DISTANCE = 4; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = - x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = - y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); - fillCircle(g, centerX, centerY, starPointRadius); - } + if (center) { + int centerX = x + scaledMargin + squareLength * gridDistance; + int centerY = y + scaledMargin + squareLength * gridDistance; + fillCircle(g, centerX, centerY, starPointRadius); } - - // Draw center - int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - fillCircle(g, centerX, centerY, starPointRadius); } /** Draw the stones. We cache the image for a performance boost. */ @@ -325,16 +290,15 @@ private void drawStones() { || Lizzie.board.inScoreMode() || lastInScoreMode) { - cachedStonesImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - cachedStonesShadowImage = - new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); + cachedStonesImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); + cachedStonesShadowImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); Graphics2D g = cachedStonesImage.createGraphics(); Graphics2D gShadow = cachedStonesShadowImage.createGraphics(); // we need antialiasing to make the stones pretty. Java is a bit slow at antialiasing; that's // why we want the cache - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - gShadow.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + gShadow.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); for (int i = 0; i < Board.BOARD_SIZE; i++) { for (int j = 0; j < Board.BOARD_SIZE; j++) { @@ -389,9 +353,8 @@ private void drawScore(Graphics2D go) { /** Draw the 'ghost stones' which show a variation Leelaz is thinking about */ private void drawBranch() { showingBranch = false; - branchStonesImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - branchStonesShadowImage = - new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); + branchStonesImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); + branchStonesShadowImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); branch = null; if (Lizzie.frame.isPlayingAgainstLeelaz || Lizzie.leelaz == null) { @@ -409,7 +372,7 @@ private void drawBranch() { Graphics2D g = (Graphics2D) branchStonesImage.getGraphics(); Graphics2D gShadow = (Graphics2D) branchStonesShadowImage.getGraphics(); - MoveData suggestedMove = (isMainBoard ? mouseHoveredMove() : getBestMove()); + MoveData suggestedMove = (isMainBoard ? mouseOveredMove() : getBestMove()); if (suggestedMove == null) return; variation = suggestedMove.variation; branch = new Branch(Lizzie.board, variation); @@ -417,7 +380,7 @@ private void drawBranch() { if (branch == null) return; showingBranch = true; - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); for (int i = 0; i < Board.BOARD_SIZE; i++) { for (int j = 0; j < Board.BOARD_SIZE; j++) { @@ -436,8 +399,8 @@ private void drawBranch() { gShadow.dispose(); } - private MoveData mouseHoveredMove() { - if (Lizzie.frame.mouseHoverCoordinate != null) { + private MoveData mouseOveredMove() { + if (Lizzie.frame.mouseOverCoordinate != null) { for (int i = 0; i < bestMoves.size(); i++) { MoveData move = bestMoves.get(i); int[] coord = Board.convertNameToCoordinates(move.coordinate); @@ -445,8 +408,7 @@ private MoveData mouseHoveredMove() { continue; } - if (coord[0] == Lizzie.frame.mouseHoverCoordinate[0] - && coord[1] == Lizzie.frame.mouseHoverCoordinate[1]) { + if (Lizzie.frame.isMouseOver(coord[0], coord[1])) { return move; } } @@ -460,7 +422,7 @@ private MoveData getBestMove() { /** render the shadows and stones in correct background-foreground order */ private void renderImages(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); g.drawImage(cachedStonesShadowImage, x, y, null); if (Lizzie.config.showBranch) { g.drawImage(branchStonesShadowImage, x, y, null); @@ -473,9 +435,9 @@ private void renderImages(Graphics2D g) { /** Draw move numbers and/or mark the last played move */ private void drawMoveNumbers(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - int[] lastMove = branch == null ? Lizzie.board.getLastMove() : branch.data.lastMove; + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + Board board = Lizzie.board; + int[] lastMove = branch == null ? board.getLastMove() : branch.data.lastMove; if (!Lizzie.config.showMoveNumber && branch == null) { if (lastMove != null) { // mark the last coordinate @@ -484,27 +446,20 @@ private void drawMoveNumbers(Graphics2D g) { int stoneY = y + scaledMargin + squareLength * lastMove[1]; // set color to the opposite color of whatever is on the board - g.setColor( - Lizzie.board.getStones()[Board.getIndex(lastMove[0], lastMove[1])].isWhite() - ? Color.BLACK - : Color.WHITE); + boolean isWhite = board.getStones()[Board.getIndex(lastMove[0], lastMove[1])].isWhite(); + g.setColor(isWhite ? Color.BLACK : Color.WHITE); + drawCircle(g, stoneX, stoneY, lastMoveMarkerRadius); - } else if (lastMove == null - && Lizzie.board.getData().moveNumber != 0 - && !Lizzie.board.inScoreMode()) { + } else if (lastMove == null && board.getData().moveNumber != 0 && !board.inScoreMode()) { g.setColor( - Lizzie.board.getData().blackToPlay - ? new Color(255, 255, 255, 150) - : new Color(0, 0, 0, 150)); + board.getData().blackToPlay ? new Color(255, 255, 255, 150) : new Color(0, 0, 0, 150)); g.fillOval( x + boardLength / 2 - 4 * stoneRadius, y + boardLength / 2 - 4 * stoneRadius, stoneRadius * 8, stoneRadius * 8); g.setColor( - Lizzie.board.getData().blackToPlay - ? new Color(0, 0, 0, 255) - : new Color(255, 255, 255, 255)); + board.getData().blackToPlay ? new Color(0, 0, 0, 255) : new Color(255, 255, 255, 255)); drawString( g, x + boardLength / 2, @@ -518,12 +473,10 @@ private void drawMoveNumbers(Graphics2D g) { return; } - int[] moveNumberList = - branch == null ? Lizzie.board.getMoveNumberList() : branch.data.moveNumberList; + int[] moveNumberList = branch == null ? board.getMoveNumberList() : branch.data.moveNumberList; // Allow to display only last move number - int lastMoveNumber = - branch == null ? Lizzie.board.getData().moveNumber : branch.data.moveNumber; + int lastMoveNumber = branch == null ? board.getData().moveNumber : branch.data.moveNumber; int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); for (int i = 0; i < Board.BOARD_SIZE; i++) { @@ -536,28 +489,21 @@ private void drawMoveNumbers(Graphics2D g) { continue; } - Stone stoneAtThisPoint = - branch == null - ? Lizzie.board.getStones()[Board.getIndex(i, j)] - : branch.data.stones[Board.getIndex(i, j)]; + int here = Board.getIndex(i, j); + Stone stoneHere = branch == null ? board.getStones()[here] : branch.data.stones[here]; // don't write the move number if either: the move number is 0, or there will already be // playout information written if (moveNumberList[Board.getIndex(i, j)] > 0 - && !(branch != null - && Lizzie.frame.mouseHoverCoordinate != null - && i == Lizzie.frame.mouseHoverCoordinate[0] - && j == Lizzie.frame.mouseHoverCoordinate[1])) { + && !(branch != null && Lizzie.frame.isMouseOver(i, j))) { if (lastMove != null && i == lastMove[0] && j == lastMove[1]) - g.setColor( - Color.RED - .brighter()); // stoneAtThisPoint.isBlack() ? Color.RED.brighter() : - // Color.BLUE.brighter()); + g.setColor(Color.RED.brighter()); // stoneHere.isBlack() ? Color.RED.brighter() : + // Color.BLUE.brighter()); else { // Draw white letters on black stones nomally. // But use black letters for showing black moves without stones. boolean reverse = (moveNumberList[Board.getIndex(i, j)] > maxBranchMoves()); - g.setColor(stoneAtThisPoint.isBlack() ^ reverse ? Color.WHITE : Color.BLACK); + g.setColor(stoneHere.isBlack() ^ reverse ? Color.WHITE : Color.BLACK); } String moveNumberString = moveNumberList[Board.getIndex(i, j)] + ""; @@ -580,11 +526,11 @@ private void drawMoveNumbers(Graphics2D g) { private void drawLeelazSuggestions(Graphics2D g) { if (Lizzie.leelaz == null) return; - final int MIN_ALPHA = 32; - final double HUE_SCALING_FACTOR = 3.0; - final double ALPHA_SCALING_FACTOR = 5.0; - final float GREEN_HUE = Color.RGBtoHSB(0, 1, 0, null)[0]; - final float CYAN_HUE = Color.RGBtoHSB(0, 1, 1, null)[0]; + int minAlpha = 32; + float hueFactor = 3.0f; + float alphaFactor = 5.0f; + float greenHue = Color.RGBtoHSB(0, 1, 0, null)[0]; + float cyanHue = Color.RGBtoHSB(0, 1, 1, null)[0]; if (!bestMoves.isEmpty()) { @@ -620,44 +566,32 @@ private void drawLeelazSuggestions(Graphics2D g) { if (move.playouts == 0) // this actually can happen continue; - double percentPlayouts = (double) move.playouts / maxPlayouts; + float percentPlayouts = (float) move.playouts / maxPlayouts; int[] coordinates = Board.convertNameToCoordinates(move.coordinate); int suggestionX = x + scaledMargin + squareLength * coordinates[0]; int suggestionY = y + scaledMargin + squareLength * coordinates[1]; // 0 = Reddest hue - float hue = - isBestMove - ? CYAN_HUE - : (float) - (-GREEN_HUE - * Math.max(0, Math.log(percentPlayouts) / HUE_SCALING_FACTOR + 1)); - float saturation = 0.75f; // saturation - float brightness = 0.85f; // brightness - int alpha = - (int) - (MIN_ALPHA - + (maxAlpha - MIN_ALPHA) - * Math.max(0, Math.log(percentPlayouts) / ALPHA_SCALING_FACTOR + 1)); - // if (uiConfig.getBoolean("shadows-enabled")) - // alpha = 255; + float logPlayouts = (float) log(percentPlayouts); + float otherHue = -greenHue * max(0, logPlayouts / hueFactor + 1); + float hue = isBestMove ? cyanHue : otherHue; + float saturation = 0.75f; + float brightness = 0.85f; + float alpha = (minAlpha + (maxAlpha - minAlpha) * max(0, logPlayouts / alphaFactor + 1)); Color hsbColor = Color.getHSBColor(hue, saturation, brightness); Color color = - new Color(hsbColor.getRed(), hsbColor.getBlue(), hsbColor.getGreen(), alpha); + new Color(hsbColor.getRed(), hsbColor.getBlue(), hsbColor.getGreen(), (int) alpha); + boolean isMouseOver = Lizzie.frame.isMouseOver(coordinates[0], coordinates[1]); if (branch == null) { - drawShadow(g, suggestionX, suggestionY, true, (float) alpha / 255); + drawShadow(g, suggestionX, suggestionY, true, alpha / 255.0f); g.setColor(color); fillCircle(g, suggestionX, suggestionY, stoneRadius); } - if (branch == null - || (isBestMove - && Lizzie.frame.mouseHoverCoordinate != null - && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] - && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { + if (branch == null || isBestMove && isMouseOver) { int strokeWidth = 1; if (isBestMove != hasMaxWinrate) { strokeWidth = 2; @@ -670,13 +604,11 @@ private void drawLeelazSuggestions(Graphics2D g) { g.setStroke(new BasicStroke(1)); } - if ((branch == null + if (branch == null && (hasMaxWinrate - || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats"))) - || (Lizzie.frame.mouseHoverCoordinate != null - && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] - && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { - double roundedWinrate = Math.round(move.winrate * 10) / 10.0; + || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats")) + || isMouseOver) { + double roundedWinrate = round(move.winrate * 10) / 10.0; if (uiConfig.getBoolean("win-rate-always-black") && !Lizzie.board.getData().blackToPlay) { roundedWinrate = 100.0 - roundedWinrate; @@ -701,6 +633,7 @@ private void drawLeelazSuggestions(Graphics2D g) { stoneRadius, stoneRadius * 1.5, 1); + drawString( g, suggestionX, @@ -759,6 +692,7 @@ private void drawWoodenBoard(Graphics2D g) { boardLength + 4 * shadowRadius); g.setStroke(new BasicStroke(shadowRadius * 2)); + // draw border g.setColor(new Color(0, 0, 0, 50)); g.drawRect( @@ -770,7 +704,7 @@ private void drawWoodenBoard(Graphics2D g) { } else { JSONArray boardColor = uiConfig.getJSONArray("board-color"); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); g.setColor(new Color(boardColor.getInt(0), boardColor.getInt(1), boardColor.getInt(2))); g.fillRect(x, y, boardLength, boardLength); } @@ -870,11 +804,10 @@ private void drawShadow( /** Draws a stone centered at (centerX, centerY) */ private void drawStone( Graphics2D g, Graphics2D gShadow, int centerX, int centerY, Stone color, int x, int y) { - // g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, - // RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); - g.setRenderingHint( - RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + // g.setRenderingHint(KEY_ALPHA_INTERPOLATION, + // VALUE_ALPHA_INTERPOLATION_QUALITY); + g.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); // if no shadow graphics is supplied, just draw onto the same graphics if (gShadow == null) gShadow = g; @@ -894,13 +827,12 @@ private void drawStone( null); } else { drawShadow(gShadow, centerX, centerY, true); - g.setColor( - isBlack - ? (isGhost ? new Color(0, 0, 0) : Color.BLACK) - : (isGhost ? new Color(255, 255, 255) : Color.WHITE)); + Color blackColor = isGhost ? new Color(0, 0, 0) : Color.BLACK; + Color whiteColor = isGhost ? new Color(255, 255, 255) : Color.WHITE; + g.setColor(isBlack ? blackColor : whiteColor); fillCircle(g, centerX, centerY, stoneRadius); if (!isBlack) { - g.setColor(isGhost ? new Color(0, 0, 0) : Color.BLACK); + g.setColor(blackColor); drawCircle(g, centerX, centerY, stoneRadius); } } @@ -911,7 +843,7 @@ private void drawStone( private BufferedImage getScaleStone(boolean isBlack, int size) { BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; if (stone == null) { - stone = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + stone = new BufferedImage(size, size, TYPE_INT_ARGB); String imgPath = isBlack ? "/assets/black0.png" : "/assets/white0.png"; Image img = null; try { @@ -934,8 +866,8 @@ private BufferedImage getScaleStone(boolean isBlack, int size) { public BufferedImage getWallpaper() { if (cachedWallpaperImage == null) { try { - cachedWallpaperImage = - ImageIO.read(getClass().getResourceAsStream("/assets/background.jpg")); + String wallpaperPath = "/assets/background.jpg"; + cachedWallpaperImage = ImageIO.read(getClass().getResourceAsStream(wallpaperPath)); } catch (IOException e) { e.printStackTrace(); } @@ -950,7 +882,7 @@ public BufferedImage getWallpaper() { */ // public void drawScaleSmoothImage(Graphics2D g, BufferedImage img, int x, int y, int width, // int height, ImageObserver observer) { - // BufferedImage newstone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + // BufferedImage newstone = new BufferedImage(width, height, TYPE_INT_ARGB); // Graphics2D g2 = newstone.createGraphics(); // g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, // observer); @@ -981,10 +913,10 @@ public BufferedImage getWallpaper() { // h = height; // } // } - // BufferedImage tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + // BufferedImage tmp = new BufferedImage(w, h, TYPE_INT_ARGB); // Graphics2D g2 = tmp.createGraphics(); - // g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - // RenderingHints.VALUE_INTERPOLATION_BICUBIC); + // g2.setRenderingHint(KEY_INTERPOLATION, + // VALUE_INTERPOLATION_BICUBIC); // g2.drawImage(newstone, 0, 0, w, h, null); // g2.dispose(); // newstone = tmp; @@ -1034,33 +966,25 @@ private void drawString( Font font = makeFont(fontBase, style); // set maximum size of font - font = - font.deriveFont( - (float) - (font.getSize2D() * maximumFontWidth / g.getFontMetrics(font).stringWidth(string))); - font = font.deriveFont(Math.min(maximumFontHeight, font.getSize())); + FontMetrics fm = g.getFontMetrics(font); + font = font.deriveFont((float) (font.getSize2D() * maximumFontWidth / fm.stringWidth(string))); + font = font.deriveFont(min(maximumFontHeight, font.getSize())); g.setFont(font); - FontMetrics metrics = g.getFontMetrics(font); - - int height = metrics.getAscent() - metrics.getDescent(); + int height = fm.getAscent() - fm.getDescent(); int verticalOffset; - switch (aboveOrBelow) { - case -1: - verticalOffset = height / 2; - break; - - case 1: - verticalOffset = -height / 2; - break; - - default: - verticalOffset = 0; + if (aboveOrBelow == -1) { + verticalOffset = height / 2; + } else if (aboveOrBelow == 1) { + verticalOffset = -height / 2; + } else { + verticalOffset = 0; } + // bounding box for debugging // g.drawRect(x-(int)maximumFontWidth/2, y - height/2 + verticalOffset, (int)maximumFontWidth, // height+verticalOffset ); - g.drawString(string, x - metrics.stringWidth(string) / 2, y + height / 2 + verticalOffset); + g.drawString(string, x - fm.stringWidth(string) / 2, y + height / 2 + verticalOffset); } private void drawString( @@ -1089,13 +1013,13 @@ private Font makeFont(Font fontBase, int style) { private String getPlayoutsString(int playouts) { if (playouts >= 1_000_000) { double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567 - return Math.round(playoutsDouble) / 10.0 + "m"; + return round(playoutsDouble) / 10.0 + "m"; } else if (playouts >= 10_000) { double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265 - return Math.round(playoutsDouble) + "k"; + return round(playoutsDouble) + "k"; } else if (playouts >= 1_000) { double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65 - return Math.round(playoutsDouble) / 10.0 + "k"; + return round(playoutsDouble) / 10.0 + "k"; } else { return String.valueOf(playouts); } @@ -1205,13 +1129,13 @@ public boolean incrementDisplayedBranchLength(int n) { return false; default: // force nonnegative - displayedBranchLength = Math.max(0, displayedBranchLength + n); + displayedBranchLength = max(0, displayedBranchLength + n); return true; } } public boolean isInside(int x1, int y1) { - return (x <= x1 && x1 < x + boardLength && y <= y1 && y1 < y + boardLength); + return x <= x1 && x1 < x + boardLength && y <= y1 && y1 < y + boardLength; } private boolean showCoordinates() { @@ -1219,7 +1143,7 @@ private boolean showCoordinates() { } public void increaseMaxAlpha(int k) { - maxAlpha = Math.min(maxAlpha + k, 255); + maxAlpha = min(maxAlpha + k, 255); uiPersist.put("max-alpha", maxAlpha); } } diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index fe8df0ca1..15341845b 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -81,7 +81,7 @@ public class LizzieFrame extends JFrame { private final BufferStrategy bs; - public int[] mouseHoverCoordinate; + public int[] mouseOverCoordinate; public boolean showControls = false; public boolean showCoordinates = false; public boolean isPlayingAgainstLeelaz = false; @@ -436,26 +436,16 @@ public void paint(Graphics g0) { if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { if (Lizzie.config.showStatus) { - drawPonderingState( - g, - resourceBundle.getString("LizzieFrame.display.pondering") - + (Lizzie.leelaz.isPondering() - ? resourceBundle.getString("LizzieFrame.display.on") - : resourceBundle.getString("LizzieFrame.display.off")), - ponderingX, - ponderingY, - ponderingSize); + String key = "LizzieFrame.display." + (Lizzie.leelaz.isPondering() ? "on" : "off"); + String text = resourceBundle.getString(key); + drawPonderingState(g, text, ponderingX, ponderingY, ponderingSize); } - if (Lizzie.config.showDynamicKomi && Lizzie.leelaz.getDynamicKomi() != null) { - drawPonderingState( - g, - resourceBundle.getString("LizzieFrame.display.dynamic-komi"), - dynamicKomiLabelX, - dynamicKomiLabelY, - dynamicKomiSize); - drawPonderingState( - g, Lizzie.leelaz.getDynamicKomi(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); + String dynamicKomi = Lizzie.leelaz.getDynamicKomi(); + if (Lizzie.config.showDynamicKomi && dynamicKomi != null) { + String text = resourceBundle.getString("LizzieFrame.display.dynamic-komi"); + drawPonderingState(g, text, dynamicKomiLabelX, dynamicKomiLabelY, dynamicKomiSize); + drawPonderingState(g, dynamicKomi, dynamicKomiX, dynamicKomiY, dynamicKomiSize); } // Todo: Make board move over when there is no space beside the board @@ -479,6 +469,7 @@ public void paint(Graphics g0) { drawComment(g, vx, topInset, vw, vh - topInset + vy, true); } } + if (Lizzie.config.showSubBoard) { try { subBoardRenderer.setLocation(subBoardX, subBoardY); @@ -489,12 +480,8 @@ public void paint(Graphics g0) { } } } else if (Lizzie.config.showStatus) { - drawPonderingState( - g, - resourceBundle.getString("LizzieFrame.display.loading"), - loadingX, - loadingY, - loadingSize); + String loadingText = resourceBundle.getString("LizzieFrame.display.loading"); + drawPonderingState(g, loadingText, loadingX, loadingY, loadingSize); } if (Lizzie.config.showCaptured) drawCaptured(g, capx, capy, capw, caph); @@ -554,9 +541,8 @@ private void drawVariationTreeContainer(Graphics2D g, int vx, int vy, int vw, in } private void drawPonderingState(Graphics2D g, String text, int x, int y, double size) { - Font font = - new Font( - systemDefaultFontName, Font.PLAIN, (int) (Math.max(getWidth(), getHeight()) * size)); + int fontSize = (int) (Math.max(getWidth(), getHeight()) * size); + Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); FontMetrics fm = g.getFontMetrics(font); int stringWidth = fm.stringWidth(text); int stringHeight = fm.getAscent() - fm.getDescent(); @@ -611,17 +597,12 @@ void drawControls() { int maxSize = Math.min(getWidth(), getHeight()); Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.034)); g.setFont(font); + FontMetrics metrics = g.getFontMetrics(font); - int maxCommandWidth = - commandsToShow - .stream() - .reduce( - 0, - (Integer i, String command) -> Math.max(i, metrics.stringWidth(command)), - (Integer a, Integer b) -> Math.max(a, b)); + int maxCmdWidth = commandsToShow.stream().mapToInt(c -> metrics.stringWidth(c)).max().orElse(0); int lineHeight = (int) (font.getSize() * 1.15); - int boxWidth = Util.clamp((int) (maxCommandWidth * 1.4), 0, getWidth()); + int boxWidth = Util.clamp((int) (maxCmdWidth * 1.4), 0, getWidth()); int boxHeight = Util.clamp(commandsToShow.size() * lineHeight, 0, getHeight()); int commandsX = Util.clamp(getWidth() / 2 - boxWidth / 2, 0, getWidth()); @@ -764,11 +745,9 @@ private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int if (validLastWinrate && validWinrate) { String text; if (Lizzie.config.handicapInsteadOfWinrate) { - text = - String.format( - ": %.2f", - Lizzie.leelaz.winrateToHandicap(100 - curWR) - - Lizzie.leelaz.winrateToHandicap(lastWR)); + double currHandicapedWR = Lizzie.leelaz.winrateToHandicap(100 - curWR); + double lastHandicapedWR = Lizzie.leelaz.winrateToHandicap(lastWR); + text = String.format(": %.2f", currHandicapedWR - lastHandicapedWR); } else { text = String.format(": %.1f%%", 100 - lastWR - curWR); } @@ -949,16 +928,17 @@ public void playBestMove() { } public void onMouseMoved(int x, int y) { - int[] newMouseHoverCoordinate = boardRenderer.convertScreenToCoordinates(x, y); - if (mouseHoverCoordinate != null - && newMouseHoverCoordinate != null - && (mouseHoverCoordinate[0] != newMouseHoverCoordinate[0] - || mouseHoverCoordinate[1] != newMouseHoverCoordinate[1])) { - mouseHoverCoordinate = newMouseHoverCoordinate; + int[] c = boardRenderer.convertScreenToCoordinates(x, y); + if (c != null && !isMouseOver(c[0], c[1])) { repaint(); - } else { - mouseHoverCoordinate = newMouseHoverCoordinate; } + mouseOverCoordinate = c; + } + + public boolean isMouseOver(int x, int y) { + return mouseOverCoordinate != null + && mouseOverCoordinate[0] == x + && mouseOverCoordinate[1] == x; } public void onMouseDragged(int x, int y) { diff --git a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java index 585676d10..668c2a6ed 100644 --- a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java +++ b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java @@ -124,20 +124,11 @@ private void initButtonBar() { okButton.setText("OK"); okButton.addActionListener(e -> apply()); + int center = GridBagConstraints.CENTER; + int both = GridBagConstraints.BOTH; buttonBar.add( okButton, - new GridBagConstraints( - 1, - 0, - 1, - 1, - 0.0, - 0.0, - GridBagConstraints.CENTER, - GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), - 0, - 0)); + new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, center, both, new Insets(0, 0, 0, 0), 0, 0)); dialogPane.add(buttonBar, BorderLayout.SOUTH); } diff --git a/src/main/java/featurecat/lizzie/gui/VariationTree.java b/src/main/java/featurecat/lizzie/gui/VariationTree.java index 72f072bf0..aef810e2b 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -126,7 +126,9 @@ public void drawTree( } public void draw(Graphics2D g, int posx, int posy, int width, int height) { - if (width <= 0 || height <= 0) return; // we don't have enough space + if (width <= 0 || height <= 0) { + return; // we don't have enough space + } // Use dense tree for saving space if large-subboard YSPACING = (Lizzie.config.showLargeSubBoard() ? 20 : 30); diff --git a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java index 1b0de91c6..3b4915c96 100644 --- a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java +++ b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java @@ -114,21 +114,19 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { wr = bwr; playouts = stats.totalPlayouts; } - { - // Draw a vertical line at the current move - Stroke previousStroke = g.getStroke(); - int x = posx + (movenum * width / numMoves); - g.setStroke(dashed); - g.setColor(Color.white); - g.drawLine(x, posy, x, posy + height); - // Show move number - String moveNumString = "" + node.getData().moveNumber; - int mw = g.getFontMetrics().stringWidth(moveNumString); - int margin = strokeRadius; - int mx = x - posx < width / 2 ? x + margin : x - mw - margin; - g.drawString(moveNumString, mx, posy + height - margin); - g.setStroke(previousStroke); - } + // Draw a vertical line at the current move + Stroke previousStroke = g.getStroke(); + int x = posx + (movenum * width / numMoves); + g.setStroke(dashed); + g.setColor(Color.white); + g.drawLine(x, posy, x, posy + height); + // Show move number + String moveNumString = "" + node.getData().moveNumber; + int mw = g.getFontMetrics().stringWidth(moveNumString); + int margin = strokeRadius; + int mx = x - posx < width / 2 ? x + margin : x - mw - margin; + g.drawString(moveNumString, mx, posy + height - margin); + g.setStroke(previousStroke); } if (playouts > 0) { if (wr < 0) { diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index f70752d55..65c9f51a8 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -32,28 +32,17 @@ public Board() { /** Initialize the board completely */ private void initialize() { Stone[] stones = new Stone[BOARD_SIZE * BOARD_SIZE]; - for (int i = 0; i < stones.length; i++) stones[i] = Stone.EMPTY; - - boolean blackToPlay = true; - int[] lastMove = null; + for (int i = 0; i < stones.length; i++) { + stones[i] = Stone.EMPTY; + } capturedStones = null; scoreMode = false; - history = - new BoardHistoryList( - new BoardData( - stones, - lastMove, - Stone.EMPTY, - blackToPlay, - new Zobrist(), - 0, - new int[BOARD_SIZE * BOARD_SIZE], - 0, - 0, - 50, - 0)); + int[] boardArray = new int[BOARD_SIZE * BOARD_SIZE]; + BoardData boardData = + new BoardData(stones, null, Stone.EMPTY, true, new Zobrist(), 0, boardArray, 0, 0, 50, 0); + history = new BoardHistoryList(boardData); } /** diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index ac91dd40a..9b266ceef 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -9,12 +9,10 @@ public class BoardData { public Stone lastMoveColor; public Stone[] stones; public Zobrist zobrist; - public boolean verify; public double winrate; public int playouts; - public int blackCaptures; public int whiteCaptures; diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 48bc2795c..bafb37173 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -249,8 +249,8 @@ private static void saveToStream(Board board, Writer writer) throws IOException // collect game info BoardHistoryList history = board.getHistory().shallowCopy(); GameInfo gameInfo = history.getGameInfo(); - String playerBlack = gameInfo.getPlayerBlack(); - String playerWhite = gameInfo.getPlayerWhite(); + String playerB = gameInfo.getPlayerBlack(); + String playerW = gameInfo.getPlayerWhite(); Double komi = gameInfo.getKomi(); Integer handicap = gameInfo.getHandicap(); String date = SGF_DATE_FORMAT.format(gameInfo.getDate()); @@ -258,10 +258,8 @@ private static void saveToStream(Board board, Writer writer) throws IOException // add SGF header StringBuilder builder = new StringBuilder("(;"); if (handicap != 0) builder.append(String.format("HA[%s]", handicap)); - builder.append( - String.format( - "KM[%s]PW[%s]PB[%s]DT[%s]AP[Lizzie: %s]", - komi, playerWhite, playerBlack, date, Lizzie.lizzieVersion)); + String header = "KM[%s]PW[%s]PB[%s]DT[%s]AP[Lizzie: %s]"; + builder.append(String.format(header, komi, playerW, playerB, date, Lizzie.lizzieVersion)); // move to the first move history.toStart(); diff --git a/src/test/java/common/Util.java b/src/test/java/common/Util.java index 2811152d5..7fa429488 100644 --- a/src/test/java/common/Util.java +++ b/src/test/java/common/Util.java @@ -1,6 +1,5 @@ package common; - import featurecat.lizzie.Lizzie; import featurecat.lizzie.rules.Board; import featurecat.lizzie.rules.BoardData;