From d45406b09336b3401e1be33529ab2b6d267dda35 Mon Sep 17 00:00:00 2001 From: zsalch Date: Tue, 18 Sep 2018 13:41:07 +0800 Subject: [PATCH 1/5] Support Read/Write Sgf with Variation, AW/AB & Comment --- .../java/featurecat/lizzie/rules/Board.java | 13 ++ .../featurecat/lizzie/rules/BoardData.java | 3 + .../lizzie/rules/BoardHistoryList.java | 14 ++ .../featurecat/lizzie/rules/SGFParser.java | 180 ++++++++++++------ 4 files changed, 151 insertions(+), 59 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 81fe97825..c58a4d433 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -100,6 +100,19 @@ public static boolean isValid(int x, int y) { return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE; } + /** + * The comment. Thread safe + * @param comment the comment of stone + */ + public void comment(String comment) { + synchronized (this) { + + if (history.getData() != null) { + history.getData().comment = comment; + } + } + } + /** * The pass. Thread safe * diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index 1fce99366..be77d1619 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -18,6 +18,9 @@ public class BoardData { public int blackCaptures; public int whiteCaptures; + // Comment in the Sgf move + public String comment; + public BoardData(Stone[] stones, int[] lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, int moveNumber, int[] moveNumberList, int blackCaptures, int whiteCaptures, double winrate, int playouts) { this.moveNumber = moveNumber; this.lastMove = lastMove; diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index 30ba2b81c..d1be0a3db 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -88,6 +88,20 @@ public BoardData next() { return head.getData(); } + /** + * moves the pointer to the right, returns the node stored there + * + * @return the next node, null if there is no next node + */ + public BoardHistoryNode nextNode() { + if (head.next() == null) + return null; + else + head = head.next(); + + return head; + } + /** * moves the pointer to the variation number idx, returns the data stored there * diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index c5f274b12..73f6eceb5 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -1,5 +1,7 @@ package featurecat.lizzie.rules; +import java.util.HashMap; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -66,6 +68,12 @@ private static boolean parse(String value) { return false; } int subTreeDepth = 0; + // Save the variation step count + Map subTreeStepMap = new HashMap(); + // Comment of the AW/AB (Add White/Add Black) stone + String awabComment = null; + // Previous Tag + String prevTag = null; boolean inTag = false, isMultiGo = false, escaping = false; String tag = null; StringBuilder tagBuilder = new StringBuilder(); @@ -79,12 +87,9 @@ private static boolean parse(String value) { String blackPlayer = "", whitePlayer = ""; PARSE_LOOP: - for (byte b : value.getBytes()) { - // Check unicode charactors (UTF-8) - char c = (char) b; - if (((int) b & 0x80) != 0) { - continue; - } + // Suppoert unicode charactors (UTF-8) + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); if (escaping) { // Any char following "\" is inserted verbatim // (ref) "3.2. Text" in https://www.red-bean.com/sgf/sgf4.html @@ -96,14 +101,27 @@ private static boolean parse(String value) { case '(': if (!inTag) { subTreeDepth += 1; + // Initialize the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(0)); + } else { + if (i > 0) { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); + } } break; case ')': if (!inTag) { - subTreeDepth -= 1; if (isMultiGo) { - break PARSE_LOOP; + // Restore to the variation node + for (int s = 0; s < subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue(); s++) { + Lizzie.board.previousMove(); + } } + subTreeDepth -= 1; + } else { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); } break; case '[': @@ -134,6 +152,8 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.BLACK); } else { + // Save the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.BLACK); } } else if (tag.equals("W")) { @@ -141,8 +161,17 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.WHITE); } else { + // Save the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.WHITE); } + } else if (tag.equals("C")) { + // Support comment + if ("AW".equals(prevTag) || "AB".equals(prevTag)) { + awabComment = tagContent; + } else { + Lizzie.board.comment(tagContent); + } } else if (tag.equals("AB")) { int[] move = convertSgfPosToCoord(tagContent); if (move == null) { @@ -173,6 +202,7 @@ private static boolean parse(String value) { e.printStackTrace(); } } + prevTag = tag; break; case ';': break; @@ -199,6 +229,11 @@ private static boolean parse(String value) { // Rewind to game start while (Lizzie.board.previousMove()) ; + // Set AW/AB Comment + if (awabComment != null) { + Lizzie.board.comment(awabComment); + } + return true; } @@ -250,65 +285,92 @@ private static void saveToStream(Board board, Writer writer) throws IOException builder.append(String.format("[%c%c]", x, y)); } } + } else { + // Process the AW/AB stone + Stone[] stones = history.getStones(); + StringBuilder abStone = new StringBuilder(); + StringBuilder awStone = new StringBuilder(); + for (int i = 0; i < stones.length; i++) { + Stone stone = stones[i]; + if (stone.isBlack() || stone.isWhite()) { + // i = x * Board.BOARD_SIZE + y; + int corY = i % Board.BOARD_SIZE; + int corX = (i - corY) / Board.BOARD_SIZE; + + char x = (char) (corX + 'a'); + char y = (char) (corY + 'a'); + + if (stone.isBlack()) { + abStone.append(String.format("[%c%c]", x, y)); + } else { + awStone.append(String.format("[%c%c]", x, y)); + } + } + } + if (abStone.length() > 0) { + builder.append("AB").append(abStone); + } + if (awStone.length() > 0) { + builder.append("AW").append(awStone); + } } + // The AW/AB Comment + if (history.getData().comment != null) { + builder.append(String.format("C[%s]", history.getData().comment)); + } + // replay moves, and convert them to tags. // * format: ";B[xy]" or ";W[xy]" // * with 'xy' = coordinates ; or 'tt' for pass. - BoardData data; - - // TODO: this code comes from cngoodboy's plugin PR #65. It looks like it might be useful for handling - // AB/AW commands for sgfs in general -- we can extend it beyond just handicap. TODO integrate it -// data = history.getData(); -// -// // For handicap -// ArrayList abList = new ArrayList(); -// ArrayList awList = new ArrayList(); -// -// for (int i = 0; i < Board.BOARD_SIZE; i++) { -// for (int j = 0; j < Board.BOARD_SIZE; j++) { -// switch (data.stones[Board.getIndex(i, j)]) { -// case BLACK: -// abList.add(new int[]{i, j}); -// break; -// case WHITE: -// awList.add(new int[]{i, j}); -// break; -// default: -// break; -// } -// } -// } -// -// if (!abList.isEmpty()) { -// builder.append(";AB"); -// for (int i = 0; i < abList.size(); i++) { -// builder.append(String.format("[%s]", convertCoordToSgfPos(abList.get(i)))); -// } -// } -// -// if (!awList.isEmpty()) { -// builder.append(";AW"); -// for (int i = 0; i < awList.size(); i++) { -// builder.append(String.format("[%s]", convertCoordToSgfPos(awList.get(i)))); -// } -// } - - while ((data = history.next()) != null) { - - String stone; - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - else continue; - - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); - - builder.append(String.format(";%s[%c%c]", stone, x, y)); - } + + + // Write variation tree + builder.append(generateNode(board, writer, history.nextNode())); // close file builder.append(')'); writer.append(builder.toString()); } + + // Generate node + private static String generateNode(Board board, Writer writer, BoardHistoryNode node) throws IOException { + StringBuilder builder = new StringBuilder(""); + + if (node != null) { + + BoardData data = node.getData(); + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { + + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + builder.append(String.format(";%s[%c%c]", stone, x, y)); + + // Write the comment + if (data.comment != null) { + builder.append(String.format("C[%s]", data.comment)); + } + + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, writer, sub)); + builder.append(")"); + } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, writer, node.next())); + } else { + return builder.toString(); + } + } + } + + return builder.toString(); + } } From 2b6358aebd6e90d46fb739d73f7b762ffce629c6 Mon Sep 17 00:00:00 2001 From: zsalch Date: Wed, 19 Sep 2018 17:30:07 +0800 Subject: [PATCH 2/5] Use Spaces instead of Tab --- .../lizzie/rules/BoardHistoryList.java | 4 +- .../featurecat/lizzie/rules/SGFParser.java | 99 +++++++++---------- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index d1be0a3db..044ec0f02 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -101,7 +101,7 @@ public BoardHistoryNode nextNode() { return head; } - + /** * moves the pointer to the variation number idx, returns the data stored there * @@ -134,7 +134,7 @@ public BoardData getNext() { public List getNexts() { return head.getNexts(); } - + /** * Does not change the pointer position * diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 73f6eceb5..27e83a2ff 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -72,7 +72,7 @@ private static boolean parse(String value) { Map subTreeStepMap = new HashMap(); // Comment of the AW/AB (Add White/Add Black) stone String awabComment = null; - // Previous Tag + // Previous Tag String prevTag = null; boolean inTag = false, isMultiGo = false, escaping = false; String tag = null; @@ -86,7 +86,6 @@ private static boolean parse(String value) { String blackPlayer = "", whitePlayer = ""; - PARSE_LOOP: // Suppoert unicode charactors (UTF-8) for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); @@ -104,10 +103,10 @@ private static boolean parse(String value) { // Initialize the step count subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(0)); } else { - if (i > 0) { - // Allow the comment tag includes '(' - tagContentBuilder.append(c); - } + if (i > 0) { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); + } } break; case ')': @@ -152,8 +151,8 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.BLACK); } else { - // Save the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); + // Save the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.BLACK); } } else if (tag.equals("W")) { @@ -161,17 +160,17 @@ private static boolean parse(String value) { if (move == null) { Lizzie.board.pass(Stone.WHITE); } else { - // Save the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); + // Save the step count + subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); Lizzie.board.place(move[0], move[1], Stone.WHITE); } } else if (tag.equals("C")) { - // Support comment - if ("AW".equals(prevTag) || "AB".equals(prevTag)) { - awabComment = tagContent; - } else { - Lizzie.board.comment(tagContent); - } + // Support comment + if ("AW".equals(prevTag) || "AB".equals(prevTag)) { + awabComment = tagContent; + } else { + Lizzie.board.comment(tagContent); + } } else if (tag.equals("AB")) { int[] move = convertSgfPosToCoord(tagContent); if (move == null) { @@ -231,7 +230,7 @@ private static boolean parse(String value) { // Set AW/AB Comment if (awabComment != null) { - Lizzie.board.comment(awabComment); + Lizzie.board.comment(awabComment); } return true; @@ -286,7 +285,7 @@ private static void saveToStream(Board board, Writer writer) throws IOException } } } else { - // Process the AW/AB stone + // Process the AW/AB stone Stone[] stones = history.getStones(); StringBuilder abStone = new StringBuilder(); StringBuilder awStone = new StringBuilder(); @@ -301,24 +300,24 @@ private static void saveToStream(Board board, Writer writer) throws IOException char y = (char) (corY + 'a'); if (stone.isBlack()) { - abStone.append(String.format("[%c%c]", x, y)); + abStone.append(String.format("[%c%c]", x, y)); } else { - awStone.append(String.format("[%c%c]", x, y)); + awStone.append(String.format("[%c%c]", x, y)); } } } if (abStone.length() > 0) { - builder.append("AB").append(abStone); + builder.append("AB").append(abStone); } if (awStone.length() > 0) { - builder.append("AW").append(awStone); + builder.append("AW").append(awStone); } } // The AW/AB Comment if (history.getData().comment != null) { - builder.append(String.format("C[%s]", history.getData().comment)); - } + builder.append(String.format("C[%s]", history.getData().comment)); + } // replay moves, and convert them to tags. // * format: ";B[xy]" or ";W[xy]" @@ -339,35 +338,35 @@ private static String generateNode(Board board, Writer writer, BoardHistoryNode if (node != null) { - BoardData data = node.getData(); + BoardData data = node.getData(); String stone = ""; if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); - - builder.append(String.format(";%s[%c%c]", stone, x, y)); - - // Write the comment - if (data.comment != null) { - builder.append(String.format("C[%s]", data.comment)); - } - - if (node.numberOfChildren() > 1) { - // Variation - for (BoardHistoryNode sub : node.getNexts()) { - builder.append("("); - builder.append(generateNode(board, writer, sub)); - builder.append(")"); - } - } else if (node.numberOfChildren() == 1) { - builder.append(generateNode(board, writer, node.next())); - } else { - return builder.toString(); - } + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + builder.append(String.format(";%s[%c%c]", stone, x, y)); + + // Write the comment + if (data.comment != null) { + builder.append(String.format("C[%s]", data.comment)); + } + + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, writer, sub)); + builder.append(")"); + } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, writer, node.next())); + } else { + return builder.toString(); + } } } From 8ebe43eff15cfe778f29c913b30bfa176f1e6aa2 Mon Sep 17 00:00:00 2001 From: zsalch Date: Wed, 19 Sep 2018 18:06:04 +0800 Subject: [PATCH 3/5] Remove unnecessary code --- .../featurecat/lizzie/rules/SGFParser.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 27e83a2ff..3311b34fc 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -86,7 +86,7 @@ private static boolean parse(String value) { String blackPlayer = "", whitePlayer = ""; - // Suppoert unicode charactors (UTF-8) + // Support unicode characters (UTF-8) for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (escaping) { @@ -101,7 +101,7 @@ private static boolean parse(String value) { if (!inTag) { subTreeDepth += 1; // Initialize the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(0)); + subTreeStepMap.put(subTreeDepth, 0); } else { if (i > 0) { // Allow the comment tag includes '(' @@ -113,7 +113,8 @@ private static boolean parse(String value) { if (!inTag) { if (isMultiGo) { // Restore to the variation node - for (int s = 0; s < subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue(); s++) { + int varStep = subTreeStepMap.get(subTreeDepth); + for (int s = 0; s < varStep; s++) { Lizzie.board.previousMove(); } } @@ -152,7 +153,7 @@ private static boolean parse(String value) { Lizzie.board.pass(Stone.BLACK); } else { // Save the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); Lizzie.board.place(move[0], move[1], Stone.BLACK); } } else if (tag.equals("W")) { @@ -161,7 +162,7 @@ private static boolean parse(String value) { Lizzie.board.pass(Stone.WHITE); } else { // Save the step count - subTreeStepMap.put(Integer.valueOf(subTreeDepth), Integer.valueOf(subTreeStepMap.get(Integer.valueOf(subTreeDepth)).intValue() + 1)); + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); Lizzie.board.place(move[0], move[1], Stone.WHITE); } } else if (tag.equals("C")) { @@ -325,15 +326,17 @@ private static void saveToStream(Board board, Writer writer) throws IOException // Write variation tree - builder.append(generateNode(board, writer, history.nextNode())); + builder.append(generateNode(board, history.nextNode())); // close file builder.append(')'); writer.append(builder.toString()); } - // Generate node - private static String generateNode(Board board, Writer writer, BoardHistoryNode node) throws IOException { + /** + * Generate node with variations + */ + private static String generateNode(Board board, BoardHistoryNode node) throws IOException { StringBuilder builder = new StringBuilder(""); if (node != null) { @@ -359,11 +362,11 @@ private static String generateNode(Board board, Writer writer, BoardHistoryNode // Variation for (BoardHistoryNode sub : node.getNexts()) { builder.append("("); - builder.append(generateNode(board, writer, sub)); + builder.append(generateNode(board, sub)); builder.append(")"); } } else if (node.numberOfChildren() == 1) { - builder.append(generateNode(board, writer, node.next())); + builder.append(generateNode(board, node.next())); } else { return builder.toString(); } From bf7587f744959fce878b2eaacdf135c0f4ff4762 Mon Sep 17 00:00:00 2001 From: zsalch Date: Sat, 22 Sep 2018 23:59:03 +0800 Subject: [PATCH 4/5] Add Test Case --- pom.xml | 19 +++ src/test/java/common/Util.java | 149 ++++++++++++++++++ .../lizzie/rules/SGFParserTest.java | 124 +++++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 src/test/java/common/Util.java create mode 100644 src/test/java/featurecat/lizzie/rules/SGFParserTest.java diff --git a/pom.xml b/pom.xml index 6f02aa141..49139edda 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,9 @@ 1.8 1.8 + + -Xlint:all + @@ -52,6 +55,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.9 + + + false + + @@ -63,5 +75,12 @@ 20180130 + + + junit + junit + 4.11 + test + diff --git a/src/test/java/common/Util.java b/src/test/java/common/Util.java new file mode 100644 index 000000000..e201d57f3 --- /dev/null +++ b/src/test/java/common/Util.java @@ -0,0 +1,149 @@ +package common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.UnsupportedLookAndFeelException; + +import org.json.JSONException; +import org.junit.Test; + +import featurecat.lizzie.Config; +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.analysis.Leelaz; +import featurecat.lizzie.analysis.MoveData; +import featurecat.lizzie.gui.LizzieFrame; +import featurecat.lizzie.rules.Board; +import featurecat.lizzie.rules.BoardData; +import featurecat.lizzie.rules.BoardHistoryList; +import featurecat.lizzie.rules.BoardHistoryNode; +import featurecat.lizzie.rules.SGFParser; +import featurecat.lizzie.rules.Stone; + +public class Util { + + private static ArrayList laneUsageList = new ArrayList(); + + /** + * Get Variation Tree as String List + * The logic is same as the function VariationTree.drawTree + * + * @param startLane + * @param startNode + * @param variationNumber + * @param isMain + */ + public static void getVariationTree(List moveList, int startLane, BoardHistoryNode startNode, int variationNumber, boolean isMain) { + // Finds depth on leftmost variation of this tree + int depth = BoardHistoryList.getDepth(startNode) + 1; + int lane = startLane; + // Figures out how far out too the right (which lane) we have to go not to collide with other variations + while (lane < laneUsageList.size() && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { + // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" + laneUsageList.set(lane, startNode.getData().moveNumber - 1); + lane++; + } + if (lane >= laneUsageList.size()) + { + laneUsageList.add(0); + } + if (variationNumber > 1) + laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); + laneUsageList.set(lane, startNode.getData().moveNumber); + + // At this point, lane contains the lane we should use (the main branch is in lane 0) + BoardHistoryNode cur = startNode; + + // Draw main line + StringBuilder sb = new StringBuilder(); + sb.append(formatMove(cur.getData())); + while (cur.next() != null) { + cur = cur.next(); + sb.append(formatMove(cur.getData())); + } + moveList.add(sb.toString()); + // Now we have drawn all the nodes in this variation, and has reached the bottom of this variation + // Move back up, and for each, draw any variations we find + while (cur.previous() != null && cur != startNode) { + cur = cur.previous(); + int curwidth = lane; + // Draw each variation, uses recursion + for (int i = 1; i < cur.numberOfChildren(); i++) { + curwidth++; + // Recursion, depth of recursion will normally not be very deep (one recursion level for every variation that has a variation (sort of)) + getVariationTree(moveList, curwidth, cur.getVariation(i), i, false); + } + } + } + + private static String formatMove(BoardData data) { + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + else return stone; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + String comment = ""; + if (data.comment != null && data.comment.trim().length() > 0) { + comment = String.format("C[%s]", data.comment); + } + return String.format(";%s[%c%c]%s", stone, x, y, comment); + } + + public static String trimGameInfo(String sgf) { + String gameInfo = String.format("(?s).*AP\\[Lizzie: %s\\]", + Lizzie.lizzieVersion); + return sgf.replaceFirst(gameInfo, "("); + } + + public static String[] splitAwAbSgf(String sgf) { + String[] ret = new String[2]; + String regex = "(A[BW]{1}(\\[[a-z]{2}\\])+)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(sgf); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + sb.append(matcher.group(0)); + } + ret[0] = sb.toString(); + ret[1] = sgf.replaceAll(regex, ""); + return ret; + } + + public static Stone[] convertStones(String awAb) { + Stone[] stones = new Stone[Board.BOARD_SIZE * Board.BOARD_SIZE]; + for (int i = 0; i < stones.length; i++) { + stones[i] = Stone.EMPTY; + } + String regex = "(A[BW]{1})|(?<=\\[)([a-z]{2})(?=\\])"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(awAb); + StringBuilder sb = new StringBuilder(); + Stone stone = Stone.EMPTY; + while (matcher.find()) { + String str = matcher.group(0); + if("AB".equals(str)) { + stone = Stone.BLACK; + } else if("AW".equals(str)) { + stone = Stone.WHITE; + } else { + int[] move = SGFParser.convertSgfPosToCoord(str); + int index = Board.getIndex(move[0], move[1]); + stones[index] = stone; + } + } + return stones; + } +} diff --git a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java new file mode 100644 index 000000000..893cb64e2 --- /dev/null +++ b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java @@ -0,0 +1,124 @@ +package featurecat.lizzie.rules; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertArrayEquals; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import common.Util; + +import featurecat.lizzie.Config; +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.analysis.Leelaz; +import featurecat.lizzie.gui.LizzieFrame; + +public class SGFParserTest { + + private Lizzie lizzie = null; + + @Test + + public void run() throws IOException { + lizzie = new Lizzie(); + lizzie.config = new Config(); + lizzie.board = new Board(); + lizzie.frame = new LizzieFrame(); + // new Thread( () -> { + lizzie.leelaz = new Leelaz(); + // }).start(); + + testVariaionOnly1(); + testFull1(); + } + + public void testVariaionOnly1() throws IOException { + + String sgfString = "(;B[pd];W[dp];B[pp];W[dd];B[fq]" + + "(;W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd]" + + "(;B[gb]" + + "(;W[hc];B[nq])" + + "(;W[gc];B[ec];W[hc];B[hb];W[ic]))" + + "(;B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]))" + + "(;W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]))"; + + int variationNum = 4; + String mainBranch = ";B[pd];W[dp];B[pp];W[dd];B[fq];W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd];B[gb];W[hc];B[nq]"; + String variation1 = ";W[gc];B[ec];W[hc];B[hb];W[ic]"; + String variation2 = ";B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]"; + String variation3 = ";W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]"; + + // Load correctly + boolean loaded = SGFParser.loadFromString(sgfString); + assertTrue(loaded); + + // Variations + List moveList = new ArrayList(); + Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); + + assertTrue(moveList != null); + assertEquals(moveList.size(), variationNum); + + assertEquals(moveList.get(0), mainBranch); + assertEquals(moveList.get(1), variation1); + assertEquals(moveList.get(2), variation2); + assertEquals(moveList.get(3), variation3); + + // Save correctly + String saveSgf = SGFParser.saveToString(); + assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + + assertEquals(sgfString, Util.trimGameInfo(saveSgf)); + } + + public void testFull1() throws IOException { + + String sgfInfo = "(;CA[utf8]AP[MultiGo:4.4.4]SZ[19]"; + String sgfAwAb = "AB[pe][pq][oq][nq][mq][cp][dq][eq][fp]AB[qd]AW[dc][cf][oc][qo][op][np][mp][ep][fq]"; + String sgfContent = ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?]" + + "(;B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.])" + + "(;B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]))"; + String sgfString = sgfInfo + sgfAwAb + sgfContent; + + int variationNum = 2; + String mainBranch = ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?];B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.]"; + String variation1 = ";B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]"; + + Stone[] expectStones = Util.convertStones(sgfAwAb); + + // Load correctly + boolean loaded = SGFParser.loadFromString(sgfString); + assertTrue(loaded); + + // Variations + List moveList = new ArrayList(); + Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); + + assertTrue(moveList != null); + assertEquals(moveList.size(), variationNum); + assertEquals(moveList.get(0), mainBranch); + assertEquals(moveList.get(1), variation1); + + // AW/AB + assertArrayEquals(expectStones, Lizzie.board.getHistory().getStones()); + + // Save correctly + String saveSgf = SGFParser.saveToString(); + assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + + String sgf = Util.trimGameInfo(saveSgf); + String[] ret = Util.splitAwAbSgf(sgf); + Stone[] actualStones = Util.convertStones(ret[0]); + + // AW/AB + assertArrayEquals(expectStones, actualStones); + + // Content + assertEquals("(" + sgfContent, ret[1]); + } + +} From 339feef547c951f0b2986839e690afb2699fdaac Mon Sep 17 00:00:00 2001 From: zsalch Date: Thu, 27 Sep 2018 16:19:42 +0800 Subject: [PATCH 5/5] Allow top branch --- .../featurecat/lizzie/rules/SGFParser.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 3311b34fc..42b725814 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -324,9 +324,8 @@ private static void saveToStream(Board board, Writer writer) throws IOException // * format: ";B[xy]" or ";W[xy]" // * with 'xy' = coordinates ; or 'tt' for pass. - // Write variation tree - builder.append(generateNode(board, history.nextNode())); + builder.append(generateNode(board, history.getCurrentHistoryNode())); // close file builder.append(')'); @@ -357,19 +356,19 @@ private static String generateNode(Board board, BoardHistoryNode node) throws IO if (data.comment != null) { builder.append(String.format("C[%s]", data.comment)); } + } - if (node.numberOfChildren() > 1) { - // Variation - for (BoardHistoryNode sub : node.getNexts()) { - builder.append("("); - builder.append(generateNode(board, sub)); - builder.append(")"); - } - } else if (node.numberOfChildren() == 1) { - builder.append(generateNode(board, node.next())); - } else { - return builder.toString(); + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, sub)); + builder.append(")"); } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, node.next())); + } else { + return builder.toString(); } }