diff --git a/src/controller/BoardPanelMouseListener.java b/src/controller/BoardPanelMouseListener.java index b9602a7..86db065 100644 --- a/src/controller/BoardPanelMouseListener.java +++ b/src/controller/BoardPanelMouseListener.java @@ -26,7 +26,6 @@ public class BoardPanelMouseListener implements MouseListener, MouseWheelListene public void mouseClicked(MouseEvent e) { Tile tile = (Tile) e.getComponent(); humanPlayer.setCell(tile.getRow(), tile.getCol()); - System.out.println("mouseClicked() " + (clickNum++)); } @Override diff --git a/src/model/Board.java b/src/model/Board.java index 5cf31af..eb84ca5 100644 --- a/src/model/Board.java +++ b/src/model/Board.java @@ -8,8 +8,8 @@ public class Board { BLUE, GREEN, NONE, RED, YELLOW } - public static final int NUM_COLS = 4; - public static final int NUM_ROWS = 4; + public static final int NUM_COLS = 5; + public static final int NUM_ROWS = 5; public static final int ROW_REMOVAL_SIZE = 3; private final TileColor[][] board; @@ -20,9 +20,9 @@ public class Board { public Board(Board that) { board = new TileColor[NUM_ROWS][NUM_COLS]; - for (int i = 0; i < NUM_COLS; i++) { - for (int j = 0; j < NUM_ROWS; j++) { - this.board[j][i] = that.board[j][i]; + for (int r = 0; r < NUM_ROWS; r++) { + for (int c = 0; c < NUM_COLS; c++) { + this.board[r][c] = that.board[r][c]; } } } @@ -230,4 +230,18 @@ public class Board { numPlies++; return true; } + + @Override + public String toString() { + StringBuilder sb1 = new StringBuilder(); + for (int r = 0; r < NUM_ROWS; r++) { + StringBuilder sb2 = new StringBuilder(); + for (int c = 0; c < NUM_COLS; c++) { + sb2.append(board[r][c].toString().charAt(0)); + } + sb1.append(sb2); + sb1.append("\n"); + } + return sb1.toString(); + } } \ No newline at end of file diff --git a/src/model/CellPointer.java b/src/model/CellPointer.java index 8ffd57c..d397109 100644 --- a/src/model/CellPointer.java +++ b/src/model/CellPointer.java @@ -1,11 +1,18 @@ package model; public class CellPointer { + public static final CellPointer NONE = new CellPointer(-1,-1); + + public final int c; + public final int r; + public CellPointer(int row, int col) { - r = row; - c = col; + this.r = row; + this.c = col; } - - public int r; - public int c; -} + + @Override + public String toString() { + return "(" + r + "," + c +")"; + } +} \ No newline at end of file diff --git a/src/model/Move.java b/src/model/Move.java index 539b5a9..6445bf2 100644 --- a/src/model/Move.java +++ b/src/model/Move.java @@ -28,4 +28,9 @@ public class Move { public TileColor getColor() { return color; } + + @Override + public String toString() { + return "Play " + color + "@" + cp; + } } diff --git a/src/model/Referee.java b/src/model/Referee.java index 98fe4c2..7c1f945 100644 --- a/src/model/Referee.java +++ b/src/model/Referee.java @@ -1,14 +1,16 @@ package model; +import model.Board.TileColor; + import org.apache.log4j.Logger; import player.AlphaBetaComPlayer; import player.HumanPlayer; +import player.MinimaxComPlayer; import player.Player; import view.BoardPanel; import view.MessagePanel; import view.ScorePanel; -import model.Board.TileColor; public class Referee implements Runnable { diff --git a/src/model/SearchResult.java b/src/model/SearchResult.java new file mode 100644 index 0000000..57fbd82 --- /dev/null +++ b/src/model/SearchResult.java @@ -0,0 +1,16 @@ +package model; + +public class SearchResult { + public final Move move; + public final int score; + + public SearchResult(Move move, int score) { + this.move = move; + this.score = score; + } + + @Override + public String toString() { + return move + ", score: " + score; + } +} diff --git a/src/player/HumanPlayer.java b/src/player/HumanPlayer.java index ab30ff7..f07f6fe 100644 --- a/src/player/HumanPlayer.java +++ b/src/player/HumanPlayer.java @@ -7,7 +7,7 @@ import model.Board.TileColor; public class HumanPlayer implements Player { - private final CellPointer cell = new CellPointer(0, 0); + private CellPointer cell = CellPointer.NONE; private boolean ready = false; private TileColor color = TileColor.BLUE; @@ -72,9 +72,7 @@ public class HumanPlayer implements Player { } public void setCell(int row, int col) { - cell.r = row; - cell.c = col; - + cell = new CellPointer(row, col); ready = true; } diff --git a/src/player/MinimaxComPlayer.java b/src/player/MinimaxComPlayer.java index 9f6d5c1..5fc4e07 100644 --- a/src/player/MinimaxComPlayer.java +++ b/src/player/MinimaxComPlayer.java @@ -1,5 +1,25 @@ package player; -public class MinimaxComPlayer { +import model.Board; +import model.Move; +import player.generator.MinimaxMoveGenerator; +import player.generator.MoveGenerator; +public class MinimaxComPlayer implements Player{ + private MoveGenerator moveGenerator = new MinimaxMoveGenerator(); + + @Override + public Move getMove(Board board) { + return moveGenerator.genMove(board, false); + } + + @Override + public void denyMove() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public boolean isReady() { + return true; // always ready to play a random valid move + } } diff --git a/src/player/generator/AlphaBetaMoveGenerator.java b/src/player/generator/AlphaBetaMoveGenerator.java index 79ffa75..32b3ed9 100644 --- a/src/player/generator/AlphaBetaMoveGenerator.java +++ b/src/player/generator/AlphaBetaMoveGenerator.java @@ -6,46 +6,90 @@ import java.util.List; import model.Board; import model.BoardScorer; import model.Move; +import model.SearchResult; import org.apache.log4j.Logger; public class AlphaBetaMoveGenerator implements MoveGenerator { private static final Logger LOGGER = Logger .getLogger(AlphaBetaMoveGenerator.class.getName()); - private static final int DEFAULT_RECURSIVE_PLAYS = 3; + private static final int DEFAULT_RECURSIVE_PLAYS = 2; private final BoardScorer scorer = new BoardScorer(); private final ValidMoveGenerator validMoveGenerator = new ValidMoveGenerator(); - private Move bestPick = Move.NONE; - @Override public Move genMove(Board board, boolean asHuman) { - - int alpha = Integer.MIN_VALUE; - int beta = Integer.MAX_VALUE; - + if (!asHuman) { - getMaxValue(board, asHuman, DEFAULT_RECURSIVE_PLAYS * 2, alpha, - beta); + return getMaxValue(board, asHuman, DEFAULT_RECURSIVE_PLAYS * 2, Integer.MIN_VALUE, + Integer.MAX_VALUE).move; } else { - getMinValue(board, asHuman, DEFAULT_RECURSIVE_PLAYS * 2, alpha, - beta); + return getMinValue(board, asHuman, DEFAULT_RECURSIVE_PLAYS * 2, Integer.MIN_VALUE, + Integer.MAX_VALUE).move; } - - return bestPick; } - private int getMaxValue(Board board, boolean asHuman, int recursionLevel, + private SearchResult getMax(SearchResult sr1, SearchResult sr2) { + if (sr1.score >= sr2.score) { + return sr1; + } else { + return sr2; + } + } + + private SearchResult getMin(SearchResult sr1, SearchResult sr2) { + if (sr1.score <= sr2.score) { + return sr1; + } else { + return sr2; + } + } + + private SearchResult getMaxValue(Board board, boolean asHuman, int recursionLevel, int alpha, int beta) { - if (terminalTest(recursionLevel)) { - return getUtility(board); + if (recursionLevel < 1) { + return new SearchResult(Move.NONE,scorer.getScore(board)); } List validMoves = validMoveGenerator.genMoves(board, asHuman, MoveGenerator.ALL_MOVES); - int value = Integer.MIN_VALUE; + SearchResult bestResult = new SearchResult(Move.NONE,Integer.MIN_VALUE); + + for (Move nextMove : validMoves) { + Board nextBoard = new Board(board); + + if (!nextBoard.playTile(nextMove.getCell(), nextMove.getColor())) { + throw new RuntimeException( + "Illegal move attempted during search!"); + } + + SearchResult searchResult = new SearchResult(nextMove,getMinValue(nextBoard, !asHuman, recursionLevel - 1, + alpha, beta).score); + + bestResult = getMax(bestResult,searchResult); + + if (bestResult.score >= beta) { + return bestResult; + } + + alpha = Math.max(alpha, bestResult.score); + } + + return bestResult; + } + + private SearchResult getMinValue(Board board, boolean asHuman, int recursionLevel, + int alpha, int beta) { + if (recursionLevel < 1) { + return new SearchResult(Move.NONE,scorer.getScore(board)); + } + + List validMoves = validMoveGenerator.genMoves(board, asHuman, + MoveGenerator.ALL_MOVES); + + SearchResult bestResult = new SearchResult(Move.NONE,Integer.MAX_VALUE); for (Move nextMove : validMoves) { Board nextBoard = new Board(board); @@ -55,69 +99,19 @@ public class AlphaBetaMoveGenerator implements MoveGenerator { "Illegal move attempted during search!"); } - int minValue = getMinValue(nextBoard, !asHuman, recursionLevel - 1, - alpha, beta); + SearchResult searchResult = new SearchResult(nextMove,getMaxValue(nextBoard, !asHuman, recursionLevel - 1, + alpha, beta).score); - if (minValue > value) { - value = minValue; - if (recursionLevel == DEFAULT_RECURSIVE_PLAYS * 2) { - bestPick = nextMove; - } - } + bestResult = getMin(bestResult,searchResult); - if (value >= beta) { - return value; + if (bestResult.score <= alpha) { + return bestResult; } - alpha = Math.max(alpha, value); + + beta = Math.min(beta, bestResult.score); } - return value; - } - - private int getMinValue(Board board, boolean asHuman, int recursionLevel, - int alpha, int beta) { - if (terminalTest(recursionLevel)) { - return getUtility(board); - } - - List validMoves = validMoveGenerator.genMoves(board, asHuman, - MoveGenerator.ALL_MOVES); - - int value = Integer.MAX_VALUE; - - for (Move nextMove : validMoves) { - Board nextBoard = new Board(board); - - if (!nextBoard.playTile(nextMove.getCell(), nextMove.getColor())) { - throw new RuntimeException( - "Illegal move attempted during search!"); - } - - int maxValue = getMaxValue(nextBoard, !asHuman, recursionLevel - 1, - alpha, beta); - - if (maxValue < value) { - value = maxValue; - if (recursionLevel == 2 * DEFAULT_RECURSIVE_PLAYS) { - bestPick = nextMove; - } - } - - if (value <= alpha) { - return value; - } - beta = Math.min(beta, value); - } - - return value; - } - - private boolean terminalTest(int recursionLevel) { - return recursionLevel < 1; - } - - private int getUtility(Board board) { - return scorer.getScore(board); + return bestResult; } /** @@ -129,4 +123,4 @@ public class AlphaBetaMoveGenerator implements MoveGenerator { LOGGER.info("Minimax genMoves() stub returning []"); return Arrays.asList(doNothing); } -} +} \ No newline at end of file diff --git a/src/player/generator/MinimaxMoveGenerator.java b/src/player/generator/MinimaxMoveGenerator.java new file mode 100644 index 0000000..d90ef91 --- /dev/null +++ b/src/player/generator/MinimaxMoveGenerator.java @@ -0,0 +1,105 @@ +package player.generator; + +import java.util.Arrays; +import java.util.List; + +import model.Board; +import model.BoardScorer; +import model.Move; +import model.SearchResult; + +import org.apache.log4j.Logger; + +public class MinimaxMoveGenerator implements MoveGenerator { + private static final Logger LOGGER = Logger + .getLogger(MinimaxMoveGenerator.class.getName()); + private static final int DEFAULT_RECURSIVE_PLAYS = 2; + + private final BoardScorer scorer = new BoardScorer(); + private final ValidMoveGenerator validMoveGenerator = new ValidMoveGenerator(); + + @Override + public Move genMove(Board board, boolean asHuman) { + //asHuman may appear redundant, but is for later use when the com and human + //have different types of moves. + SearchResult searchResult; + if (!asHuman) { + searchResult = getMaxValue(board, Move.NONE, asHuman, DEFAULT_RECURSIVE_PLAYS * 2); + } else { + searchResult = getMinValue(board, Move.NONE, asHuman, DEFAULT_RECURSIVE_PLAYS * 2); + } + + System.out.println(searchResult); + return searchResult.move; + } + + private SearchResult getMaxValue(Board board, Move lastMove, boolean asHuman, int recursionLevel) { + if (terminalTest(recursionLevel)) { + return new SearchResult(lastMove,scorer.getScore(board)); + } + + List validMoves = validMoveGenerator.genMoves(board, asHuman, + MoveGenerator.ALL_MOVES); + + SearchResult bestResult = new SearchResult(Move.NONE,Integer.MIN_VALUE); + + for (Move nextMove : validMoves) { + Board nextBoard = new Board(board); + + if (!nextBoard.playTile(nextMove.getCell(), nextMove.getColor())) { + throw new RuntimeException( + "Illegal move attempted during search!"); + } + + SearchResult searchResult = getMinValue(nextBoard, nextMove, !asHuman, recursionLevel - 1); + + if (searchResult.score > bestResult.score) { + bestResult = new SearchResult(nextMove,searchResult.score); + } + } + + return bestResult; + } + + private SearchResult getMinValue(Board board, Move lastMove, boolean asHuman, int recursionLevel) { + if (terminalTest(recursionLevel)) { + return new SearchResult(lastMove,scorer.getScore(board)); + } + + List validMoves = validMoveGenerator.genMoves(board, asHuman, + MoveGenerator.ALL_MOVES); + + SearchResult bestResult = new SearchResult(Move.NONE,Integer.MAX_VALUE); + + for (Move nextMove : validMoves) { + Board nextBoard = new Board(board); + + if (!nextBoard.playTile(nextMove.getCell(), nextMove.getColor())) { + throw new RuntimeException( + "Illegal move attempted during search!"); + } + + SearchResult searchResult = getMaxValue(nextBoard, nextMove,!asHuman, recursionLevel - 1); + + if (searchResult.score < bestResult.score) { + bestResult = new SearchResult(nextMove,searchResult.score); + } + } + + return bestResult; + } + + private boolean terminalTest(int recursionLevel) { + return recursionLevel < 1; + } + + /** + * AlphaBetaMoveGenerator2 does not support this method. + */ + @Override + public List genMoves(Board board, boolean asHuman, int nMoves) { + Move[] doNothing = new Move[] { Move.NONE }; + LOGGER.info("Minimax genMoves() stub returning []"); + return Arrays.asList(doNothing); + } +} diff --git a/src/player/generator/ValidMoveGenerator.java b/src/player/generator/ValidMoveGenerator.java index 9fa16f7..59f4475 100644 --- a/src/player/generator/ValidMoveGenerator.java +++ b/src/player/generator/ValidMoveGenerator.java @@ -1,6 +1,7 @@ package player.generator; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import model.Board; @@ -10,8 +11,9 @@ import model.Move; import org.apache.log4j.Logger; public class ValidMoveGenerator implements MoveGenerator { - private static final Logger LOGGER = Logger.getLogger(ValidMoveGenerator.class.getName()); - + private static final Logger LOGGER = Logger + .getLogger(ValidMoveGenerator.class.getName()); + @Override public Move genMove(Board board, boolean asHuman) { LOGGER.info("ValidMoveGenerator genMove() stub returning NONE"); @@ -20,22 +22,23 @@ public class ValidMoveGenerator implements MoveGenerator { @Override public List genMoves(Board board, boolean asHuman, int nMoves) { - + List validMoves = new ArrayList(); for (int i = 0; i < Board.NUM_ROWS; i++) { for (int j = 0; j < Board.NUM_COLS; j++) { if (board.getTile(i, j) == TileColor.NONE) { - for (TileColor color : TileColor.values()) { - if (color == TileColor.NONE) { - continue; - } - validMoves.add(new Move(color, i, j)); + for (TileColor color : TileColor.values()) { + if (color == TileColor.NONE) { + continue; + } + validMoves.add(new Move(color, i, j)); } } } } + Collections.shuffle(validMoves); return validMoves; } } diff --git a/test/model/BoardScorerTest.java b/test/model/BoardScorerTest.java new file mode 100644 index 0000000..f40195f --- /dev/null +++ b/test/model/BoardScorerTest.java @@ -0,0 +1,32 @@ +package model; + +import static org.junit.Assert.assertEquals; +import model.Board.TileColor; + +import org.junit.Test; + +public class BoardScorerTest { + + @Test + public void testScore() { + Board board = new Board(); + board.playTile(new CellPointer(0,0), TileColor.BLUE); + board.playTile(new CellPointer(0,1), TileColor.GREEN); + board.playTile(new CellPointer(1,0), TileColor.BLUE); + board.playTile(new CellPointer(2,0), TileColor.GREEN); + board.playTile(new CellPointer(1,1), TileColor.BLUE); + board.playTile(new CellPointer(0,2), TileColor.BLUE); + board.playTile(new CellPointer(3,0), TileColor.BLUE); + board.playTile(new CellPointer(0,3), TileColor.BLUE); + + BoardScorer boardScorer = new BoardScorer(); + System.out.println("Board 1: "); + System.out.println(board); + assertEquals(8,boardScorer.getScore(board)); + board.playTile(new CellPointer(1,2), TileColor.BLUE); + + System.out.println("Board 2: "); + System.out.println(board); + assertEquals(6,boardScorer.getScore(board)); + } +}