diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 3c02ac9d3..c6ee469d5 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -7,6 +7,7 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.rules.Board; import featurecat.lizzie.rules.BoardData; +import featurecat.lizzie.rules.BoardHistoryNode; import featurecat.lizzie.rules.GIBParser; import featurecat.lizzie.rules.SGFParser; import java.awt.*; @@ -165,8 +166,8 @@ public LizzieFrame() { this.addMouseWheelListener(input); this.addMouseMotionListener(input); - // necessary for Windows users - otherwise Lizzie shows a blank white screen on startup until - // updates occur. + // 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() @@ -569,9 +570,9 @@ private void drawPonderingState(Graphics2D g, String text, int x, int y, double int height = (int) (stringHeight * 1.2); BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - // commenting this out for now... always causing an exception on startup. will fix in the - // upcoming refactoring - // filter20.filter(cachedBackground.getSubimage(x, y, result.getWidth(), + // commenting this out for now... always causing an exception on startup. will + // fix in the upcoming refactoring + // filter20.filter(cachedBackground.getSubimage(x, y, result.getWidth(), // result.getHeight()), result); g.drawImage(result, x, y, null); @@ -739,14 +740,17 @@ private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int int strokeRadius = 3; g.setStroke(new BasicStroke(2 * strokeRadius)); g.drawLine( - posX + strokeRadius, posY + strokeRadius, - posX - strokeRadius + width, posY + strokeRadius); + posX + strokeRadius, posY + strokeRadius, posX - strokeRadius + width, posY + strokeRadius); g.drawLine( - posX + strokeRadius, posY + 3 * strokeRadius, - posX + strokeRadius, posY - strokeRadius + height); + posX + strokeRadius, + posY + 3 * strokeRadius, + posX + strokeRadius, + posY - strokeRadius + height); g.drawLine( - posX - strokeRadius + width, posY + 3 * strokeRadius, - posX - strokeRadius + width, posY - strokeRadius + height); + posX - strokeRadius + width, + posY + 3 * strokeRadius, + posX - strokeRadius + width, + posY - strokeRadius + height); // resize the box now so it's inside the border posX += 2 * strokeRadius; @@ -778,8 +782,9 @@ private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int } else { // I think it's more elegant to just not display anything when we don't have // valid data --dfannius - // g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + ": ?%", - // posX + 2 * strokeRadius, posY + height - 2 * strokeRadius); + // g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + ": + // ?%", + // posX + 2 * strokeRadius, posY + height - 2 * strokeRadius); } if (validWinrate || validLastWinrate) { @@ -909,7 +914,6 @@ public void onClicked(int x, int y) { // check for board click int[] boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); int moveNumber = winrateGraph.moveNumber(x, y); - if (boardCoordinates != null) { if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); if (!isPlayingAgainstLeelaz || (playerIsBlack == Lizzie.board.getData().blackToPlay)) @@ -922,6 +926,11 @@ public void onClicked(int x, int y) { if (Lizzie.config.showSubBoard && subBoardRenderer.isInside(x, y)) { Lizzie.config.toggleLargeSubBoard(); } + + if (Lizzie.config.showVariationGraph) { + Lizzie.frame.variationTree.jumpVariationTree(x, y); + } + repaint(); } @@ -1146,4 +1155,43 @@ private int drawComment(Graphics2D g, int x, int y, int w, int h, boolean full) cachedComment = comment; return cHeight; } + + public double deltaWinrate(BoardHistoryNode boardNode) { + double lastWR = -1; // winrate of the previous move + double curWR = -1; // winrate of current one + boolean validLastWinrate = false; // whether it was actually calculated + boolean validWinrate = false; + BoardData lastNode = null, currentNode = null; + final double InValidWR = 1000; // return Invalid Winrate if can't find proper one + + if (boardNode != null) currentNode = boardNode.getData(); + + if (boardNode.previous() != null) lastNode = boardNode.previous().getData(); + + if (lastNode != null && lastNode.playouts > 0) { + lastWR = lastNode.winrate; + validLastWinrate = true; + } else return InValidWR; + + if (boardNode == Lizzie.board.getHistory().getCurrentHistoryNode()) { + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + curWR = stats.maxWinrate; // winrate on this move + validWinrate = (stats.totalPlayouts > 0); // and whether it was actually calculated + if (isPlayingAgainstLeelaz + && playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) + validWinrate = false; + } + + if (!validWinrate) + if (currentNode != null && currentNode.playouts > 0) { + curWR = currentNode.winrate; + validWinrate = true; + } + + // Last move + if (validWinrate && validLastWinrate) { + return 100 - curWR - lastWR; + } + return InValidWR; + } } diff --git a/src/main/java/featurecat/lizzie/gui/VariationTree.java b/src/main/java/featurecat/lizzie/gui/VariationTree.java index aef810e2b..768322b08 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -12,11 +12,14 @@ public class VariationTree { private int XSPACING; private int DOT_DIAM = 11; // Should be odd number - private ArrayList laneUsageList; - private BoardHistoryNode curMove; + private ArrayList laneUsageList, + laneUsageList1; // laneUsageList1 is lane used to cacalate (x,y) + private BoardHistoryNode curMove, curMove1; + private int posx1, posy1, width1, height1; // Reserve a copy of varaition window axis public VariationTree() { laneUsageList = new ArrayList(); + laneUsageList1 = new ArrayList(); // for mouse click event } public void drawTree( @@ -34,11 +37,12 @@ public void drawTree( // 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 + // 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 keeps a list of how far down it is to a variation in the + // different "lanes" laneUsageList.set(lane, startNode.getData().moveNumber - 1); lane++; } @@ -48,7 +52,8 @@ public void drawTree( 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) + // At this point, lane contains the lane we should use (the main branch is in + // lane 0) BoardHistoryNode cur = startNode; int curposx = posx + lane * XSPACING; @@ -80,17 +85,19 @@ public void drawTree( // Draw all the nodes and lines in this lane (not variations) Color curcolor = g.getColor(); + g.setColor(getWinrateColor(startNode, curcolor)); if (startNode == curMove) { - g.setColor(Color.green.brighter().brighter()); - } - if (startNode.previous() != null) { - g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); - g.setColor(Color.BLACK); - g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); - } else { g.fillRect(curposx, posy, DOT_DIAM, DOT_DIAM); g.setColor(Color.BLACK); g.drawRect(curposx, posy, DOT_DIAM, DOT_DIAM); + if (cur.getData().comment != null) // Does this node have coments? + g.drawOval(curposx - 3, posy - 3, DOT_DIAM + 6, DOT_DIAM + 6); + } else { + g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); + if (cur.getData().comment != null) // Does this node have coments? + g.drawOval(curposx - 3, posy - 3, DOT_DIAM + 6, DOT_DIAM + 6); } g.setColor(curcolor); @@ -98,18 +105,27 @@ public void drawTree( while (cur.next() != null && posy + YSPACING < maxposy) { posy += YSPACING; cur = cur.next(); + // Choose color for move which winrate rise/fall dramatically + g.setColor(getWinrateColor(cur, curcolor)); if (cur == curMove) { - g.setColor(Color.green.brighter().brighter()); + g.fillRect(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawRect(curposx, posy, DOT_DIAM, DOT_DIAM); + if (cur.getData().comment != null) // Does this node have coments? + g.drawOval(curposx - 3, posy - 3, DOT_DIAM + 6, DOT_DIAM + 6); + } else { + g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); + if (cur.getData().comment != null) // Does this node have coments? + g.drawOval(curposx - 3, posy - 3, DOT_DIAM + 6, DOT_DIAM + 6); } - g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); - g.setColor(Color.BLACK); - g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); g.setColor(curcolor); g.drawLine( curposx + dotoffset, posy - 1, curposx + dotoffset, posy - YSPACING + 2 * dotoffset + 2); } - // Now we have drawn all the nodes in this variation, and has reached the bottom of this - // variation + // 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(); @@ -117,8 +133,8 @@ public void drawTree( // 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)) + // Recursion, depth of recursion will normally not be very deep (one recursion + // level for every variation that has a variation (sort of)) drawTree(g, posx, posy, curwidth, maxposy, cur.getVariation(i), i, false); } posy -= YSPACING; @@ -134,6 +150,11 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { YSPACING = (Lizzie.config.showLargeSubBoard() ? 20 : 30); XSPACING = YSPACING; + posx1 = posx; + posy1 = posy; + width1 = width; + height1 = height; // backup for mouse click event + // Draw background g.setColor(new Color(0, 0, 0, 60)); g.fillRect(posx, posy, width, height); @@ -152,6 +173,10 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { int xoffset = 30; laneUsageList.clear(); + // Draw mark of current node + g.setColor(new Color(0, 255, 0, 255)); + g.fillOval(posx + 10, middleY, DOT_DIAM, DOT_DIAM); + curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); // Is current move a variation? If so, find top of variation @@ -165,4 +190,119 @@ public void draw(Graphics2D g, int posx, int posy, int width, int height) { } drawTree(g, posx + xoffset, curposy, 0, posy + height, node, 0, true); } + + // Clone from drawTree() method but draw nothing, just caculate (x,y)->BoardNode + public BoardHistoryNode inSubtree( + int posx, + int posy, + int mouseX, + int mouseY, + int startLane, + int maxposy, + BoardHistoryNode startNode, + int variationNumber) { + // 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 < laneUsageList1.size() + && laneUsageList1.get(lane) <= startNode.getData().moveNumber + depth) { + // laneUsageList keeps a list of how far down it is to a variation in the + // different "lanes" + laneUsageList1.set(lane, startNode.getData().moveNumber - 1); + lane++; + } + if (lane >= laneUsageList1.size()) { + laneUsageList1.add(0); + } + if (variationNumber > 1) laneUsageList1.set(lane - 1, startNode.getData().moveNumber - 1); + laneUsageList1.set(lane, startNode.getData().moveNumber); + + // At this point, lane contains the lane we should use (the main branch is in + // lane 0) + + BoardHistoryNode cur = startNode; + int curposx = posx + lane * XSPACING; + int dotoffset = DOT_DIAM / 2; + + // Check first node + if (Math.abs(mouseX - curposx - dotoffset) < XSPACING / 2 + && Math.abs(mouseY - posy - dotoffset) < YSPACING / 2) return startNode; + + // Draw main line + while (cur.next() != null && posy + YSPACING < maxposy) { + posy += YSPACING; + cur = cur.next(); + if (Math.abs(mouseX - curposx - dotoffset) < XSPACING / 2 + && Math.abs(mouseY - posy - dotoffset) < YSPACING / 2) return cur; + } + // 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; + BoardHistoryNode ret; + // 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)) + if ((ret = inSubtree(posx, posy, mouseX, mouseY, curwidth, maxposy, cur.getVariation(i), i)) + != null) return ret; + } + posy -= YSPACING; + } + return null; + } + + // Clone from draw() method but draw nothing, just caculate (x,y)->BoardNode + public BoardHistoryNode inVariationTree(int mouseX, int mouseY) { + + // check if (x,y) is in the variation window + if (mouseX < posx1 || mouseX > posx1 + width1 || mouseY < posy1 || mouseY > posy1 + height1) + return null; + + // Use dense tree for saving space if large-subboard + YSPACING = (Lizzie.config.showLargeSubBoard() ? 20 : 30); + XSPACING = YSPACING; + + int middleY = posy1 + height1 / 2; + int xoffset = 30; + laneUsageList1.clear(); + + curMove1 = Lizzie.board.getHistory().getCurrentHistoryNode(); + + // Is current move a variation? If so, find top of variation + BoardHistoryNode top = BoardHistoryList.findTop(curMove1); + int curposy = middleY - YSPACING * (curMove1.getData().moveNumber - top.getData().moveNumber); + // Go to very top of tree (visible in assigned area) + BoardHistoryNode node = top; + while (curposy > posy1 + YSPACING && node.previous() != null) { + node = node.previous(); + curposy -= YSPACING; + } + return inSubtree(posx1 + xoffset, curposy, mouseX, mouseY, 0, posy1 + height1, node, 0); + } + + public int jumpVariationTree(int mouseX, int mouseY) { + BoardHistoryNode node; + node = inVariationTree(mouseX, mouseY); // check if (x,y) hit proper branch node + if (node == null) return -1; // check if any error occur + Lizzie.board.jumpToAnyPosition(node); + return 0; + } + + private Color getWinrateColor(BoardHistoryNode Node, Color current) { + double delta = Lizzie.frame.deltaWinrate(Node); + if (delta < -20) return Color.BLACK; + else if (delta < -10 && delta >= -20) return Color.RED; + else if (delta < -5 && delta >= -10) return Color.MAGENTA; + else if (delta < -2 && delta >= -5) return Color.YELLOW; + else if (delta < 5 && delta >= -2) return current; // Do nothing if it is normal move + else if (delta >= 5 && delta <= 20) return Color.BLUE; + else if (delta >= 20 && delta < 100) return Color.BLUE.brighter().brighter(); + else return current; + } } diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 2de079d57..bfb5ea359 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -606,6 +606,58 @@ public boolean nextVariation(int idx) { } } + // Jump anywhere in the board history tree. Written for mouse click navigation + public void jumpToAnyPosition(BoardHistoryNode targetNode) { + BoardHistoryNode srcNode, tarNode, prevNode; + + // tar[] to track path from target node to root node + // src[] to track path from source node to root node + int[] tar = new int[512], src = new int[512]; + + int i = 0, j = 0, k = 0; // i is index for target node, j for source node, k is working variable + + // find path from target node to root node + tarNode = targetNode; + prevNode = tarNode.previous(); + while (prevNode != null) { + for (k = 0; k < prevNode.numberOfChildren(); k++) + if (prevNode.getVariation(k) == tarNode) tar[i++] = k; + tarNode = prevNode; + prevNode = prevNode.previous(); + } + + // find path from source node to root node + srcNode = history.getCurrentHistoryNode(); + prevNode = srcNode.previous(); + while (prevNode != null) { + for (k = 0; k < prevNode.numberOfChildren(); k++) + if (prevNode.getVariation(k) == srcNode) src[j++] = k; + srcNode = prevNode; + prevNode = prevNode.previous(); + } + + // Compare tar[] with src[] , and try to find nearest branch + for (k = 0; k < Math.min(i, j); k++) { + if (tar[i - k - 1] == src[j - k - 1]) continue; + break; + } + + // Move to the target node from source node + if (k == i) { + // target node is shorter, just move back + for (int m = 0; m < j - k; m++) previousMove(); + + } else if (k == j) { + // source node is shorter, move forward + for (int m = i - k; m > 0; m--) nextVariation(tar[m - 1]); + + } else { + // in the different branchs, must move back first, then move forword + for (int m = 0; m < j - k; m++) previousMove(); + for (int m = i - k; m > 0; m--) nextVariation(tar[m - 1]); + } + } + /* * Moves to next variation (variation to the right) if possible * To move to another variation, the variation must have a move with the same move number as the current move in it.