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.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.swing.JOptionPane; import org.json.JSONException; public class Board implements LeelazListener { public static int boardSize0 = (Lizzie.config != null && Lizzie.config.config != null && Lizzie.config.config.getJSONObject("ui") != null) ? Lizzie.config.config.getJSONObject("ui").optInt("board-size", 19) : 19; public static int boardWidth = (Lizzie.config != null && Lizzie.config.config != null && Lizzie.config.config.getJSONObject("ui") != null) ? Lizzie.config.config.getJSONObject("ui").optInt("board-width", boardSize0) : boardSize0; public static int boardHeight = (Lizzie.config != null && Lizzie.config.config != null && Lizzie.config.config.getJSONObject("ui") != null) ? Lizzie.config.config.getJSONObject("ui").optInt("board-height", boardSize0) : boardSize0; private static final String alphabet = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; private BoardHistoryList history; private Stone[] capturedStones; private boolean scoreMode; private boolean analysisMode; private int playoutsAnalysis; public String allowCoords = ""; public String avoidCoords = ""; public boolean isForcing = false; public boolean isAllowing = false; public boolean isAvoding = false; public boolean isKeepingAvoid = false; // Save the node for restore move when in the branch private Optional<BoardHistoryNode> saveNode; public Board() { initialize(); } /** Initialize the board completely */ private void initialize() { capturedStones = new Stone[] {}; scoreMode = false; analysisMode = false; playoutsAnalysis = 100; saveNode = Optional.empty(); Lizzie.frame.setForceRefresh(false); history = new BoardHistoryList(BoardData.empty(boardWidth, boardHeight)); } /** * Calculates the array index of a stone stored at (x, y) * * @param x the x coordinate * @param y the y coordinate * @return the array index */ public static int getIndex(int x, int y) { return x * Board.boardHeight + y; } public static int[] getCoord(int index) { int y = index % Board.boardHeight; int x = (index - y) / Board.boardHeight; return new int[] {x, y}; } /** * Converts a named coordinate eg C16, T5, K10, etc to an x and y coordinate * * @param namedCoordinate a capitalized version of the named coordinate. Must be a valid 19x19 Go * coordinate, without I * @return an optional array of coordinates, empty for pass and resign */ public static Optional<int[]> asCoordinates(String namedCoordinate) { namedCoordinate = namedCoordinate.trim(); if (namedCoordinate.equalsIgnoreCase("pass") || namedCoordinate.equalsIgnoreCase("resign")) { return Optional.empty(); } // coordinates take the form C16 A19 Q5 K10 etc. I is not used. String reg = "([A-HJ-Z]+)(\\d+)"; Pattern p = Pattern.compile(reg); Matcher m = p.matcher(namedCoordinate); if (m.find() && m.groupCount() == 2) { int x = asDigit(m.group(1)); int y = boardHeight - Integer.parseInt(m.group(2)); return Optional.of(new int[] {x, y}); } else { reg = "\\(([\\d]+),([\\d]+)\\)"; p = Pattern.compile(reg); m = p.matcher(namedCoordinate); if (m.find() && m.groupCount() == 2) { int x = Integer.parseInt(m.group(1)); int y = Integer.parseInt(m.group(2)); // boardHeight - Integer.parseInt(m.group(2)) - 1; return Optional.of(new int[] {x, y}); } else { return Optional.empty(); } } } public static int asDigit(String name) { // coordinates take the form C16 A19 Q5 K10 etc. I is not used. int base = alphabet.length(); char names[] = name.toCharArray(); int length = names.length; if (length > 0) { int x = 0; for (int i = length - 1; i >= 0; i--) { int index = alphabet.indexOf(names[i]); if (index == -1) { return index; } x += index * Math.pow(base, length - i - 1); } return x; } else { return -1; } } public static String asName(int c) { return asName(c, true); } public void clearAnalysis() { getData().tryToClearBestMoves(); } public static String asName(int c, boolean isName) { if (boardWidth > 25 && isName) { return String.valueOf(c + 1); } StringBuilder name = new StringBuilder(); int base = alphabet.length(); int n = c; ArrayDeque<Integer> ad = new ArrayDeque<Integer>(); if (n > 0) { while (n > 0) { ad.addFirst(n < 25 && c >= 25 ? n % base - 1 : n % base); n /= base; } } else { ad.addFirst(n); } ad.forEach(i -> name.append(alphabet.charAt(i))); return name.toString(); } /** * Converts a x and y coordinate to a named coordinate eg C16, T5, K10, etc * * @param x x coordinate -- must be valid * @param y y coordinate -- must be valid * @return a string representing the coordinate */ public static String convertCoordinatesToName(int x, int y) { // coordinates take the form C16 A19 Q5 K10 etc. I is not used. if (boardWidth > 25 || boardHeight > 25) { return String.format("(%d,%d)", x, y); // boardHeight - y - 1); } else { return asName(x, false) + (boardHeight - y); } } /** * Checks if a coordinate is valid * * @param x x coordinate * @param y y coordinate * @return whether or not this coordinate is part of the board */ public static boolean isValid(int x, int y) { return x >= 0 && x < boardWidth && y >= 0 && y < boardHeight; } public static boolean isValid(int[] c) { return c != null && c.length == 2 && isValid(c[0], c[1]); } /** * Open board again when the SZ property is setup by sgf * * @param size */ public void reopen(int width, int height) { width = (width >= 2) ? width : 19; height = (height >= 2) ? height : 19; if (width != boardWidth || height != boardHeight) { boardWidth = width; boardHeight = height; Zobrist.init(); clear(); Lizzie.leelaz.boardSize(boardWidth, boardHeight); Lizzie.frame.setForceRefresh(true); } } /** * The comment. Thread safe * * @param comment the comment of stone */ public void comment(String comment) { synchronized (this) { history.getData().comment = comment; } } /** * Update the move number. Thread safe * * @param moveNumber the move number of stone */ public void moveNumber(int moveNumber) { synchronized (this) { BoardData data = history.getData(); if (data.lastMove.isPresent()) { int[] moveNumberList = history.getMoveNumberList(); moveNumberList[Board.getIndex(data.lastMove.get()[0], data.lastMove.get()[1])] = moveNumber; Optional<BoardHistoryNode> 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.get()[0], nodeData.lastMove.get()[1])] = moveNumber; } node = node.get().previous(); } } } } public int moveNumberByCoord(int[] coord) { int moveNumber = 0; if (Lizzie.board.isValid(coord)) { int index = Lizzie.board.getIndex(coord[0], coord[1]); if (Lizzie.board.getHistory().getStones()[index] != Stone.EMPTY) { BoardHistoryNode cur = Lizzie.board.getHistory().getCurrentHistoryNode(); moveNumber = cur.getData().moveNumberList[index]; if (!cur.isMainTrunk()) { if (moveNumber > 0) { moveNumber = cur.getData().moveNumber - cur.getData().moveMNNumber + moveNumber; } else { BoardHistoryNode p = cur.firstParentWithVariations().orElse(cur); while (p != cur && moveNumber == 0) { moveNumber = p.getData().moveNumberList[index]; if (moveNumber > 0) { BoardHistoryNode topOfTop = p.firstParentWithVariations().orElse(p); if (topOfTop != p) { moveNumber = p.getData().moveNumber - p.getData().moveMNNumber + moveNumber; } } else { cur = p; p = cur.firstParentWithVariations().orElse(cur); } } } } } } return moveNumber; } /** * Add a stone to the board representation. Thread safe * * @param x x coordinate * @param y y coordinate * @param color the type of stone to place */ public void addStone(int x, int y, Stone color) { synchronized (this) { if (!isValid(x, y) || history.getStones()[getIndex(x, y)] != Stone.EMPTY) return; Stone[] stones = history.getData().stones; Zobrist zobrist = history.getData().zobrist; // set the stone at (x, y) to color stones[getIndex(x, y)] = color; zobrist.toggleStone(x, y, color); Lizzie.frame.refresh(); } } /** * Remove a stone from the board representation. Thread safe * * @param x x coordinate * @param y y coordinate * @param color the type of stone to place */ public void removeStone(int x, int y, Stone color) { synchronized (this) { if (!isValid(x, y) || history.getStones()[getIndex(x, y)] == Stone.EMPTY) return; BoardData data = history.getData(); Stone[] stones = data.stones; Zobrist zobrist = data.zobrist; // set the stone at (x, y) to empty Stone oriColor = stones[getIndex(x, y)]; stones[getIndex(x, y)] = Stone.EMPTY; zobrist.toggleStone(x, y, oriColor); data.moveNumberList[Board.getIndex(x, y)] = 0; Lizzie.frame.refresh(); } } /** * Add a key and value to node * * @param key * @param value */ public void addNodeProperty(String key, String value) { synchronized (this) { history.getData().addProperty(key, value); if ("MN".equals(key)) { moveNumber(Integer.parseInt(value)); } } } /** * Add a keys and values to node * * @param properties */ public void addNodeProperties(Map<String, String> properties) { synchronized (this) { history.getData().addProperties(properties); } } public void setKomi(double komi) { getHistory().getGameInfo().setKomi(komi); Lizzie.leelaz.komi(komi); } /** * The pass. Thread safe * * @param color the type of pass */ public void pass(Stone color) { pass(color, false, false, false); } public void pass(Stone color, boolean newBranch) { pass(color, newBranch, false, false); } public void pass(Stone color, boolean newBranch, boolean dummy) { pass(color, newBranch, dummy, false); } /** * The pass. Thread safe * * @param color the type of pass * @param newBranch add a new branch */ public void pass(Stone color, boolean newBranch, boolean dummy, boolean changeMove) { synchronized (this) { // check to see if this move is being replayed in history if (history.getNext().map(n -> !n.lastMove.isPresent()).orElse(false) && !newBranch) { // this is the next move in history. Just increment history so that we don't erase the // redo's history.next(); Lizzie.leelaz.playMove(color, "pass"); if (Lizzie.frame.isPlayingAgainstLeelaz) Lizzie.leelaz.genmove((history.isBlacksTurn() ? "B" : "W")); return; } Stone[] stones = history.getStones().clone(); Zobrist zobrist = history.getZobrist(); int moveNumber = history.getMoveNumber() + 1; int[] moveNumberList = newBranch && history.getNext(true).isPresent() ? new int[Board.boardWidth * Board.boardHeight] : history.getMoveNumberList().clone(); // build the new game state BoardData newState = new BoardData( stones, Optional.empty(), color, color.equals(Stone.WHITE), zobrist, moveNumber, moveNumberList, history.getData().blackCaptures, history.getData().whiteCaptures, 0.0, 0, 0.0); newState.dummy = dummy; // update leelaz with pass if (!Lizzie.leelaz.isInputCommand) Lizzie.leelaz.playMove(color, "pass"); if (Lizzie.frame.isPlayingAgainstLeelaz) Lizzie.leelaz.genmove((history.isBlacksTurn() ? "W" : "B")); // update history with pass history.addOrGoto(newState, newBranch, changeMove); Lizzie.frame.refresh(); } } /** overloaded method for pass(), chooses color in an alternating pattern */ public void pass() { pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); } /** * Places a stone onto the board representation. Thread safe * * @param x x coordinate * @param y y coordinate * @param color the type of stone to place */ public void place(int x, int y, Stone color) { place(x, y, color, false); } public void place(int x, int y, Stone color, boolean newBranch) { place(x, y, color, newBranch, false); } /** * Places a stone onto the board representation. Thread safe * * @param x x coordinate * @param y y coordinate * @param color the type of stone to place * @param newBranch add a new branch */ public void place(int x, int y, Stone color, boolean newBranch, boolean changeMove) { if (Lizzie.frame.isEstimating) { Lizzie.frame.noEstimateByZen(false); } synchronized (this) { if (scoreMode) { // Mark clicked stone as dead Stone[] stones = history.getStones(); toggleLiveStatus(capturedStones, x, y); return; } if (!isValid(x, y) || (history.getStones()[getIndex(x, y)] != Stone.EMPTY && !newBranch)) return; updateWinrate(); double nextWinrate = -100; if (history.getData().winrate >= 0) nextWinrate = 100 - history.getData().winrate; double nextScoreMean = -history.getData().scoreMean; // check to see if this coordinate is being replayed in history Optional<int[]> nextLast = history.getNext().flatMap(n -> n.lastMove); if (nextLast.isPresent() && nextLast.get()[0] == x && nextLast.get()[1] == y && !newBranch && !changeMove) { // this is the next coordinate in history. Just increment history so that we don't erase the // redo's Lizzie.leelaz.beginModifyingBoard(); history.next(); // should be opposite from the bottom case if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack != getData().blackToPlay) { Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); } Lizzie.leelaz.endModifyingBoard(); return; } // load a copy of the data at the current node of history Stone[] stones = history.getStones().clone(); Zobrist zobrist = history.getZobrist(); Optional<int[]> lastMove = Optional.of(new int[] {x, y}); int moveNumber = history.getMoveNumber() + 1; int moveMNNumber = history.getMoveMNNumber() > -1 && !newBranch ? history.getMoveMNNumber() + 1 : -1; int[] moveNumberList = newBranch && history.getNext(true).isPresent() ? new int[Board.boardWidth * Board.boardHeight] : history.getMoveNumberList().clone(); moveNumberList[Board.getIndex(x, y)] = moveMNNumber > -1 ? moveMNNumber : moveNumber; // set the stone at (x, y) to color stones[getIndex(x, y)] = color; zobrist.toggleStone(x, y, color); // remove enemy stones int capturedStones = 0; capturedStones += removeDeadChain(x + 1, y, color.opposite(), stones, zobrist); capturedStones += removeDeadChain(x, y + 1, color.opposite(), stones, zobrist); capturedStones += removeDeadChain(x - 1, y, color.opposite(), stones, zobrist); capturedStones += removeDeadChain(x, y - 1, color.opposite(), stones, zobrist); // check to see if the player made a suicidal coordinate int isSuicidal = removeDeadChain(x, y, color, stones, zobrist); for (int i = 0; i < boardWidth * boardHeight; i++) { if (stones[i].equals(Stone.EMPTY)) { moveNumberList[i] = 0; } } int bc = history.getData().blackCaptures; int wc = history.getData().whiteCaptures; if (color.isBlack()) bc += capturedStones; else wc += capturedStones; BoardData newState = new BoardData( stones, lastMove, color, color.equals(Stone.WHITE), zobrist, moveNumber, moveNumberList, bc, wc, nextWinrate, 0, nextScoreMean); newState.moveMNNumber = moveMNNumber; newState.dummy = false; // don't make this coordinate if it is suicidal or violates superko if (isSuicidal > 0 || history.violatesKoRule(newState)) return; // update leelaz with board position Lizzie.leelaz.beginModifyingBoard(); if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack == getData().blackToPlay) { Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); } else if (!Lizzie.frame.isPlayingAgainstLeelaz && !Lizzie.leelaz.isInputCommand) { Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); } // update history with this coordinate history.addOrGoto(newState, newBranch, changeMove); Lizzie.leelaz.endModifyingBoard(); Lizzie.frame.refresh(); } } /** * overloaded method for place(), chooses color in an alternating pattern * * @param x x coordinate * @param y y coordinate */ public void place(int x, int y) { place(x, y, history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); } /** * overloaded method for place. To be used by the LeelaZ engine. Color is then assumed to be * alternating * * @param namedCoordinate the coordinate to place a stone, */ public void place(String namedCoordinate) { Optional<int[]> coords = asCoordinates(namedCoordinate); if (coords.isPresent()) { place(coords.get()[0], coords.get()[1]); } else { pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); } } /** for handicap */ public void flatten() { Stone[] stones = history.getStones(); boolean blackToPlay = history.isBlacksTurn(); Zobrist zobrist = history.getZobrist().clone(); BoardHistoryList oldHistory = history; history = new BoardHistoryList( new BoardData( stones, Optional.empty(), Stone.EMPTY, blackToPlay, zobrist, 0, new int[boardWidth * boardHeight], 0, 0, 0.0, 0, 0.0)); history.setGameInfo(oldHistory.getGameInfo()); } /** * Removes a chain if it has no liberties * * @param x x coordinate -- needn't be valid * @param y y coordinate -- needn't be valid * @param color the color of the chain to remove * @param stones the stones array to modify * @param zobrist the zobrist object to modify * @return number of removed stones */ public static int removeDeadChain(int x, int y, Stone color, Stone[] stones, Zobrist zobrist) { if (!isValid(x, y) || stones[getIndex(x, y)] != color) return 0; boolean hasLiberties = hasLibertiesHelper(x, y, color, stones); // either remove stones or reset what hasLibertiesHelper does to the board return cleanupHasLibertiesHelper(x, y, color.recursed(), stones, zobrist, !hasLiberties); } /** * Recursively determines if a chain has liberties. Alters the state of stones, so it must be * counteracted * * @param x x coordinate -- needn't be valid * @param y y coordinate -- needn't be valid * @param color the color of the chain to be investigated * @param stones the stones array to modify * @return whether or not this chain has liberties */ private static boolean hasLibertiesHelper(int x, int y, Stone color, Stone[] stones) { if (!isValid(x, y)) return false; if (stones[getIndex(x, y)] == Stone.EMPTY) return true; // a liberty was found else if (stones[getIndex(x, y)] != color) return false; // we are either neighboring an enemy stone, or one we've already recursed on // set this index to be the recursed color to keep track of where we've already searched stones[getIndex(x, y)] = color.recursed(); // set removeDeadChain to true if any recursive calls return true. Recurse in all 4 directions boolean hasLiberties = hasLibertiesHelper(x + 1, y, color, stones) || hasLibertiesHelper(x, y + 1, color, stones) || hasLibertiesHelper(x - 1, y, color, stones) || hasLibertiesHelper(x, y - 1, color, stones); return hasLiberties; } /** * cleans up what hasLibertyHelper does to the board state * * @param x x coordinate -- needn't be valid * @param y y coordinate -- needn't be valid * @param color color to clean up. Must be a recursed stone type * @param stones the stones array to modify * @param zobrist the zobrist object to modify * @param removeStones if true, we will remove all these stones. otherwise, we will set them to * their unrecursed version * @return number of removed stones */ private static int cleanupHasLibertiesHelper( int x, int y, Stone color, Stone[] stones, Zobrist zobrist, boolean removeStones) { int removed = 0; if (!isValid(x, y) || stones[getIndex(x, y)] != color) return 0; stones[getIndex(x, y)] = removeStones ? Stone.EMPTY : color.unrecursed(); if (removeStones) { zobrist.toggleStone(x, y, color.unrecursed()); removed = 1; } // use the flood fill algorithm to replace all adjacent recursed stones removed += cleanupHasLibertiesHelper(x + 1, y, color, stones, zobrist, removeStones); removed += cleanupHasLibertiesHelper(x, y + 1, color, stones, zobrist, removeStones); removed += cleanupHasLibertiesHelper(x - 1, y, color, stones, zobrist, removeStones); removed += cleanupHasLibertiesHelper(x, y - 1, color, stones, zobrist, removeStones); return removed; } /** * Get current board state * * @return the stones array corresponding to the current board state */ public Stone[] getStones() { return history.getStones(); } /** * Shows where to mark the last coordinate * * @return the last played stone, if any, Optional.empty otherwise */ public Optional<int[]> getLastMove() { return history.getLastMove(); } /** * Gets the move played in this position * * @return the next move, if any, Optional.empty otherwise */ public Optional<int[]> getNextMove() { return history.getNextMove(); } /** * Gets current board move number * * @return the int array corresponding to the current board move number */ public int[] getMoveNumberList() { return history.getMoveNumberList(); } /** Goes to the next coordinate, thread safe */ public boolean nextMove() { if (Lizzie.frame.isEstimating) { Lizzie.frame.noEstimateByZen(false); } synchronized (this) { updateWinrate(); Lizzie.leelaz.beginModifyingBoard(); if (history.next().isPresent()) { // update leelaz board position, before updating to next node Optional<int[]> 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(), "pass"); } Lizzie.frame.refresh(); Lizzie.leelaz.endModifyingBoard(); return true; } Lizzie.leelaz.endModifyingBoard(); return false; } } /** * Goes to the next coordinate, thread safe * * @param fromBackChildren by back children branch * @return true when has next variation */ public boolean nextMove(int fromBackChildren) { synchronized (this) { updateWinrate(); return nextVariation(fromBackChildren); } } /** Save the move number for restore If in the branch, save the back routing from children */ public void saveMoveNumber() { BoardHistoryNode currentNode = history.getCurrentHistoryNode(); int curMoveNum = currentNode.getData().moveNumber; if (curMoveNum > 0) { if (!currentNode.isMainTrunk()) { // If in branch, save the back routing from children saveBackRouting(currentNode); } goToMoveNumber(0); } saveNode = Optional.of(currentNode); } /** Save the back routing from children */ public void saveBackRouting(BoardHistoryNode node) { Optional<BoardHistoryNode> 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() { saveNode.ifPresent(n -> restoreMoveNumber(n)); } /** Restore move number by node */ public void restoreMoveNumber(BoardHistoryNode node) { Stone[] stones = history.getStones(); for (int i = 0; i < stones.length; i++) { Stone stone = stones[i]; if (stone.isBlack() || stone.isWhite()) { int y = i % Board.boardWidth; int x = (i - y) / Board.boardHeight; Lizzie.leelaz.playMove(stone, convertCoordinatesToName(x, y)); } } int moveNumber = node.getData().moveNumber; if (moveNumber > 0) { if (node.isMainTrunk()) { goToMoveNumber(moveNumber); } else { // If in Branch, restore by the back routing goToMoveNumberByBackChildren(moveNumber); } } } /** Go to move number by back routing from children when in branch */ public void goToMoveNumberByBackChildren(int moveNumber) { int delta = moveNumber - history.getMoveNumber(); for (int i = 0; i < Math.abs(delta); i++) { BoardHistoryNode currentNode = history.getCurrentHistoryNode(); if (currentNode.hasVariations() && delta > 0) { nextMove(currentNode.getFromBackChildren()); } else { if (!(delta > 0 ? nextMove() : previousMove())) { break; } } } } public boolean goToMoveNumber(int moveNumber) { return goToMoveNumberHelper(moveNumber, false); } public boolean goToMoveNumberWithinBranch(int moveNumber) { return goToMoveNumberHelper(moveNumber, true); } public boolean goToMoveNumberBeyondBranch(int moveNumber) { // Go to main trunk if current branch is shorter than moveNumber. if (moveNumber > history.currentBranchLength() && moveNumber <= history.mainTrunkLength()) { goToMoveNumber(0); } return goToMoveNumber(moveNumber); } public boolean goToMoveNumberHelper(int moveNumber, boolean withinBranch) { int delta = moveNumber - history.getMoveNumber(); boolean moved = false; for (int i = 0; i < Math.abs(delta); i++) { if (withinBranch && delta < 0) { BoardHistoryNode currentNode = history.getCurrentHistoryNode(); if (!currentNode.isFirstChild()) { break; } } if (!(delta > 0 ? nextMove() : previousMove())) { break; } moved = true; } return moved; } /** Goes to the next variation, thread safe */ public boolean nextVariation(int idx) { synchronized (this) { // Don't update winrate here as this is usually called when jumping between variations if (history.nextVariation(idx).isPresent()) { // Update leelaz board position, before updating to next node Optional<int[]> 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(), "pass"); } Lizzie.frame.refresh(); return true; } return false; } } /** * 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 */ private List<BoardHistoryNode> branchCandidates(BoardHistoryNode node) { int targetDepth = node.getData().moveNumber; Stream<BoardHistoryNode> nodes = singletonList(history.root()).stream(); for (int i = 0; i < targetDepth; i++) { nodes = nodes.flatMap(n -> n.getVariations().stream()); } LinkedList<BoardHistoryNode> 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; } } /** * 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<BoardHistoryNode> targetNode = Optional.empty(); boolean foundIt = false; for (BoardHistoryNode candidate : branchCandidates(currentNode)) { if (foundIt) { targetNode = Optional.of(candidate); break; } else if (candidate == currentNode) { foundIt = true; } } if (targetNode.isPresent()) { moveToAnyPosition(targetNode.get()); } 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. * * <p>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 currentNode = history.getCurrentHistoryNode(); Optional<BoardHistoryNode> targetNode = Optional.empty(); for (BoardHistoryNode candidate : branchCandidates(currentNode)) { if (candidate == currentNode) { break; } else { targetNode = Optional.of(candidate); } } if (targetNode.isPresent()) { moveToAnyPosition(targetNode.get()); } return targetNode.isPresent(); } } /** * Jump anywhere in the board history tree. * * @param targetNode history node to be located * @return void */ public void moveToAnyPosition(BoardHistoryNode targetNode) { List<Integer> targetParents = new ArrayList<Integer>(); List<Integer> sourceParents = new ArrayList<Integer>(); BiConsumer<BoardHistoryNode, List<Integer>> populateParent = (node, parentList) -> { Optional<BoardHistoryNode> 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(); } }; // 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; } } // 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)); } } public void moveBranchUp() { synchronized (this) { history.getCurrentHistoryNode().topOfBranch().moveUp(); } } public void moveBranchDown() { synchronized (this) { history.getCurrentHistoryNode().topOfBranch().moveDown(); } } public void deleteMove() { synchronized (this) { BoardHistoryNode currentNode = history.getCurrentHistoryNode(); if (currentNode.next(true).isPresent()) { // Will delete more than one move, ask for confirmation int ret = JOptionPane.showConfirmDialog( null, "This will delete all moves and branches after this move", "Delete", JOptionPane.OK_CANCEL_OPTION); if (ret != JOptionPane.OK_OPTION) { return; } } if (currentNode.previous().isPresent()) { BoardHistoryNode pre = currentNode.previous().get(); previousMove(); int idx = pre.indexOfNode(currentNode); pre.deleteChild(idx); } else { clear(); // Clear the board if we're at the top } } } public void deleteBranch() { int originalMoveNumber = history.getMoveNumber(); undoToChildOfPreviousWithVariation(); int moveNumberBeforeOperation = history.getMoveNumber(); deleteMove(); boolean canceled = (history.getMoveNumber() == moveNumberBeforeOperation); if (canceled) { goToMoveNumber(originalMoveNumber); } } public BoardData getData() { return history.getData(); } public BoardHistoryList getHistory() { return history; } /** Clears all history and starts over from empty board. */ public void clear() { Lizzie.leelaz.clear(); Lizzie.frame.resetTitle(); Lizzie.frame.clear(); initialize(); setKomi(getHistory().getGameInfo().getKomi()); } /** Goes to the previous coordinate, thread safe */ public boolean previousMove() { if (Lizzie.frame.isEstimating) { Lizzie.frame.noEstimateByZen(false); } synchronized (this) { if (inScoreMode()) setScoreMode(false); updateWinrate(); Lizzie.leelaz.beginModifyingBoard(); if (history.previous().isPresent()) { Lizzie.leelaz.undo(); Lizzie.frame.refresh(); Lizzie.leelaz.endModifyingBoard(); return true; } Lizzie.leelaz.endModifyingBoard(); return false; } } public boolean undoToChildOfPreviousWithVariation() { BoardHistoryNode start = history.getCurrentHistoryNode(); Optional<BoardHistoryNode> goal = start.findChildOfPreviousWithVariation(); if (!goal.isPresent() || start == goal.get()) return false; while (history.getCurrentHistoryNode() != goal.get() && previousMove()) ; return true; } public void setScoreMode(boolean on) { if (on) { // load a copy of the data at the current node of history capturedStones = history.getStones().clone(); } else { capturedStones = new Stone[] {}; } scoreMode = on; } /* * Starting at position stonex, stoney, remove all stones with same color within an area bordered by stones * of opposite color (AKA captured stones) */ private void toggleLiveStatus(Stone[] stones, int stonex, int stoney) { Stone[] shdwstones = stones.clone(); Stone toggle = stones[getIndex(stonex, stoney)]; Stone toggleToo; switch (toggle) { case BLACK: toggleToo = Stone.BLACK_CAPTURED; break; case BLACK_CAPTURED: toggleToo = Stone.BLACK; break; case WHITE: toggleToo = Stone.WHITE_CAPTURED; break; case WHITE_CAPTURED: toggleToo = Stone.WHITE; break; default: return; } boolean lastup, lastdown; // This is using a flood fill algorithm that uses a Q instead of being recursive Queue<int[]> visitQ = new ArrayDeque<>(); visitQ.add(new int[] {stonex, stoney}); while (!visitQ.isEmpty()) { int[] curpos = visitQ.remove(); int x = curpos[0]; int y = curpos[1]; // Move all the way left while (x > 0 && (stones[getIndex(x - 1, y)] == Stone.EMPTY || stones[getIndex(x - 1, y)] == toggle)) { x--; } lastup = lastdown = false; // Find all stones within empty area line by line (new lines added to Q) while (x < boardWidth) { if (shdwstones[getIndex(x, y)] == Stone.EMPTY) { shdwstones[getIndex(x, y)] = Stone.DAME; // Too mark that it has been visited } else if (stones[getIndex(x, y)] == toggle) { stones[getIndex(x, y)] = toggleToo; } else { break; } // Check above if (y - 1 >= 0 && (shdwstones[getIndex(x, y - 1)] == Stone.EMPTY || stones[getIndex(x, y - 1)] == toggle)) { if (!lastup) visitQ.add(new int[] {x, y - 1}); lastup = true; } else { lastup = false; } // Check below if (y + 1 < boardHeight && (shdwstones[getIndex(x, y + 1)] == Stone.EMPTY || stones[getIndex(x, y + 1)] == toggle)) { if (!lastdown) visitQ.add(new int[] {x, y + 1}); lastdown = true; } else { lastdown = false; } x++; } } Lizzie.frame.refresh(); } /* * Check if a point on the board is empty or contains a captured stone */ private boolean emptyOrCaptured(Stone[] stones, int x, int y) { int curidx = getIndex(x, y); if (stones[curidx] == Stone.EMPTY || stones[curidx] == Stone.BLACK_CAPTURED || stones[curidx] == Stone.WHITE_CAPTURED) return true; return false; } /* * Starting from startx, starty, mark all empty points within area as either white, black or dame. * If two stones of opposite color (neither marked as captured) is encountered, the area is dame. * * @return A stone with color white, black or dame */ private Stone markEmptyArea(Stone[] stones, int startx, int starty) { Stone[] shdwstones = stones.clone(); // Found will either be black or white, or dame if both are found in area Stone found = Stone.EMPTY; boolean lastup, lastdown; Queue<int[]> visitQ = new ArrayDeque<>(); visitQ.add(new int[] {startx, starty}); Deque<Integer> allPoints = new ArrayDeque<>(); // Check one line at the time, new lines added to visitQ while (!visitQ.isEmpty()) { int[] curpos = visitQ.remove(); int x = curpos[0]; int y = curpos[1]; if (!emptyOrCaptured(shdwstones, x, y)) { continue; } // Move all the way left while (x > 0 && emptyOrCaptured(shdwstones, x - 1, y)) { x--; } // Are we on the border, or do we have a stone to the left? if (x > 0 && shdwstones[getIndex(x - 1, y)] != found) { if (found == Stone.EMPTY) found = shdwstones[getIndex(x - 1, y)]; else found = Stone.DAME; } lastup = lastdown = false; while (x < boardWidth && emptyOrCaptured(shdwstones, x, y)) { // Check above if (y - 1 >= 0 && shdwstones[getIndex(x, y - 1)] != Stone.DAME) { if (emptyOrCaptured(shdwstones, x, y - 1)) { if (!lastup) visitQ.add(new int[] {x, y - 1}); lastup = true; } else { lastup = false; if (found != shdwstones[getIndex(x, y - 1)]) { if (found == Stone.EMPTY) { found = shdwstones[getIndex(x, y - 1)]; } else { found = Stone.DAME; } } } } // Check below if (y + 1 < boardHeight && shdwstones[getIndex(x, y + 1)] != Stone.DAME) { if (emptyOrCaptured(shdwstones, x, y + 1)) { if (!lastdown) { visitQ.add(new int[] {x, y + 1}); } lastdown = true; } else { lastdown = false; if (found != shdwstones[getIndex(x, y + 1)]) { if (found == Stone.EMPTY) { found = shdwstones[getIndex(x, y + 1)]; } else { found = Stone.DAME; } } } } // Add current stone to empty area and mark as visited if (shdwstones[getIndex(x, y)] == Stone.EMPTY) allPoints.add(getIndex(x, y)); // Use dame stone to mark as visited shdwstones[getIndex(x, y)] = Stone.DAME; x++; } // At this point x is at the edge of the board or on a stone if (x < boardWidth && shdwstones[getIndex(x, y)] != found) { if (found == Stone.EMPTY) found = shdwstones[getIndex(x, y)]; else found = Stone.DAME; } } // Finally mark all points as black or white captured if they were surronded by white or black if (found == Stone.WHITE) found = Stone.WHITE_POINT; else if (found == Stone.BLACK) found = Stone.BLACK_POINT; // else found == DAME and will be set as this. while (!allPoints.isEmpty()) { int idx = allPoints.remove(); stones[idx] = found; } return found; } /* * Mark all empty points on board as black point, white point or dame */ public Stone[] scoreStones() { Stone[] scoreStones = capturedStones.clone(); for (int i = 0; i < boardWidth; i++) { for (int j = 0; j < boardHeight; j++) { if (scoreStones[getIndex(i, j)] == Stone.EMPTY) { markEmptyArea(scoreStones, i, j); } } } return scoreStones; } /* * Count score for whole board, including komi and captured stones */ public double[] getScore(Stone[] scoreStones) { double score[] = new double[] { getData().blackCaptures, getData().whiteCaptures + getHistory().getGameInfo().getKomi() }; for (int i = 0; i < boardWidth; i++) { for (int j = 0; j < boardHeight; j++) { switch (scoreStones[getIndex(i, j)]) { case BLACK_POINT: score[0]++; break; case BLACK_CAPTURED: score[1] += 2; break; case WHITE_POINT: score[1]++; break; case WHITE_CAPTURED: score[0] += 2; break; } } } return score; } public boolean inAnalysisMode() { return analysisMode; } public boolean inScoreMode() { return scoreMode; } public void toggleAnalysis() { if (analysisMode) { Lizzie.leelaz.removeListener(this); analysisMode = false; } else { if (!getNextMove().isPresent()) return; String answer = JOptionPane.showInputDialog( "# playouts for analysis (e.g. 100 (fast) or 50000 (slow)): "); try { playoutsAnalysis = Integer.parseInt(answer); } catch (NumberFormatException err) { System.out.println("Not a valid number"); return; } Lizzie.leelaz.addListener(this); analysisMode = true; if (!Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); } } public void bestMoveNotification(List<MoveData> bestMoves) { if (analysisMode) { boolean isSuccessivePass = (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().isPresent() || isSuccessivePass) { // Reached the end... toggleAnalysis(); Lizzie.leelaz.togglePonder(); } 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. int sum = 0; for (MoveData move : bestMoves) { sum += move.playouts; } if (sum >= playoutsAnalysis) { nextMove(); } } } } public void autosave() { if (autosaveToMemory()) { try { Lizzie.config.persist(); } catch (IOException err) { } } } public boolean autosaveToMemory() { try { String sgf = SGFParser.saveToString(); if (sgf.equals(Lizzie.config.persisted.getString("autosave"))) { return false; } Lizzie.config.persisted.put("autosave", sgf); } catch (Exception err) { // IOException or JSONException return false; } return true; } public void resumePreviousGame() { try { SGFParser.loadFromString(Lizzie.config.persisted.getString("autosave")); while (nextMove()) ; } catch (JSONException err) { } } public void updateWinrate() { Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); if (stats.maxWinrate >= 0 && stats.totalPlayouts > history.getData().getPlayouts()) { history.getData().winrate = stats.maxWinrate; if (Lizzie.leelaz.isKataGo) { history.getData().scoreMean = stats.maxScoreMean; } // we won't set playouts here. but setting winrate is ok... it shows the user that we are // computing. i think its fine. } } public boolean changeMove(int moveNumber, String changeMove) { Optional<int[]> changeCoord = asCoordinates(changeMove); if ("pass".equalsIgnoreCase(changeMove)) { changeMove(moveNumber, (int[]) null); return true; } else if ("swap".equalsIgnoreCase(changeMove)) { changeMove(moveNumber, null, true); return true; } else if (changeCoord.isPresent() && Board.isValid(changeCoord.get()[0], changeCoord.get()[1])) { changeCoord.map(c -> changeMove(moveNumber, c, false)); return true; } else { return false; } } public boolean changeMove(int moveNumber, int[] coords) { return changeMove(moveNumber, coords, false); } public boolean changeMove(int moveNumber, int[] coords, boolean swapColorOnly) { if (moveNumber <= 0) { return false; } int endMoveNumber = history.getEnd().moveNumberOfNode(); if (moveNumber > endMoveNumber) { return false; } int currentMoveNumber = history.getMoveNumber(); goToMoveNumber(moveNumber - 1); Optional<BoardHistoryNode> changeNode = history.getCurrentHistoryNode().next(); Optional<BoardHistoryNode> relink = changeNode.flatMap(n -> n.next()); // Change Move if (swapColorOnly) { if (changeNode.isPresent()) { Optional<int[]> c = changeNode.get().getData().lastMove; if (c.isPresent() && isValid(c.get())) { changeNode .map(n -> n.getData()) .map(d -> d.lastMoveColor) .ifPresent(s -> place(c.get()[0], c.get()[1], s.opposite(), false, true)); } else { pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE, false, false, true); } } } else { if (coords != null && Board.isValid(coords[0], coords[1])) { place( coords[0], coords[1], history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE, false, true); } else { pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE, false, false, true); } } Optional<BoardHistoryNode> node = relink; while (node.isPresent()) { Optional<int[]> lastMove = node.get().getData().lastMove; if (lastMove.isPresent()) { int[] m = lastMove.get(); if (Board.isValid(m[0], m[1])) { place( m[0], m[1], swapColorOnly ? node.get().getData().lastMoveColor : history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE, false, true); } else { pass( swapColorOnly ? node.get().getData().lastMoveColor : history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE, false, false, true); } node = node.get().next(); } } goToMoveNumber(currentMoveNumber); return true; } public ArrayList<MoveList> getMoveList() { ArrayList<MoveList> moveList = new ArrayList<MoveList>(); Optional<BoardHistoryNode> node = history.getCurrentHistoryNode().now(); Optional<int[]> passstep = Optional.empty(); while (node.isPresent()) { Optional<int[]> lastMove = node.get().getData().lastMove; if (lastMove == passstep) { MoveList move = new MoveList(); move.isPass = true; move.isBlack = node.get().getData().lastMoveColor.isBlack(); moveList.add(move); node = node.get().previous(); } else { if (lastMove.isPresent()) { int[] n = lastMove.get(); MoveList move = new MoveList(); move.x = n[0]; move.y = n[1]; move.isPass = false; move.isBlack = node.get().getData().lastMoveColor.isBlack(); move.moveNum = node.get().getData().moveNumber; moveList.add(move); node = node.get().previous(); } } } moveList.remove(moveList.size() - 1); return moveList; } public boolean setAvoidCoords(int x, int y) { Optional<int[]> boardCoordinates = Lizzie.frame.convertScreenToCoordinates(x, y); if (boardCoordinates.isPresent()) { if (isCoordsEmpty(boardCoordinates.get()[0], boardCoordinates.get()[1])) { if (avoidCoords == "") avoidCoords = convertCoordinatesToName(boardCoordinates.get()[0], boardCoordinates.get()[1]); else avoidCoords = avoidCoords + "," + convertCoordinatesToName(boardCoordinates.get()[0], boardCoordinates.get()[1]); return true; } else return false; } return false; } public boolean setAllowCoords(int x, int y) { Optional<int[]> boardCoordinates = Lizzie.frame.convertScreenToCoordinates(x, y); if (boardCoordinates.isPresent()) { if (isCoordsEmpty(boardCoordinates.get()[0], boardCoordinates.get()[1])) { allowCoords = convertCoordinatesToName(boardCoordinates.get()[0], boardCoordinates.get()[1]); return true; } else return false; } return false; } public boolean addAllowCoords(int x, int y) { Optional<int[]> boardCoordinates = Lizzie.frame.convertScreenToCoordinates(x, y); if (boardCoordinates.isPresent()) { if (isCoordsEmpty(boardCoordinates.get()[0], boardCoordinates.get()[1])) { allowCoords = allowCoords + "," + convertCoordinatesToName(boardCoordinates.get()[0], boardCoordinates.get()[1]); return true; } else return false; } return false; } public boolean isCoordsEmpty(int x, int y) { if (history.getStones()[getIndex(x, y)] != Stone.EMPTY) { return false; } return true; } public boolean setAsMainBranch() { if (history.getCurrentHistoryNode().isMainTrunk()) return false; BoardHistoryNode topNode = history.getCurrentHistoryNode().topOfFatherBranch(); BoardHistoryNode mainNode = history.getCurrentHistoryNode().nodeBeforeTopOfFatherBranch(); BoardHistoryNode oldFirstVar = mainNode.variations.get(0); for (int i = 0; i < mainNode.variations.size(); i++) { if (mainNode.variations.get(i) == topNode) { mainNode.variations.remove(i); mainNode.variations.add(i, oldFirstVar); mainNode.variations.remove(0); mainNode.variations.add(0, topNode); return true; } } return false; } }