diff --git a/.gitignore b/.gitignore index 79e484680..e1d4690cd 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,9 @@ src/main/resources/META-INF target leelaz_opencl_tuning lizzie.sh + +.classpath + +.project + +*.prefs diff --git a/src/main/java/featurecat/lizzie/WrapString.java b/src/main/java/featurecat/lizzie/WrapString.java new file mode 100644 index 000000000..7ddeab79a --- /dev/null +++ b/src/main/java/featurecat/lizzie/WrapString.java @@ -0,0 +1,181 @@ +package featurecat.lizzie; + +import java.awt.FontMetrics; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * Globally available utility classes, mostly for string manipulation. + * + * @author Jim Menard, jimm@io.com + */ +public class WrapString { + /** + * Returns an array of strings, one for each line in the string after it has + * been wrapped to fit lines of maxWidth. Lines end with any of cr, + * lf, or cr lf. A line ending at the end of the string will not output a + * further, empty string. + *

+ * This code assumes str is not null. + * + * @param str + * the string to split + * @param fm + * needed for string width calculations + * @param maxWidth + * the max line width, in points + * @return a non-empty list of strings + */ + public static List wrap(String str, FontMetrics fm, int maxWidth) { + List lines = splitIntoLines(str); + if (lines.size() == 0) + return lines; + + ArrayList strings = new ArrayList(); + for (Iterator iter = lines.iterator(); iter.hasNext();) + wrapLineInto((String) iter.next(), strings, fm, maxWidth); + return strings; + } + + /** + * Given a line of text and font metrics information, wrap the line and add the + * new line(s) to list. + * + * @param line + * a line of text + * @param list + * an output list of strings + * @param fm + * font metrics + * @param maxWidth + * maximum width of the line(s) + */ + public static void wrapLineInto(String line, List list, FontMetrics fm, int maxWidth) { + int len = line.length(); + int width; + while (len > 0 && (width = fm.stringWidth(line)) > maxWidth) { + // Guess where to split the line. Look for the next space before + // or after the guess. + int guess = len * maxWidth / width; + String before = line.substring(0, guess).trim(); + + width = fm.stringWidth(before); + int pos = 0; + if (width > maxWidth) { // Too long + pos = findBreakBefore(line, guess); + // fix too long bug + if (pos <= 0 || (width = fm.stringWidth(line.substring(0, pos).trim())) > maxWidth) { + int diff = width - maxWidth; + int i = 0; + for (; (diff > 0 && i < 4); i++) { + diff = diff - fm.stringWidth(line.substring(guess - i - 1, guess - i)); + } + pos = guess - i; + } + } + else { // Too short or possibly just right + pos = findBreakAfter(line, guess); + if (pos != -1) { // Make sure this doesn't make us too long + before = line.substring(0, pos).trim(); + if (fm.stringWidth(before) > maxWidth) + pos = findBreakBefore(line, guess); + } + } + // fix the bug for '-' +// if (pos == -1) + if (pos <= 0) + pos = guess; // Split in the middle of the word + + list.add(line.substring(0, pos).trim()); + line = line.substring(pos).trim(); + len = line.length(); + } + if (len > 0) + list.add(line); + } + + /** + * Returns the index of the first whitespace character or '-' in line + * that is at or before start. Returns -1 if no such character is + * found. + * + * @param line + * a string + * @param start + * where to star looking + */ + public static int findBreakBefore(String line, int start) { + for (int i = start; i >= 0; --i) { + char c = line.charAt(i); + if (Character.isWhitespace(c) || c == '-') + return i; + } + return -1; + } + + /** + * Returns the index of the first whitespace character or '-' in line + * that is at or after start. Returns -1 if no such character is + * found. + * + * @param line + * a string + * @param start + * where to star looking + */ + public static int findBreakAfter(String line, int start) { + int len = line.length(); + for (int i = start; i < len; ++i) { + char c = line.charAt(i); + if (Character.isWhitespace(c) || c == '-') + return i; + } + return -1; + } + + /** + * Returns an array of strings, one for each line in the string. Lines end with + * any of cr, lf, or cr lf. A line ending at the end of the string will not + * output a further, empty string. + *

+ * This code assumes str is not null. + * + * @param str + * the string to split + * @return a non-empty list of strings + */ + public static List splitIntoLines(String str) { + ArrayList strings = new ArrayList(); + + int len = str.length(); + if (len == 0) { + strings.add(""); + return strings; + } + + int lineStart = 0; + + for (int i = 0; i < len; ++i) { + char c = str.charAt(i); + if (c == '\r') { + int newlineLength = 1; + if ((i + 1) < len && str.charAt(i + 1) == '\n') + newlineLength = 2; + strings.add(str.substring(lineStart, i)); + lineStart = i + newlineLength; + if (newlineLength == 2) // skip \n next time through loop + ++i; + } else if (c == '\n') { + strings.add(str.substring(lineStart, i)); + lineStart = i + 1; + } + } + if (lineStart < len) + strings.add(str.substring(lineStart)); + + return strings; + } + +} diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 3bb067a5d..f52935854 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -13,6 +13,7 @@ import com.jhlabs.image.GaussianFilter; import featurecat.lizzie.Lizzie; import featurecat.lizzie.Util; +import featurecat.lizzie.WrapString; import featurecat.lizzie.analysis.GameInfo; import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.rules.Board; @@ -21,6 +22,7 @@ import featurecat.lizzie.rules.SGFParser; import org.json.JSONObject; import org.json.JSONArray; +import org.json.JSONException; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; @@ -414,7 +416,12 @@ public void paint(Graphics g0) { if (Lizzie.config.showVariationGraph) { drawVariationTreeContainer(backgroundG, vx, vy, vw, vh); - variationTree.draw(g, treex, treey, treew, treeh); + // Draw the Comment of the Sgf + int cHeight = drawCommnet(g, vx, vy, vw, vh, false); + variationTree.draw(g, treex, treey, treew, treeh - cHeight); + } else { + // Draw the Comment of the Sgf + int cHeight = drawCommnet(g, vx, boardY, vw, vh, true); } if (Lizzie.config.showSubBoard) { try { @@ -490,6 +497,16 @@ private void drawVariationTreeContainer(Graphics2D g, int vx, int vy, int vw, in private void drawPonderingState(Graphics2D g, String text, int x, int y, double size) { Font font = new Font(systemDefaultFontName, Font.PLAIN, (int)(Math.max(getWidth(), getHeight()) * size)); FontMetrics fm = g.getFontMetrics(font); + // for trim long text + if (Lizzie.leelaz.isLoaded()) { + int mainBoardX = (boardRenderer != null && boardRenderer.getLocation() != null) ? boardRenderer.getLocation().x : 0; + if (mainBoardX > x) { + ArrayList list = (ArrayList) WrapString.wrap(text, fm, mainBoardX - x); + if (list != null && list.size() > 0) { + text = list.get(0); + } + } + } int stringWidth = fm.stringWidth(text); int stringHeight = fm.getAscent() - fm.getDescent(); int width = stringWidth; @@ -945,4 +962,71 @@ public void pasteSgf() { public void increaseMaxAlpha(int k) { boardRenderer.increaseMaxAlpha(k); } + + /** + * Draw the Comment of the Sgf file + * + * @param g + * @param x + * @param y + * @param w + * @param h + * @param full + * @return + */ + private int drawCommnet(Graphics2D g, int x, int y, int w, int h, boolean full) { + int cHeight = 0; + String comment = (Lizzie.board.getHistory().getData() != null && Lizzie.board.getHistory().getData().comment != null) ? Lizzie.board.getHistory().getData().comment : ""; + if (comment != null && comment.trim().length() > 0) { + double rate = full ? 1 : 0.1; + cHeight = (int)(h * rate); + // May be need to set up a Chinese Font for display a Chinese Text in the non-Chinese environment +// String systemDefaultFontName = "宋体"; + int fontSize = (int)(Math.min(getWidth(), getHeight()) * 0.98 * 0.03); + try { + fontSize = Lizzie.config.uiConfig.getInt("comment-font-size"); + } catch (JSONException e) { + if (fontSize < 16) { + fontSize = 16; + } else if (fontSize < 16) { + fontSize = 16; + } + } + Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); + FontMetrics fm = g.getFontMetrics(font); + int stringWidth = fm.stringWidth(comment); + int stringHeight = fm.getHeight(); //fm.getAscent() - fm.getDescent(); + int width = stringWidth; + int height = stringHeight; //(int)(stringHeight * 1.2); + + ArrayList list = (ArrayList) WrapString.wrap(comment, fm, (int)(w - height*0.9)); + if (list != null && list.size() > 0) { + if (!full) { + if (list.size() * height > cHeight) { + cHeight = list.size() * height; + if (cHeight > (int)(h * 0.4)) { + cHeight = (int)(h * 0.4); + } + } + } + int ystart = full ? y : h - cHeight; + // Draw background + Color oriColor = g.getColor(); + g.setColor(new Color(0, 0, 0, 150)); + g.fillRect(x, ystart - height, w, cHeight + height * 2); + g.setColor(Color.white); + g.setFont(font); + int i = 0; + for (String s : list) { + g.drawString(s, x + (int)(height * 0.2), ystart + height * (full?(i+1):i)); + i++; + } + g.setColor(oriColor); + cHeight = cHeight + height; + } else { + cHeight = 0; + } + } + return cHeight; + } } diff --git a/src/main/java/featurecat/lizzie/gui/VariationTree.java b/src/main/java/featurecat/lizzie/gui/VariationTree.java index 6c873dc0e..ff05d30bc 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -79,7 +79,7 @@ public void drawTree(Graphics2D g, int posx, int posy, int startLane, int maxpos g.setColor(curcolor); // Draw main line - while (cur.next() != null && posy + YSPACING < maxposy) { + while (cur.next() != null && ((posy + YSPACING + DOT_DIAM) < maxposy)) { // Fix oval cover issue sometimes posy += YSPACING; cur = cur.next(); if (cur == curMove) { diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 81fe97825..6746128ad 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -99,6 +99,19 @@ public static String convertCoordinatesToName(int x, int y) { public static boolean isValid(int x, int y) { return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE; } + + /** + * The comment. Thread safe + * @param comment the comment of stone + */ + public void comment(String comment) { + synchronized (this) { + + if (history.getData() != null) { + history.getData().comment = comment; + } + } + } /** * The pass. Thread safe diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index 1fce99366..167566089 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -17,6 +17,8 @@ public class BoardData { public int blackCaptures; public int whiteCaptures; + + public String comment; public BoardData(Stone[] stones, int[] lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, int moveNumber, int[] moveNumberList, int blackCaptures, int whiteCaptures, double winrate, int playouts) { this.moveNumber = moveNumber; diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index 30ba2b81c..45d301440 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -88,6 +88,20 @@ public BoardData next() { return head.getData(); } + /** + * moves the pointer to the right, returns the node stored there + * + * @return the next node, null if there is no next node + */ + public BoardHistoryNode nextNode() { + if (head.next() == null) + return null; + else + head = head.next(); + + return head; + } + /** * moves the pointer to the variation number idx, returns the data stored there * @@ -185,6 +199,10 @@ public int[] getMoveNumberList() { public BoardHistoryNode getCurrentHistoryNode() { return head; } + + public void setCurrentHistoryNode(BoardHistoryNode head) { + this.head = head; + } /** * @param data the board position to check against superko diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java old mode 100644 new mode 100755 index c5f274b12..cf1e52820 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -1,10 +1,13 @@ package featurecat.lizzie.rules; +import java.util.HashMap; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.GameInfo; +import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.plugin.PluginManager; import java.io.*; @@ -66,6 +69,9 @@ private static boolean parse(String value) { return false; } int subTreeDepth = 0; + // the variation step count + Map subTreeStepMap = new HashMap(); + String awabComment = null, prevTag = null; boolean inTag = false, isMultiGo = false, escaping = false; String tag = null; StringBuilder tagBuilder = new StringBuilder(); @@ -79,12 +85,16 @@ private static boolean parse(String value) { String blackPlayer = "", whitePlayer = ""; PARSE_LOOP: - for (byte b : value.getBytes()) { + // for suppoert unicode char + // for (byte b : value.getBytes()) { + for (int i = 0; i < value.length(); i++) { // Check unicode charactors (UTF-8) - char c = (char) b; - if (((int) b & 0x80) != 0) { - continue; - } + // for suppoert unicode char + // char c = (char) b; + char c = value.charAt(i); + // if (((int) b & 0x80) != 0) { + // continue; + // } if (escaping) { // Any char following "\" is inserted verbatim // (ref) "3.2. Text" in https://www.red-bean.com/sgf/sgf4.html @@ -96,14 +106,26 @@ private static boolean parse(String value) { case '(': if (!inTag) { subTreeDepth += 1; + // init the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(0)); + } else { + if (i > 0) { + tagContentBuilder.append(c); + } } break; case ')': if (!inTag) { - subTreeDepth -= 1; if (isMultiGo) { - break PARSE_LOOP; + // restore the variation nodes + for (int s = 0; s < subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue(); s++) { + Lizzie.board.previousMove(); + } +// break PARSE_LOOP; } + subTreeDepth -= 1; + } else { + tagContentBuilder.append(c); } break; case '[': @@ -134,6 +156,7 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.BLACK); } else { + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.BLACK); } } else if (tag.equals("W")) { @@ -141,8 +164,16 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.WHITE); } else { + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.WHITE); } + } else if (tag.equals("C")) { + // for comment + if ("AW".equals(prevTag) || "AB".equals(prevTag)) { + awabComment = tagContent; + } else { + Lizzie.board.comment(tagContent); + } } else if (tag.equals("AB")) { int[] move = convertSgfPosToCoord(tagContent); if (move == null) { @@ -173,6 +204,7 @@ private static boolean parse(String value) { e.printStackTrace(); } } + prevTag = tag; break; case ';': break; @@ -198,6 +230,11 @@ private static boolean parse(String value) { // Rewind to game start while (Lizzie.board.previousMove()) ; + + // Set AW/AB Comment + if (awabComment != null) { + Lizzie.board.comment(awabComment); + } return true; } @@ -231,6 +268,15 @@ private static void saveToStream(Board board, Writer writer) throws IOException builder.append(String.format("KM[%s]PW[%s]PB[%s]DT[%s]AP[Lizzie: %s]", komi, playerWhite, playerBlack, date, Lizzie.lizzieVersion)); + // Update winrate + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + + if (stats.maxWinrate >= 0 && stats.totalPlayouts > history.getData().playouts) + { + history.getData().winrate = stats.maxWinrate; + history.getData().playouts = stats.totalPlayouts; + } + // move to the first move history.toStart(); @@ -250,12 +296,45 @@ private static void saveToStream(Board board, Writer writer) throws IOException builder.append(String.format("[%c%c]", x, y)); } } + } else { + //has AW/AB? + Stone[] stones = history.getStones(); + StringBuilder abStone = new StringBuilder(); + StringBuilder awStone = new StringBuilder(); + for (int i = 0; i < stones.length; i++) { + Stone stone = stones[i]; + if (stone.isBlack() || stone.isWhite()) { + // i = x * Board.BOARD_SIZE + y; + int corY = i % Board.BOARD_SIZE; + int corX = (i - corY) / Board.BOARD_SIZE; + + char x = (char) (corX + 'a'); + char y = (char) (corY + 'a'); + + if (stone.isBlack()) { + abStone.append(String.format("[%c%c]", x, y)); + } else { + awStone.append(String.format("[%c%c]", x, y)); + } + } + } + if (abStone.length() > 0) { + builder.append("AB").append(abStone); + } + if (awStone.length() > 0) { + builder.append("AW").append(awStone); + } } + // Start Comment + if (history.getData().comment != null) { + builder.append(String.format("C[%s]", history.getData().comment)); + } + // replay moves, and convert them to tags. // * format: ";B[xy]" or ";W[xy]" // * with 'xy' = coordinates ; or 'tt' for pass. - BoardData data; +// BoardData data; // TODO: this code comes from cngoodboy's plugin PR #65. It looks like it might be useful for handling // AB/AW commands for sgfs in general -- we can extend it beyond just handicap. TODO integrate it @@ -294,21 +373,139 @@ private static void saveToStream(Board board, Writer writer) throws IOException // } // } - while ((data = history.next()) != null) { + // Write variation tree +// while ((data = history.next()) != null) { +// +// String stone; +// if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; +// else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; +// else continue; +// +// char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); +// char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); +// +// builder.append(String.format(";%s[%c%c]", stone, x, y)); +// } + builder.append(generateNode(board, writer, history.nextNode())); + + // close file + builder.append(')'); + writer.append(builder.toString()); + } + + + private static String generateNode(Board board, Writer writer, BoardHistoryNode node) throws IOException { + StringBuilder builder = new StringBuilder(""); + + if (node != null) { - String stone; - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - else continue; + BoardData data = node.getData(); + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + builder.append(String.format(";%s[%c%c]", stone, x, y)); + + // Write the comment with win rate + String winrateComment = formatWinrate(node); + if (data.comment != null) { + String winratePattern = "\\([^\\(\\)/]*\\/[0-9\\.]*[kmKM]*\\)[0-9\\.\\-]+%*\\(*[0-9\\.\\-]+%*\\)*"; + if (data.comment.matches("(?s).*" + winratePattern + "(?s).*")) { + winrateComment = data.comment.replaceAll(winratePattern, winrateComment); + } else { + winrateComment = String.format("%s %s", data.comment, winrateComment); + } + } + builder.append(String.format("C[%s]", winrateComment)); + + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, writer, sub)); + builder.append(")"); + } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, writer, node.next())); + } else { + return builder.toString(); + } + } + } + + return builder.toString(); + } + + /** + * Format Winrate + * + */ + private static String formatWinrate(BoardHistoryNode node) { + if (node == null) { + return ""; + } + BoardData data = node.getData(); + String engine = "Leelaz"; //Lizzie.leelaz.currentWeight(); + String playouts = ""; + String curWinrate = ""; + String lastMoveWinrate = ""; - builder.append(String.format(";%s[%c%c]", stone, x, y)); + double lastWR = 50; // winrate the previous move + boolean validLastWinrate = false; // whether it was actually calculated + BoardData lastNode = node.previous().getData(); + if (lastNode != null && lastNode.playouts > 0) { + lastWR = lastNode.winrate; + validLastWinrate = true; } + double curWR = data.winrate; // winrate on this move + boolean validWinrate = (data.playouts > 0); // and whether it was actually calculated + if (!validWinrate) { + curWR = 100 - lastWR; // display last move's winrate for now (with color difference) + } + + // Playouts + playouts = getPlayoutsString(data.playouts); - // close file - builder.append(')'); - writer.append(builder.toString()); + // Winrate + if(Lizzie.config.handicapInsteadOfWinrate) { + curWinrate = String.format("%.2f", Lizzie.leelaz.winrateToHandicap(100-curWR)); + } else { + curWinrate = String.format("%.1f%%", 100 - curWR); + } + + // Last move + if (validLastWinrate && validWinrate) { + if(Lizzie.config.handicapInsteadOfWinrate) { + lastMoveWinrate = String.format("(%.2f)", Lizzie.leelaz.winrateToHandicap(100-curWR) - Lizzie.leelaz.winrateToHandicap(lastWR)); + } else { + lastMoveWinrate = String.format("(%.1f%%)", 100 - lastWR - curWR); + } + } + + return String.format("(%s/%s)%s%s", engine, playouts, curWinrate, lastMoveWinrate); + } + + /** + * Temp TODO + * @return a shorter, rounded string version of playouts. e.g. 345 -> 345, 1265 -> 1.3k, 44556 -> 45k, 133523 -> 134k, 1234567 -> 1.2m + */ + private static String getPlayoutsString(int playouts) { + if (playouts >= 1_000_000) { + double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567 + return Math.round(playoutsDouble) / 10.0 + "m"; + } else if (playouts >= 10_000) { + double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265 + return Math.round(playoutsDouble) + "k"; + } else if (playouts >= 1_000) { + double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65 + return Math.round(playoutsDouble) / 10.0 + "k"; + } else { + return String.valueOf(playouts); + } } }