commit 36291171e57d0b171844e2db89dadc7dd6ca24b3 Author: cs6601 Date: Sun Aug 26 11:48:21 2012 -0400 Initial commit. diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..4ee4c98 --- /dev/null +++ b/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3286e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist +bin +build \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..fb1dada --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + CS6601_P1 + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..82cba79 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Wed Feb 08 12:12:07 EST 2012 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..64d7f8a --- /dev/null +++ b/build.xml @@ -0,0 +1,81 @@ + + + Simple Framework for Testing Tree Search and Monte-Carlo Go + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/gogame.bat b/data/gogame.bat new file mode 100644 index 0000000..90b8cf0 --- /dev/null +++ b/data/gogame.bat @@ -0,0 +1 @@ +java -jar kgsGtp.jar kgsGtp.ini \ No newline at end of file diff --git a/data/kgsGtp.ini b/data/kgsGtp.ini new file mode 100644 index 0000000..de2ee20 --- /dev/null +++ b/data/kgsGtp.ini @@ -0,0 +1,13 @@ +engine=java -cp GoGame.jar;log4j-1.2.16.jar cs6601.p1.GoGame alphabeta +name=cs6601p1 +password=mwz6fe +room=cs6601p1 +mode=custom +talk=I'm a simple Computer Go program that plays random moves. +opponent=cs6601p2 +reconnect=t +automatch.rank=20k +rules=chinese +rules.boardSize=9 +rules.time=0 +rules.komi=5.5 \ No newline at end of file diff --git a/data/kgsGtp.jar b/data/kgsGtp.jar new file mode 100644 index 0000000..1eb3f97 Binary files /dev/null and b/data/kgsGtp.jar differ diff --git a/data/log4j-1.2.16.jar b/data/log4j-1.2.16.jar new file mode 100644 index 0000000..3f9d847 Binary files /dev/null and b/data/log4j-1.2.16.jar differ diff --git a/data/log4j.xml b/data/log4j.xml new file mode 100644 index 0000000..d6c9e0d --- /dev/null +++ b/data/log4j.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/junit-4.10.jar b/lib/junit-4.10.jar new file mode 100644 index 0000000..bf5c0b9 Binary files /dev/null and b/lib/junit-4.10.jar differ diff --git a/lib/log4j-1.2.16.jar b/lib/log4j-1.2.16.jar new file mode 100644 index 0000000..3f9d847 Binary files /dev/null and b/lib/log4j-1.2.16.jar differ diff --git a/src/cs6601/p1/Command.java b/src/cs6601/p1/Command.java new file mode 100644 index 0000000..363ba23 --- /dev/null +++ b/src/cs6601/p1/Command.java @@ -0,0 +1,39 @@ +package cs6601.p1; + +public class Command { + public enum TYPE { boardsize, clear_board, final_status_list, genmove, INVALID, komi, list_commands, name, play, quit, version }; + + private String[] cmdFields; + private String text; + private TYPE type; + + public Command(TYPE type, String text, String[] cmdFields) { + this.type = type; + this.text = text; + this.cmdFields = cmdFields; + } + + public double getDoubleField(int index) { + return Double.valueOf(cmdFields[index]); + } + + public int getIntField(int index) { + return Integer.valueOf(cmdFields[index]); + } + + public String getStringField(int index) { + return cmdFields[index]; + } + + public String getText() { + return text; + } + + public TYPE getType() { + return type; + } + + public String toString() { + return "[" + type.toString() + "] " + text; + } +} \ No newline at end of file diff --git a/src/cs6601/p1/CommandParser.java b/src/cs6601/p1/CommandParser.java new file mode 100644 index 0000000..512c1a1 --- /dev/null +++ b/src/cs6601/p1/CommandParser.java @@ -0,0 +1,54 @@ +package cs6601.p1; + +import org.apache.log4j.Logger; + +public class CommandParser { + private static final Logger LOGGER = Logger.getLogger(CommandParser.class + .getName()); + + //private static final String INT_PATTERN = "^(\\d+.*|-\\d+.*)"; + + /** + * This very simple parser converts a small subset of GTP command strings + * into Commands. + * + * @param commandString + * @return + */ + public static Command parse(String commandString) { + LOGGER.info("Parsing command: " + commandString); + //split on whitespace + if (commandString == null) { + return new Command(Command.TYPE.INVALID,commandString,new String[0]); + } + + String cmdFields[] = commandString.split("\\s+"); + if (cmdFields.length == 0) { + return new Command(Command.TYPE.INVALID,commandString,new String[0]); + } + + if ("boardsize".equals(cmdFields[0])) { + return new Command(Command.TYPE.boardsize,commandString,cmdFields); + } else if ("clear_board".equals(cmdFields[0])) { + return new Command(Command.TYPE.clear_board,commandString,cmdFields); + } else if ("final_status_list".equals(cmdFields[0])) { + return new Command(Command.TYPE.final_status_list,commandString,cmdFields); + } else if ("genmove".equals(cmdFields[0])) { + return new Command(Command.TYPE.genmove,commandString,cmdFields); + } else if ("komi".equals(cmdFields[0])) { + return new Command(Command.TYPE.komi,commandString,cmdFields); + } else if ("list_commands".equals(cmdFields[0])) { + return new Command(Command.TYPE.list_commands,commandString,cmdFields); + } else if ("name".equals(cmdFields[0])) { + return new Command(Command.TYPE.name,commandString,cmdFields); + } else if ("play".equals(cmdFields[0])) { + return new Command(Command.TYPE.play,commandString,cmdFields); + } else if ("quit".equals(cmdFields[0])) { + return new Command(Command.TYPE.quit,commandString,cmdFields); + } else if ("version".equals(cmdFields[0])) { + return new Command(Command.TYPE.version,commandString,cmdFields); + } else { + return new Command(Command.TYPE.INVALID,commandString,cmdFields); + } + } +} diff --git a/src/cs6601/p1/GameBoard.java b/src/cs6601/p1/GameBoard.java new file mode 100644 index 0000000..d0fa790 --- /dev/null +++ b/src/cs6601/p1/GameBoard.java @@ -0,0 +1,210 @@ +package cs6601.p1; + +import java.util.Arrays; + +public class GameBoard { + public static final char BLACK_STONE = 'X'; + public static final char BLACK_TERRITORY = 'x'; + public static final char EMPTY_INTERSECTION = '.'; + public static final char MARKED_GROUP = 'g'; + public static final char MARKED_TERRITORY = '?'; + public static final char UNOWNED_TERRITORY = '-'; + public static final char WHITE_STONE = 'O'; + public static final char WHITE_TERRITORY = 'o'; + + private boolean territoryMarked = false; + private int size; + private char[] board; + + public GameBoard(int size) { + this.size = size; + board = new char[size * size]; + Arrays.fill(board, '.'); + } + + public GameBoard(GameBoard that) { + this.size = that.size; + this.board = Arrays.copyOf(that.board, that.board.length); + } + + public void clear() { + territoryMarked = false; + Arrays.fill(board, EMPTY_INTERSECTION); + } + + public int countSymbols(char... symbols) { + int stoneCount = 0; + for (int i = 0; i < board.length; i++) { + for (int j = 0; j < symbols.length; j++) { + if (board[i] == symbols[j]) { + stoneCount++; + } + } + } + return stoneCount; + } + + @Override + //TODO: implement as Zobrist hash. + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(board); + result = prime * result + size; + result = prime * result + (territoryMarked ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + GameBoard other = (GameBoard) obj; + if (!Arrays.equals(board, other.board)) + return false; + if (size != other.size) + return false; + if (territoryMarked != other.territoryMarked) + return false; + return true; + } + + public static int getColumnIndex(char columnLabel) { + if (columnLabel < 'I') { + return columnLabel - 'A'; + } else if (columnLabel > 'I') { + return columnLabel - 'A' - 1; + } else { + throw new IllegalArgumentException("Invalid column label: " + + columnLabel); + } + } + + public static char getColumnLabel(int columnIndex) { + if (columnIndex > 7) { + return (char) ('A' + columnIndex + 1); + } else { + return (char) ('A' + columnIndex); + } + } + + public static String getCoordinate(int colIndex, int rowIndex) { + return String.valueOf(getColumnLabel(colIndex)) + (rowIndex + 1); + } + + public static char getOpponentSymbol(char stoneSymbol) { + if (stoneSymbol == GameBoard.BLACK_STONE) { + return GameBoard.WHITE_STONE; + } else if (stoneSymbol == GameBoard.WHITE_STONE) { + return GameBoard.BLACK_STONE; + } else { + throw new IllegalArgumentException("StoneSymbol must be BLACK_STONE or WHITE_STONE"); + } + } + + public int getSize() { + return size; + } + + /** + * @param colLabel [A..T] (skipping I + * @param rowNumber 1-based + * @return + */ + public char getSymbolAt(char colLabel, int rowNumber) { + return getSymbolAt(getColumnIndex(colLabel), rowNumber - 1); + } + + /** + * 0-based. + * @param col + * @param row + * @return + */ + public char getSymbolAt(int col, int row) { + try { + return board[(size - row - 1) * size + col]; + } catch (ArrayIndexOutOfBoundsException ae) { + ae.printStackTrace(); + throw ae; + } + } + + public boolean isEmpty(char col, int row) { + return getSymbolAt(col, row) == EMPTY_INTERSECTION; + } + + public boolean isTerritoryMarked() { + return territoryMarked; + } + + public boolean isStarPoint(char col, int row) { + switch (size) { + case 3: + if (col == 'B' && row == 2) { + return true; + } else { + return false; + } + case 4: + return false; + default: + return false; + } + } + + public boolean markTerritory(int col, int row, char mark) { + char symbol = getSymbolAt(col, row); + if (symbol != '.') { + return false; + } + setSymbolAt(col,row,mark); + return true; + } + + public boolean removeStone(char colLabel, int rowNum) { + if (getSymbolAt(colLabel,rowNum) == EMPTY_INTERSECTION) { + return false; + } + setSymbolAt(colLabel,rowNum,EMPTY_INTERSECTION); + return true; + } + + public int replaceSymbol(char symbol, char replacement) { + int numReplaced = 0; + for (int i = 0; i < board.length; i++) { + if (board[i] == symbol) { + board[i] = replacement; + numReplaced++; + } + } + return numReplaced; + } + + public void setSymbolAt(char colLabel, int rowNumber, char symbol) { + setSymbolAt(getColumnIndex(colLabel),rowNumber-1,symbol); + } + public void setSymbolAt(int col, int row, char symbol) { + board[(size - row - 1) * size + col] = symbol; + } + + public void setTerritoryMarked(boolean territoryMarked) { + this.territoryMarked = territoryMarked; + } + + public void unmarkTerritory() { + if (territoryMarked == false) { + return; + } + + replaceSymbol(BLACK_TERRITORY,EMPTY_INTERSECTION); + replaceSymbol(WHITE_TERRITORY,EMPTY_INTERSECTION); + replaceSymbol(UNOWNED_TERRITORY,EMPTY_INTERSECTION); + + territoryMarked = false; + } +} \ No newline at end of file diff --git a/src/cs6601/p1/GameConfig.java b/src/cs6601/p1/GameConfig.java new file mode 100644 index 0000000..d8b3cbf --- /dev/null +++ b/src/cs6601/p1/GameConfig.java @@ -0,0 +1,27 @@ +package cs6601.p1; + +public class GameConfig { + private double komi; + private int timeLimit; + + public GameConfig() { + timeLimit = 0; + komi = 0; + } + + public double getKomi() { + return komi; + } + + public int getTimeLimit(){ + return timeLimit; + } + + public void setKomi(double komi) { + this.komi = komi; + } + + public void setTimeLimit(int timeLimit) { + this.timeLimit = timeLimit; + } +} \ No newline at end of file diff --git a/src/cs6601/p1/GameController.java b/src/cs6601/p1/GameController.java new file mode 100644 index 0000000..9d201dd --- /dev/null +++ b/src/cs6601/p1/GameController.java @@ -0,0 +1,5 @@ +package cs6601.p1; + +public class GameController { + +} \ No newline at end of file diff --git a/src/cs6601/p1/GameScore.java b/src/cs6601/p1/GameScore.java new file mode 100644 index 0000000..ac31680 --- /dev/null +++ b/src/cs6601/p1/GameScore.java @@ -0,0 +1,65 @@ +package cs6601.p1; + + +public class GameScore { + public static final int NORMALIZED_ZERO_SCORE = 379; + + private double komi; + private int blackScore; + private int whiteScore; + + public GameScore(int blackScore, int whiteScore, double komi) { + this.blackScore = blackScore; + this.komi = komi; + this.whiteScore = whiteScore; + } + + public double getBlackScore() { + return (double)blackScore - komi; + } + + /** + * Gets a representation for the game score as an integer. Lower numbers are better for white. + * The minimum value is 0 (Chinese scoring - white owns every intersection on 19x19 and has 9 stone komi). + * Likewise, the maximum value if 379x2 (black owns every intersection with zero komi). + * @return + */ + public int getAggregateScore() { + return NORMALIZED_ZERO_SCORE + 2 * blackScore - ((int)(2 * (whiteScore + komi))); + } + + public double getScore(String color) { + if ("w".equals(color)) { + return getWhiteScore(); + } else if ("b".equals(color)) { + return getBlackScore(); + } else { + return 0.0; + } + } + + public double getWhiteScore() { + return (double)whiteScore + komi; + } + + public String toString() { + return "B: " + blackScore + "W: "+ whiteScore+"K:" + komi; + } + + public String getScoreReport() { + double blackScore = getBlackScore(); + double whiteScore = getWhiteScore(); + + boolean gameTied = Math.abs(blackScore - whiteScore) < 0.5; + + if (gameTied) { + return "Black +0 (tie)"; + } else if (blackScore > whiteScore) { + return "Black +" + + (blackScore - whiteScore - komi); + } else { + return "White +" + + (whiteScore + komi - blackScore); + } + } +} \ No newline at end of file diff --git a/src/cs6601/p1/GameState.java b/src/cs6601/p1/GameState.java new file mode 100644 index 0000000..ce22a79 --- /dev/null +++ b/src/cs6601/p1/GameState.java @@ -0,0 +1,207 @@ +package cs6601.p1; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.Logger; + +import cs6601.p1.generator.MoveGenerator; + +public class GameState { + private static final Logger LOGGER = Logger.getLogger(GameState.class.getName()); + + private int blackPrisoners = 0; + private int whitePrisoners = 0; + private GameBoard gameBoard; + + public GameState(int size) { + if (size < 1 || size > 19) { + throw new IllegalArgumentException("Invalid board size: " + size); + } + gameBoard = new GameBoard(size); + LOGGER.info("Created new GameBoard of size " + size); + } + + public GameState(GameState that) { + this.blackPrisoners = that.blackPrisoners; + this.whitePrisoners = that.whitePrisoners; + gameBoard = new GameBoard(that.gameBoard); + } + + public void clearBoard() { + blackPrisoners = 0; + whitePrisoners = 0; + gameBoard.clear(); + } + + public int getBlackPrisoners() { + return blackPrisoners; + } + + public List getEmptyCoords() { + List emptyCoords = new ArrayList(); + + for (int colIndex = 0; colIndex < gameBoard.getSize(); colIndex++) { + for (int rowIndex = 0; rowIndex < gameBoard.getSize(); rowIndex++) { + if (GameBoard.EMPTY_INTERSECTION == gameBoard.getSymbolAt(colIndex, rowIndex)); + emptyCoords.add(GameBoard.getCoordinate(colIndex,rowIndex)); + } + } + + return emptyCoords; + } + + public GameBoard getGameBoard() { + return gameBoard; + } + + public int getWhitePrisoners() { + return whitePrisoners; + } + + public boolean playStone(String player, String coord) { + //Opponent passes? Just ignore it. + if (MoveGenerator.PASS.equalsIgnoreCase(coord)) { + return true; + } + + //LOGGER.info("Playing " + player + " at " + coord); + + char stoneColor; + if ("b".equals(player)) { + stoneColor = GameBoard.BLACK_STONE; + } else if ("w".equals(player)) { + stoneColor = GameBoard.WHITE_STONE; + } else { + return false; + } + + char col = coord.charAt(0); + int row = Integer.valueOf(coord.substring(1)); + + //LOGGER.info("Playing " + stoneColor + " at " + col + row); + return playStone(col,row, stoneColor); + } + /** + * Places a stone at the requested coordinate. Placement is legal if the + * coordinate is currently empty, has at least one liberty (empty neighbor), + * or at least one neighbor has this (recursive) property, or results in the + * capture and removal of at least one stone to produce a legal placement. + * Note also that placement is illegal if it would result in the capture of + * one's own group. Hence, placement must result in the capture of an + * adjacent stone or stones, if any. + * + * @param colLabel + * @param row + * @param stone + * @return + */ + public boolean playStone(char colLabel, int rowNum, char stoneSymbol) { + char currentStone = gameBoard.getSymbolAt(colLabel, rowNum); + if (currentStone != GameBoard.EMPTY_INTERSECTION) { + return false; + } + + //Place stone as requested, then check for (1) captured neighbors and (2) illegal move due to 0 liberties. + gameBoard.setSymbolAt(colLabel, rowNum, stoneSymbol); + + //look for captured adjacent groups and increment the prisoner counter + char opponentSymbol = GameBoard.getOpponentSymbol(stoneSymbol); + + int col = GameBoard.getColumnIndex(colLabel); + int row = rowNum - 1; + + int prisonerCount = 0; + if (col > 0 && gameBoard.getSymbolAt(col-1, row) == opponentSymbol) { + int liberties = LibertyCounter.countLiberties(gameBoard, col-1, row, opponentSymbol,true); + if (liberties == 0) { + prisonerCount += gameBoard.replaceSymbol(GameBoard.MARKED_GROUP,GameBoard.EMPTY_INTERSECTION); + } else { + gameBoard.replaceSymbol(GameBoard.MARKED_GROUP, opponentSymbol); + } + } + if (col < gameBoard.getSize() - 1 && gameBoard.getSymbolAt(col+1, row) == opponentSymbol) { + int liberties = LibertyCounter.countLiberties(gameBoard, col+1, row, opponentSymbol,true); + if (liberties == 0) { + prisonerCount += gameBoard.replaceSymbol(GameBoard.MARKED_GROUP,GameBoard.EMPTY_INTERSECTION); + } else { + gameBoard.replaceSymbol(GameBoard.MARKED_GROUP, opponentSymbol); + } + } + if (row > 0 && gameBoard.getSymbolAt(col, row-1) == opponentSymbol) { + int liberties = LibertyCounter.countLiberties(gameBoard, col, row-1, opponentSymbol,true); + if (liberties == 0) { + prisonerCount += gameBoard.replaceSymbol(GameBoard.MARKED_GROUP,GameBoard.EMPTY_INTERSECTION); + } else { + gameBoard.replaceSymbol(GameBoard.MARKED_GROUP, opponentSymbol); + } + } + if (row < gameBoard.getSize() - 1 && gameBoard.getSymbolAt(col, row+1) == opponentSymbol) { + int liberties = LibertyCounter.countLiberties(gameBoard, col, row+1, opponentSymbol,true); + if (liberties == 0) { + prisonerCount += gameBoard.replaceSymbol(GameBoard.MARKED_GROUP,GameBoard.EMPTY_INTERSECTION); + } else { + gameBoard.replaceSymbol(GameBoard.MARKED_GROUP, opponentSymbol); + } + } + + if (stoneSymbol == GameBoard.BLACK_STONE) { + blackPrisoners += prisonerCount; + } else if (stoneSymbol == GameBoard.WHITE_STONE) { + whitePrisoners += prisonerCount; + } + + //Moved test for 0 liberties until after attempting to capture neighboring groups. + if (0 == LibertyCounter.countLiberties(gameBoard, colLabel, rowNum, stoneSymbol)) { + gameBoard.removeStone(colLabel,rowNum); + return false; + } + return true; + } + + public String toString() { + int boardSize = gameBoard.getSize(); + StringBuilder sb = new StringBuilder(" "); + for (int cIndex = 0; cIndex < boardSize; cIndex++) { + sb.append(' '); + if (cIndex < 'I'-'A') { + sb.append((char)('A'+cIndex)); + } else { + sb.append((char)('A' + cIndex + 1)); + } + } + //note the extra space + sb.append(System.lineSeparator()); + for (int rIndex = boardSize - 1; rIndex >= 0; rIndex--) { + if (rIndex < 9) { + sb.append(' '); + } + sb.append(rIndex+1); + for (int cIndex = 0; cIndex < boardSize; cIndex++) { + sb.append(' '); + sb.append(gameBoard.getSymbolAt(cIndex, rIndex)); + } + sb.append(" " + (rIndex+1)); + if (rIndex == boardSize/2) { + sb.append(" WHITE(O) has captured "); + sb.append(getWhitePrisoners()); + sb.append(" stones"); + } else if (rIndex == boardSize/2 -1) { + sb.append(" BLACK(X) has captured "); + sb.append(getBlackPrisoners()); + sb.append(" stones"); + } + sb.append(System.lineSeparator()); + } + sb.append(" "); + for (int cIndex = 0; cIndex < boardSize; cIndex++) { + sb.append(' '); + if (cIndex < 'I'-'A') { + sb.append((char)('A'+cIndex)); + } else { + sb.append((char)('A' + cIndex + 1)); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/cs6601/p1/GoGame.java b/src/cs6601/p1/GoGame.java new file mode 100644 index 0000000..911f2de --- /dev/null +++ b/src/cs6601/p1/GoGame.java @@ -0,0 +1,149 @@ +package cs6601.p1; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.apache.log4j.Logger; +import org.apache.log4j.xml.DOMConfigurator; + +import cs6601.p1.Command.TYPE; +import cs6601.p1.generator.AlphaBetaMoveGenerator; +import cs6601.p1.generator.MinimaxMoveGenerator; +import cs6601.p1.generator.MonteCarloMoveGenerator; +import cs6601.p1.generator.MoveGenerator; +import cs6601.p1.generator.RandomMoveGenerator; + +public class GoGame { + private static final int INVALID_MOVE_GENERATOR = 1; + private static final Logger LOGGER = Logger.getLogger(GoGame.class + .getName()); + + private boolean shutDown = false; + private GameConfig gameConfig = new GameConfig(); + private GameState gameState = new GameState(9); + private MoveGenerator moveGenerator; + + public GoGame(MoveGenerator moveGenerator) { + this.moveGenerator = moveGenerator; + } + + public static void main(String[] args) { + configureLogging(); + if (args.length == 0) { + MoveGenerator defaultMoveGenerator = new MonteCarloMoveGenerator(); + LOGGER.info("No MoveGenerator specified. Using default: " + defaultMoveGenerator.getClass().getName()); + new GoGame(defaultMoveGenerator).play(); + } else { + new GoGame(createMoveGenerator(args[0])).play(); + } + } + + private static MoveGenerator createMoveGenerator(String generatorName) { + if ("random".equals(generatorName)) { + return new RandomMoveGenerator(); + } else if ("minimax".equals(generatorName)) { + return new MinimaxMoveGenerator(); + } else if ("alphabeta".equals(generatorName)) { + return new AlphaBetaMoveGenerator(); + } else if ("montecarlo".equals(generatorName)) { + return new MonteCarloMoveGenerator(); + } else { + LOGGER.info("Unable to create MoveGenerator for unsupported name: " + generatorName); + System.exit(INVALID_MOVE_GENERATOR); + //This line will never be executed but prevents the compiler from complaining. + return null; + } + } + + private void executeCommand(Command cmd) { + switch (cmd.getType()) { + case INVALID: + LOGGER.info("Invalid command ignored: " + cmd.getText()); + System.out.println("? Invalid command: " + cmd.getText() + "\n"); + break; + case boardsize: + gameState = new GameState(cmd.getIntField(1)); + System.out.println("=\n"); + break; + case clear_board: + gameState.clearBoard(); + System.out.println("=\n"); + break; + case final_status_list: + LOGGER.info(new StateEvaluator(gameConfig).scoreGame(gameState)); + System.out.println("=\n"); + break; + case genmove: + LOGGER.info("Generating move for:\n" + gameState); + + String player = cmd.getStringField(1); + String nextMove = moveGenerator.genMove(gameConfig, gameState, + player); + + gameState.playStone(player, nextMove); + LOGGER.info(new StateEvaluator(gameConfig).scoreGame(gameState)); + System.out.println("=" + + nextMove.toLowerCase() + "\n"); + + break; + case komi: + gameConfig.setKomi(cmd.getDoubleField(1)); + System.out.println("=\n"); + break; + case list_commands: + StringBuilder sb = new StringBuilder("= "); + for (TYPE type : Command.TYPE.values()) { + if (type != TYPE.INVALID) { + sb.append(type.toString()); + sb.append("\n"); + } + } + System.out.println(sb.toString()); + break; + case name: + System.out.println("= CS6601P1\n"); + break; + case quit: + System.out.println("=\n"); + shutDown = true; + break; + case play: + if (gameState.playStone(cmd.getStringField(1), cmd + .getStringField(2).toUpperCase())) { + System.out.println("=\n"); + } else { + String errMsg = "Unable to make requested play: " + + cmd.getText(); + LOGGER.info(errMsg); + LOGGER.info("GameState: " + gameState); + System.out.println("?" + errMsg + "\n"); + } + LOGGER.info(new StateEvaluator(gameConfig).scoreGame(gameState)); + break; + case version: + System.out.println("= 0.1\n"); + break; + default: + LOGGER.warn("An error occured. Unhandled command: " + cmd); + } + } + + public void play() { + byte[] buf = new byte[128]; + int inputLength; + try { + while (!shutDown && (inputLength = System.in.read(buf)) > 0) { + String commandText = new String(buf, 0, inputLength, + Charset.forName("UTF-8")); + LOGGER.info("Command received: " + commandText); + executeCommand(CommandParser.parse(commandText)); + } + } catch (IOException ioe) { + LOGGER.warn(ioe.getMessage()); + } + } + + private static void configureLogging() { + DOMConfigurator.configure("log4j.xml"); + } +} \ No newline at end of file diff --git a/src/cs6601/p1/GtpClient.java b/src/cs6601/p1/GtpClient.java new file mode 100644 index 0000000..176d863 --- /dev/null +++ b/src/cs6601/p1/GtpClient.java @@ -0,0 +1,48 @@ +package cs6601.p1; + +public class GtpClient { + + /** + * Create a new GTP Client. + * + * @param in + * An input stream that will give us any responses from the + * engine. + * @param out + * An output stream that will go to the engine with our commands. + * @param args + * Our options. + */ + public GtpClient(java.io.InputStream in, java.io.OutputStream out, + Options options) { + } + + /** + * Connect to the server and operate the GTP interface. + * + * @return true on success, false if we were + * unable to do what was requested. + */ + public boolean go() { + return false; + } +} + +class Options { + + /** + * Construct a new set of options for the kgsGtp client by pulling values + * out of a set of properties. Any values that we use will be removed from + * the properties, so anything left over is unused. + * + * @param props + * The properties that holds our options as string values. + * @param logName + * The name of the java logger that we will construct. + * @throws IllegalArgumentException + * If any required values are missing or if any illegal values + * are detected. + */ + public Options(java.util.Properties props, String logName) { + } +} diff --git a/src/cs6601/p1/LibertyCounter.java b/src/cs6601/p1/LibertyCounter.java new file mode 100644 index 0000000..8935842 --- /dev/null +++ b/src/cs6601/p1/LibertyCounter.java @@ -0,0 +1,51 @@ +package cs6601.p1; + +public class LibertyCounter { + public static int countLiberties(GameBoard gameBoard, char colLabel, int rowNum, char groupColor) { + return countLiberties(gameBoard, GameBoard.getColumnIndex(colLabel),rowNum-1,groupColor,false); + } + + public static int countLiberties(GameBoard gameBoard, int col, int row, char groupColor, boolean markGroup) { + int liberties = markGroup(gameBoard, col, row, groupColor); + if (!markGroup) { + gameBoard.replaceSymbol(GameBoard.MARKED_GROUP,groupColor); + } + return liberties; + } + + public static int markGroup(GameBoard gameBoard, int col, int row, char groupColor) { + char stoneSymbol = gameBoard.getSymbolAt(col, row); + if (stoneSymbol == GameBoard.EMPTY_INTERSECTION) { + return 1; // one liberty + } + if (stoneSymbol == GameBoard.MARKED_GROUP) { + return 0; // intersection already counted + } + if (stoneSymbol == groupColor) { + int liberties = 0; + gameBoard.setSymbolAt(col, row, GameBoard.MARKED_GROUP); + if (col > 0) { + liberties += markGroup(gameBoard,col-1,row,groupColor); + } + if (col < gameBoard.getSize() - 1) { + liberties += markGroup(gameBoard,col+1,row,groupColor); + } + if (row > 0) { + liberties += markGroup(gameBoard,col,row-1,groupColor); + } + if (row < gameBoard.getSize() - 1) { + liberties += markGroup(gameBoard,col,row+1,groupColor); + } + return liberties; + } + + if (groupColor == GameBoard.WHITE_STONE && stoneSymbol == GameBoard.BLACK_STONE) { + return 0; //opposing stone - not a liberty + } + if (groupColor == GameBoard.BLACK_STONE && stoneSymbol == GameBoard.WHITE_STONE) { + return 0; //opposing stone - not a liberty + } + + throw new IllegalStateException("Cannot continue marking group - symbol at current location is not empty intersection, white stone, black stone or group marker"); + } +} diff --git a/src/cs6601/p1/StateEvaluator.java b/src/cs6601/p1/StateEvaluator.java new file mode 100644 index 0000000..2090bd6 --- /dev/null +++ b/src/cs6601/p1/StateEvaluator.java @@ -0,0 +1,23 @@ +package cs6601.p1; + +public class StateEvaluator { + private final GameConfig gameConfig; + + public StateEvaluator(GameConfig gameConfig) { + this.gameConfig = gameConfig; + } + + public GameScore scoreGame(GameState gameState) { + GameBoard gameBoard; + if (gameState.getGameBoard().isTerritoryMarked()) { + gameBoard = gameState.getGameBoard(); + } else { + gameBoard = new GameBoard(gameState.getGameBoard()); + TerritoryMarker.markTerritory(gameBoard); + } + + //TODO include komi from gameConfig + return new GameScore(gameBoard.countSymbols(GameBoard.BLACK_STONE,GameBoard.BLACK_TERRITORY), + gameBoard.countSymbols(GameBoard.WHITE_STONE,GameBoard.WHITE_TERRITORY),gameConfig.getKomi()); + } +} \ No newline at end of file diff --git a/src/cs6601/p1/TerritoryMarker.java b/src/cs6601/p1/TerritoryMarker.java new file mode 100644 index 0000000..2f7436e --- /dev/null +++ b/src/cs6601/p1/TerritoryMarker.java @@ -0,0 +1,74 @@ +package cs6601.p1; + +public class TerritoryMarker { + public static final char BLACK_TERRITORY = 'x'; + public static final char WHITE_TERRITORY = 'o'; + public static final char UNOWNED_TERRITORY = '-'; + public static final char TERRITORY_MARKER = '?'; + + public static final int EMPTY = 0; + public static final int BLACK = 1; + public static final int WHITE = 2; + + public static GameBoard markTerritory(GameBoard gameBoard) { + for (int row = 0; row < gameBoard.getSize(); row++) { + for (int col = 0; col < gameBoard.getSize(); col++) { + char symbol = gameBoard.getSymbolAt(col,row); + if (symbol != '.') { + continue; + } + int ownedBy = findTerritory(gameBoard,col,row); + if (ownedBy == BLACK) { + gameBoard.replaceSymbol(TERRITORY_MARKER, BLACK_TERRITORY); + } else if (ownedBy == WHITE) { + gameBoard.replaceSymbol(TERRITORY_MARKER, WHITE_TERRITORY); + } else { + gameBoard.replaceSymbol(TERRITORY_MARKER, UNOWNED_TERRITORY); + } + } + } + gameBoard.setTerritoryMarked(true); + return gameBoard; + } + + public static int getStoneColor(GameBoard gameBoard, int col, int row) { + char symbol = gameBoard.getSymbolAt(col,row); + if (symbol == GameBoard.BLACK_STONE) { + return BLACK; + } else if (symbol == GameBoard.WHITE_STONE) { + return WHITE; + } else { + return EMPTY; + } + } + + public static int findTerritory(GameBoard gameBoard, int col, int row) { + char symbol = gameBoard.getSymbolAt(col,row); + + if (symbol == GameBoard.BLACK_STONE) { + return BLACK; + } else if (symbol == GameBoard.WHITE_STONE) { + return WHITE; + } else if (symbol == TERRITORY_MARKER) { + return EMPTY; + } + + gameBoard.markTerritory(col,row,TERRITORY_MARKER); + int borderBits = EMPTY; + + if (col > 0) { + borderBits |= findTerritory(gameBoard,col-1,row); + } + if (col < gameBoard.getSize() - 1) { + borderBits |= findTerritory(gameBoard,col+1,row); + } + if (row > 0) { + borderBits |= findTerritory(gameBoard,col,row-1); + } + if (row < gameBoard.getSize() - 1) { + borderBits |= findTerritory(gameBoard,col,row+1); + } + + return borderBits; + } +} \ No newline at end of file diff --git a/src/cs6601/p1/generator/AlphaBetaMoveGenerator.java b/src/cs6601/p1/generator/AlphaBetaMoveGenerator.java new file mode 100644 index 0000000..e6a35e3 --- /dev/null +++ b/src/cs6601/p1/generator/AlphaBetaMoveGenerator.java @@ -0,0 +1,168 @@ +package cs6601.p1.generator; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Logger; + +import cs6601.p1.GameConfig; +import cs6601.p1.GameState; +import cs6601.p1.StateEvaluator; + +public class AlphaBetaMoveGenerator implements MoveGenerator { + private static final Logger LOGGER = Logger.getLogger(AlphaBetaMoveGenerator.class.getName()); + private static final int DEFAULT_RECURSIVE_PLAYS = 3; + private final ValidMoveGenerator validMoveGenerator = new ValidMoveGenerator(); + + private String bestPick = MoveGenerator.PASS; + + @Override + public String genMove(GameConfig gameConfig, GameState gameState, + String initialColor) { + + int alpha = Integer.MIN_VALUE; + int beta = Integer.MAX_VALUE; + + if ("b".equals(initialColor)) { + getMaxValue(gameConfig,gameState,initialColor,false,DEFAULT_RECURSIVE_PLAYS*2,alpha,beta); + return bestPick; + } else if ("w".equals(initialColor)) { + getMinValue(gameConfig,gameState,initialColor,false,DEFAULT_RECURSIVE_PLAYS*2,alpha,beta); + return bestPick; + } else { + return MoveGenerator.PASS; + } + } + + private int getMaxValue(GameConfig gameConfig, GameState gameState, + String initialColor, boolean playAsOpponent, int recursionLevel, int alpha, int beta) { + if (terminalTest(recursionLevel)) { + return getUtility(gameConfig,gameState); + } + + String colorPlaying = getColorToPlay(initialColor, playAsOpponent); + + List validMoves = validMoveGenerator.genMoves(gameConfig, + gameState, colorPlaying, MoveGenerator.ALL_MOVES); + + int value = Integer.MIN_VALUE; + + //Map firstMovesByScore = new HashMap (); + + for (String nextMove : validMoves) { + GameState nextState = new GameState(gameState); + + if (!nextState.playStone(colorPlaying, nextMove)) { + throw new RuntimeException("Illegal move attempted during search!"); + } + + int minValue = getMinValue(gameConfig, nextState, + initialColor, !playAsOpponent, recursionLevel-1, alpha, beta); + //value = Math.max(value, minResult); + if (minValue > value) { + value = minValue; + if (recursionLevel == DEFAULT_RECURSIVE_PLAYS * 2) { + bestPick = nextMove; + } + } + /// + //if (recursionLevel == 2 * DEFAULT_RECURSIVE_PLAYS) { + // firstMovesByScore.put(value, nextMove); + //} + /// + if (value >= beta) { + return value; + } + alpha = Math.max(alpha,value); + } + + return value; + } + + private int getMinValue(GameConfig gameConfig, GameState gameState, + String initialColor, boolean playAsOpponent, int recursionLevel, int alpha, int beta) { + if (terminalTest(recursionLevel)) { + return getUtility(gameConfig,gameState); + } + + String colorPlaying = getColorToPlay(initialColor, playAsOpponent); + + List validMoves = validMoveGenerator.genMoves(gameConfig, + gameState, colorPlaying, MoveGenerator.ALL_MOVES); + + int value = Integer.MAX_VALUE; + + //Map firstMovesByScore = new HashMap (); + + for (String nextMove : validMoves) { + GameState nextState = new GameState(gameState); + + if (!nextState.playStone(colorPlaying, nextMove)) { + throw new RuntimeException("Illegal move attempted during search!"); + } + + int maxValue = getMaxValue(gameConfig, nextState, + initialColor, !playAsOpponent, recursionLevel-1, alpha, beta); + //value = Math.min(value, maxValue); + if (maxValue < value) { + value = maxValue; + if (recursionLevel == 2 * DEFAULT_RECURSIVE_PLAYS) { + //firstMovesByScore.put(value, nextMove); + bestPick = nextMove; + } + } + /// + //if (recursionLevel == 2 * DEFAULT_RECURSIVE_PLAYS) { + // firstMovesByScore.put(value, nextMove); + //} + /// + if (value <= alpha) { + return value; + } + beta = Math.min(beta,value); + } + + //if (recursionLevel == DEFAULT_RECURSIVE_PLAYS * 2) { + // bestPick = firstMovesByScore.get(value); + //} + + return value; + } + + private boolean terminalTest(int recursionLevel) { + return recursionLevel < 1; + } + + private int getUtility(GameConfig gameConfig, GameState gameState) { + StateEvaluator stateEvaluator = new StateEvaluator(gameConfig); + return stateEvaluator.scoreGame(gameState).getAggregateScore(); + } + + private String getColorToPlay(String color, boolean playAsOpponent) { + if (playAsOpponent) { + if ("w".equals(color)) { + return "b"; + } else if ("b".equals(color)) { + return "w"; + } else { + return "?"; // invalid color will cause randomMoveGenerator to + // PASS + } + } else { + return color; + } + } + + /** + * AlphaBetaMoveGenerator2 does not support this method. + */ + @Override + public List genMoves(GameConfig gameConfig, GameState gameState, + String color, int nMoves) { + String[] pass = new String[] {PASS}; + LOGGER.info("Minimax genMoves() stub returning [PASS]"); + return Arrays.asList(pass); + } +} diff --git a/src/cs6601/p1/generator/MinimaxMoveGenerator.java b/src/cs6601/p1/generator/MinimaxMoveGenerator.java new file mode 100644 index 0000000..1173171 --- /dev/null +++ b/src/cs6601/p1/generator/MinimaxMoveGenerator.java @@ -0,0 +1,107 @@ +package cs6601.p1.generator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.log4j.Logger; + +import cs6601.p1.GameConfig; +import cs6601.p1.GameScore; +import cs6601.p1.GameState; +import cs6601.p1.StateEvaluator; + +public class MinimaxMoveGenerator implements MoveGenerator { + private static final Logger LOGGER = Logger.getLogger(MinimaxMoveGenerator.class.getName()); + + //private static final Logger LOGGER = Logger + // .getLogger(MonteCarloMoveGenerator.class.getName()); + + private static final int DEFAULT_RECURSIVE_PLAYS = 1; + + private final ValidMoveGenerator validMoveGenerator = new ValidMoveGenerator(); + + @Override + public String genMove(GameConfig gameConfig, GameState gameState, + String color) { + MoveCandidate moveCandidate = findBestMinimaxResult( + DEFAULT_RECURSIVE_PLAYS * 2, + gameConfig, gameState, color, false, PASS); + + return moveCandidate.move; + } + + private MoveCandidate findBestMinimaxResult(int recursionLevels, + GameConfig gameConfig, GameState gameState, + String initialColor, boolean playAsOpponent, String bestPrevMove) { + + StateEvaluator stateEvaluator = new StateEvaluator(gameConfig); + List randomMoveCandidates = new ArrayList(); + + String colorPlaying = getColorToPlay(initialColor, playAsOpponent); + + List validMoves = validMoveGenerator.genMoves(gameConfig, + gameState, colorPlaying, MoveGenerator.ALL_MOVES); + + for (String randomMove : validMoves) { + GameState stateCopy = new GameState(gameState); + stateCopy.playStone(colorPlaying, randomMove); + if (recursionLevels > 1) { + randomMoveCandidates.add(findBestMinimaxResult(recursionLevels - 1, + gameConfig, stateCopy, initialColor, + !playAsOpponent, randomMove)); + } else { + GameScore score = stateEvaluator.scoreGame(stateCopy); + randomMoveCandidates.add(new MoveCandidate(randomMove, score)); + } + } + + // TODO use a sorted list and just return the last element + MoveCandidate bestMove = randomMoveCandidates.get(0); + double bestScoreSoFar = bestMove.score.getScore(colorPlaying); + + for (MoveCandidate moveCandidate : randomMoveCandidates) { + if (moveCandidate.score.getScore(colorPlaying) > bestScoreSoFar) { + bestMove = moveCandidate; + bestScoreSoFar = moveCandidate.score.getScore(colorPlaying); + } + } + + // Fix to prevent thinking that the _opponent's_ best move is the move + // to make. + // If evaluating an opponent's move, the best move (for my opponent) is + // my previous move which gives the opponent the highest score. + if (playAsOpponent) { + return new MoveCandidate(bestPrevMove, bestMove.score); + } else { // if evaluating my own move, the move which gives me the + // highest score is the best. + return bestMove; + } + } + + private String getColorToPlay(String color, boolean playAsOpponent) { + if (playAsOpponent) { + if ("w".equals(color)) { + return "b"; + } else if ("b".equals(color)) { + return "w"; + } else { + return "?"; // invalid color will cause randomMoveGenerator to + // PASS + } + } else { + return color; + } + } + + /** + * MinimaxMoveGenerator does not support this method. + */ + @Override + public List genMoves(GameConfig gameConfig, GameState gameState, + String color, int nMoves) { + String[] pass = new String[] {PASS}; + LOGGER.info("Minimax genMoves() stub returning [PASS]"); + return Arrays.asList(pass); + } +} \ No newline at end of file diff --git a/src/cs6601/p1/generator/MonteCarloMoveGenerator.java b/src/cs6601/p1/generator/MonteCarloMoveGenerator.java new file mode 100644 index 0000000..22d3bc8 --- /dev/null +++ b/src/cs6601/p1/generator/MonteCarloMoveGenerator.java @@ -0,0 +1,109 @@ +package cs6601.p1.generator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import cs6601.p1.GameConfig; +import cs6601.p1.GameScore; +import cs6601.p1.GameState; +import cs6601.p1.StateEvaluator; + +public class MonteCarloMoveGenerator implements MoveGenerator { + //private static final Logger LOGGER = Logger + // .getLogger(MonteCarloMoveGenerator.class.getName()); + + private static final int DEFAULT_RECURSIVE_PLAYS = 3; + private static final int DEFAULT_PLAYS_PER_LEVEL = 10; + //private static final int MAX_RANDOM_TRIES = 10; + + private final RandomMoveGenerator randomMoveGenerator = new RandomMoveGenerator(); + + @Override + public String genMove(GameConfig gameConfig, GameState gameState, + String color) { + MoveCandidate moveCandidate = findBestMonteCarloResult( + DEFAULT_RECURSIVE_PLAYS * 2, DEFAULT_PLAYS_PER_LEVEL, + gameConfig, gameState, color, false, PASS); + + return moveCandidate.move; + } + + private MoveCandidate findBestMonteCarloResult(int recursionLevels, + int playsPerLevel, GameConfig gameConfig, GameState gameState, + String initialColor, boolean playAsOpponent, String bestPrevMove) { + + StateEvaluator stateEvaluator = new StateEvaluator(gameConfig); + List randomMoveCandidates = new ArrayList(); + + String colorPlaying = getColorToPlay(initialColor, playAsOpponent); + + List randomMoves = randomMoveGenerator.genMoves(gameConfig, + gameState, colorPlaying, DEFAULT_PLAYS_PER_LEVEL); + + for (String randomMove : randomMoves) { + GameState stateCopy = new GameState(gameState); + stateCopy.playStone(colorPlaying, randomMove); + if (recursionLevels > 1) { + randomMoveCandidates.add(findBestMonteCarloResult(recursionLevels - 1, + playsPerLevel, gameConfig, stateCopy, initialColor, + !playAsOpponent, randomMove)); + } else { + GameScore score = stateEvaluator.scoreGame(stateCopy); + randomMoveCandidates.add(new MoveCandidate(randomMove, score)); + } + } + + // TODO use a sorted list and just return the last element + MoveCandidate bestMove = randomMoveCandidates.get(0); + double bestScoreSoFar = bestMove.score.getScore(colorPlaying); + + for (MoveCandidate moveCandidate : randomMoveCandidates) { + if (moveCandidate.score.getScore(colorPlaying) > bestScoreSoFar) { + bestMove = moveCandidate; + bestScoreSoFar = moveCandidate.score.getScore(colorPlaying); + } + } + + // Fix to prevent thinking that the _opponent's_ best move is the move + // to make. + // If evaluating an opponent's move, the best move (for my opponent) is + // my previous move which gives the opponent the highest score. + if (playAsOpponent) { + return new MoveCandidate(bestPrevMove, bestMove.score); + } else { // if evaluating my own move, the move which gives me the + // highest score is the best. + return bestMove; + } + } + + private String getColorToPlay(String color, boolean playAsOpponent) { + if (playAsOpponent) { + if ("w".equals(color)) { + return "b"; + } else if ("b".equals(color)) { + return "w"; + } else { + return "?"; // invalid color will cause randomMoveGenerator to + // PASS + } + } else { + return color; + } + } + + /** + * MonteCarloMoveGenerator does not support this method - just pass. + * + * @param gameConfig + * @param gameState + * @param color + * @return + */ + @Override + public List genMoves(GameConfig gameConfig, GameState gameState, + String color, int nMoves) { + String[] pass = new String[] {PASS}; + return Arrays.asList(pass); + } +} \ No newline at end of file diff --git a/src/cs6601/p1/generator/MoveCandidate.java b/src/cs6601/p1/generator/MoveCandidate.java new file mode 100644 index 0000000..cc91fd5 --- /dev/null +++ b/src/cs6601/p1/generator/MoveCandidate.java @@ -0,0 +1,13 @@ +package cs6601.p1.generator; + +import cs6601.p1.GameScore; + +public class MoveCandidate { + public final String move; + public final GameScore score; + + public MoveCandidate(String move, GameScore score) { + this.move = move; + this.score = score; + } +} \ No newline at end of file diff --git a/src/cs6601/p1/generator/MoveGenerator.java b/src/cs6601/p1/generator/MoveGenerator.java new file mode 100644 index 0000000..5bb7217 --- /dev/null +++ b/src/cs6601/p1/generator/MoveGenerator.java @@ -0,0 +1,13 @@ +package cs6601.p1.generator; + +import java.util.List; + +import cs6601.p1.GameConfig; +import cs6601.p1.GameState; + +public interface MoveGenerator { + static final String PASS = "PASS"; + static final int ALL_MOVES = 0; + public String genMove(GameConfig gameConfig, GameState gameState, String color); + public List genMoves(GameConfig gameConfig, GameState gameState, String color, int nMoves); +} \ No newline at end of file diff --git a/src/cs6601/p1/generator/RandomMoveGenerator.java b/src/cs6601/p1/generator/RandomMoveGenerator.java new file mode 100644 index 0000000..bb1b3dc --- /dev/null +++ b/src/cs6601/p1/generator/RandomMoveGenerator.java @@ -0,0 +1,65 @@ +package cs6601.p1.generator; + +import java.util.ArrayList; +import java.util.List; + +import cs6601.p1.GameConfig; +import cs6601.p1.GameState; + +public class RandomMoveGenerator implements MoveGenerator { + + /** + * Does NOT modify the gameState. + */ + public String genMove(GameConfig gameConfig, GameState gameState, + String color) { + GameState gameStateCopy = new GameState(gameState); + List emptyCoordinates = gameStateCopy.getEmptyCoords(); + + while (emptyCoordinates.size() > 0) { + String randomMove = emptyCoordinates + .get((int) (Math.random() * emptyCoordinates.size())); + if (gameStateCopy.playStone(color, randomMove)) { + return randomMove; + } else { + emptyCoordinates.remove(randomMove); + } + } + + return PASS; + } + + /** + * Attempts to generate up to nMoves random moves on behalf of the specified + * player. Will return at least one move, which may be 'pass' if random + * search does not success in discovering a valid move. Does NOT modify the + * gameState. + * + * @param gameConfig + * @param gameState + * @param color + * + * @return + */ + public List genMoves(GameConfig gameConfig, GameState gameState, + String color, int nMoves) { + GameState gameStateCopy = new GameState(gameState); + List emptyCoordinates = gameStateCopy.getEmptyCoords(); + List randomMoves = new ArrayList(); + + while (emptyCoordinates.size() > 0 && randomMoves.size() < nMoves) { + String randomMove = emptyCoordinates + .get((int) (Math.random() * emptyCoordinates.size())); + if (gameStateCopy.playStone(color, randomMove)) { + randomMoves.add(randomMove); + } + emptyCoordinates.remove(randomMove); + } + + if (randomMoves.size() == 0) { + randomMoves.add(PASS); + } + + return randomMoves; + } +} diff --git a/src/cs6601/p1/generator/ValidMoveGenerator.java b/src/cs6601/p1/generator/ValidMoveGenerator.java new file mode 100644 index 0000000..713fa67 --- /dev/null +++ b/src/cs6601/p1/generator/ValidMoveGenerator.java @@ -0,0 +1,44 @@ +package cs6601.p1.generator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.log4j.Logger; + +import cs6601.p1.GameConfig; +import cs6601.p1.GameState; + +public class ValidMoveGenerator implements MoveGenerator { + private static final Logger LOGGER = Logger.getLogger(MinimaxMoveGenerator.class.getName()); + + @Override + public String genMove(GameConfig gameConfig, GameState gameState, + String color) { + LOGGER.info("ValidMoveGenerator genMove() stub returning PASS"); + return PASS; + } + + @Override + public List genMoves(GameConfig gameConfig, GameState gameState, + String color, int nMoves) { + + GameState gameStateCopy = new GameState(gameState); + List emptyCoordinates = gameStateCopy.getEmptyCoords(); + List validMoves = new ArrayList(); + + while (emptyCoordinates.size() > 0) { + String nextMove = emptyCoordinates.remove(emptyCoordinates.size()-1); + if (gameStateCopy.playStone(color, nextMove)) { + validMoves.add(nextMove); + gameStateCopy = new GameState(gameState); // play successful? regenerate copy of gameState + } + } + + if (validMoves.size() == 0) { + validMoves.add(PASS); + } + + return validMoves; + } +} diff --git a/test/cs6601/p1/CaptureTest.java b/test/cs6601/p1/CaptureTest.java new file mode 100644 index 0000000..19434de --- /dev/null +++ b/test/cs6601/p1/CaptureTest.java @@ -0,0 +1,80 @@ +package cs6601.p1; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class CaptureTest { + @Test + public void testCapture() { + GameState gameState = new GameState(5); + gameState.playStone('A', 2, GameBoard.BLACK_STONE); + gameState.playStone('B', 3, GameBoard.BLACK_STONE); + gameState.playStone('B', 1, GameBoard.BLACK_STONE); + assertTrue(gameState.playStone('B', 2, GameBoard.WHITE_STONE)); + + assertEquals(0,gameState.getBlackPrisoners()); + assertEquals(0,gameState.getWhitePrisoners()); + + assertTrue(gameState.playStone('C', 2, GameBoard.BLACK_STONE)); + + assertEquals(1,gameState.getBlackPrisoners()); + assertEquals(0,gameState.getWhitePrisoners()); + + System.out.println(gameState); + } + + @Test + public void testMultiGroupCapture() { + GameConfig gameConfig = new GameConfig(); + GameState gameState = new GameState(5); + + gameState.playStone('A', 2, GameBoard.BLACK_STONE); + gameState.playStone('B', 3, GameBoard.BLACK_STONE); + gameState.playStone('B', 1, GameBoard.BLACK_STONE); + gameState.playStone('C', 4, GameBoard.BLACK_STONE); + gameState.playStone('D', 3, GameBoard.BLACK_STONE); + + assertTrue(gameState.playStone('B', 2, GameBoard.WHITE_STONE)); + assertTrue(gameState.playStone('C', 3, GameBoard.WHITE_STONE)); + + assertEquals(0,gameState.getBlackPrisoners()); + assertEquals(0,gameState.getWhitePrisoners()); + + assertTrue(gameState.playStone('C', 2, GameBoard.BLACK_STONE)); + + assertEquals(2,gameState.getBlackPrisoners()); + assertEquals(0,gameState.getWhitePrisoners()); + + assertFalse(gameState.playStone('B', 2, GameBoard.WHITE_STONE)); + assertFalse(gameState.playStone('C', 3, GameBoard.WHITE_STONE)); + + System.out.println(gameState); + + GameScore gameScore = new StateEvaluator(gameConfig).scoreGame(gameState); + System.out.println(gameScore.getScoreReport()); + } + + @Test + public void testCaptureFromEye() { + GameState gameState = new GameState(5); + + gameState.playStone('A', 1, GameBoard.BLACK_STONE); + gameState.playStone('B', 2, GameBoard.BLACK_STONE); + gameState.playStone('C', 1, GameBoard.BLACK_STONE); + gameState.playStone('A', 2, GameBoard.WHITE_STONE); + gameState.playStone('B', 3, GameBoard.WHITE_STONE); + gameState.playStone('C', 2, GameBoard.WHITE_STONE); + + //This capture should be allowed. + assertTrue("Capture from within single eye should have been allowed but move was rejected.", + gameState.playStone('B', 1, GameBoard.WHITE_STONE)); + + assertEquals(0,gameState.getBlackPrisoners()); + assertEquals(2,gameState.getWhitePrisoners()); + + System.out.println(gameState); + } +} diff --git a/test/cs6601/p1/GameScoreTest.java b/test/cs6601/p1/GameScoreTest.java new file mode 100644 index 0000000..82a437c --- /dev/null +++ b/test/cs6601/p1/GameScoreTest.java @@ -0,0 +1,26 @@ +package cs6601.p1; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class GameScoreTest { + + @Test + public void testGetAggregateScoreZero() { + GameScore gameScore = new GameScore(0,0,0); + assertEquals(GameScore.NORMALIZED_ZERO_SCORE, gameScore.getAggregateScore()); + } + + @Test + public void testGetAggregateScoreBlackWinsNoKomi() { + GameScore gameScore = new GameScore(25,2,0); + assertEquals(425, gameScore.getAggregateScore()); + } + + @Test + public void testGetAggregateScoreWhiteWinsWithKomi() { + GameScore gameScore = new GameScore(10,12,6.5); + assertEquals(362, gameScore.getAggregateScore()); + } +} \ No newline at end of file diff --git a/test/cs6601/p1/IllegalMoveTest.java b/test/cs6601/p1/IllegalMoveTest.java new file mode 100644 index 0000000..a665017 --- /dev/null +++ b/test/cs6601/p1/IllegalMoveTest.java @@ -0,0 +1,62 @@ +package cs6601.p1; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class IllegalMoveTest { + + @Test + public void testIllegalMoveOnOwnStone() { + GameState gameState = new GameState(5); + gameState.playStone('B', 3, GameBoard.BLACK_STONE); + assertFalse(gameState.playStone('B', 3, GameBoard.BLACK_STONE)); + } + + @Test + public void testIllegalMoveOnOtherStone() { + GameState gameState = new GameState(5); + gameState.playStone('B', 3, GameBoard.BLACK_STONE); + assertFalse(gameState.playStone('B', 3, GameBoard.WHITE_STONE)); + } + + @Test + public void testIllegalMoveNoLiberties() { + GameState gameState = new GameState(5); + gameState.playStone('A', 2, GameBoard.BLACK_STONE); + gameState.playStone('B', 3, GameBoard.BLACK_STONE); + gameState.playStone('B', 1, GameBoard.BLACK_STONE); + gameState.playStone('C', 2, GameBoard.BLACK_STONE); + assertFalse(gameState.playStone('B', 2, GameBoard.WHITE_STONE)); + System.out.println(gameState); + } + + @Test + public void testIllegalMoveFormsTrappedGroup() { + GameState gameState = new GameState(9); + gameState.playStone('A', 5, GameBoard.BLACK_STONE); + gameState.playStone('B', 6, GameBoard.BLACK_STONE); + gameState.playStone('B', 7, GameBoard.BLACK_STONE); + gameState.playStone('A', 8, GameBoard.BLACK_STONE); + assertTrue(gameState.playStone('A', 6, GameBoard.WHITE_STONE)); + assertFalse(gameState.playStone('A', 7, GameBoard.WHITE_STONE)); + System.out.println(gameState); + } + + @Test + public void testIllegalMoveFormsTrappedGroup2() { + GameState gameState = new GameState(9); + gameState.playStone('G', 1, GameBoard.BLACK_STONE); + gameState.playStone('H', 2, GameBoard.BLACK_STONE); + gameState.playStone('H', 3, GameBoard.BLACK_STONE); + gameState.playStone('J', 4, GameBoard.BLACK_STONE); + gameState.playStone('A', 8, GameBoard.BLACK_STONE); + assertTrue(gameState.playStone('H', 1, GameBoard.WHITE_STONE)); + assertTrue(gameState.playStone('J', 2, GameBoard.WHITE_STONE)); + assertTrue(gameState.playStone('J', 3, GameBoard.WHITE_STONE)); + System.out.println(gameState); + assertFalse(gameState.playStone('J', 1, GameBoard.WHITE_STONE)); + System.out.println(gameState); + } +} \ No newline at end of file diff --git a/test/cs6601/p1/LegalMoveTest.java b/test/cs6601/p1/LegalMoveTest.java new file mode 100644 index 0000000..da4f05f --- /dev/null +++ b/test/cs6601/p1/LegalMoveTest.java @@ -0,0 +1,17 @@ +package cs6601.p1; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class LegalMoveTest { + @Test + public void testLegalMove1Liberty() { + GameState gameState = new GameState(5); + gameState.playStone('A', 2, GameBoard.BLACK_STONE); + gameState.playStone('B', 3, GameBoard.BLACK_STONE); + gameState.playStone('B', 1, GameBoard.BLACK_STONE); + assertTrue(gameState.playStone('B', 2, GameBoard.WHITE_STONE)); + System.out.println(gameState); + } +} \ No newline at end of file diff --git a/test/cs6601/p1/StateEvaluatorTest.java b/test/cs6601/p1/StateEvaluatorTest.java new file mode 100644 index 0000000..90a1c00 --- /dev/null +++ b/test/cs6601/p1/StateEvaluatorTest.java @@ -0,0 +1,96 @@ +package cs6601.p1; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import cs6601.p1.generator.AlphaBetaMoveGenerator; +import cs6601.p1.generator.MoveGenerator; + +public class StateEvaluatorTest { + GameConfig gameConfig = new GameConfig(); + + @Test + public void testScoreEmptyBoard() { + GameState gameState = new GameState(5); + GameScore gameScore = new StateEvaluator(gameConfig).scoreGame(gameState); + + assertEquals(0.0,gameScore.getWhiteScore(),0.5); + assertEquals(0.0,gameScore.getBlackScore(),0.5); + } + + @Test + public void testScoreFirstMove() { + GameState gameState = new GameState(5); + gameState.playStone('B',3,GameBoard.BLACK_STONE); + + GameScore gameScore = new StateEvaluator(gameConfig).scoreGame(gameState); + + System.out.println(gameScore.getScoreReport()); + + assertEquals(0.0,gameScore.getWhiteScore(),0.5); + assertEquals(25.0,gameScore.getBlackScore(),0.5); + } + + @Test + public void testScoreTiedAtMove2() { + GameState gameState = new GameState(5); + + gameState.playStone('B',3,GameBoard.BLACK_STONE); + gameState.playStone('A',1,GameBoard.WHITE_STONE); + GameScore gameScore = new StateEvaluator(gameConfig).scoreGame(gameState); + + System.out.println(gameScore.getScoreReport()); + + assertEquals(1.0,gameScore.getWhiteScore(),0.5); + assertEquals(1.0,gameScore.getBlackScore(),0.5); + } + + @Test + public void testScoreTerritory() { + GameState gameState = new GameState(5); + + gameState.playStone('A',2,GameBoard.BLACK_STONE); + gameState.playStone('B',3,GameBoard.BLACK_STONE); + gameState.playStone('C',2,GameBoard.BLACK_STONE); + gameState.playStone('B',1,GameBoard.BLACK_STONE); + gameState.playStone('E',5,GameBoard.WHITE_STONE); + + System.out.println(gameState); + GameScore gameScore = new StateEvaluator(gameConfig).scoreGame(gameState); + + System.out.println(gameScore.getScoreReport()); + + assertEquals(1.0,gameScore.getWhiteScore(),0.5); + assertEquals(6.0,gameScore.getBlackScore(),0.5); + //Black should be up by 5 if Black's territory at A1 & B2 is scored correctly. + } + + @Test + public void testCaptureAggScore() { + GameState gameState = new GameState(9); + gameState.playStone('A', 2, GameBoard.WHITE_STONE); + gameState.playStone('B', 1, GameBoard.WHITE_STONE); + gameState.playStone('C', 2, GameBoard.WHITE_STONE); + gameState.playStone('B', 2, GameBoard.BLACK_STONE); + + GameState moveToA1 = new GameState(gameState); + GameState capAtB3 = new GameState(gameState); + + moveToA1.playStone("w","A1"); + capAtB3.playStone("w", "B3"); + + System.out.println(moveToA1); + System.out.println(capAtB3); + + StateEvaluator eval = new StateEvaluator(new GameConfig()); + int scoreA1 = eval.scoreGame(moveToA1).getAggregateScore(); + int scoreB3 = eval.scoreGame(capAtB3).getAggregateScore(); + + System.out.println("Score at A1: " + scoreA1); + System.out.println("Score at B3: " + scoreB3); + //moving as white, lower is better + assertTrue(scoreA1 > scoreB3); + } +} \ No newline at end of file diff --git a/test/cs6601/p1/TerritoryFinderTest.java b/test/cs6601/p1/TerritoryFinderTest.java new file mode 100644 index 0000000..b7c971e --- /dev/null +++ b/test/cs6601/p1/TerritoryFinderTest.java @@ -0,0 +1,19 @@ +package cs6601.p1; + +import org.junit.Test; + +public class TerritoryFinderTest { + @Test + public void testMarkTerritory() { + GameState gameState = new GameState(5); + + gameState.playStone('A',2,GameBoard.BLACK_STONE); + gameState.playStone('B',3,GameBoard.BLACK_STONE); + gameState.playStone('C',2,GameBoard.BLACK_STONE); + gameState.playStone('B',1,GameBoard.BLACK_STONE); + gameState.playStone('E',5,GameBoard.WHITE_STONE); + + TerritoryMarker.markTerritory(gameState.getGameBoard()); + System.out.println(gameState); + } +} diff --git a/test/cs6601/p1/generator/AlphaBetaTest.java b/test/cs6601/p1/generator/AlphaBetaTest.java new file mode 100644 index 0000000..e84547e --- /dev/null +++ b/test/cs6601/p1/generator/AlphaBetaTest.java @@ -0,0 +1,53 @@ +package cs6601.p1.generator; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import cs6601.p1.GameBoard; +import cs6601.p1.GameConfig; +import cs6601.p1.GameState; + +public class AlphaBetaTest { + @Test + public void testGenmoveAsW() { + MoveGenerator moveGenerator = new AlphaBetaMoveGenerator(); + GameState gameState = new GameState(6); + gameState.playStone('A', 2, GameBoard.WHITE_STONE); + gameState.playStone('B', 1, GameBoard.WHITE_STONE); + gameState.playStone('C', 2, GameBoard.WHITE_STONE); + gameState.playStone('B', 2, GameBoard.BLACK_STONE); + + String move = moveGenerator.genMove(new GameConfig(), gameState, "b"); + System.out.println(gameState); + + System.out.println("Generated move: " + move); + assertEquals("Expected B3 but was: " + move, "B3", move); + gameState.playStone("b", move); + + System.out.println(gameState); + + assertEquals(MoveGenerator.PASS,moveGenerator.genMove(new GameConfig(), gameState, "?")); + } + + @Test + public void testGenmoveAsB() { + MoveGenerator moveGenerator = new AlphaBetaMoveGenerator(); + GameState gameState = new GameState(6); + gameState.playStone('A', 2, GameBoard.BLACK_STONE); + gameState.playStone('B', 1, GameBoard.BLACK_STONE); + gameState.playStone('C', 2, GameBoard.BLACK_STONE); + gameState.playStone('B', 2, GameBoard.WHITE_STONE); + + String move = moveGenerator.genMove(new GameConfig(), gameState, "b"); + System.out.println(gameState); + + System.out.println("Generated move: " + move); + assertEquals("Expected B3 but was: " + move, "B3", move); + gameState.playStone("b", move); + + System.out.println(gameState); + + assertEquals(MoveGenerator.PASS,moveGenerator.genMove(new GameConfig(), gameState, "?")); + } +} \ No newline at end of file diff --git a/test/cs6601/p1/generator/MinimaxTest.java b/test/cs6601/p1/generator/MinimaxTest.java new file mode 100644 index 0000000..8f5f1e2 --- /dev/null +++ b/test/cs6601/p1/generator/MinimaxTest.java @@ -0,0 +1,33 @@ +package cs6601.p1.generator; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import cs6601.p1.GameBoard; +import cs6601.p1.GameConfig; +import cs6601.p1.GameState; +import cs6601.p1.generator.MoveGenerator; + +public class MinimaxTest { + + @Test + public void testGenmove() { + MoveGenerator moveGenerator = new MinimaxMoveGenerator(); + GameState gameState = new GameState(5); + gameState.playStone('A', 2, GameBoard.BLACK_STONE); + gameState.playStone('B', 1, GameBoard.BLACK_STONE); + gameState.playStone('C', 2, GameBoard.BLACK_STONE); + gameState.playStone('B', 4, GameBoard.BLACK_STONE); + + String move = moveGenerator.genMove(new GameConfig(), gameState, "w"); + System.out.println("Generated move: " + move); + gameState.playStone("w", move); + + System.out.println(gameState); + + assertEquals(MoveGenerator.PASS,moveGenerator.genMove(new GameConfig(), gameState, "?")); + + System.out.println(gameState); + } +} diff --git a/test/cs6601/p1/generator/RandomTest.java b/test/cs6601/p1/generator/RandomTest.java new file mode 100644 index 0000000..ed78e67 --- /dev/null +++ b/test/cs6601/p1/generator/RandomTest.java @@ -0,0 +1,52 @@ +package cs6601.p1.generator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import cs6601.p1.GameBoard; +import cs6601.p1.GameConfig; +import cs6601.p1.GameState; +import cs6601.p1.generator.MoveGenerator; +import cs6601.p1.generator.RandomMoveGenerator; + +public class RandomTest { + + @Test + public void testGenmove() { + MoveGenerator moveGenerator = new RandomMoveGenerator(); + GameState gameState = new GameState(5); + moveGenerator.genMove(new GameConfig(), gameState, "b"); + gameState = new GameState(5); + moveGenerator.genMove(new GameConfig(), gameState, "w"); + + assertEquals(MoveGenerator.PASS,moveGenerator.genMove(new GameConfig(), gameState, "?")); + + System.out.println(gameState); + } + + @Test + public void testAlternativeToIllegalMove() { + GameState gameState = new GameState(4); + gameState.playStone('A', 1, GameBoard.BLACK_STONE); + gameState.playStone('A', 2, GameBoard.BLACK_STONE); + gameState.playStone('A', 3, GameBoard.BLACK_STONE); + gameState.playStone('A', 4, GameBoard.BLACK_STONE); + gameState.playStone('B', 1, GameBoard.BLACK_STONE); + gameState.playStone('B', 2, GameBoard.BLACK_STONE); + //gameState.playStone('B', 3, GameBoard.BLACK_STONE); + gameState.playStone('B', 4, GameBoard.BLACK_STONE); + gameState.playStone('C', 2, GameBoard.BLACK_STONE); + gameState.playStone('C', 3, GameBoard.BLACK_STONE); + gameState.playStone('C', 4, GameBoard.BLACK_STONE); + gameState.playStone('D', 4, GameBoard.BLACK_STONE); + assertTrue(gameState.playStone('C', 1, GameBoard.WHITE_STONE)); + assertTrue(gameState.playStone('D', 2, GameBoard.WHITE_STONE)); + assertTrue(gameState.playStone('D', 3, GameBoard.WHITE_STONE)); + System.out.println(gameState); + //This is correct - checked vs. MFOG + assertEquals("B3", new RandomMoveGenerator().genMove(new GameConfig(), gameState, "w")); + System.out.println(gameState); + } +} diff --git a/test/cs6601/p1/generator/ValidMoveGeneratorTest.java b/test/cs6601/p1/generator/ValidMoveGeneratorTest.java new file mode 100644 index 0000000..c84033c --- /dev/null +++ b/test/cs6601/p1/generator/ValidMoveGeneratorTest.java @@ -0,0 +1,43 @@ +package cs6601.p1.generator; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import cs6601.p1.GameBoard; +import cs6601.p1.GameConfig; +import cs6601.p1.GameState; + +public class ValidMoveGeneratorTest { + + @Test + public void test() { +/* move generator should not include A1 here when playing as black: + A B C D E + 5 . . . . . 5 + 4 . O . . . 4 + 3 . . . . . 3 WHITE(O) has captured 0 stones + 2 O . O . . 2 BLACK(X) has captured 0 stones + 1 . O . . . 1 + A B C D E + */ + GameState gameState = new GameState(5); + gameState.playStone('A', 2, GameBoard.WHITE_STONE); + gameState.playStone('B', 1, GameBoard.WHITE_STONE); + gameState.playStone('B', 4, GameBoard.WHITE_STONE); + gameState.playStone('C', 2, GameBoard.WHITE_STONE); + assertFalse(gameState.playStone('A', 1, GameBoard.BLACK_STONE)); + + List validMoves = new ValidMoveGenerator().genMoves(new GameConfig(), gameState, "b",0); + assertTrue(validMoves.size() > 0); + for (String vm : validMoves) { + System.out.println(vm); + } + assertFalse(validMoves.contains("A1")); + + System.out.println(gameState); + } + +}