// TwoGtp.java package net.sf.gogui.tools.twogtp; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import net.sf.gogui.game.ConstNode; import net.sf.gogui.game.ConstGameTree; import net.sf.gogui.game.Game; import net.sf.gogui.game.NodeUtil; import net.sf.gogui.game.TimeSettings; import net.sf.gogui.go.BlackWhiteSet; import net.sf.gogui.go.ConstBoard; 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.InvalidKomiException; import net.sf.gogui.go.Komi; import net.sf.gogui.go.Move; import net.sf.gogui.gtp.GtpClient; import net.sf.gogui.gtp.GtpCommand; import net.sf.gogui.gtp.GtpEngine; import net.sf.gogui.gtp.GtpError; import net.sf.gogui.gtp.GtpResponseFormatError; import net.sf.gogui.gtp.GtpUtil; import net.sf.gogui.util.ErrorMessage; import net.sf.gogui.util.ObjectUtil; import net.sf.gogui.util.Platform; import net.sf.gogui.util.StringUtil; import net.sf.gogui.version.Version; /** GTP adapter for playing games between two Go programs. */ public class TwoGtp extends GtpEngine { /** Constructor. @param komi The fixed komi. See TwoGtp documentation for option -komi */ public TwoGtp(Program black, Program white, Program referee, String observer, int size, Komi komi, int numberGames, boolean alternate, String filePrefix, boolean verbose, Openings openings, TimeSettings timeSettings, ResultFile resultFile) throws Exception { super(null); assert size > 0; assert size <= GoPoint.MAX_SIZE; assert komi != null; if (black.equals("")) throw new ErrorMessage("No black program set"); if (white.equals("")) throw new ErrorMessage("No white program set"); m_filePrefix = filePrefix; m_allPrograms = new ArrayList<Program>(); m_black = black; m_allPrograms.add(m_black); m_white = white; m_allPrograms.add(m_white); m_referee = referee; if (m_referee != null) m_allPrograms.add(m_referee); if (observer.equals("")) m_observer = null; else { m_observer = new Program(observer, "Observer", "O", verbose); m_allPrograms.add(m_observer); } for (Program program : m_allPrograms) program.setLabel(m_allPrograms); m_size = size; m_komi = komi; m_alternate = alternate; m_numberGames = numberGames; m_openings = openings; m_verbose = verbose; m_timeSettings = timeSettings; m_resultFile = resultFile; initGame(size); } public void autoPlay() throws Exception { StringBuilder response = new StringBuilder(256); while (true) { try { newGame(m_size); while (! gameOver()) { response.setLength(0); sendGenmove(getToMove(), response); } } catch (GtpError e) { if (m_gameIndex == -1) break; handleEndOfGame(true, e.getMessage()); } } if (m_black.isProgramDead()) throw new ErrorMessage("Black program died"); if (m_white.isProgramDead()) throw new ErrorMessage("White program died"); } public void close() { for (Program program : m_allPrograms) program.close(); } public void handleCommand(GtpCommand cmd) throws GtpError { String command = cmd.getCommand(); if (command.equals("boardsize")) cmdBoardSize(cmd); else if (command.equals("clear_board")) cmdClearBoard(cmd); else if (command.equals("final_score")) finalStatusCommand(cmd); else if (command.equals("final_status")) finalStatusCommand(cmd); else if (command.equals("final_status_list")) finalStatusCommand(cmd); else if (command.equals("gogui-interrupt")) ; else if (command.equals("gogui-title")) cmd.setResponse(getTitle()); else if (command.equals("gogui-twogtp-black")) twogtpColor(m_black, cmd); else if (command.equals("gogui-twogtp-white")) twogtpColor(m_white, cmd); else if (command.equals("gogui-twogtp-referee")) twogtpReferee(cmd); else if (command.equals("gogui-twogtp-observer")) twogtpObserver(cmd); else if (command.equals("quit")) { close(); setQuit(); } else if (command.equals("play")) cmdPlay(cmd); else if (command.equals("undo")) cmdUndo(cmd); else if (command.equals("genmove")) cmdGenmove(cmd); else if (command.equals("komi")) komi(cmd); else if (command.equals("scoring_system")) sendIfSupported(command, cmd.getLine()); else if (command.equals("name")) cmd.setResponse("gogui-twogtp"); else if (command.equals("version")) cmd.setResponse(Version.get()); else if (command.equals("protocol_version")) cmd.setResponse("2"); else if (command.equals("list_commands")) cmd.setResponse("boardsize\n" + "clear_board\n" + "final_score\n" + "final_status\n" + "final_status_list\n" + "genmove\n" + "gogui-interrupt\n" + "gogui-title\n" + "komi\n" + "list_commands\n" + "name\n" + "play\n" + "quit\n" + "scoring_system\n" + "time_settings\n" + "gogui-twogtp-black\n" + "gogui-twogtp-observer\n" + "gogui-twogtp-referee\n" + "gogui-twogtp-white\n" + "undo\n" + "version\n"); else if (GtpUtil.isStateChangingCommand(command)) throw new GtpError("unknown command"); else if (command.equals("time_settings")) sendIfSupported(command, cmd.getLine()); else { boolean isExtCommandBlack = m_black.isSupported(command); boolean isExtCommandWhite = m_white.isSupported(command); boolean isExtCommandReferee = false; if (m_referee != null) isExtCommandReferee = m_referee.isSupported(command); boolean isExtCommandObserver = false; if (m_observer != null) isExtCommandObserver = m_observer.isSupported(command); if (isExtCommandBlack && ! isExtCommandObserver && ! isExtCommandWhite && ! isExtCommandReferee) forward(m_black, cmd); if (isExtCommandWhite && ! isExtCommandObserver && ! isExtCommandBlack && ! isExtCommandReferee) forward(m_white, cmd); if (isExtCommandReferee && ! isExtCommandObserver && ! isExtCommandBlack && ! isExtCommandWhite) forward(m_referee, cmd); if (isExtCommandObserver && ! isExtCommandReferee && ! isExtCommandBlack && ! isExtCommandWhite) forward(m_observer, cmd); if (! isExtCommandReferee && ! isExtCommandBlack && ! isExtCommandObserver && ! isExtCommandWhite) throw new GtpError("unknown command"); throw new GtpError("use gogui-twogtp-black/white/referee/observer"); } } public void interruptCommand() { for (Program program : m_allPrograms) program.interruptProgram(); } /** Store stderr of programs during move generation in SGF comments. */ public void setDebugToComment(boolean enable) { m_black.setIOCallback(null); m_white.setIOCallback(null); m_debugToComment = enable; if (m_debugToComment) { m_black.setIOCallback(new GtpClient.IOCallback() { public void receivedInvalidResponse(String s) { } public void receivedResponse(boolean error, String s) { } public void receivedStdErr(String s) { appendDebugToCommentBuffer(BLACK, s); } public void sentCommand(String s) { } }); m_white.setIOCallback(new GtpClient.IOCallback() { public void receivedInvalidResponse(String s) { } public void receivedResponse(boolean error, String s) { } public void receivedStdErr(String s) { appendDebugToCommentBuffer(WHITE, s); } public void sentCommand(String s) { } }); } } /** Limit number of moves. @param maxMoves Maximum number of moves after which genmove will fail, -1 for no limit. */ public void setMaxMoves(int maxMoves) { m_maxMoves = maxMoves; } private final boolean m_alternate; private boolean m_gameSaved; private boolean m_debugToComment; private int m_maxMoves = 1000; private int m_gameIndex; private boolean m_resigned; private final boolean m_verbose; private final int m_numberGames; private final int m_size; /** Fixed komi. */ private final Komi m_komi; private Game m_game; private GoColor m_resignColor; private final Openings m_openings; private final Program m_black; private final Program m_white; private final Program m_referee; private final Program m_observer; private final ArrayList<Program> m_allPrograms; private final BlackWhiteSet<Double> m_realTime = new BlackWhiteSet<Double>(0., 0.); private String m_openingFile; private final String m_filePrefix; private final ArrayList<ArrayList<Compare.Placement>> m_games = new ArrayList<ArrayList<Compare.Placement>>(100); private ResultFile m_resultFile; private final TimeSettings m_timeSettings; private ConstNode m_lastOpeningNode; /** Buffers for stderr of programs if setDebugToComment() is used. This member is used by two threads. Access only through synchronized functions. */ private BlackWhiteSet<StringBuilder> m_debugToCommentBuffer = new BlackWhiteSet<StringBuilder>(new StringBuilder(), new StringBuilder()); private synchronized void appendDebugToCommentBuffer(GoColor c, String s) { m_debugToCommentBuffer.get(c).append(s); } private void checkInconsistentState() throws GtpError { for (Program program : m_allPrograms) if (program.isOutOfSync()) throw new GtpError("Inconsistent state"); } private synchronized void clearDebugToCommentBuffers() { m_debugToCommentBuffer.get(BLACK).setLength(0); m_debugToCommentBuffer.get(WHITE).setLength(0); } private void cmdBoardSize(GtpCommand cmd) throws GtpError { cmd.checkNuArg(1); int size = cmd.getIntArg(0, 1, GoPoint.MAX_SIZE); if (size != m_size) throw new GtpError("Size must be " + m_size); } private void cmdClearBoard(GtpCommand cmd) throws GtpError { cmd.checkArgNone(); newGame(m_size); } private void cmdGenmove(GtpCommand cmd) throws GtpError { try { sendGenmove(cmd.getColorArg(), cmd.getResponse()); } catch (ErrorMessage e) { throw new GtpError(e.getMessage()); } } private void cmdPlay(GtpCommand cmd) throws GtpError { cmd.checkNuArg(2); checkInconsistentState(); GoColor color = cmd.getColorArg(0); GoPoint point = cmd.getPointArg(1, m_size); Move move = Move.get(color, point); m_game.play(move); synchronize(); } private void cmdUndo(GtpCommand cmd) throws GtpError { cmd.checkArgNone(); int moveNumber = m_game.getMoveNumber(); if (moveNumber == 0) throw new GtpError("cannot undo"); m_game.gotoNode(getCurrentNode().getFatherConst()); assert m_game.getMoveNumber() == moveNumber - 1; synchronize(); } private void finalStatusCommand(GtpCommand cmd) throws GtpError { checkInconsistentState(); if (m_referee != null) forward(m_referee, cmd); else if (m_black.isSupported("final_status")) forward(m_black, cmd); else if (m_white.isSupported("final_status")) forward(m_white, cmd); else throw new GtpError("neither player supports final_status"); } private void forward(Program program, GtpCommand cmd) throws GtpError { cmd.setResponse(program.send(cmd.getLine())); } private boolean gameOver() { return (getBoard().bothPassed() || m_resigned); } private ConstBoard getBoard() { return m_game.getBoard(); } private ConstNode getCurrentNode() { return m_game.getCurrentNode(); } private synchronized String getDebugToCommentBuffer(GoColor color) { return m_debugToCommentBuffer.get(color).toString(); } private GoColor getToMove() { return m_game.getToMove(); } private ConstGameTree getTree() { return m_game.getTree(); } private String getTitle() { StringBuilder buffer = new StringBuilder(); String nameBlack = m_black.getLabel(); String nameWhite = m_white.getLabel(); if (isAlternated()) { String tmpName = nameBlack; nameBlack = nameWhite; nameWhite = tmpName; } buffer.append(nameWhite); buffer.append(" vs "); buffer.append(nameBlack); buffer.append(" (B)"); if (! m_filePrefix.equals("")) { buffer.append(" ("); buffer.append(m_gameIndex + 1); buffer.append(')'); } return buffer.toString(); } private void handleEndOfGame(boolean error, String errorMessage) throws ErrorMessage { String resultBlack; String resultWhite; String resultReferee; if (m_resigned) { String result = (m_resignColor == BLACK ? "W" : "B"); result = result + "+R"; resultBlack = result; resultWhite = result; resultReferee = result; } else { resultBlack = m_black.getResult(); resultWhite = m_white.getResult(); resultReferee = "?"; if (m_referee != null) resultReferee = m_referee.getResult(); } double cpuTimeBlack = m_black.getAndClearCpuTime(); double cpuTimeWhite = m_white.getAndClearCpuTime(); double realTimeBlack = m_realTime.get(BLACK); double realTimeWhite = m_realTime.get(WHITE); if (isAlternated()) { resultBlack = inverseResult(resultBlack); resultWhite = inverseResult(resultWhite); resultReferee = inverseResult(resultReferee); realTimeBlack = m_realTime.get(WHITE); realTimeWhite = m_realTime.get(BLACK); } // If a program is dead we wait for a few seconds, because it // could be because the TwoGtp process was killed and we don't // want to write a result in this case. if (m_black.isProgramDead() || m_white.isProgramDead()) { try { Thread.sleep(3000); } catch (InterruptedException e) { assert false; } } String nameBlack = m_black.getLabel(); String nameWhite = m_white.getLabel(); String blackCommand = m_black.getProgramCommand(); String whiteCommand = m_white.getProgramCommand(); String blackVersion = m_black.getVersion(); String whiteVersion = m_white.getVersion(); if (isAlternated()) { nameBlack = m_white.getLabel(); nameWhite = m_black.getLabel(); blackCommand = m_white.getProgramCommand(); whiteCommand = m_black.getProgramCommand(); blackVersion = m_white.getVersion(); whiteVersion = m_black.getVersion(); } m_game.setPlayer(BLACK, nameBlack); m_game.setPlayer(WHITE, nameWhite); if (m_referee != null) m_game.setResult(resultReferee); else if (resultBlack.equals(resultWhite) && ! resultBlack.equals("?")) m_game.setResult(resultBlack); String host = Platform.getHostInfo(); StringBuilder comment = new StringBuilder(); comment.append("Black command: "); comment.append(blackCommand); comment.append("\nWhite command: "); comment.append(whiteCommand); comment.append("\nBlack version: "); comment.append(blackVersion); comment.append("\nWhite version: "); comment.append(whiteVersion); if (m_openings != null) { comment.append("\nOpening: "); comment.append(m_openingFile); } comment.append("\nResult[Black]: "); comment.append(resultBlack); comment.append("\nResult[White]: "); comment.append(resultWhite); if (m_referee != null) { comment.append("\nReferee: "); comment.append(m_referee.getProgramCommand()); comment.append("\nResult[Referee]: "); comment.append(resultReferee); } comment.append("\nHost: "); comment.append(host); comment.append("\nDate: "); comment.append(StringUtil.getDate()); m_game.setComment(comment.toString(), getTree().getRootConst()); int moveNumber = NodeUtil.getMoveNumber(getCurrentNode()); if (m_resultFile != null) m_resultFile.addResult(m_gameIndex, m_game, resultBlack, resultWhite, resultReferee, isAlternated(), moveNumber, error, errorMessage, realTimeBlack, realTimeWhite, cpuTimeBlack, cpuTimeWhite); } private void initGame(int size) throws GtpError { m_game = new Game(size, m_komi, null, null, null); m_realTime.set(BLACK, 0.); m_realTime.set(WHITE, 0.); // Clock is not needed m_game.haltClock(); m_resigned = false; if (m_openings != null) { int openingFileIndex; if (m_alternate) openingFileIndex = (m_gameIndex / 2) % m_openings.getNumber(); else openingFileIndex = m_gameIndex % m_openings.getNumber(); try { m_openings.loadFile(openingFileIndex); } catch (Exception e) { throw new GtpError(e.getMessage()); } m_openingFile = m_openings.getFilename(); if (m_verbose) System.err.println("Loaded opening " + m_openingFile); if (m_openings.getBoardSize() != size) throw new GtpError("Wrong board size: " + m_openingFile); m_game.init(m_openings.getTree()); m_game.setKomi(m_komi); m_lastOpeningNode = NodeUtil.getLast(getTree().getRootConst()); // TODO: Check that root node contains no setup stones, if // TwoGtp is run as a GTP engine, see also comment in sendGenmove() } else m_lastOpeningNode = null; synchronizeInit(); } private String inverseResult(String result) { if (result.indexOf('B') >= 0) return result.replaceAll("B", "W"); else if (result.indexOf('W') >= 0) return result.replaceAll("W", "B"); else return result; } private boolean isAlternated() { return (m_alternate && m_gameIndex % 2 != 0); } private boolean isInOpening() { if (m_lastOpeningNode == null) return false; for (ConstNode node = getCurrentNode().getChildConst(); node != null; node = node.getChildConst()) if (node == m_lastOpeningNode) return true; return false; } private void komi(GtpCommand cmd) throws GtpError { String arg = cmd.getArg(); try { Komi komi = Komi.parseKomi(arg); if (! ObjectUtil.equals(komi, m_komi)) throw new GtpError("komi is fixed at " + m_komi); } catch (InvalidKomiException e) { throw new GtpError("invalid komi: " + arg); } } private void newGame(int size) throws GtpError { if (m_resultFile != null) m_gameIndex = m_resultFile.getNextGameIndex(); else { ++m_gameIndex; if (m_numberGames > 0 && m_gameIndex > m_numberGames) m_gameIndex = -1; } if (m_gameIndex == -1) throw new GtpError("maximum number of games reached"); if (m_verbose) { System.err.println("============================================"); System.err.println("Game " + m_gameIndex); System.err.println("============================================"); } m_black.getAndClearCpuTime(); m_white.getAndClearCpuTime(); initGame(size); m_gameSaved = false; if (m_timeSettings != null) sendIfSupported("time_settings", GtpUtil.getTimeSettingsCommand(m_timeSettings)); } private void sendGenmove(GoColor color, StringBuilder response) throws GtpError, ErrorMessage { checkInconsistentState(); int moveNumber = m_game.getMoveNumber(); if (m_maxMoves >= 0 && moveNumber > m_maxMoves) throw new GtpError("move limit exceeded"); if (isInOpening()) { // TODO: Check that node contains no setup stones or fully support // openings with setup stones and non-alternating moves in GTP // engine mode again (by transforming the opening file into a // sequence of alternating moves, replacing setup stones by moves // and filling in passes). See also comment in initGame() and // doc/manual/xml/reference-twogtp.xml ConstNode child = getCurrentNode().getChildConst(); Move move = child.getMove(); if (move.getColor() != color) throw new GtpError("next opening move is " + move); m_game.gotoNode(child); synchronize(); response.append(GoPoint.toString(move.getPoint())); return; } Program program; boolean exchangeColors = (color == BLACK && isAlternated()) || (color == WHITE && ! isAlternated()); if (exchangeColors) program = m_white; else program = m_black; clearDebugToCommentBuffers(); long timeMillis = System.currentTimeMillis(); String responseGenmove = program.sendCommandGenmove(color); double time = (System.currentTimeMillis() - timeMillis) / 1000.; m_realTime.set(color, m_realTime.get(color) + time); if (responseGenmove.equalsIgnoreCase("resign")) { response.append("resign"); m_resigned = true; m_resignColor = color; } else { ConstBoard board = getBoard(); GoPoint point = null; try { point = GtpUtil.parsePoint(responseGenmove, board.getSize()); } catch (GtpResponseFormatError e) { throw new GtpError(program.getLabel() + " played invalid move: " + responseGenmove); } Move move = Move.get(color, point); m_game.play(move); program.updateAfterGenmove(board); synchronize(); response.append(GoPoint.toString(move.getPoint())); if (m_debugToComment) { // All stderr that was written by the program before the // response to genmove should have been received by now, but // maybe the IO callback thread had no chance to run yet, so we // wait for an extra 10 milliseconds try { Thread.sleep(10); } catch (InterruptedException e) { } m_game.setComment(getDebugToCommentBuffer(color)); } } if (gameOver() && ! m_gameSaved) { handleEndOfGame(false, ""); m_gameSaved = true; } } private void sendIfSupported(String cmd, String cmdLine) { for (Program program : m_allPrograms) program.sendIfSupported(cmd, cmdLine); } private void synchronize() throws GtpError { for (Program program : m_allPrograms) program.synchronize(m_game); } private void synchronizeInit() throws GtpError { for (Program program : m_allPrograms) program.synchronizeInit(m_game); } private void twogtpColor(Program program, GtpCommand cmd) throws GtpError { cmd.setResponse(program.send(cmd.getArgLine())); } private void twogtpObserver(GtpCommand cmd) throws GtpError { if (m_observer == null) throw new GtpError("no observer enabled"); twogtpColor(m_observer, cmd); } private void twogtpReferee(GtpCommand cmd) throws GtpError { if (m_referee == null) throw new GtpError("no referee enabled"); twogtpColor(m_referee, cmd); } }