// GtpClientBase.java package net.sf.gogui.gtp; import java.io.IOException; import java.util.ArrayList; import java.util.Locale; import net.sf.gogui.go.GoColor; import static net.sf.gogui.go.GoColor.BLACK; import static net.sf.gogui.go.GoColor.WHITE; import net.sf.gogui.go.GoPoint; import net.sf.gogui.go.Move; import net.sf.gogui.util.StringUtil; /** Interface to a Go program that uses GTP. This class implements most of the functionality of a connection to a GTP command apart from how commands are actually sent to the program. Subclasses need to implement the abstract function send() and a few functions related to querying and using the ability to interrupt commands. */ public abstract class GtpClientBase { /** Close output connection. Should do nothing if the concrete class does not communicate through streams. */ public abstract void close(); /** Get command for setting the board size. Note: call queryProtocolVersion first @return The boardsize command for GTP version 2 programs, otherwise null. */ public String getCommandBoardsize(int size) { if (m_protocolVersion == 2) { m_buffer.setLength(0); m_buffer.append("boardsize "); m_buffer.append(size); return m_buffer.toString(); } else return null; } /** Get command for starting a new game. Note: call queryProtocolVersion first @return The boardsize command for GTP version 1 programs, otherwise the clear_board command. */ public String getCommandClearBoard(int size) { if (m_protocolVersion == 1) { m_buffer.setLength(0); m_buffer.append("boardsize "); m_buffer.append(size); return m_buffer.toString(); } else return "clear_board"; } /** Get command for generating a move. Note: call queryProtocolVersion first @param color GoColor::BLACK or GoColor::WHITE @return The right command depending on the GTP version. */ public String getCommandGenmove(GoColor color) { assert color.isBlackWhite(); if (m_protocolVersion == 1) { if (color == BLACK) return "genmove_black"; else return "genmove_white"; } if (color == BLACK) return "genmove b"; else return "genmove w"; } /** Get command for playing a move. Note: call queryProtocolVersion first @return The right command depending on the GTP version. */ public String getCommandPlay(Move move) { m_buffer.setLength(0); if (m_protocolVersion == 1) { GoColor color = move.getColor(); String point = GoPoint.toString(move.getPoint()); if (color == BLACK) m_buffer.append("black "); else if (color == WHITE) m_buffer.append("white "); m_buffer.append(point); } else { m_buffer.append("play "); m_buffer.append(move); } if (m_lowerCase) return m_buffer.toString().toLowerCase(Locale.ENGLISH); return m_buffer.toString(); } /** Send cputime command and convert the result to double. @throws GtpError if command fails or does not return a floating point number. */ public double getCpuTime() throws GtpError { try { return Double.parseDouble(send("cputime")); } catch (NumberFormatException e) { throw new GtpError("Invalid response to cputime command"); } } /** Get program name or "Unknown Program" if unknown. If queryName() was not called or the name command failed, the string "Unknown Program" is returned. */ public String getLabel() { return (m_name == null ? "Unknown Program" : m_name); } /** Get program name. If queryName() was not called or the name command failed, the string "Unknown Program" is returned. */ public String getName() { return m_name; } /** Get protocol version. You have to call queryProtocolVersion() first, otherwise this method will always return 2. */ public int getProtocolVersion() { return m_protocolVersion; } /** Get the supported commands. Note: call querySupportedCommands() first. @return A vector of strings with the supported commands. */ public ArrayList<String> getSupportedCommands() { ArrayList<String> result = new ArrayList<String>(128); if (m_supportedCommands != null) for (int i = 0; i < m_supportedCommands.length; ++i) result.add(m_supportedCommands[i]); return result; } /** Is the program in a state, that all subsequent commands will fail. Returns false, but can be reimplemented in a subclass. */ public boolean isProgramDead() { return false; } /** Check if a command is supported. Note: call querySupportedCommands() first. */ public boolean isSupported(String command) { if (m_supportedCommands == null) return false; for (int i = 0; i < m_supportedCommands.length; ++i) if (m_supportedCommands[i].equals(command)) return true; return false; } /** Check if cputime command is supported. Note: call querySupportedCommands() first. */ public boolean isCpuTimeSupported() { return isSupported("cputime"); } /** Check if a genmove command is supported. If list_commands is not supported, it is assumed that genmove is supported. Note: call querySupportedCommands() first. */ public boolean isGenmoveSupported() { if (m_protocolVersion == 1) return (! isSupported("help") || (isSupported("genmove_black") && isSupported("genmove_white"))); return (! isSupported("list_commands") || isSupported("genmove")); } /** Check if interrupting a command is supported. Interrupting can supported by ANSI C signals or the special comment line "# interrupt" as described in the GoGui documentation chapter "Interrupting commands". Note: call queryInterruptSupport() first. */ public boolean isInterruptSupported() { return (m_isInterruptCommentSupported || m_pid != null); } /** Query if interrupting is supported. @see GtpClient#isInterruptSupported */ public void queryInterruptSupport() { try { if (isSupported("gogui-interrupt")) { send("gogui-interrupt"); m_isInterruptCommentSupported = true; } else if (isSupported("gogui_interrupt")) { send("gogui_interrupt"); m_isInterruptCommentSupported = true; } else if (isSupported("gogui-sigint")) m_pid = send("gogui-sigint").trim(); else if (isSupported("gogui_sigint")) m_pid = send("gogui_sigint").trim(); } catch (GtpError e) { } } /** Queries the name. @see #getName() */ public void queryName() { try { m_name = send("name"); } catch (GtpError e) { } } /** Query the protocol version. Assumes version 2 if the protocol_version command is not available, fails, or returns a version greater 2. @see GtpClientBase#getProtocolVersion */ public void queryProtocolVersion() { m_protocolVersion = 2; try { String response = send("protocol_version"); int v = Integer.parseInt(response); if (v == 1 || v == 2) m_protocolVersion = v; } catch (NumberFormatException e) { } catch (GtpError e) { } } /** Query the supported commands. @see GtpClientBase#getSupportedCommands @see GtpClientBase#isSupported */ public void querySupportedCommands() throws GtpError { String command = (m_protocolVersion == 1 ? "help" : "list_commands"); String response = send(command); m_supportedCommands = StringUtil.splitArguments(response); for (int i = 0; i < m_supportedCommands.length; ++i) m_supportedCommands[i] = m_supportedCommands[i].trim(); } /** Queries the program version. @return The version or an empty string if the version command fails. */ public String queryVersion() { try { return send("version"); } catch (GtpError e) { return ""; } } /** Send a command. @return The response text of the successful response not including the status character. @throws GtpError containing the response if the command fails. */ public abstract String send(String command) throws GtpError; /** Send comment. @param comment comment line (must start with '#'). */ public abstract void sendComment(String comment); /** Send command for setting the board size. Send the command if it exists in the GTP protocol version. Note: call queryProtocolVersion first @see GtpClientBase#getCommandBoardsize */ public void sendBoardsize(int size) throws GtpError { String command = getCommandBoardsize(size); if (command != null) send(command); } /** Send command for staring a new game. Note: call queryProtocolVersion first @see GtpClientBase#getCommandClearBoard */ public void sendClearBoard(int size) throws GtpError { send(getCommandClearBoard(size)); } /** Send command for playing a move. Note: call queryProtocolVersion first */ public void sendPlay(Move move) throws GtpError { send(getCommandPlay(move)); } /** Interrupt current command. Can be called from a different thread during a send. Note: call queryInterruptSupport first @see GtpClient#isInterruptSupported @throws GtpError if interrupting commands is not supported. */ public void sendInterrupt() throws GtpError { if (m_isInterruptCommentSupported) sendComment("# interrupt"); else if (m_pid != null) { String command = "kill -INT " + m_pid; Runtime runtime = Runtime.getRuntime(); try { Process process = runtime.exec(command); int result = process.waitFor(); if (result != 0) throw new GtpError("Command \"" + command + "\" returned " + result); } catch (IOException e) { throw new GtpError("Could not run command " + command + ":\n" + e); } catch (InterruptedException e) { printInterrupted(); } } else throw new GtpError("Interrupt not supported"); } /** Enable lower case mode for play commands. For engines that don't implement GTP correctly and understand only lower case moves in the play command. */ public void setLowerCase() { m_lowerCase = true; } /** Wait until the process of the program exits. Should do nothing if the concrete class does not create a process. */ public abstract void waitForExit(); private boolean m_isInterruptCommentSupported; protected String m_name; private String m_pid; /** Local variable in some functions, reused for efficiency. */ private final StringBuilder m_buffer = new StringBuilder(128); private boolean m_lowerCase; private int m_protocolVersion = 2; private String[] m_supportedCommands; private void printInterrupted() { System.err.println("GtpClient: InterruptedException"); Thread.dumpStack(); } }