diff --git a/.classpath b/.classpath index 371ffc0..ae23745 100644 --- a/.classpath +++ b/.classpath @@ -5,5 +5,6 @@ + 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/controller/BoardPanelMouseListener.java b/src/controller/BoardPanelMouseListener.java index 9e0e76d..b9602a7 100644 --- a/src/controller/BoardPanelMouseListener.java +++ b/src/controller/BoardPanelMouseListener.java @@ -5,7 +5,8 @@ import java.awt.event.MouseListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; -import model.HumanPlayer; +import player.HumanPlayer; + import view.Tile; import view.TileSelectionPanel; diff --git a/src/controller/TSPMouseListener.java b/src/controller/TSPMouseListener.java index 04b931e..c92cd63 100644 --- a/src/controller/TSPMouseListener.java +++ b/src/controller/TSPMouseListener.java @@ -3,9 +3,10 @@ package controller; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; +import player.HumanPlayer; + import view.TileSelectionPanel; -import model.HumanPlayer; import model.Board.TileColor; public class TSPMouseListener implements MouseListener{ diff --git a/src/model/Board.java b/src/model/Board.java index 2b4d3ca..32666bb 100644 --- a/src/model/Board.java +++ b/src/model/Board.java @@ -8,12 +8,15 @@ public class Board { BLUE, GREEN, NONE, RED, YELLOW } - public static final int NUM_COLS = 5; - public static final int NUM_ROWS = 5; + public static final int NUM_COLS = 4; + public static final int NUM_ROWS = 4; public static final int ROW_REMOVAL_SIZE = 3; private final TileColor[][] board; + // a Ply is a play by 1 side. Increments each time setTile() is called. + private int numPlies = 0; + public Board(Board that) { this(); for (int i = 0; i < NUM_COLS; i++) { @@ -22,17 +25,16 @@ public class Board { } } } - + public Board() { board = new TileColor[NUM_ROWS][NUM_COLS]; for (int r = 0; r < NUM_ROWS; r++) { for (int c = 0; c < NUM_COLS; c++) { - setTile(new CellPointer(r, c), TileColor.NONE); + board[r][c] = TileColor.NONE; } } } - @Override public int hashCode() { final int prime = 31; @@ -50,7 +52,7 @@ public class Board { if (getClass() != obj.getClass()) return false; Board other = (Board) obj; - + for (int r = 0; r < NUM_ROWS; r++) { for (int c = 0; c < NUM_COLS; c++) { if (this.board[r][c] != other.board[r][c]) { @@ -58,7 +60,7 @@ public class Board { } } } - + return true; } @@ -66,7 +68,31 @@ public class Board { return board[r][c]; } - public void setTile(CellPointer cp, TileColor tile) { + public int getTurn() { + return numPlies / 2; + } + + public boolean isPlayerTurn() { + return numPlies % 2 == 0; + } + + public boolean isTerminalState() { + for (int r = 0; r < Board.NUM_ROWS; r++) { + for (int c = 0; c < Board.NUM_COLS; c++) { + if (board[r][c] == TileColor.NONE) { + return false; + } + } + } + + return true; + } + + public boolean playTile(CellPointer cp, TileColor tile) { + if (board[cp.r][cp.c] != TileColor.NONE) { + return false; //illegal move + } + board[cp.r][cp.c] = tile; if (tile != TileColor.NONE) { @@ -199,5 +225,8 @@ public class Board { board[cell.r][cell.c] = TileColor.NONE; } } + + numPlies++; + return true; } -} +} \ No newline at end of file diff --git a/src/model/BoardScorer.java b/src/model/BoardScorer.java new file mode 100644 index 0000000..a63edb5 --- /dev/null +++ b/src/model/BoardScorer.java @@ -0,0 +1,24 @@ +package model; + +/** + * Scorer for use by various ComPlayer implementations. + * + * @author Woody + * + */ +public class BoardScorer { + + public int getMaxScore(Board board) { + return Board.NUM_ROWS * Board.NUM_COLS; + } + + public int getScore(Board board) { + int score = 0; + for (int r = 0; r < Board.NUM_ROWS; r++) { + for (int c = 0; c < Board.NUM_COLS; c++) { + score += board.getTile(r, c) == Board.TileColor.NONE ? 0 : 1; + } + } + return score; + } +} \ No newline at end of file diff --git a/src/model/Move.java b/src/model/Move.java index db586d6..539b5a9 100644 --- a/src/model/Move.java +++ b/src/model/Move.java @@ -3,9 +3,19 @@ package model; import model.Board.TileColor; public class Move { + /** + * Used when genMove() is called but no valid move exists for that player. + */ + public static final Move NONE = new Move(TileColor.NONE, -1, -1); + private final TileColor color; private final CellPointer cp; + public Move(TileColor color, int row, int column) { + cp = new CellPointer(row,column); + this.color = color; + } + public Move(CellPointer cllPntr, TileColor tlClr) { cp = cllPntr; color = tlClr; diff --git a/src/model/Referee.java b/src/model/Referee.java index 61457f1..98fe4c2 100644 --- a/src/model/Referee.java +++ b/src/model/Referee.java @@ -1,7 +1,13 @@ package model; +import org.apache.log4j.Logger; + +import player.AlphaBetaComPlayer; +import player.HumanPlayer; +import player.Player; import view.BoardPanel; import view.MessagePanel; +import view.ScorePanel; import model.Board.TileColor; public class Referee implements Runnable { @@ -10,26 +16,28 @@ public class Referee implements Runnable { public static final String GAME_OVER = "Game over!"; public static final String PLAYER_TURN = "Waiting for the player's move."; + public static final Logger LOGGER = Logger.getLogger(Referee.class + .getName()); + private final Board board; private final HumanPlayer humanPlayer = new HumanPlayer(); - private final Player cc; + private final Player computerPlayer; - private boolean playerTurn; private BoardPanel boardPanel; - private int score = 0; private MessagePanel messagePanel; + private ScorePanel scorePanel; public Referee() { board = new Board(); - cc = new RandomComPlayer(); - playerTurn = true; + computerPlayer = new AlphaBetaComPlayer(); } @Override public void run() { int plies = 0; while (!isOver()) { - if (playerTurn) { + scorePanel.updateScore(getPlayerScore()); + if (board.isPlayerTurn()) { boolean wasInterrupted = false; while (!humanPlayer.isReady()) { try { @@ -39,7 +47,8 @@ public class Referee implements Runnable { } } if (wasInterrupted) { - System.out.println("Interrupted while waiting for human to move!"); + System.out + .println("Interrupted while waiting for human to move!"); } else { Move mv = humanPlayer.getMove(board); if (board.getTile(mv.getCell().r, mv.getCell().c) == TileColor.NONE) { @@ -49,79 +58,65 @@ public class Referee implements Runnable { } } } else { - Move mv = cc.getMove(board); + Move mv = computerPlayer.getMove(board); playToken(mv); } - + messagePanel.updateMessage(getMessage()); boardPanel.updateIcons(); - System.out.println("plies: " + plies++); + LOGGER.info("plies: " + plies++); try { Thread.sleep(1000L); } catch (InterruptedException ie) { - System.out.println("Interrupted while waiting for human view ply!"); + System.out + .println("Interrupted while waiting for human view ply!"); } } } public Player getComputerPlayer() { - return cc; + return computerPlayer; } public HumanPlayer getHumanPlayer() { return humanPlayer; } - + public String getMessage() { if (isOver()) { return GAME_OVER; - } else if (isPlayersTurn()) { + } else if (board.isPlayerTurn()) { return PLAYER_TURN; } else { return COM_TURN; } } - - public boolean isOver() { - for (int r = 0; r < Board.NUM_ROWS; r++) { - for (int c = 0; c < Board.NUM_COLS; c++) { - if (board.getTile(r, c) == TileColor.NONE) { - return false; - } - } - } - return true; + public int getPlayerScore() { + return board.getTurn(); } - - private boolean isPlayersTurn() { - return playerTurn; - } - - public int getScore() { - return score; - } - + public TileColor getTile(int r, int c) { return board.getTile(r, c); } + public boolean isOver() { + return board.isTerminalState(); + } + public void playToken(Move move) { - - board.setTile(move.getCell(), move.getColor()); - - if (playerTurn) { - score++; - } - - playerTurn = !playerTurn; + board.playTile(move.getCell(), move.getColor()); } public void setBoardPanel(BoardPanel boardPanel) { this.boardPanel = boardPanel; } - + public void setMessagePanel(MessagePanel messagePanel) { this.messagePanel = messagePanel; } -} + + public void setScorePanel(ScorePanel scorePanel) { + this.scorePanel = scorePanel; + } +} \ No newline at end of file diff --git a/src/player/AlphaBetaComPlayer.java b/src/player/AlphaBetaComPlayer.java new file mode 100644 index 0000000..79a51ed --- /dev/null +++ b/src/player/AlphaBetaComPlayer.java @@ -0,0 +1,25 @@ +package player; + +import model.Board; +import model.Move; +import player.generator.AlphaBetaMoveGenerator; +import player.generator.MoveGenerator; + +public class AlphaBetaComPlayer implements Player { + private MoveGenerator moveGenerator = new AlphaBetaMoveGenerator(); + + @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 + } +} \ No newline at end of file diff --git a/src/model/HumanPlayer.java b/src/player/HumanPlayer.java similarity index 95% rename from src/model/HumanPlayer.java rename to src/player/HumanPlayer.java index 5119ab0..ab30ff7 100644 --- a/src/model/HumanPlayer.java +++ b/src/player/HumanPlayer.java @@ -1,5 +1,8 @@ -package model; +package player; +import model.Board; +import model.CellPointer; +import model.Move; import model.Board.TileColor; diff --git a/src/player/MinimaxComPlayer.java b/src/player/MinimaxComPlayer.java new file mode 100644 index 0000000..9f6d5c1 --- /dev/null +++ b/src/player/MinimaxComPlayer.java @@ -0,0 +1,5 @@ +package player; + +public class MinimaxComPlayer { + +} diff --git a/src/model/MonteCarloComPlayer.java b/src/player/MonteCarloComPlayer.java similarity index 92% rename from src/model/MonteCarloComPlayer.java rename to src/player/MonteCarloComPlayer.java index d98f87a..061393a 100644 --- a/src/model/MonteCarloComPlayer.java +++ b/src/player/MonteCarloComPlayer.java @@ -1,7 +1,10 @@ -package model; +package player; import java.util.Random; +import model.Board; +import model.CellPointer; +import model.Move; import model.Board.TileColor; diff --git a/src/model/Player.java b/src/player/Player.java similarity index 87% rename from src/model/Player.java rename to src/player/Player.java index f59d61b..625a77e 100644 --- a/src/model/Player.java +++ b/src/player/Player.java @@ -1,4 +1,7 @@ -package model; +package player; + +import model.Board; +import model.Move; public interface Player { diff --git a/src/model/RandomComPlayer.java b/src/player/RandomComPlayer.java similarity index 91% rename from src/model/RandomComPlayer.java rename to src/player/RandomComPlayer.java index 6143c20..cf791ac 100644 --- a/src/model/RandomComPlayer.java +++ b/src/player/RandomComPlayer.java @@ -1,7 +1,10 @@ -package model; +package player; import java.util.Random; +import model.Board; +import model.CellPointer; +import model.Move; import model.Board.TileColor; diff --git a/src/player/generator/AlphaBetaMoveGenerator.java b/src/player/generator/AlphaBetaMoveGenerator.java new file mode 100644 index 0000000..8da6ab2 --- /dev/null +++ b/src/player/generator/AlphaBetaMoveGenerator.java @@ -0,0 +1,132 @@ +package player.generator; + +import java.util.Arrays; +import java.util.List; + +import model.Board; +import model.BoardScorer; +import model.Move; + +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 = 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); + } else { + getMinValue(board, asHuman, DEFAULT_RECURSIVE_PLAYS * 2, alpha, + beta); + } + + return bestPick; + } + + private int getMaxValue(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.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!"); + } + + int minValue = getMinValue(nextBoard, !asHuman, recursionLevel - 1, + alpha, beta); + + if (minValue > value) { + value = minValue; + if (recursionLevel == DEFAULT_RECURSIVE_PLAYS * 2) { + bestPick = nextMove; + } + } + + if (value >= beta) { + return value; + } + alpha = Math.max(alpha, value); + } + + 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); + } + + /** + * 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/GameScore.java b/src/player/generator/GameScore.java new file mode 100644 index 0000000..e99c281 --- /dev/null +++ b/src/player/generator/GameScore.java @@ -0,0 +1,26 @@ +package player.generator; + +public class GameScore { + boolean terminal; + int maxScore; + int score; + + public GameScore(boolean terminal, int maxScore, int score) { + super(); + this.terminal = terminal; + this.maxScore = maxScore; + this.score = score; + } + + public boolean isTerminal() { + return terminal; + } + + public int getMaxScore() { + return maxScore; + } + + public int getScore() { + return score; + } +} \ No newline at end of file diff --git a/src/player/generator/MoveCandidate.java b/src/player/generator/MoveCandidate.java new file mode 100644 index 0000000..562a0d4 --- /dev/null +++ b/src/player/generator/MoveCandidate.java @@ -0,0 +1,13 @@ +package player.generator; + +import model.Move; + +public class MoveCandidate { + public final Move move; + public final GameScore score; + + public MoveCandidate(Move move, GameScore score) { + this.move = move; + this.score = score; + } +} \ No newline at end of file diff --git a/src/player/generator/MoveGenerator.java b/src/player/generator/MoveGenerator.java new file mode 100644 index 0000000..ee64b91 --- /dev/null +++ b/src/player/generator/MoveGenerator.java @@ -0,0 +1,13 @@ +package player.generator; + +import java.util.List; + +import model.Board; +import model.Move; + +public interface MoveGenerator { + public static final int ALL_MOVES = 0; + + public Move genMove(Board board, boolean asHuman); + public List genMoves(Board board, boolean asHuman, int nMoves); +} \ No newline at end of file diff --git a/src/player/generator/ValidMoveGenerator.java b/src/player/generator/ValidMoveGenerator.java new file mode 100644 index 0000000..cc10c05 --- /dev/null +++ b/src/player/generator/ValidMoveGenerator.java @@ -0,0 +1,38 @@ +package player.generator; + +import java.util.ArrayList; +import java.util.List; + +import model.Board; +import model.Board.TileColor; +import model.Move; + +import org.apache.log4j.Logger; + +public class ValidMoveGenerator implements MoveGenerator { + 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"); + return Move.NONE; + } + + @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()) { + validMoves.add(new Move(color, i, j)); + } + } + } + } + + return validMoves; + } +} diff --git a/src/view/MainFrame.java b/src/view/MainFrame.java index e44320a..ef5fa19 100644 --- a/src/view/MainFrame.java +++ b/src/view/MainFrame.java @@ -35,8 +35,9 @@ public class MainFrame extends JFrame { private void init() { ScorePanel sp = new ScorePanel(referee); - TileSelectionPanel tp = new TileSelectionPanel(referee.getHumanPlayer()); + referee.setScorePanel(sp); + TileSelectionPanel tp = new TileSelectionPanel(referee.getHumanPlayer()); BoardPanel bp = new BoardPanel(referee,tp); referee.setBoardPanel(bp); @@ -82,7 +83,7 @@ public class MainFrame extends JFrame { add(vWrapper,BorderLayout.CENTER); //To ensure correct size, pre-populate the score and message panels with text. - sp.updateScore(); + sp.updateScore(0); mp.updateMessage("Loading new game..."); pack(); diff --git a/src/view/ScorePanel.java b/src/view/ScorePanel.java index 9e0aa8e..df638df 100644 --- a/src/view/ScorePanel.java +++ b/src/view/ScorePanel.java @@ -11,17 +11,15 @@ public class ScorePanel extends JPanel { private static final long serialVersionUID = 1L; private final JLabel message = new JLabel(); - private final Referee referee; - + public ScorePanel(Referee ref) { - referee = ref; add(message); } - public void updateScore() { + public void updateScore(final int score) { SwingUtilities.invokeLater(new Runnable() { public void run() { - message.setText("Score: " + referee.getScore()); + message.setText("Score: " + score); } }); } diff --git a/src/view/TileSelectionPanel.java b/src/view/TileSelectionPanel.java index 852b682..425e09e 100644 --- a/src/view/TileSelectionPanel.java +++ b/src/view/TileSelectionPanel.java @@ -9,9 +9,10 @@ import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingUtilities; +import player.HumanPlayer; + import controller.TSPMouseListener; -import model.HumanPlayer; import model.Board.TileColor; public class TileSelectionPanel extends JPanel { diff --git a/test/model/BoardTest.java b/test/model/BoardTest.java index d713f71..cb111bc 100644 --- a/test/model/BoardTest.java +++ b/test/model/BoardTest.java @@ -11,7 +11,9 @@ public class BoardTest { @Test public void testConstructor() { - new Board(); + Board board = new Board(); + assertTrue(board.isPlayerTurn()); + assertFalse(board.isTerminalState()); } @Test @@ -19,10 +21,10 @@ public class BoardTest { Board board = new Board(); Board copy = new Board(board); - board.setTile(new CellPointer(1,2), TileColor.BLUE); + board.playTile(new CellPointer(1,2), TileColor.BLUE); assertFalse(board.equals(copy)); - copy.setTile(new CellPointer(1,2),TileColor.BLUE); + copy.playTile(new CellPointer(1,2),TileColor.BLUE); assertTrue(board.equals(copy)); } }