diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index b7ef59bda..bb72ce255 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -72,15 +72,8 @@ private JSONObject loadAndMergeConfig( 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(); - } + JSONObject mergedcfg = new JSONObject(new JSONTokener(fp)); + boolean modified = mergeDefaults(mergedcfg, defaultCfg); fp.close(); @@ -105,10 +98,6 @@ private JSONObject loadAndMergeConfig( * @return if any correction has been made. */ private boolean validateAndCorrectSettings(JSONObject config) { - if (config == null) { - return false; - } - boolean madeCorrections = false; // Check ui configs @@ -185,22 +174,22 @@ public Config() throws IOException { // 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) { + public boolean mergeDefaults(JSONObject config, JSONObject defaultsConfig) { boolean modified = false; - Iterator keys = defaults_config.keys(); + Iterator keys = defaultsConfig.keys(); while (keys.hasNext()) { String key = keys.next(); - Object new_val = defaults_config.get(key); - if (new_val instanceof JSONObject) { + Object newVal = defaultsConfig.get(key); + if (newVal 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); + Object oldVal = config.get(key); + modified |= mergeDefaults((JSONObject) oldVal, (JSONObject) newVal); } else { if (!config.has(key)) { - config.put(key, new_val); + config.put(key, newVal); modified = true; } } diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index a9a1150de..cc4a1ce23 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -10,10 +10,10 @@ /** Main class. */ public class Lizzie { + public static Config config; public static LizzieFrame frame; - public static Leelaz leelaz; public static Board board; - public static Config config; + public static Leelaz leelaz; public static String lizzieVersion = "0.5"; private static String[] mainArgs; @@ -24,25 +24,17 @@ public static void main(String[] args) throws IOException { config = new Config(); board = new Board(); frame = new LizzieFrame(); - new Thread(Lizzie::run).start(); - } + leelaz = new Leelaz(); - public static void run() { - try { - leelaz = new Leelaz(); - if (config.handicapInsteadOfWinrate) { - leelaz.estimatePassWinrate(); - } - if (mainArgs.length == 1) { - frame.loadFile(new File(mainArgs[0])); - } else if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { - board.resumePreviousGame(); - } - leelaz.togglePonder(); - } catch (IOException e) { - e.printStackTrace(); - System.exit(-1); + if (config.handicapInsteadOfWinrate) { + leelaz.estimatePassWinrate(); } + if (mainArgs.length == 1) { + frame.loadFile(new File(mainArgs[0])); + } else if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { + board.resumePreviousGame(); + } + leelaz.togglePonder(); } public static void setLookAndFeel() { @@ -60,7 +52,7 @@ public static void setLookAndFeel() { } public static void shutdown() { - if (board != null && config.config.getJSONObject("ui").getBoolean("confirm-exit")) { + if (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); @@ -68,9 +60,7 @@ public static void shutdown() { LizzieFrame.saveFile(); } } - if (board != null) { - board.autosaveToMemory(); - } + board.autosaveToMemory(); try { config.persist(); @@ -78,7 +68,7 @@ public static void shutdown() { e.printStackTrace(); // Failed to save config } - if (leelaz != null) leelaz.shutdown(); + leelaz.shutdown(); System.exit(0); } @@ -88,33 +78,27 @@ public static void shutdown() { * @param index engine index */ public static void switchEngine(int index) { - - String commandLine = null; + String commandLine; if (index == 0) { + String networkFile = Lizzie.config.leelazConfig.getString("network-file"); commandLine = Lizzie.config.leelazConfig.getString("engine-command"); - commandLine = - commandLine.replaceAll( - "%network-file", Lizzie.config.leelazConfig.getString("network-file")); + commandLine = commandLine.replaceAll("%network-file", networkFile); } else { - JSONArray commandList = Lizzie.config.leelazConfig.getJSONArray("engine-command-list"); - if (commandList != null && commandList.length() >= index) { - commandLine = commandList.getString(index - 1); - } else { - index = -1; + JSONArray engines = Lizzie.config.leelazConfig.getJSONArray("engine-command-list"); + if (engines.length() < index) { + return; } + commandLine = engines.getString(index - 1); } - if (index < 0 - || commandLine == null - || commandLine.trim().isEmpty() - || index == Lizzie.leelaz.currentEngineN()) { + if (commandLine.trim().isEmpty() || index == Lizzie.leelaz.currentEngineN()) { return; } - // Workaround for leelaz cannot exit when restarting + // Workaround for leelaz no exiting 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.togglePonder(); // Toggle twice for to restart pondering Lizzie.leelaz.isThinking = false; } Lizzie.leelaz.togglePonder(); diff --git a/src/main/java/featurecat/lizzie/analysis/Branch.java b/src/main/java/featurecat/lizzie/analysis/Branch.java index 4dd91b19d..f624a412a 100644 --- a/src/main/java/featurecat/lizzie/analysis/Branch.java +++ b/src/main/java/featurecat/lizzie/analysis/Branch.java @@ -3,41 +3,39 @@ import featurecat.lizzie.rules.Board; import featurecat.lizzie.rules.BoardData; import featurecat.lizzie.rules.Stone; -import featurecat.lizzie.rules.Zobrist; import java.util.List; +import java.util.Optional; public class Branch { public BoardData data; public Branch(Board board, List variation) { - int moveNumber = 0; - int[] lastMove = board.getLastMove(); int[] moveNumberList = new int[Board.boardSize * Board.boardSize]; - 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(); + int moveNumber = 0; + double winrate = 0.0; + int playouts = 0; - // Dont care about winrate for branch this.data = new BoardData( - stones, - lastMove, - lastMoveColor, - blackToPlay, - zobrist, + board.getStones().clone(), + board.getLastMove(), + board.getData().lastMoveColor, + board.getData().blackToPlay, + board.getData().zobrist.clone(), moveNumber, moveNumberList, board.getData().blackCaptures, board.getData().whiteCaptures, - 0.0, - 0); + winrate, + playouts); for (int i = 0; i < variation.size(); i++) { - int[] coord = Board.convertNameToCoordinates(variation.get(i)); - if (coord == null) break; - data.lastMove = coord; + Optional coordOpt = Board.asCoordinates(variation.get(i)); + if (!coordOpt.isPresent()) { + break; + } + int[] coord = coordOpt.get(); + data.lastMove = coordOpt; 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; diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 76c926934..85bf9ef91 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -58,17 +58,18 @@ public class Leelaz { private boolean isCheckingVersion; // for Multiple Engine - private String engineCommand = null; - private List commands = null; - private JSONObject config = null; - private String currentWeightFile = null; - private String currentWeight = null; + private String engineCommand; + private List commands; + private JSONObject config; + private String currentWeightFile; + private String currentWeight; private boolean switching = false; private int currentEngineN = -1; - private ScheduledExecutorService executor = null; + private ScheduledExecutorService executor; // dynamic komi and opponent komi as reported by dynamic-komi version of leelaz - private float dynamicKomi = Float.NaN, dynamicOppKomi = Float.NaN; + private float dynamicKomi = Float.NaN; + private float dynamicOppKomi = Float.NaN; /** * Initializes the leelaz process and starts reading output * @@ -103,29 +104,20 @@ public Leelaz() throws IOException, JSONException { } public void startEngine(String engineCommand) throws IOException { - // Check engine command - if (engineCommand == null || engineCommand.trim().isEmpty()) { + if (engineCommand.trim().isEmpty()) { return; } - // create this as a list which gets passed into the processbuilder + // Create this as a list which gets passed into the processbuilder 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()) { - currentWeightFile = wMatcher.group(2); - if (currentWeightFile != null) { - String[] names = currentWeightFile.split("[\\\\|/]"); - if (names != null && names.length > 1) { - currentWeight = names[names.length - 1]; - } else { - currentWeight = currentWeightFile; - } - } - } + // Get weight name + Pattern wPattern = Pattern.compile("(?s).*?(--weights |-w )([^ ]+)(?s).*"); + Matcher wMatcher = wPattern.matcher(engineCommand); + if (wMatcher.matches() && wMatcher.groupCount() == 2) { + currentWeightFile = wMatcher.group(2); + String[] names = currentWeightFile.split("[\\\\|/]"); + currentWeight = names.length > 1 ? names[names.length - 1] : currentWeightFile; } // Check if engine is present @@ -169,7 +161,7 @@ public void startEngine(String engineCommand) throws IOException { } public void restartEngine(String engineCommand, int index) throws IOException { - if (engineCommand == null || engineCommand.trim().isEmpty()) { + if (engineCommand.trim().isEmpty()) { return; } switching = true; @@ -240,12 +232,12 @@ private void parseLine(String line) { // Clear switching prompt switching = false; // Display engine command in the title - if (Lizzie.frame != null) Lizzie.frame.updateTitle(); + Lizzie.frame.updateTitle(); 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(); + Lizzie.frame.repaint(); // don't follow the maxAnalyzeTime rule if we are in analysis mode if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis && !Lizzie.board.inAnalysisMode()) { @@ -258,7 +250,7 @@ private void parseLine(String line) { || isThinking && !isPondering && Lizzie.frame.isPlayingAgainstLeelaz) { bestMoves.add(MoveData.fromSummary(line)); notifyBestMoveListeners(); - if (Lizzie.frame != null) Lizzie.frame.repaint(); + Lizzie.frame.repaint(); } } else if (line.startsWith("play")) { // In lz-genmove_analyze @@ -267,7 +259,7 @@ private void parseLine(String line) { } isThinking = false; - } else if (Lizzie.frame != null && (line.startsWith("=") || line.startsWith("?"))) { + } else if (line.startsWith("=") || line.startsWith("?")) { if (printCommunication) { System.out.print(line); } @@ -281,8 +273,9 @@ private void parseLine(String line) { if (isSettingHandicap) { bestMoves = new ArrayList<>(); for (int i = 1; i < params.length; i++) { - int[] coordinates = Lizzie.board.convertNameToCoordinates(params[i]); - Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); + Lizzie.board + .asCoordinates(params[i]) + .ifPresent(coords -> Lizzie.board.getHistory().setStone(coords, Stone.BLACK)); } isSettingHandicap = false; } else if (isThinking && !isPondering) { @@ -318,8 +311,7 @@ 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 + if (!(Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay)) { try { bestMovesTemp.add(MoveData.fromInfo(line)); @@ -361,9 +353,8 @@ private void read() { */ 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")) { + if (!cmdQueue.isEmpty() && cmdQueue.peekLast().startsWith("lz-analyze")) { cmdQueue.removeLast(); } cmdQueue.addLast(command); @@ -380,11 +371,11 @@ private void trySendCommandFromQueue() { // 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())) { + if (cmdQueue.isEmpty() + || cmdQueue.peekFirst().startsWith("lz-analyze") && !isResponseUpToDate()) { return; } - cmdQueue.removeFirst(); + String command = cmdQueue.removeFirst(); sendCommandToLeelaz(command); } } @@ -494,11 +485,12 @@ public List getBestMoves() { } } - public String getDynamicKomi() { + public Optional getDynamicKomi() { if (Float.isNaN(dynamicKomi) || Float.isNaN(dynamicOppKomi)) { - return null; + return Optional.empty(); + } else { + return Optional.of(String.format("%.1f / %.1f", dynamicKomi, dynamicOppKomi)); } - return String.format("%.1f / %.1f", dynamicKomi, dynamicOppKomi); } public boolean isPondering() { @@ -522,7 +514,7 @@ public WinrateStats(double maxWinrate, int totalPlayouts) { public WinrateStats getWinrateStats() { WinrateStats stats = new WinrateStats(-100, 0); - if (bestMoves != null && !bestMoves.isEmpty()) { + if (!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) diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 2ff2a8766..21458ff1e 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -33,34 +34,35 @@ public class BoardRenderer { private static final double MARGIN = 0.03; private static final double MARGIN_WITH_COORDINATES = 0.06; private static final double STARPOINT_DIAMETER = 0.015; + private static final BufferedImage emptyImage = new BufferedImage(1, 1, TYPE_INT_ARGB); private int x, y; private int boardLength; private JSONObject uiConfig, uiPersist; private int scaledMargin, availableLength, squareLength, stoneRadius; - private Branch branch; + private Optional branchOpt = Optional.empty(); private List bestMoves; - private BufferedImage cachedBackgroundImage = null; + private BufferedImage cachedBackgroundImage = emptyImage; private boolean cachedBackgroundImageHasCoordinatesEnabled = false; private int cachedX, cachedY; - private BufferedImage cachedStonesImage = null; - private BufferedImage cachedBoardImage = null; - private BufferedImage cachedWallpaperImage = null; - private BufferedImage cachedStonesShadowImage = null; + private BufferedImage cachedStonesImage = emptyImage; + private BufferedImage cachedBoardImage = emptyImage; + private BufferedImage cachedWallpaperImage = emptyImage; + private BufferedImage cachedStonesShadowImage = emptyImage; private Zobrist cachedZhash = new Zobrist(); // defaults to an empty board - private BufferedImage cachedBlackStoneImage = null; - private BufferedImage cachedWhiteStoneImage = null; + private BufferedImage cachedBlackStoneImage = emptyImage; + private BufferedImage cachedWhiteStoneImage = emptyImage; - private BufferedImage branchStonesImage = null; - private BufferedImage branchStonesShadowImage = null; + private BufferedImage branchStonesImage = emptyImage; + private BufferedImage branchStonesShadowImage; private boolean lastInScoreMode = false; - public List variation; + public Optional> variationOpt; // special values of displayedBranchLength public static final int SHOW_RAW_BOARD = -1; @@ -85,8 +87,6 @@ public BoardRenderer(boolean isMainBoard) { /** Draw a go board */ public void draw(Graphics2D g) { - if (Lizzie.frame == null || Lizzie.board == null) return; - setupSizeParameters(); // Stopwatch timer = new Stopwatch(); @@ -130,14 +130,10 @@ public void draw(Graphics2D g) { /** * Return the best move of Leelaz's suggestions * - * @return the coordinate name of the best move + * @return the optional coordinate name of the best move */ - public String bestMoveCoordinateName() { - if (bestMoves == null || bestMoves.size() == 0) { - return null; - } else { - return bestMoves.get(0).coordinate; - } + public Optional bestMoveCoordinateName() { + return bestMoves.isEmpty() ? Optional.empty() : Optional.of(bestMoves.get(0).coordinate); } /** Calculate good values for boardLength, scaledMargin, availableLength, and squareLength */ @@ -163,9 +159,8 @@ 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() != width + // Draw the cached background image if frame size changes + if (cachedBackgroundImage.getWidth() != width || cachedBackgroundImage.getHeight() != height || cachedX != x || cachedY != y @@ -177,10 +172,10 @@ private void drawGoban(Graphics2D g0) { cachedBackgroundImage = new BufferedImage(width, height, TYPE_INT_ARGB); Graphics2D g = cachedBackgroundImage.createGraphics(); - // draw the wooden background + // Draw the wooden background drawWoodenBoard(g); - // draw the lines + // Draw the lines g.setColor(Color.BLACK); for (int i = 0; i < Board.boardSize; i++) { g.drawLine( @@ -197,10 +192,10 @@ private void drawGoban(Graphics2D g0) { y + scaledMargin + availableLength - 1); } - // draw the star points + // Draw the star points drawStarPoints(g); - // draw coordinates if enabled + // Draw coordinates if enabled if (showCoordinates()) { g.setColor(Color.BLACK); String alphabet = "ABCDEFGHJKLMNOPQRST"; @@ -252,7 +247,7 @@ private void drawGoban(Graphics2D g0) { } /** - * Draw the star points on the board, according to board size + * Draws the star points on the board, according to board size * * @param g graphics2d object to draw */ @@ -288,8 +283,7 @@ private void drawStarPoints0( /** 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 + if (cachedStonesImage.getWidth() != boardLength || cachedStonesImage.getHeight() != boardLength || cachedDisplayedBranchLength != displayedBranchLength || !cachedZhash.equals(Lizzie.board.getData().zobrist) @@ -356,20 +350,19 @@ private void drawScore(Graphics2D go) { g.dispose(); } - /** Draw the 'ghost stones' which show a variation Leelaz is thinking about */ + /** Draw the 'ghost stones' which show a variationOpt Leelaz is thinking about */ private void drawBranch() { showingBranch = false; branchStonesImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); branchStonesShadowImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); - branch = null; + branchOpt = Optional.empty(); - if (Lizzie.frame.isPlayingAgainstLeelaz || Lizzie.leelaz == null) { + if (Lizzie.frame.isPlayingAgainstLeelaz) { return; } // calculate best moves and branch bestMoves = Lizzie.leelaz.getBestMoves(); - branch = null; - variation = null; + variationOpt = Optional.empty(); if (isMainBoard && (isShowingRawBoard() || !Lizzie.config.showBranch)) { return; @@ -378,12 +371,14 @@ private void drawBranch() { Graphics2D g = (Graphics2D) branchStonesImage.getGraphics(); Graphics2D gShadow = (Graphics2D) branchStonesShadowImage.getGraphics(); - MoveData suggestedMove = (isMainBoard ? mouseOveredMove() : getBestMove()); - if (suggestedMove == null) return; - variation = suggestedMove.variation; - branch = new Branch(Lizzie.board, variation); - - if (branch == null) return; + Optional suggestedMove = (isMainBoard ? mouseOveredMove() : getBestMove()); + if (!suggestedMove.isPresent()) { + return; + } + List variation = suggestedMove.get().variation; + Branch branch = new Branch(Lizzie.board, variation); + branchOpt = Optional.of(branch); + variationOpt = Optional.of(variation); showingBranch = true; g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); @@ -405,28 +400,22 @@ private void drawBranch() { gShadow.dispose(); } - 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); - if (coord == null) { - continue; - } - - if (Lizzie.frame.isMouseOver(coord[0], coord[1])) { - return move; - } - } - } - return null; + private Optional mouseOveredMove() { + return bestMoves + .stream() + .filter( + move -> + Board.asCoordinates(move.coordinate) + .map(c -> Lizzie.frame.isMouseOver(c[0], c[1])) + .orElse(false)) + .findFirst(); } - private MoveData getBestMove() { - return bestMoves.isEmpty() ? null : bestMoves.get(0); + private Optional getBestMove() { + return bestMoves.isEmpty() ? Optional.empty() : Optional.of(bestMoves.get(0)); } - /** render the shadows and stones in correct background-foreground order */ + /** Render the shadows and stones in correct background-foreground order */ private void renderImages(Graphics2D g) { g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); g.drawImage(cachedStonesShadowImage, x, y, null); @@ -443,15 +432,17 @@ private void renderImages(Graphics2D g) { private void drawMoveNumbers(Graphics2D g) { 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 + Optional lastMoveOpt = branchOpt.map(b -> b.data.lastMove).orElse(board.getLastMove()); + if (!Lizzie.config.showMoveNumber && !branchOpt.isPresent()) { + if (lastMoveOpt.isPresent()) { + int[] lastMove = lastMoveOpt.get(); + + // 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 + // Set color to the opposite color of whatever is on the board boolean isWhite = board.getStones()[Board.getIndex(lastMove[0], lastMove[1])].isWhite(); g.setColor(isWhite ? Color.BLACK : Color.WHITE); @@ -461,7 +452,7 @@ private void drawMoveNumbers(Graphics2D g) { } else { drawCircle(g, stoneX, stoneY, lastMoveMarkerRadius); } - } else if (lastMove == null && board.getData().moveNumber != 0 && !board.inScoreMode()) { + } else if (board.getData().moveNumber != 0 && !board.inScoreMode()) { g.setColor( board.getData().blackToPlay ? new Color(255, 255, 255, 150) : new Color(0, 0, 0, 150)); g.fillOval( @@ -484,10 +475,11 @@ private void drawMoveNumbers(Graphics2D g) { return; } - int[] moveNumberList = branch == null ? board.getMoveNumberList() : branch.data.moveNumberList; + int[] moveNumberList = + branchOpt.map(b -> b.data.moveNumberList).orElse(board.getMoveNumberList()); // Allow to display only last move number - int lastMoveNumber = branch == null ? board.getData().moveNumber : branch.data.moveNumber; + int lastMoveNumber = branchOpt.map(b -> b.data.moveNumber).orElse(board.getData().moveNumber); int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); for (int i = 0; i < Board.boardSize; i++) { @@ -501,13 +493,13 @@ private void drawMoveNumbers(Graphics2D g) { } int here = Board.getIndex(i, j); - Stone stoneHere = branch == null ? board.getStones()[here] : branch.data.stones[here]; + Stone stoneHere = branchOpt.map(b -> b.data.stones[here]).orElse(board.getStones()[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.isMouseOver(i, j))) { - if (lastMove != null && i == lastMove[0] && j == lastMove[1]) + && (branchOpt.isPresent() || !Lizzie.frame.isMouseOver(i, j))) { + if (lastMoveOpt.isPresent() && lastMoveOpt.get()[0] == i && lastMoveOpt.get()[1] == j) g.setColor(Color.RED.brighter()); // stoneHere.isBlack() ? Color.RED.brighter() : // Color.BLUE.brighter()); else { @@ -535,8 +527,6 @@ private void drawMoveNumbers(Graphics2D g) { * Draw all of Leelaz's suggestions as colored stones with winrate/playout statistics overlayed */ private void drawLeelazSuggestions(Graphics2D g) { - if (Lizzie.leelaz == null) return; - int minAlpha = 32; float hueFactor = 3.0f; float alphaFactor = 5.0f; @@ -554,34 +544,42 @@ private void drawLeelazSuggestions(Graphics2D g) { for (int i = 0; i < Board.boardSize; i++) { for (int j = 0; j < Board.boardSize; j++) { - MoveData move = null; + Optional moveOpt = Optional.empty(); - // this is inefficient but it looks better with shadows + // 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; + Optional coord = Board.asCoordinates(m.coordinate); + if (coord.isPresent()) { + int[] c = coord.get(); + if (c[0] == i && c[1] == j) { + moveOpt = Optional.of(m); + break; + } } } - if (move == null) continue; + if (!moveOpt.isPresent()) { + continue; + } + MoveData move = moveOpt.get(); boolean isBestMove = bestMoves.get(0) == move; boolean hasMaxWinrate = move.winrate == maxWinrate; - if (move.playouts == 0) // this actually can happen - continue; + if (move.playouts == 0) { + continue; // This actually can happen + } 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]; + Optional coordsOpt = Board.asCoordinates(move.coordinate); + if (!coordsOpt.isPresent()) { + continue; + } + int[] coords = coordsOpt.get(); + + int suggestionX = x + scaledMargin + squareLength * coords[0]; + int suggestionY = y + scaledMargin + squareLength * coords[1]; // 0 = Reddest hue float logPlayouts = (float) log(percentPlayouts); @@ -595,14 +593,14 @@ private void drawLeelazSuggestions(Graphics2D g) { Color color = new Color(hsbColor.getRed(), hsbColor.getBlue(), hsbColor.getGreen(), (int) alpha); - boolean isMouseOver = Lizzie.frame.isMouseOver(coordinates[0], coordinates[1]); - if (branch == null) { + boolean isMouseOver = Lizzie.frame.isMouseOver(coords[0], coords[1]); + if (!branchOpt.isPresent()) { drawShadow(g, suggestionX, suggestionY, true, alpha / 255.0f); g.setColor(color); fillCircle(g, suggestionX, suggestionY, stoneRadius); } - if (branch == null || isBestMove && isMouseOver) { + if (!branchOpt.isPresent() || isBestMove && isMouseOver) { int strokeWidth = 1; if (isBestMove != hasMaxWinrate) { strokeWidth = 2; @@ -615,7 +613,7 @@ private void drawLeelazSuggestions(Graphics2D g) { g.setStroke(new BasicStroke(1)); } - if (branch == null + if (!branchOpt.isPresent() && (hasMaxWinrate || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats")) || isMouseOver) { @@ -625,7 +623,8 @@ private void drawLeelazSuggestions(Graphics2D g) { roundedWinrate = 100.0 - roundedWinrate; } g.setColor(Color.BLACK); - if (branch != null && Lizzie.board.getData().blackToPlay) g.setColor(Color.WHITE); + if (branchOpt.isPresent() && Lizzie.board.getData().blackToPlay) + g.setColor(Color.WHITE); String text; if (Lizzie.config.handicapInsteadOfWinrate) { @@ -660,33 +659,31 @@ private void drawLeelazSuggestions(Graphics2D g) { } private void drawNextMoves(Graphics2D g) { + g.setColor(Lizzie.board.getData().blackToPlay ? Color.BLACK : Color.WHITE); List nexts = Lizzie.board.getHistory().getNexts(); 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)); - } + boolean first = (i == 0); + nexts + .get(i) + .getData() + .lastMove + .ifPresent( + nextMove -> { + int moveX = x + scaledMargin + squareLength * nextMove[0]; + int moveY = y + scaledMargin + squareLength * nextMove[1]; + if (first) g.setStroke(new BasicStroke(3.0f)); + drawCircle(g, moveX, moveY, stoneRadius + 1); // Slightly outside best move circle + if (first) g.setStroke(new BasicStroke(1.0f)); + }); } } private void drawWoodenBoard(Graphics2D g) { if (uiConfig.getBoolean("fancy-board")) { // fancy version - if (cachedBoardImage == null) { + if (cachedBoardImage == emptyImage) { cachedBoardImage = Lizzie.config.theme.board(); } @@ -818,9 +815,6 @@ private void drawStone( 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; - if (color.isBlack() || color.isWhite()) { boolean isBlack = color.isBlack(); boolean isGhost = (color == Stone.BLACK_GHOST || color == Stone.WHITE_GHOST); @@ -849,25 +843,25 @@ private void drawStone( } /** Get scaled stone, if cached then return cached */ - public BufferedImage getScaleStone(boolean isBlack, int size) { - BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; - if (stone == null || stone.getWidth() != size || stone.getHeight() != size) { - stone = new BufferedImage(size, size, TYPE_INT_ARGB); - Graphics2D g2 = stone.createGraphics(); + private BufferedImage getScaleStone(boolean isBlack, int size) { + BufferedImage stoneImage = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; + if (stoneImage.getWidth() != size || stoneImage.getHeight() != size) { + stoneImage = new BufferedImage(size, size, TYPE_INT_ARGB); Image img = isBlack ? Lizzie.config.theme.blackStone() : Lizzie.config.theme.whiteStone(); + Graphics2D g2 = stoneImage.createGraphics(); g2.drawImage(img.getScaledInstance(size, size, java.awt.Image.SCALE_SMOOTH), 0, 0, null); g2.dispose(); if (isBlack) { - cachedBlackStoneImage = stone; + cachedBlackStoneImage = stoneImage; } else { - cachedWhiteStoneImage = stone; + cachedWhiteStoneImage = stoneImage; } } - return stone; + return stoneImage; } public BufferedImage getWallpaper() { - if (cachedWallpaperImage == null) { + if (cachedWallpaperImage == emptyImage) { cachedWallpaperImage = Lizzie.config.theme.background(); } return cachedWallpaperImage; @@ -950,9 +944,9 @@ private void drawStoneMarkup(Graphics2D g) { String[] moves = label.split(":"); int[] move = SGFParser.convertSgfPosToCoord(moves[0]); if (move != null) { - int[] lastMove = - branch == null ? Lizzie.board.getLastMove() : branch.data.lastMove; - if (!Arrays.equals(move, lastMove)) { + Optional lastMove = + branchOpt.map(b -> b.data.lastMove).orElse(Lizzie.board.getLastMove()); + if (lastMove.isPresent() && !Arrays.equals(move, lastMove.get())) { int moveX = x + scaledMargin + squareLength * move[0]; int moveY = y + scaledMargin + squareLength * move[1]; g.setColor( @@ -1138,9 +1132,9 @@ public int getActualBoardLength() { * @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 + * BOARD_SIZE - 1. Otherwise, returns Optional.empty */ - public int[] convertScreenToCoordinates(int x, int y) { + public Optional convertScreenToCoordinates(int x, int y) { int marginLength; // the pixel width of the margins int boardLengthWithoutMargins; // the pixel width of the game board without margins @@ -1157,8 +1151,7 @@ public int[] convertScreenToCoordinates(int x, int y) { 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; + return Board.isValid(x, y) ? Optional.of(new int[] {x, y}) : Optional.empty(); } /** diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index f1cda65e8..f082fdddb 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -332,7 +332,7 @@ public void keyPressed(KeyEvent e) { break; case VK_PERIOD: - if (Lizzie.board.getHistory().getNext() == null) { + if (!Lizzie.board.getHistory().getNext().isPresent()) { Lizzie.board.setScoreMode(!Lizzie.board.inScoreMode()); } break; diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index c95481114..c98e07203 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -1,5 +1,7 @@ package featurecat.lizzie.gui; +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; +import static java.awt.image.BufferedImage.TYPE_INT_RGB; import static java.lang.Math.max; import static java.lang.Math.min; @@ -25,6 +27,7 @@ import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.MouseWheelEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; @@ -34,7 +37,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.ResourceBundle; +import java.util.function.Consumer; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; import org.json.JSONArray; @@ -83,7 +88,8 @@ public class LizzieFrame extends JFrame { private final BufferStrategy bs; - public int[] mouseOverCoordinate; + private static final int[] outOfBoundCoordinate = new int[] {-1, -1}; + public int[] mouseOverCoordinate = outOfBoundCoordinate; public boolean showControls = false; public boolean showCoordinates = false; public boolean isPlayingAgainstLeelaz = false; @@ -93,14 +99,14 @@ public class LizzieFrame extends JFrame { private long lastAutosaveTime = System.currentTimeMillis(); // Save the player title - private String playerTitle = null; + private String playerTitle = ""; // Display Comment - private JScrollPane scrollPane = null; - private JTextPane commentPane = null; - private BufferedImage commentImage = null; - private String cachedComment = null; - private Rectangle commentRect = null; + private JScrollPane scrollPane; + private JTextPane commentPane; + private BufferedImage cachedCommentImage = new BufferedImage(1, 1, TYPE_INT_ARGB); + private String cachedComment; + private Rectangle commentRect; static { // load fonts @@ -132,9 +138,9 @@ public LizzieFrame() { 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 + setSize(windowSize.getInt(0), windowSize.getInt(1)); + setLocationRelativeTo(null); // Start centered, needs to be called *after* setSize... // Allow change font in the config if (Lizzie.config.uiFontName != null) { @@ -145,10 +151,9 @@ public LizzieFrame() { } if (Lizzie.config.startMaximized) { - setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized + setExtendedState(Frame.MAXIMIZED_BOTH); } - // Comment Pane commentPane = new JTextPane(); commentPane.setEditable(false); commentPane.setMargin(new Insets(5, 5, 5, 5)); @@ -159,6 +164,7 @@ public LizzieFrame() { scrollPane.setBorder(null); scrollPane.setVerticalScrollBarPolicy( javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + commentRect = new Rectangle(0, 0, 0, 0); setVisible(true); @@ -167,16 +173,16 @@ public LizzieFrame() { Input input = new Input(); - this.addMouseListener(input); - this.addKeyListener(input); - this.addMouseWheelListener(input); - this.addMouseMotionListener(input); + addMouseListener(input); + addKeyListener(input); + addMouseWheelListener(input); + 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() + // When the window is closed: save the SGF file, then run shutdown() this.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { @@ -295,9 +301,9 @@ public static void loadFile(File file) { } } - private BufferedImage cachedImage = null; + private BufferedImage cachedImage; - private BufferedImage cachedBackground = null; + private BufferedImage cachedBackground; private int cachedBackgroundWidth = 0, cachedBackgroundHeight = 0; private boolean cachedBackgroundShowControls = false; private boolean cachedShowWinrate = true; @@ -311,16 +317,18 @@ public static void loadFile(File file) { */ public void paint(Graphics g0) { autosaveMaybe(); - if (bs == null) return; - Graphics2D backgroundG; + Optional backgroundG; if (cachedBackgroundWidth != getWidth() || cachedBackgroundHeight != getHeight() || cachedBackgroundShowControls != showControls || cachedShowWinrate != Lizzie.config.showWinrate || cachedShowVariationGraph != Lizzie.config.showVariationGraph - || redrawBackgroundAnyway) backgroundG = createBackground(); - else backgroundG = null; + || redrawBackgroundAnyway) { + backgroundG = Optional.of(createBackground()); + } else { + backgroundG = Optional.empty(); + } if (!showControls) { // layout parameters @@ -328,7 +336,7 @@ public void paint(Graphics g0) { int topInset = this.getInsets().top; // board - int maxSize = (int) (min(getWidth(), getHeight() - topInset) * 0.98); + int maxSize = (int) (min(getWidth(), getHeight() - topInset)); maxSize = max(maxSize, Board.boardSize + 5); // don't let maxWidth become too small int boardX = (getWidth() - maxSize) / 2; int boardY = topInset + (getHeight() - topInset - maxSize) / 2 + 3; @@ -436,7 +444,7 @@ public void paint(Graphics g0) { // initialize - cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + cachedImage = new BufferedImage(getWidth(), getHeight(), TYPE_INT_ARGB); Graphics2D g = (Graphics2D) cachedImage.getGraphics(); if (Lizzie.config.showStatus) drawCommandString(g); @@ -445,7 +453,7 @@ public void paint(Graphics g0) { boardRenderer.setBoardLength(maxSize); boardRenderer.draw(g); - if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + if (Lizzie.leelaz.isLoaded()) { if (Lizzie.config.showStatus) { String statusKey = "LizzieFrame.display." + (Lizzie.leelaz.isPondering() ? "on" : "off"); String statusText = resourceBundle.getString(statusKey); @@ -457,22 +465,26 @@ public void paint(Graphics g0) { drawPonderingState(g, text, ponderingX, ponderingY, ponderingSize); } - String dynamicKomi = Lizzie.leelaz.getDynamicKomi(); - if (Lizzie.config.showDynamicKomi && dynamicKomi != null) { + Optional dynamicKomi = Lizzie.leelaz.getDynamicKomi(); + if (Lizzie.config.showDynamicKomi && dynamicKomi.isPresent()) { String text = resourceBundle.getString("LizzieFrame.display.dynamic-komi"); drawPonderingState(g, text, dynamicKomiLabelX, dynamicKomiLabelY, dynamicKomiSize); - drawPonderingState(g, dynamicKomi, dynamicKomiX, dynamicKomiY, dynamicKomiSize); + drawPonderingState(g, dynamicKomi.get(), 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); + if (backgroundG.isPresent()) { + drawContainer(backgroundG.get(), 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); + if (backgroundG.isPresent()) { + drawContainer(backgroundG.get(), vx, vy, vw, vh); + } int cHeight = 0; if (Lizzie.config.showComment) { // Draw the Comment of the Sgf @@ -526,7 +538,7 @@ public void refreshBackground() { } private Graphics2D createBackground() { - cachedBackground = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); + cachedBackground = new BufferedImage(getWidth(), getHeight(), TYPE_INT_RGB); cachedBackgroundWidth = cachedBackground.getWidth(); cachedBackgroundHeight = cachedBackground.getHeight(); cachedBackgroundShowControls = showControls; @@ -546,12 +558,15 @@ private Graphics2D createBackground() { 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; + private void drawContainer(Graphics g, int vx, int vy, int vw, int vh) { + if (vx < cachedBackground.getMinX() + || vx + vw > cachedBackground.getMinX() + cachedBackground.getWidth() + || vy < cachedBackground.getMinY() + || vy + vh > cachedBackground.getMinY() + cachedBackground.getHeight()) { + return; + } - BufferedImage result = new BufferedImage(vw, vh, BufferedImage.TYPE_INT_ARGB); + BufferedImage result = new BufferedImage(vw, vh, TYPE_INT_ARGB); filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); g.drawImage(result, vx, vy, null); } @@ -563,20 +578,17 @@ private void drawPonderingState(Graphics2D g, String text, int x, int y, double 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; + int mainBoardX = boardRenderer.getLocation().x; if ((mainBoardX > x) && stringWidth > (mainBoardX - x)) { text = truncateStringByWidth(text, fm, mainBoardX - x); stringWidth = fm.stringWidth(text); } } int stringHeight = fm.getAscent() - fm.getDescent(); - int width = stringWidth; - int height = (int) (stringHeight * 1.2); + int width = max(stringWidth, 1); + int height = max((int) (stringHeight * 1.2), 1); - BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + BufferedImage result = new BufferedImage(width, height, 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(), @@ -602,7 +614,7 @@ private void drawPonderingState(Graphics2D g, String text, int x, int y, double * @return fitted */ private static String truncateStringByWidth(String line, FontMetrics fm, int fitWidth) { - if (line == null || line.length() == 0) { + if (line.isEmpty()) { return ""; } int width = fm.stringWidth(line); @@ -625,15 +637,6 @@ private static String truncateStringByWidth(String line, FontMetrics fm, int fit } } - 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); @@ -641,13 +644,13 @@ private void drawWinrateGraphContainer(Graphics g, int statx, int staty, int sta void drawControls() { userAlreadyKnowsAboutCommandString = true; - cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + cachedImage = new BufferedImage(getWidth(), getHeight(), TYPE_INT_ARGB); // redraw background createBackground(); List commandsToShow = new ArrayList<>(Arrays.asList(commands)); - if (Lizzie.leelaz.getDynamicKomi() != null) { + if (Lizzie.leelaz.getDynamicKomi().isPresent()) { commandsToShow.add(resourceBundle.getString("LizzieFrame.commands.keyD")); } @@ -667,7 +670,7 @@ void drawControls() { int commandsX = min(getWidth() / 2 - boxWidth / 2, getWidth()); int commandsY = min(getHeight() / 2 - boxHeight / 2, getHeight()); - BufferedImage result = new BufferedImage(boxWidth, boxHeight, BufferedImage.TYPE_INT_ARGB); + BufferedImage result = new BufferedImage(boxWidth, boxHeight, TYPE_INT_ARGB); filter10.filter( cachedBackground.getSubimage(commandsX, commandsY, boxWidth, boxHeight), result); g.drawImage(result, commandsX, commandsY, null); @@ -747,17 +750,19 @@ private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int 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; + Optional previous = Lizzie.board.getHistory().getPrevious(); + if (previous.isPresent() && previous.get().playouts > 0) { + lastWR = previous.get().winrate; validLastWinrate = true; } 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) + 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) @@ -946,14 +951,15 @@ private void setPanelFont(Graphics2D g, float size) { * @param y y coordinate */ public void onClicked(int x, int y) { - // check for board click - int[] boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); + // Check for board click + Optional boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); int moveNumber = winrateGraph.moveNumber(x, y); - if (boardCoordinates != null) { + if (boardCoordinates.isPresent()) { + int[] coords = boardCoordinates.get(); if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); if (!isPlayingAgainstLeelaz || (playerIsBlack == Lizzie.board.getData().blackToPlay)) - Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); + Lizzie.board.place(coords[0], coords[1]); } if (Lizzie.config.showWinrate && moveNumber >= 0) { isPlayingAgainstLeelaz = false; @@ -965,39 +971,27 @@ public void onClicked(int x, int y) { repaint(); } + private final Consumer placeVariation = + v -> Board.asCoordinates(v).ifPresent(c -> Lizzie.board.place(c[0], c[1])); + 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; + boardRenderer.variationOpt.ifPresent(vs -> vs.forEach(placeVariation)); + return boardRenderer.variationOpt.isPresent(); } 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]); - } + boardRenderer.bestMoveCoordinateName().ifPresent(placeVariation); } public void onMouseMoved(int x, int y) { - int[] c = boardRenderer.convertScreenToCoordinates(x, y); - if (c != null && !isMouseOver(c[0], c[1])) { - repaint(); - } - mouseOverCoordinate = c; + mouseOverCoordinate = outOfBoundCoordinate; + Optional coords = boardRenderer.convertScreenToCoordinates(x, y); + coords.filter(c -> !isMouseOver(c[0], c[1])).ifPresent(c -> repaint()); + coords.ifPresent(c -> mouseOverCoordinate = c); } public boolean isMouseOver(int x, int y) { - return mouseOverCoordinate != null - && mouseOverCoordinate[0] == x - && mouseOverCoordinate[1] == y; + return mouseOverCoordinate[0] == x && mouseOverCoordinate[1] == y; } public void onMouseDragged(int x, int y) { @@ -1015,14 +1009,12 @@ public void onMouseDragged(int x, int y) { * @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())) { + if (Lizzie.config.showComment && commentRect.contains(e.getX(), e.getY())) { scrollPane.dispatchEvent(e); createCommentImage(true, 0, 0); getGraphics() .drawImage( - commentImage, + cachedCommentImage, commentRect.x, commentRect.y, commentRect.width, @@ -1042,17 +1034,13 @@ public boolean processCommentMouseWheelMoved(MouseWheelEvent e) { * @param h */ public void createCommentImage(boolean forceRefresh, int w, int h) { - if (forceRefresh - || commentImage == null - || scrollPane.getWidth() != w - || scrollPane.getHeight() != h) { + if (forceRefresh || 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(); + cachedCommentImage = + new BufferedImage(scrollPane.getWidth(), scrollPane.getHeight(), TYPE_INT_ARGB); + Graphics2D g2 = cachedCommentImage.createGraphics(); scrollPane.doLayout(); scrollPane.addNotify(); scrollPane.validate(); @@ -1076,15 +1064,14 @@ public void toggleCoordinates() { } public void setPlayers(String whitePlayer, String blackPlayer) { - this.playerTitle = String.format("(%s [W] vs %s [B])", whitePlayer, blackPlayer); - this.updateTitle(); + playerTitle = String.format("(%s [W] vs %s [B])", whitePlayer, blackPlayer); + 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() + "]" : ""); + sb.append(playerTitle); + sb.append(" [" + Lizzie.leelaz.engineCommand() + "]"); setTitle(sb.toString()); } @@ -1107,8 +1094,8 @@ public boolean incrementDisplayedBranchLength(int n) { } public void resetTitle() { - this.playerTitle = null; - this.updateTitle(); + playerTitle = ""; + updateTitle(); } public void copySgf() { @@ -1126,23 +1113,26 @@ public void copySgf() { } 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(); + // Get string from clipboard + String sgfContent = + Optional.ofNullable(Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null)) + .filter(cc -> cc.isDataFlavorSupported(DataFlavor.stringFlavor)) + .flatMap( + cc -> { + try { + return Optional.of((String) cc.getTransferData(DataFlavor.stringFlavor)); + } catch (UnsupportedFlavorException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return Optional.empty(); + }) + .orElse(""); + + // Load game contents from sgf string + if (!sgfContent.isEmpty()) { + SGFParser.loadFromString(sgfContent); } } @@ -1162,11 +1152,7 @@ public void increaseMaxAlpha(int k) { * @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 - : ""; + String comment = Lizzie.board.getHistory().getData().comment; int cHeight = full ? h : (int) (h * 0.5); int fontSize = (int) (min(getWidth(), getHeight()) * 0.0294); if (Lizzie.config.commentFontSize > 0) { @@ -1178,11 +1164,16 @@ private int drawComment(Graphics2D g, int x, int y, int w, int h, boolean full) commentPane.setFont(font); commentPane.setText(comment); commentPane.setSize(w, cHeight); - createCommentImage(comment != null && !comment.equals(this.cachedComment), w, cHeight); + createCommentImage(!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); + cachedCommentImage, + 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 aef810e2b..68e24d02a 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -1,10 +1,10 @@ package featurecat.lizzie.gui; import featurecat.lizzie.Lizzie; -import featurecat.lizzie.rules.BoardHistoryList; import featurecat.lizzie.rules.BoardHistoryNode; import java.awt.*; import java.util.ArrayList; +import java.util.Optional; public class VariationTree { @@ -32,7 +32,7 @@ public void drawTree( else g.setColor(Color.gray.brighter()); // Finds depth on leftmost variation of this tree - int depth = BoardHistoryList.getDepth(startNode) + 1; + int depth = startNode.getDepth() + 1; int lane = startLane; // Figures out how far out too the right (which lane) we have to go not to collide with other // variations @@ -83,7 +83,7 @@ public void drawTree( if (startNode == curMove) { g.setColor(Color.green.brighter().brighter()); } - if (startNode.previous() != null) { + if (startNode.previous().isPresent()) { g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); g.setColor(Color.BLACK); g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); @@ -95,9 +95,9 @@ public void drawTree( g.setColor(curcolor); // Draw main line - while (cur.next() != null && posy + YSPACING < maxposy) { + while (cur.next().isPresent() && posy + YSPACING < maxposy) { posy += YSPACING; - cur = cur.next(); + cur = cur.next().get(); if (cur == curMove) { g.setColor(Color.green.brighter().brighter()); } @@ -111,15 +111,18 @@ public void drawTree( // 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(); + while (cur.previous().isPresent() && cur != startNode) { + cur = cur.previous().get(); 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); + Optional variation = cur.getVariation(i); + if (variation.isPresent()) { + drawTree(g, posx, posy, curwidth, maxposy, variation.get(), i, false); + } } posy -= YSPACING; } @@ -155,12 +158,12 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); // Is current move a variation? If so, find top of variation - BoardHistoryNode top = BoardHistoryList.findTop(curMove); + BoardHistoryNode top = curMove.findTop(); 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(); + while (curposy > posy + YSPACING && node.previous().isPresent()) { + node = node.previous().get(); 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 0ab6351af..051f80bf2 100644 --- a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java +++ b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java @@ -2,10 +2,10 @@ import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Leelaz; -import featurecat.lizzie.rules.BoardHistoryList; import featurecat.lizzie.rules.BoardHistoryNode; import java.awt.*; import java.awt.geom.Point2D; +import java.util.Optional; public class WinrateGraph { @@ -76,20 +76,22 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { g.setColor(Lizzie.config.winrateLineColor); g.setStroke(new BasicStroke(Lizzie.config.winrateStrokeWidth)); - BoardHistoryNode topOfVariation = null; + Optional topOfVariation = Optional.empty(); int numMoves = 0; - if (!BoardHistoryList.isMainTrunk(curMove)) { + if (!curMove.isMainTrunk()) { // We're in a variation, need to draw both main trunk and variation // Find top of variation - topOfVariation = BoardHistoryList.findTop(curMove); + BoardHistoryNode top = curMove.findTop(); + topOfVariation = Optional.of(top); // Find depth of main trunk, need this for plot scaling - numMoves = - BoardHistoryList.getDepth(topOfVariation) + topOfVariation.getData().moveNumber - 1; + numMoves = top.getDepth() + top.getData().moveNumber - 1; g.setStroke(dashed); } // Go to end of variation and work our way backwards to the root - while (node.next() != null) node = node.next(); + while (node.next().isPresent()) { + node = node.next().get(); + } if (numMoves < node.getData().moveNumber - 1) { numMoves = node.getData().moveNumber - 1; } @@ -104,7 +106,8 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { int movenum = node.getData().moveNumber - 1; int lastOkMove = -1; - while (node.previous() != null) { + while (node.previous().isPresent()) { + BoardHistoryNode previous = node.previous().get(); double wr = node.getData().winrate; int playouts = node.getData().playouts; if (node == curMove) { @@ -161,14 +164,16 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { lastWr = wr; lastNodeOk = true; // Check if we were in a variation and has reached the main trunk - if (node == topOfVariation) { + if (topOfVariation.isPresent() && topOfVariation.get() == node) { // Reached top of variation, go to end of main trunk before continuing - while (node.next() != null) node = node.next(); + while (node.next().isPresent()) { + node = node.next().get(); + } movenum = node.getData().moveNumber - 1; lastWr = node.getData().winrate; if (!node.getData().blackToPlay) lastWr = 100 - lastWr; g.setStroke(new BasicStroke(3)); - topOfVariation = null; + topOfVariation = Optional.empty(); if (node.getData().playouts == 0) { lastNodeOk = false; } @@ -179,7 +184,7 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { lastNodeOk = false; } - node = node.previous(); + node = previous; movenum--; } diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 9259be39b..b3ff4214a 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -1,15 +1,24 @@ package featurecat.lizzie.rules; +import static java.lang.Math.min; +import static java.util.Collections.singletonList; + import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.analysis.LeelazListener; import featurecat.lizzie.analysis.MoveData; import java.io.IOException; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Queue; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.*; import org.json.JSONException; @@ -19,17 +28,15 @@ public class Board implements LeelazListener { private BoardHistoryList history; private Stone[] capturedStones; - private boolean scoreMode; - - private boolean analysisMode = false; - private int playoutsAnalysis = 100; + private boolean analysisMode; + private int playoutsAnalysis; // Save the node for restore move when in the branch - private BoardHistoryNode saveNode = null; + private Optional saveNode; // Force refresh board - private boolean forceRefresh = false; + private boolean forceRefresh; public Board() { initialize(); @@ -37,18 +44,13 @@ public Board() { /** Initialize the board completely */ private void initialize() { - Stone[] stones = new Stone[boardSize * boardSize]; - for (int i = 0; i < stones.length; i++) { - stones[i] = Stone.EMPTY; - } - - capturedStones = null; + capturedStones = new Stone[] {}; scoreMode = false; - - int[] boardArray = new int[boardSize * boardSize]; - BoardData boardData = - new BoardData(stones, null, Stone.EMPTY, true, new Zobrist(), 0, boardArray, 0, 0, 50, 0); - history = new BoardHistoryList(boardData); + analysisMode = false; + playoutsAnalysis = 100; + saveNode = Optional.empty(); + forceRefresh = false; + history = new BoardHistoryList(BoardData.empty(boardSize)); } /** @@ -67,17 +69,17 @@ public static int getIndex(int x, int y) { * * @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 + * @return an optional array of coordinates, empty for pass and resign */ - public static int[] convertNameToCoordinates(String namedCoordinate) { + public static Optional asCoordinates(String namedCoordinate) { namedCoordinate = namedCoordinate.trim(); - if (namedCoordinate.equalsIgnoreCase("pass")) { - return null; + if (namedCoordinate.equalsIgnoreCase("pass") || namedCoordinate.equalsIgnoreCase("resign")) { + return Optional.empty(); } // coordinates take the form C16 A19 Q5 K10 etc. I is not used. int x = alphabet.indexOf(namedCoordinate.charAt(0)); int y = boardSize - Integer.parseInt(namedCoordinate.substring(1)); - return new int[] {x, y}; + return Optional.of(new int[] {x, y}); } /** @@ -132,9 +134,7 @@ public void setForceRefresh(boolean forceRefresh) { */ public void comment(String comment) { synchronized (this) { - if (history.getData() != null) { - history.getData().comment = comment; - } + history.getData().comment = comment; } } @@ -147,17 +147,18 @@ public void moveNumber(int moveNumber) { synchronized (this) { BoardData data = history.getData(); data.moveNumber = moveNumber; - if (data.lastMove != null) { + if (data.lastMove.isPresent()) { int[] moveNumberList = history.getMoveNumberList(); - moveNumberList[Board.getIndex(data.lastMove[0], data.lastMove[1])] = moveNumber; - BoardHistoryNode node = history.getCurrentHistoryNode().previous(); - while (node != null && node.numberOfChildren() <= 1) { - BoardData nodeData = node.getData(); - if (nodeData != null && nodeData.lastMove != null && nodeData.moveNumber >= moveNumber) { + moveNumberList[Board.getIndex(data.lastMove.get()[0], data.lastMove.get()[1])] = moveNumber; + Optional node = history.getCurrentHistoryNode().previous(); + while (node.isPresent() && node.get().numberOfChildren() <= 1) { + BoardData nodeData = node.get().getData(); + if (nodeData.lastMove.isPresent() && nodeData.moveNumber >= moveNumber) { moveNumber = (moveNumber > 1) ? moveNumber - 1 : 0; - moveNumberList[Board.getIndex(nodeData.lastMove[0], nodeData.lastMove[1])] = moveNumber; + moveNumberList[Board.getIndex(nodeData.lastMove.get()[0], nodeData.lastMove.get()[1])] = + moveNumber; } - node = node.previous(); + node = node.get().previous(); } } } @@ -243,8 +244,7 @@ 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) { + if (history.getNext().map(n -> !n.lastMove.isPresent()).orElse(false)) { // this is the next move in history. Just increment history so that we don't erase the // redo's history.next(); @@ -264,7 +264,7 @@ public void pass(Stone color) { BoardData newState = new BoardData( stones, - null, + Optional.empty(), color, color.equals(Stone.WHITE), zobrist, @@ -334,8 +334,8 @@ public void place(int x, int y, Stone color, boolean newBranch) { 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) { + Optional nextLast = history.getNext().flatMap(n -> n.lastMove); + if (nextLast.isPresent() && nextLast.get()[0] == x && nextLast.get()[1] == y) { // this is the next coordinate in history. Just increment history so that we don't erase the // redo's history.next(); @@ -353,7 +353,7 @@ public void place(int x, int y, Stone color, boolean newBranch) { // 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 + Optional lastMove = Optional.of(new int[] {x, y}); int moveNumber = history.getMoveNumber() + 1; int[] moveNumberList = history.getMoveNumberList().clone(); @@ -433,17 +433,12 @@ public void place(int x, int y) { * @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")) { + Optional coords = asCoordinates(namedCoordinate); + if (coords.isPresent()) { + place(coords.get()[0], coords.get()[1]); + } else { pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); - return; } - - int[] coordinates = convertNameToCoordinates(namedCoordinate); - - place(coordinates[0], coordinates[1]); } /** for handicap */ @@ -456,7 +451,7 @@ public void flatten() { new BoardHistoryList( new BoardData( stones, - null, + Optional.empty(), Stone.EMPTY, blackToPlay, zobrist, @@ -550,7 +545,7 @@ private int cleanupHasLibertiesHelper( } /** - * get current board state + * Get current board state * * @return the stones array corresponding to the current board state */ @@ -559,25 +554,25 @@ public Stone[] getStones() { } /** - * shows where to mark the last coordinate + * Shows where to mark the last coordinate * - * @return the last played stone + * @return the last played stone, if any, Optional.empty otherwise */ - public int[] getLastMove() { + public Optional getLastMove() { return history.getLastMove(); } /** - * get the move played in this position + * Gets the move played in this position * - * @return the next move, if any + * @return the next move, if any, Optional.empty otherwise */ - public int[] getNextMove() { + public Optional getNextMove() { return history.getNextMove(); } /** - * get current board move number + * Gets current board move number * * @return the int array corresponding to the current board move number */ @@ -594,14 +589,15 @@ public boolean nextMove() { history.getData().winrate = stats.maxWinrate; history.getData().playouts = stats.totalPlayouts; } - if (history.next() != null) { + if (history.next().isPresent()) { // update leelaz board position, before updating to next node - if (history.getData().lastMove == null) { - Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); + Optional lastMoveOpt = history.getData().lastMove; + if (lastMoveOpt.isPresent()) { + int[] lastMove = lastMoveOpt.get(); + String name = convertCoordinatesToName(lastMove[0], lastMove[1]); + Lizzie.leelaz.playMove(history.getLastMoveColor(), name); } else { - Lizzie.leelaz.playMove( - history.getLastMoveColor(), - convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); + Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); } Lizzie.frame.repaint(); return true; @@ -630,36 +626,32 @@ public boolean nextMove(int 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; + BoardHistoryNode currentNode = history.getCurrentHistoryNode(); + int curMoveNum = currentNode.getData().moveNumber; if (curMoveNum > 0) { - if (!BoardHistoryList.isMainTrunk(curNode)) { + if (!currentNode.isMainTrunk()) { // If in branch, save the back routing from children - saveBackRouting(curNode); + saveBackRouting(currentNode); } goToMoveNumber(0); } - saveNode = curNode; + saveNode = Optional.of(currentNode); } /** 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()); - } + Optional prev = node.previous(); + prev.ifPresent(n -> n.setFromBackChildren(n.getVariations().indexOf(node))); + prev.ifPresent(n -> n.previous().ifPresent(p -> saveBackRouting(p))); } /** Restore move number by saved node */ public void restoreMoveNumber() { - restoreMoveNumber(saveNode); + saveNode.ifPresent(n -> restoreMoveNumber(n)); } /** Restore move number by node */ public void restoreMoveNumber(BoardHistoryNode node) { - if (node == null) { - return; - } Stone[] stones = history.getStones(); for (int i = 0; i < stones.length; i++) { Stone stone = stones[i]; @@ -671,7 +663,7 @@ public void restoreMoveNumber(BoardHistoryNode node) { } int moveNumber = node.getData().moveNumber; if (moveNumber > 0) { - if (BoardHistoryList.isMainTrunk(node)) { + if (node.isMainTrunk()) { goToMoveNumber(moveNumber); } else { // If in Branch, restore by the back routing @@ -684,9 +676,9 @@ public void restoreMoveNumber(BoardHistoryNode node) { 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()); + BoardHistoryNode currentNode = history.getCurrentHistoryNode(); + if (currentNode.hasVariations() && delta > 0) { + nextMove(currentNode.getFromBackChildren()); } else { if (!(delta > 0 ? nextMove() : previousMove())) { break; @@ -716,8 +708,8 @@ public boolean goToMoveNumberHelper(int moveNumber, boolean withinBranch) { boolean moved = false; for (int i = 0; i < Math.abs(delta); i++) { if (withinBranch && delta < 0) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - if (!curNode.isFirstChild()) { + BoardHistoryNode currentNode = history.getCurrentHistoryNode(); + if (!currentNode.isFirstChild()) { break; } } @@ -733,14 +725,15 @@ public boolean goToMoveNumberHelper(int moveNumber, boolean withinBranch) { 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"); + if (history.nextVariation(idx).isPresent()) { + // Update leelaz board position, before updating to next node + Optional lastMoveOpt = history.getData().lastMove; + if (lastMoveOpt.isPresent()) { + int[] lastMove = lastMoveOpt.get(); + String name = convertCoordinatesToName(lastMove[0], lastMove[1]); + Lizzie.leelaz.playMove(history.getLastMoveColor(), name); } else { - Lizzie.leelaz.playMove( - history.getLastMoveColor(), - convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); + Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); } Lizzie.frame.repaint(); return true; @@ -749,191 +742,137 @@ public boolean nextVariation(int idx) { } } - /* - * 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 + /** + * Returns all the nodes at the given depth in the history tree, always including a node from the + * main variation (possibly less deep that the given depth). + * + * @return the list of candidate nodes */ - 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(); - } + private List branchCandidates(BoardHistoryNode node) { + int targetDepth = node.getData().moveNumber; + Stream nodes = singletonList(history.root()).stream(); + for (int i = 0; i < targetDepth; i++) { + nodes = nodes.flatMap(n -> n.getVariations().stream()); + } + LinkedList result = nodes.collect(Collectors.toCollection(LinkedList::new)); + + if (result.isEmpty() || !result.get(0).isMainTrunk()) { + BoardHistoryNode endOfMainTrunk = history.root(); + while (endOfMainTrunk.next().isPresent()) { + endOfMainTrunk = endOfMainTrunk.next().get(); } + result.addFirst(endOfMainTrunk); + return result; + } else { + return result; + } + } - // 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; + /** + * Moves to next variation (variation to the right) if possible. The variation must have a move + * with the same move number as the current move in it. + * + * @return true if there exist a target variation + */ + public boolean nextBranch() { + synchronized (this) { + BoardHistoryNode currentNode = history.getCurrentHistoryNode(); + Optional targetNode = Optional.empty(); + boolean foundIt = false; + for (BoardHistoryNode candidate : branchCandidates(currentNode)) { + if (foundIt) { + targetNode = Optional.of(candidate); + break; + } else if (candidate == currentNode) { + foundIt = 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; - } - } - } - startIdx = 0; - curNode = history.getCurrentHistoryNode(); + if (targetNode.isPresent()) { + moveToAnyPosition(targetNode.get()); } - return true; + return targetNode.isPresent(); } } - /* - * 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) + /** + * 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. + * + *

Note: This method will always move back to main trunk, even if variation has more moves than + * main trunk (if this case it will move to the last move in the trunk). + * + * @return true if there exist a target variation */ 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 + BoardHistoryNode currentNode = history.getCurrentHistoryNode(); + Optional targetNode = Optional.empty(); + for (BoardHistoryNode candidate : branchCandidates(currentNode)) { + if (candidate == currentNode) { + break; + } else { + targetNode = Optional.of(candidate); } } - - 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 (targetNode.isPresent()) { + moveToAnyPosition(targetNode.get()); } + return targetNode.isPresent(); + } + } - // 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; + /** + * Jump anywhere in the board history tree. + * + * @param targetNode history node to be located + * @return void + */ + private void moveToAnyPosition(BoardHistoryNode targetNode) { + List targetParents = new ArrayList(); + List sourceParents = new ArrayList(); + + BiConsumer> populateParent = + (node, parentList) -> { + Optional prevNode = node.previous(); + while (prevNode.isPresent()) { + BoardHistoryNode p = prevNode.get(); + for (int m = 0; m < p.numberOfChildren(); m++) { + if (p.getVariation(m).get() == node) { + parentList.add(m); + } } + node = p; + prevNode = p.previous(); } - 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; + }; + + // Compute the path from the current node to the root + populateParent.accept(history.getCurrentHistoryNode(), sourceParents); + + // Compute the path from the target node to the root + populateParent.accept(targetNode, targetParents); + + // Compute the distance from source to the deepest common answer + int targetDepth = targetParents.size(); + int sourceDepth = sourceParents.size(); + int maxDepth = min(targetParents.size(), sourceParents.size()); + int depth; + for (depth = 0; depth < maxDepth; depth++) { + int sourceParent = sourceParents.get(sourceDepth - depth - 1); + int targetParent = targetParents.get(targetDepth - depth - 1); + if (sourceParent != targetParent) { + break; } - return true; + } + + // Move all the way up to the deepest common ansestor + for (int m = 0; m < sourceDepth - depth; m++) { + previousMove(); + } + + // Then all the way down to the target + for (int m = targetDepth - depth; m > 0; m--) { + nextVariation(targetParents.get(m - 1)); } } @@ -951,8 +890,8 @@ public void moveBranchDown() { public void deleteMove() { synchronized (this) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - if (curNode.next() != null) { + BoardHistoryNode currentNode = history.getCurrentHistoryNode(); + if (currentNode.next().isPresent()) { // Will delete more than one move, ask for confirmation int ret = JOptionPane.showConfirmDialog( @@ -964,14 +903,14 @@ public void deleteMove() { return; } } - // Clear the board if we're at the top - if (curNode.previous() == null) { - clear(); - return; + if (currentNode.previous().isPresent()) { + BoardHistoryNode pre = currentNode.previous().get(); + previousMove(); + int idx = pre.findIndexOfNode(currentNode); + pre.deleteChild(idx); + } else { + clear(); // Clear the board if we're at the top } - previousMove(); - int idx = BoardHistoryList.findIndexOfNode(curNode.previous(), curNode); - curNode.previous().deleteChild(idx); } } @@ -1012,7 +951,7 @@ public boolean previousMove() { history.getData().winrate = stats.maxWinrate; history.getData().playouts = stats.totalPlayouts; } - if (history.previous() != null) { + if (history.previous().isPresent()) { Lizzie.leelaz.undo(); Lizzie.frame.repaint(); return true; @@ -1023,9 +962,9 @@ public boolean previousMove() { public boolean undoToChildOfPreviousWithVariation() { BoardHistoryNode start = history.getCurrentHistoryNode(); - BoardHistoryNode goal = history.findChildOfPreviousWithVariation(start); - if (start == goal) return false; - while ((history.getCurrentHistoryNode() != goal) && previousMove()) ; + Optional goal = start.findChildOfPreviousWithVariation(); + if (!goal.isPresent() || start == goal.get()) return false; + while (history.getCurrentHistoryNode() != goal.get() && previousMove()) ; return true; } @@ -1034,7 +973,7 @@ public void setScoreMode(boolean on) { // load a copy of the data at the current node of history capturedStones = history.getStones().clone(); } else { - capturedStones = null; + capturedStones = new Stone[] {}; } scoreMode = on; } @@ -1131,8 +1070,8 @@ private boolean emptyOrCaptured(Stone[] stones, int x, int y) { */ 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 + // Found will either be black or white, or dame if both are found in area + Stone found = Stone.EMPTY; boolean lastup, lastdown; Queue visitQ = new ArrayDeque<>(); visitQ.add(new int[] {startx, starty}); @@ -1275,11 +1214,10 @@ public void toggleAnalysis() { Lizzie.leelaz.removeListener(this); analysisMode = false; } else { - if (getNextMove() == null) return; + if (!getNextMove().isPresent()) 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) { @@ -1295,15 +1233,15 @@ public void toggleAnalysis() { public void bestMoveNotification(List bestMoves) { if (analysisMode) { boolean isSuccessivePass = - (history.getPrevious() != null - && history.getPrevious().lastMove == null - && getLastMove() == null); + (history.getPrevious().isPresent() + && !history.getPrevious().get().lastMove.isPresent() + && !getLastMove().isPresent()); // 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) { + if (!history.getNext().isPresent() || isSuccessivePass) { // Reached the end... toggleAnalysis(); - } else if (bestMoves == null || bestMoves.size() == 0) { + } else if (bestMoves.isEmpty()) { // If we get empty list, something strange happened, ignore notification } else { // sum the playouts to proceed like leelaz's --visits option. diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index 1a1d2754b..d874f42d6 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -2,10 +2,11 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; public class BoardData { public int moveNumber; - public int[] lastMove; + public Optional lastMove; public int[] moveNumberList; public boolean blackToPlay; @@ -19,15 +20,14 @@ public class BoardData { public int blackCaptures; public int whiteCaptures; - // Comment in the Sgf move - public String comment; + public String comment = ""; // Node properties private final Map properties = new HashMap(); public BoardData( Stone[] stones, - int[] lastMove, + Optional lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, @@ -53,6 +53,17 @@ public BoardData( this.whiteCaptures = whiteCaptures; } + public static BoardData empty(int size) { + Stone[] stones = new Stone[size * size]; + for (int i = 0; i < stones.length; i++) { + stones[i] = Stone.EMPTY; + } + + int[] boardArray = new int[size * size]; + return new BoardData( + stones, Optional.empty(), Stone.EMPTY, true, new Zobrist(), 0, boardArray, 0, 0, 50, 0); + } + /** * Add a key and value * diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index 7de27a1a2..22d14d57d 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -2,6 +2,7 @@ import featurecat.lizzie.analysis.GameInfo; import java.util.List; +import java.util.Optional; /** Linked list data structure to store board history */ public class BoardHistoryList { @@ -58,66 +59,62 @@ public void addOrGoto(BoardData data, boolean newBranch) { /** * moves the pointer to the left, returns the data stored there * - * @return data of previous node, null if there is no previous node + * @return data of previous node, Optional.empty if there is no previous node */ - public BoardData previous() { - if (head.previous() == null) return null; - else head = head.previous(); + public Optional previous() { + if (!head.previous().isPresent()) return Optional.empty(); + else head = head.previous().get(); - return head.getData(); + return Optional.of(head.getData()); } public void toStart() { - while (previous() != null) ; + while (previous().isPresent()) ; } /** * moves the pointer to the right, returns the data stored there * - * @return the data of next node, null if there is no next node + * @return the data of next node, Optional.empty if there is no next node */ - public BoardData next() { - if (head.next() == null) return null; - else head = head.next(); - - return head.getData(); + public Optional next() { + Optional n = head.next(); + n.ifPresent(x -> head = x); + return n.map(x -> x.getData()); } /** - * moves the pointer to the variation number idx, returns the data stored there + * 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 + * @return the data of next node, Optional.empty if there is no variation with index. */ - public BoardData nextVariation(int idx) { - if (head.getVariation(idx) == null) return null; - else head = head.getVariation(idx); - - return head.getData(); + public Optional nextVariation(int idx) { + Optional n = head.getVariation(idx); + n.ifPresent(x -> head = x); + return n.map(x -> x.getData()); } /** * Does not change the pointer position * - * @return the data stored at the next index. null if not present + * @return the data stored at the next index, if any, Optional.empty otherwise. */ - public BoardData getNext() { - if (head.next() == null) return null; - else return head.next().getData(); + public Optional getNext() { + return head.next().map(x -> x.getData()); } /** @return nexts for display */ public List getNexts() { - return head.getNexts(); + return head.getVariations(); } /** - * Does not change the pointer position + * Does not change the pointer position. * - * @return the data stored at the previous index. null if not present + * @return the data stored at the previous index, if any, Optional.empty otherwise. */ - public BoardData getPrevious() { - if (head.previous() == null) return null; - else return head.previous().getData(); + public Optional getPrevious() { + return head.previous().map(p -> p.getData()); } /** @return the data of the current node */ @@ -135,14 +132,12 @@ public Stone[] getStones() { return head.getData().stones; } - public int[] getLastMove() { + public Optional getLastMove() { return head.getData().lastMove; } - public int[] getNextMove() { - BoardData next = getNext(); - if (next == null) return null; - else return next.lastMove; + public Optional getNextMove() { + return getNext().flatMap(n -> n.lastMove); } public Stone getLastMoveColor() { @@ -177,13 +172,13 @@ public boolean violatesSuperko(BoardData data) { BoardHistoryNode head = this.head; // check to see if this position has occurred before - while (head.previous() != null) { + while (head.previous().isPresent()) { // 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(); + head = head.previous().get(); } // no position matched this position, so it's valid @@ -197,8 +192,8 @@ public boolean violatesSuperko(BoardData data) { */ public BoardHistoryNode root() { BoardHistoryNode top = head; - while (top.previous() != null) { - top = top.previous(); + while (top.previous().isPresent()) { + top = top.previous().get(); } return top; } @@ -209,7 +204,7 @@ public BoardHistoryNode root() { * @return length of current branch */ public int currentBranchLength() { - return getMoveNumber() + BoardHistoryList.getDepth(head); + return getMoveNumber() + head.getDepth(); } /** @@ -218,106 +213,6 @@ public int currentBranchLength() { * @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; - } - return false; - } 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(); - } - return true; + return root().getDepth(); } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java index d038123d2..be2df08dd 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java @@ -2,11 +2,12 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** Node structure for the board history / sgf tree */ public class BoardHistoryNode { - private BoardHistoryNode previous; - private ArrayList nexts; + private Optional previous; + private ArrayList variations; private BoardData data; @@ -15,14 +16,14 @@ public class BoardHistoryNode { /** Initializes a new list node */ public BoardHistoryNode(BoardData data) { - previous = null; - nexts = new ArrayList(); + previous = Optional.empty(); + variations = new ArrayList(); this.data = data; } /** Remove all subsequent nodes. */ public void clear() { - nexts.clear(); + variations.clear(); } /** @@ -32,9 +33,9 @@ public void clear() { * @return the node that was just set */ public BoardHistoryNode add(BoardHistoryNode node) { - nexts.clear(); - nexts.add(node); - node.previous = this; + variations.clear(); + variations.add(node); + node.previous = Optional.of(this); return node; } @@ -61,36 +62,36 @@ public BoardHistoryNode addOrGoto(BoardData data) { public BoardHistoryNode addOrGoto(BoardData data, boolean newBranch) { // 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)) { + // if (!variations.isEmpty() && !variations.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) { + // if (!variations.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); + // variations.remove(0); // } else { - // nexts.get(0).data.verify = true; + // variations.get(0).data.verify = true; // } // } // } if (!newBranch) { - for (int i = 0; i < nexts.size(); i++) { - if (nexts.get(i).data.zobrist.equals(data.zobrist)) { + for (int i = 0; i < variations.size(); i++) { + if (variations.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); + // BoardHistoryNode currentNext = variations.get(i); + // variations.set(i, variations.get(0)); + // variations.set(0, currentNext); // } - return nexts.get(i); + return variations.get(i); } } } BoardHistoryNode node = new BoardHistoryNode(data); // Add node - nexts.add(node); - node.previous = this; + variations.add(node); + node.previous = Optional.of(this); return node; } @@ -100,80 +101,72 @@ public BoardData getData() { return data; } - /** @return nexts for display */ - public List getNexts() { - return nexts; + /** @return variations for display */ + public List getVariations() { + return variations; } - public BoardHistoryNode previous() { + public Optional previous() { return previous; } - public BoardHistoryNode next() { - if (nexts.size() == 0) { - return null; - } else { - return nexts.get(0); - } + public Optional next() { + return variations.isEmpty() ? Optional.empty() : Optional.of(variations.get(0)); } public BoardHistoryNode topOfBranch() { BoardHistoryNode top = this; - while (top.previous != null && top.previous.nexts.size() == 1) { - top = top.previous; + while (top.previous.isPresent() && top.previous.get().variations.size() == 1) { + top = top.previous.get(); } return top; } public int numberOfChildren() { - if (nexts == null) { - return 0; - } else { - return nexts.size(); - } + return variations.size(); + } + + public boolean hasVariations() { + return numberOfChildren() > 1; } public boolean isFirstChild() { - return (previous != null) && previous.next() == this; + return previous.flatMap(p -> p.next().map(n -> n == this)).orElse(false); } - public BoardHistoryNode getVariation(int idx) { - if (nexts.size() <= idx) { - return null; + public Optional getVariation(int idx) { + if (variations.size() <= idx) { + return Optional.empty(); } else { - return nexts.get(idx); + return Optional.of(variations.get(idx)); } } public void moveUp() { - if (previous != null) { - previous.moveChildUp(this); - } + previous.ifPresent(p -> p.moveChildUp(this)); } public void moveDown() { - if (previous != null) { - previous.moveChildDown(this); - } + previous.ifPresent(p -> p.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); + for (int i = 1; i < variations.size(); i++) { + if (variations.get(i).data.zobrist.equals(child.data.zobrist)) { + BoardHistoryNode tmp = variations.get(i - 1); + variations.set(i - 1, child); + variations.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); + for (int i = 0; i < variations.size() - 1; i++) { + if (variations.get(i).data.zobrist.equals(child.data.zobrist)) { + BoardHistoryNode tmp = variations.get(i + 1); + variations.set(i + 1, child); + variations.set(i, tmp); return; } } @@ -181,7 +174,7 @@ public void moveChildDown(BoardHistoryNode child) { public void deleteChild(int idx) { if (idx < numberOfChildren()) { - nexts.remove(idx); + variations.remove(idx); } } @@ -194,4 +187,139 @@ public void setFromBackChildren(int fromBackChildren) { public int getFromBackChildren() { return fromBackChildren; } + + /** + * Returns the number of moves in a tree (only the left-most (trunk) variation) + * + * @return number of moves in a tree + */ + public int getDepth() { + BoardHistoryNode node = this; + int c = 0; + while (node.next().isPresent()) { + c++; + node = node.next().get(); + } + return c; + } + + /** + * Given some depth, returns the child at the given depth in the main trunk. If the main variation + * is too short, silently stops early. + * + * @return the child at the given depth + */ + public BoardHistoryNode childAtDepth(int depth) { + BoardHistoryNode node = this; + for (int i = 0; i < depth; i++) { + Optional next = node.next(); + if (next.isPresent()) { + node = next.get(); + } else { + break; + } + } + return node; + } + + /** + * 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 boolean hasDepth(int depth) { + BoardHistoryNode node = this; + int c = 0; + while (node.next().isPresent()) { + if (node.hasVariations()) { + int variationDepth = depth - c - 1; + return node.getVariations().stream().anyMatch(v -> v.hasDepth(variationDepth)); + } else { + node = node.next().get(); + c++; + if (c >= depth) { + return true; + } + } + } + return false; + } + + /** + * Find top of variation (the first move that is on the main trunk) + * + * @return top of variation, if on main trunk, return start move + */ + public BoardHistoryNode findTop() { + BoardHistoryNode start = this; + BoardHistoryNode top = start; + while (start.previous().isPresent()) { + BoardHistoryNode pre = start.previous().get(); + if (pre.next().isPresent() && pre.next().get() != start) { + top = pre; + } + start = pre; + } + return top; + } + + /** + * Finds the first parent with variations. + * + * @return the first parent with variations, if any, Optional.empty otherwise + */ + public Optional firstParentWithVariations() { + return this.findChildOfPreviousWithVariation().flatMap(v -> v.previous()); + } + + /** + * Find first move with variations in tree above node. + * + * @return The child (in the current variation) of the first node with variations + */ + public Optional findChildOfPreviousWithVariation() { + BoardHistoryNode node = this; + while (node.previous().isPresent()) { + BoardHistoryNode pre = node.previous().get(); + if (pre.hasVariations()) { + return Optional.of(node); + } + node = pre; + } + return Optional.empty(); + } + + /** + * Given a child node, find the index of that child node in it's parent + * + * @return index of child node, -1 if child node not a child of parent + */ + public int findIndexOfNode(BoardHistoryNode childNode) { + if (!next().isPresent()) { + return -1; + } + for (int i = 0; i < numberOfChildren(); i++) { + if (getVariation(i).map(v -> v == childNode).orElse(false)) { + 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 boolean isMainTrunk() { + BoardHistoryNode node = this; + while (node.previous().isPresent()) { + BoardHistoryNode pre = node.previous().get(); + if (pre.next().isPresent() && pre.next().get() != node) { + return false; + } + node = pre; + } + return true; + } } diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 98f75df74..5784dff9e 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -82,7 +82,7 @@ private static boolean parse(String value) { // Save the variation step count Map subTreeStepMap = new HashMap(); // Comment of the AW/AB (Add White/Add Black) stone - String awabComment = null; + String awabComment = ""; // Game properties Map gameProperties = new HashMap(); boolean inTag = false, @@ -90,7 +90,7 @@ private static boolean parse(String value) { escaping = false, moveStart = false, addPassForAwAb = true; - String tag = null; + String tag = ""; StringBuilder tagBuilder = new StringBuilder(); StringBuilder tagContentBuilder = new StringBuilder(); // MultiGo 's branch: (Main Branch (Main Branch) (Branch) ) @@ -265,7 +265,7 @@ private static boolean parse(String value) { while (Lizzie.board.previousMove()) ; // Set AW/AB Comment - if (awabComment != null) { + if (!awabComment.isEmpty()) { Lizzie.board.comment(awabComment); } if (gameProperties.size() > 0) { @@ -361,7 +361,7 @@ private static void saveToStream(Board board, Writer writer) throws IOException } // The AW/AB Comment - if (history.getData().comment != null) { + if (!history.getData().comment.isEmpty()) { builder.append(String.format("C[%s]", history.getData().comment)); } @@ -390,8 +390,8 @@ private static String generateNode(Board board, BoardHistoryNode node) throws IO 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'); + char x = data.lastMove.isPresent() ? (char) (data.lastMove.get()[0] + 'a') : 't'; + char y = data.lastMove.isPresent() ? (char) (data.lastMove.get()[1] + 'a') : 't'; builder.append(String.format(";%s[%c%c]", stone, x, y)); @@ -399,20 +399,20 @@ private static String generateNode(Board board, BoardHistoryNode node) throws IO builder.append(data.propertiesString()); // Write the comment - if (data.comment != null) { + if (!data.comment.isEmpty()) { builder.append(String.format("C[%s]", data.comment)); } } if (node.numberOfChildren() > 1) { // Variation - for (BoardHistoryNode sub : node.getNexts()) { + for (BoardHistoryNode sub : node.getVariations()) { builder.append("("); builder.append(generateNode(board, sub)); builder.append(")"); } } else if (node.numberOfChildren() == 1) { - builder.append(generateNode(board, node.next())); + builder.append(generateNode(board, node.next().orElse(null))); } else { return builder.toString(); } @@ -471,7 +471,7 @@ public static void addProperties(Map props, Map */ public static void addProperties(Map props, String propsStr) { boolean inTag = false, escaping = false; - String tag = null; + String tag = ""; StringBuilder tagBuilder = new StringBuilder(); StringBuilder tagContentBuilder = new StringBuilder(); @@ -533,9 +533,7 @@ public static void addProperties(Map props, String propsStr) { */ public static String propertiesString(Map props) { StringBuilder sb = new StringBuilder(); - if (props != null) { - props.forEach((key, value) -> sb.append(nodeString(key, value))); - } + props.forEach((key, value) -> sb.append(nodeString(key, value))); return sb.toString(); } diff --git a/src/test/java/common/Util.java b/src/test/java/common/Util.java index 2b46ec789..197a9bdd8 100644 --- a/src/test/java/common/Util.java +++ b/src/test/java/common/Util.java @@ -3,12 +3,12 @@ import featurecat.lizzie.Lizzie; 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.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -31,7 +31,7 @@ public static void getVariationTree( int variationNumber, boolean isMain) { // Finds depth on leftmost variation of this tree - int depth = BoardHistoryList.getDepth(startNode) + 1; + int depth = startNode.getDepth() + 1; int lane = startLane; // Figures out how far out too the right (which lane) we have to go not to collide with other // variations @@ -53,23 +53,26 @@ public static void getVariationTree( // Draw main line StringBuilder sb = new StringBuilder(); sb.append(formatMove(cur.getData())); - while (cur.next() != null) { - cur = cur.next(); + while (cur.next().isPresent()) { + cur = cur.next().get(); 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(); + while (cur.previous().isPresent() && cur != startNode) { + cur = cur.previous().get(); 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); + Optional variation = cur.getVariation(i); + if (variation.isPresent()) { + getVariationTree(moveList, curwidth, variation.get(), i, false); + } } } } @@ -80,11 +83,11 @@ private static String formatMove(BoardData data) { 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'); + char x = data.lastMove.map(m -> (char) (m[0] + 'a')).orElse('t'); + char y = data.lastMove.map(m -> (char) (m[1] + 'a')).orElse('t'); String comment = ""; - if (data.comment != null && data.comment.trim().length() > 0) { + if (!data.comment.trim().isEmpty()) { comment = String.format("C[%s]", data.comment); } return String.format(";%s[%c%c]%s%s", stone, x, y, comment, data.propertiesString()); @@ -127,8 +130,10 @@ public static Stone[] convertStones(String awAb) { stone = Stone.WHITE; } else { int[] move = SGFParser.convertSgfPosToCoord(str); - int index = Board.getIndex(move[0], move[1]); - stones[index] = stone; + if (move != null) { + 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 index 494a23a53..d42bab753 100644 --- a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java +++ b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java @@ -16,7 +16,7 @@ public class SGFParserTest { - private Lizzie lizzie = null; + private Lizzie lizzie; @Test public void run() throws IOException { @@ -59,9 +59,7 @@ public void testVariaionOnly1() throws IOException { 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); @@ -69,7 +67,7 @@ public void testVariaionOnly1() throws IOException { // Save correctly String saveSgf = SGFParser.saveToString(); - assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + assertTrue(saveSgf.trim().length() > 0); assertEquals(sgfString, Util.trimGameInfo(saveSgf)); } @@ -101,7 +99,6 @@ public void testFull1() throws IOException { 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); @@ -111,7 +108,7 @@ public void testFull1() throws IOException { // Save correctly String saveSgf = SGFParser.saveToString(); - assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + assertTrue(saveSgf.trim().length() > 0); String sgf = Util.trimGameInfo(saveSgf); String[] ret = Util.splitAwAbSgf(sgf); @@ -153,7 +150,6 @@ public void testMore1() throws IOException { 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); @@ -176,7 +172,7 @@ public void testMore1() throws IOException { // Save correctly String saveSgf = SGFParser.saveToString(); - assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + assertTrue(saveSgf.trim().length() > 0); String sgf = Util.trimGameInfo(saveSgf); String[] ret = Util.splitAwAbSgf(sgf);