diff --git a/src/main/java/wagner/stephanie/lizzie/Config.java b/src/main/java/wagner/stephanie/lizzie/Config.java index 70862ad5a..a404132c4 100644 --- a/src/main/java/wagner/stephanie/lizzie/Config.java +++ b/src/main/java/wagner/stephanie/lizzie/Config.java @@ -148,6 +148,7 @@ private JSONObject createDefaultConfig() { leelaz.put("max-analyze-time-minutes", 2); leelaz.put("max-game-thinking-time-seconds", 2); leelaz.put("print-comms", false); + leelaz.put("analyze-update-interval-centisec", 10); config.put("leelaz", leelaz); diff --git a/src/main/java/wagner/stephanie/lizzie/analysis/Leelaz.java b/src/main/java/wagner/stephanie/lizzie/analysis/Leelaz.java index 360dfdafc..5cf64dc77 100644 --- a/src/main/java/wagner/stephanie/lizzie/analysis/Leelaz.java +++ b/src/main/java/wagner/stephanie/lizzie/analysis/Leelaz.java @@ -1,11 +1,9 @@ package wagner.stephanie.lizzie.analysis; -import jdk.nashorn.internal.runtime.arrays.ArrayIndex; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import wagner.stephanie.lizzie.Lizzie; -import wagner.stephanie.lizzie.analysis.Leelaz.WinrateStats; import wagner.stephanie.lizzie.rules.Stone; import javax.swing.*; @@ -18,6 +16,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.Timer; import java.util.TimerTask; +import java.util.Collections; /** * an interface with leelaz.exe go engine. Can be adapted for GTP, but is specifically designed for GCP's Leela Zero. @@ -28,6 +27,8 @@ public class Leelaz { private static final long MINUTE = 60 * 1000; // number of milliseconds in a minute // private static final long SECOND = 1000; private long maxAnalyzeTimeMillis;//, maxThinkingTimeMillis; + private int cmdNumber; + private int currentCmdNum; private Process process; @@ -35,9 +36,7 @@ public class Leelaz { private BufferedOutputStream outputStream; private boolean printCommunication; - private boolean isValidVersion = false; - private boolean isReadingPonderOutput; private List bestMoves; private List bestMovesTemp; @@ -46,16 +45,13 @@ public class Leelaz { private boolean isPondering; private long startPonderTime; - // True while we're waiting for old ponder output to get flushed - private boolean isWaitingToStartPonder = false; - // fixed_handicap public boolean isSettingHandicap = false; // genmove public boolean isThinking = false; - private boolean failSafeMode = false; + private boolean isCheckingVersion; /** * Initializes the leelaz process and starts reading output @@ -63,13 +59,14 @@ public class Leelaz { * @throws IOException */ public Leelaz() throws IOException, JSONException { - isReadingPonderOutput = false; bestMoves = new ArrayList<>(); bestMovesTemp = new ArrayList<>(); listeners = new CopyOnWriteArrayList<>(); isPondering = false; startPonderTime = System.currentTimeMillis(); + cmdNumber = 1; + currentCmdNum = -1; JSONObject config = Lizzie.config.config.getJSONObject("leelaz"); @@ -115,68 +112,15 @@ public Leelaz() throws IOException, JSONException { initializeStreams(); - if (!isCorrectVersion()) { - // warn user - JOptionPane.showMessageDialog(Lizzie.frame, "This version of Leela Zero is incompatible with Lizzie.\nPlease follow the instructions in the readme.\n(Trying failsafe mode...)"); - failSafeMode = true; - } + // Send a version request to check that we have a supported version + // Response handled in parseLine + isCheckingVersion = true; + sendCommand("version"); + // start a thread to continuously read Leelaz output new Thread(this::read).start(); } - /** - * verify that leelaz is the lizzie-variant with ~begin etc ponder output - * - * @return true if this leelaz will work for lizzie, false otherwise - */ - private boolean isCorrectVersion() { - final int maxTestTime = 1000; // 1 second test - try { - boolean hasSeenName = false; - int c; - StringBuilder line = new StringBuilder(); - sendCommand("time_left B 0 0"); - - while ((c = inputStream.read()) != -1) { - line.append((char) c); - if ((c == '\n')) { - String lineString = line.toString(); - if (lineString.startsWith("=")) { - if (!hasSeenName) { - hasSeenName = true; - // break from this loop in up to maxTestTime. - new Thread(() -> { - try { - Thread.sleep(maxTestTime); - } catch (InterruptedException e) { - e.printStackTrace(); - } - if (!isValidVersion) - sendCommand("name"); - }).start(); - } else { - return false; - } - } else if (lineString.startsWith("~begin")) { - isValidVersion = true; - return true; - } - - line = new StringBuilder(); - } - } - // this line will be reached when Leelaz shuts down - System.out.println("Leelaz process ended."); - - shutdown(); - System.exit(-1); - } catch (IOException e) { - e.printStackTrace(); - } - - return false; - } - /** * Initializes the input and output streams */ @@ -185,138 +129,72 @@ private void initializeStreams() { outputStream = new BufferedOutputStream(process.getOutputStream()); } + private void parseInfo(String line) { + + bestMoves = new ArrayList<>(); + String[] variations = line.split(" info "); + for (String var : variations) { + bestMoves.add(new MoveData(var)); + } + // Not actually necessary to sort with current version of LZ (0.15) + // but not guaranteed to be ordered in the future + Collections.sort(bestMoves); + } + /** * Parse a line of Leelaz output * * @param line output line */ private void parseLine(String line) { - if (failSafeMode) { - parseLineFailSafe(line); - } else { - parseLineNormal(line); - } - } - - private void parseLineNormal(String line) { synchronized (this) { - if (line.startsWith("~begin") && !isWaitingToStartPonder) { - if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis) { - // we have pondered for enough time. pause pondering - togglePonder(); - } - - isReadingPonderOutput = true; - bestMovesTemp = new ArrayList<>(); - } else if (line.startsWith("~end") && !isWaitingToStartPonder) { - isReadingPonderOutput = false; - bestMoves = bestMovesTemp; - - notifyBestMoveListeners(); - - if (Lizzie.frame != null) Lizzie.frame.repaint(); - } else { - - if (isReadingPonderOutput && !isWaitingToStartPonder) { - line = line.trim(); - // ignore passes, and only accept lines that start with a coordinate letter - if (line.length() > 0 && Character.isLetter(line.charAt(0)) && !line.startsWith("pass")) { - if (!(Lizzie.frame != null && Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay)) { - try { - bestMovesTemp.add(new MoveData(line)); - } catch (ArrayIndexOutOfBoundsException e) { - // this is very rare but is possible. ignore - } - } - } - } else { - if (printCommunication) { - System.out.print(line); - } - - line = line.trim(); - if (Lizzie.frame != null && line.startsWith("=") && line.length() > 2) { - - if (isWaitingToStartPonder) { - // Now we can tell it to actually start pondering - sendCommand("time_left b 0 0"); - isWaitingToStartPonder = false; - } - - if (isSettingHandicap) { - line = line.substring(2); - String[] stones = line.split(" "); - for (String stone : stones) { - int[] coordinates = Lizzie.board.convertNameToCoordinates(stone); - Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); - } - isSettingHandicap = false; - } else if (isThinking) { - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.board.place(line.substring(2)); - } - isThinking = false; - } + if (line.equals("\n")) { + // End of response + } else if (line.startsWith("info")) { + if (currentCmdNum == cmdNumber -1) { + // This should not be stale data when the command number match + parseInfo(line.substring(5)); + notifyBestMoveListeners(); + if (Lizzie.frame != null) Lizzie.frame.repaint(); + if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis) { + sendCommand("name"); // Send any command to stop ponder } } - } - } - } - - private void parseLineFailSafe(String line) { - synchronized (this) { - boolean isMoveDataLine = line.matches("(?s) *([A-T][0-9]+|pass) -> +[0-9].*"); - if (!isReadingPonderOutput && isMoveDataLine && !isWaitingToStartPonder) { - if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis) { - // we have pondered for enough time. pause pondering - togglePonder(); + } else if (line.startsWith("play")) { + // In lz-genmove_analyze + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.board.place(line.substring(5).trim()); } + isThinking = false; - isReadingPonderOutput = true; - bestMovesTemp = new ArrayList<>(); - parseMoveDataLine(line); - } else if (isReadingPonderOutput && !isMoveDataLine && !isWaitingToStartPonder) { - isReadingPonderOutput = false; - bestMoves = bestMovesTemp; + } else if (Lizzie.frame != null && line.startsWith("=")) { + if (printCommunication) { + System.out.print(line); + } + String[] params = line.trim().split(" "); + currentCmdNum = Integer.parseInt(params[0].substring(1).trim()); - notifyBestMoveListeners(); + if (params.length == 1) return; - if (Lizzie.frame != null) Lizzie.frame.repaint(); - if (isPondering) { - startPonder(); - } - } else { - if (isReadingPonderOutput && !isWaitingToStartPonder) { - parseMoveDataLine(line); - } else { - if (printCommunication) { - System.out.print(line); + if (isSettingHandicap) { + for (int i = 2; i < params.length; i++) { + int[] coordinates = Lizzie.board.convertNameToCoordinates(params[i]); + Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); } - - line = line.trim(); - if (Lizzie.frame != null && line.startsWith("=") && line.length() > 2) { - - if (isWaitingToStartPonder) { - // Now we can tell it to actually start pondering - startPonder(); - } - - if (isSettingHandicap) { - line = line.substring(2); - String[] stones = line.split(" "); - for (String stone : stones) { - int[] coordinates = Lizzie.board.convertNameToCoordinates(stone); - Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); - } - isSettingHandicap = false; - } else if (isThinking) { - if (Lizzie.frame.isPlayingAgainstLeelaz && line.matches("(?s)= ([A-T][0-9]+|pass).*")) { - Lizzie.board.place(line.substring(2)); - isThinking = false; - } - } + isSettingHandicap = false; + } else if (isThinking && !isPondering) { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.board.place(params[1]); + } + } else if (isCheckingVersion) { + String[] ver = params[1].split("\\."); + int minor = Integer.parseInt(ver[1]); + // Gtp support added in version 15 + if (minor < 15) { + JOptionPane.showMessageDialog(Lizzie.frame, "Lizzie requires version 0.15 or later of Leela Zero for analysis (found " + params[1] + ")"); } + isCheckingVersion = false; } } } @@ -341,25 +219,6 @@ private void parseMoveDataLine(String line) { } } - /** - * Tell Leelaz to start pondering - * - */ - private void startPonder() { - sendCommand("time_left b 0 0"); - isWaitingToStartPonder = false; - Timer ponderStopTimer = new Timer(); - ponderStopTimer.schedule(new ponderStopTask(), 1000); - } - - class ponderStopTask extends TimerTask { - public void run() { - // Send any command to terminate pondering. - sendCommand("name"); - isReadingPonderOutput = false; - } - } - /** * Continually reads and processes output from leelaz */ @@ -391,13 +250,13 @@ private void read() { * @param command a GTP command containing no newline characters */ public void sendCommand(String command) { + command = cmdNumber + " " + command; + cmdNumber++; if (printCommunication) { System.out.printf("> %s\n", command); } if (command.startsWith("fixed_handicap")) isSettingHandicap = true; - if (command.startsWith("genmove")) - isThinking = true; try { outputStream.write((command + "\n").getBytes()); outputStream.flush(); @@ -432,6 +291,17 @@ public void playMove(Stone color, String move) { } } + public void genmove(String color) { + String command = "genmove " + color; + /* + * We don't support displaying this while playing, so no reason to request it (for now) + if (isPondering) { + command = "lz-genmove_analyze " + color + " 10"; + }*/ + sendCommand(command); + isThinking = true; + } + public void undo() { synchronized (this) { sendCommand("undo"); @@ -446,9 +316,8 @@ public void undo() { */ private void ponder() { isPondering = true; - isWaitingToStartPonder = true; startPonderTime = System.currentTimeMillis(); - sendCommand("name"); // until it responds to this, incoming ponder results are obsolete + sendCommand("lz-analyze " + Lizzie.config.config.getJSONObject("leelaz").getInt("analyze-update-interval-centisec")); // until it responds to this, incoming ponder results are obsolete } public void togglePonder() { diff --git a/src/main/java/wagner/stephanie/lizzie/analysis/MoveData.java b/src/main/java/wagner/stephanie/lizzie/analysis/MoveData.java index 7ebd5b20b..ee9f207be 100644 --- a/src/main/java/wagner/stephanie/lizzie/analysis/MoveData.java +++ b/src/main/java/wagner/stephanie/lizzie/analysis/MoveData.java @@ -1,7 +1,5 @@ package wagner.stephanie.lizzie.analysis; -import jdk.nashorn.internal.runtime.arrays.ArrayIndex; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -9,10 +7,11 @@ /** * holds the data from Leelaz's pondering mode */ -public class MoveData { +public class MoveData implements Comparable { public String coordinate; public int playouts; public double winrate; + public int order; public List variation; /** @@ -20,16 +19,19 @@ public class MoveData { * @param line line of ponder output */ public MoveData(String line) throws ArrayIndexOutOfBoundsException { - String[] data = line.trim().split(" +"); + String[] data = line.trim().split(" "); + + // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed + coordinate = data[1]; + playouts = Integer.parseInt(data[3]); + winrate = Integer.parseInt(data[5])/100.0; + order = Integer.parseInt(data[7]); - coordinate = data[0]; - playouts = Integer.parseInt(data[2]); - winrate = Double.parseDouble(data[4].substring(0, data[4].length() - 2)); + variation = new ArrayList<>(Arrays.asList(data)); + variation = variation.subList(9, variation.size()); + } - variation = new ArrayList<>(); - // Leela 0.11.0 has extra fields before PV. - line = line.split("PV: *")[1]; - data = line.trim().split(" +"); - variation.addAll(Arrays.asList(data)); + public int compareTo(MoveData b) { + return order - b.order; } } diff --git a/src/main/java/wagner/stephanie/lizzie/gui/Input.java b/src/main/java/wagner/stephanie/lizzie/gui/Input.java index e1e775ad1..8433a0b6b 100644 --- a/src/main/java/wagner/stephanie/lizzie/gui/Input.java +++ b/src/main/java/wagner/stephanie/lizzie/gui/Input.java @@ -306,7 +306,7 @@ public void keyPressed(KeyEvent e) { Lizzie.leelaz.sendCommand("time_settings 0 " + Lizzie.config.config.getJSONObject("leelaz").getInt("max-game-thinking-time-seconds") + " 1"); Lizzie.frame.playerIsBlack = !Lizzie.board.getData().blackToPlay; Lizzie.frame.isPlayingAgainstLeelaz = true; - Lizzie.leelaz.sendCommand("genmove " + (Lizzie.board.getData().blackToPlay ? "B" : "W")); + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "B" : "W")); } break; diff --git a/src/main/java/wagner/stephanie/lizzie/gui/LizzieFrame.java b/src/main/java/wagner/stephanie/lizzie/gui/LizzieFrame.java index dba2996d0..fae788144 100644 --- a/src/main/java/wagner/stephanie/lizzie/gui/LizzieFrame.java +++ b/src/main/java/wagner/stephanie/lizzie/gui/LizzieFrame.java @@ -161,9 +161,9 @@ public static void startNewGame() { if (isHandicapGame) { Lizzie.board.getHistory().getData().blackToPlay = false; Lizzie.leelaz.sendCommand("fixed_handicap " + gameInfo.getHandicap()); - if (playerIsBlack) Lizzie.leelaz.sendCommand("genmove W"); + if (playerIsBlack) Lizzie.leelaz.genmove("W"); } else if (!playerIsBlack) { - Lizzie.leelaz.sendCommand("genmove B"); + Lizzie.leelaz.genmove("B"); } } diff --git a/src/main/java/wagner/stephanie/lizzie/rules/Board.java b/src/main/java/wagner/stephanie/lizzie/rules/Board.java index e9cc618de..9adcae98a 100644 --- a/src/main/java/wagner/stephanie/lizzie/rules/Board.java +++ b/src/main/java/wagner/stephanie/lizzie/rules/Board.java @@ -109,7 +109,7 @@ public void pass(Stone color) { history.next(); Lizzie.leelaz.playMove(color, "pass"); if (Lizzie.frame.isPlayingAgainstLeelaz) - Lizzie.leelaz.sendCommand("genmove " + (history.isBlacksTurn()? "B" : "W")); + Lizzie.leelaz.genmove((history.isBlacksTurn()? "B" : "W")); return; } @@ -126,7 +126,7 @@ public void pass(Stone color) { // update leelaz with pass Lizzie.leelaz.playMove(color, "pass"); if (Lizzie.frame.isPlayingAgainstLeelaz) - Lizzie.leelaz.sendCommand("genmove " + (history.isBlacksTurn()? "W" : "B")); + Lizzie.leelaz.genmove((history.isBlacksTurn()? "W" : "B")); // update history with pass history.addOrGoto(newState); @@ -180,7 +180,7 @@ public void place(int x, int y, Stone color) { // 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.sendCommand("genmove " + (Lizzie.board.getData().blackToPlay ? "W" : "B")); + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); } @@ -232,7 +232,7 @@ public void place(int x, int y, Stone color) { // update leelaz with board position if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack == getData().blackToPlay) { Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - Lizzie.leelaz.sendCommand("genmove " + (Lizzie.board.getData().blackToPlay ? "W" : "B")); + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); }