Alpha-Beta move generator can look 2 plays (4 plies) ahead on a 4x4 board and blocks every possible attempt by the player to connect 3.

It should be possible to play on larger boards when the computers 'move' is changed from playing a tile to picking the player's next available color.
This commit is contained in:
Woody Folsom
2012-04-14 15:36:02 -04:00
parent 74b8eb4622
commit d9ec72d0fb
23 changed files with 396 additions and 69 deletions

View File

@@ -5,5 +5,6 @@
<classpathentry kind="src" path="test"/> <classpathentry kind="src" path="test"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="lib" path="lib/junit-4.10.jar"/> <classpathentry kind="lib" path="lib/junit-4.10.jar"/>
<classpathentry kind="lib" path="lib/log4j-1.2.16.jar"/>
<classpathentry kind="output" path="bin"/> <classpathentry kind="output" path="bin"/>
</classpath> </classpath>

BIN
lib/log4j-1.2.16.jar Normal file

Binary file not shown.

View File

@@ -5,7 +5,8 @@ import java.awt.event.MouseListener;
import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener; import java.awt.event.MouseWheelListener;
import model.HumanPlayer; import player.HumanPlayer;
import view.Tile; import view.Tile;
import view.TileSelectionPanel; import view.TileSelectionPanel;

View File

@@ -3,9 +3,10 @@ package controller;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.event.MouseListener; import java.awt.event.MouseListener;
import player.HumanPlayer;
import view.TileSelectionPanel; import view.TileSelectionPanel;
import model.HumanPlayer;
import model.Board.TileColor; import model.Board.TileColor;
public class TSPMouseListener implements MouseListener{ public class TSPMouseListener implements MouseListener{

View File

@@ -8,12 +8,15 @@ public class Board {
BLUE, GREEN, NONE, RED, YELLOW BLUE, GREEN, NONE, RED, YELLOW
} }
public static final int NUM_COLS = 5; public static final int NUM_COLS = 4;
public static final int NUM_ROWS = 5; public static final int NUM_ROWS = 4;
public static final int ROW_REMOVAL_SIZE = 3; public static final int ROW_REMOVAL_SIZE = 3;
private final TileColor[][] board; 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) { public Board(Board that) {
this(); this();
for (int i = 0; i < NUM_COLS; i++) { for (int i = 0; i < NUM_COLS; i++) {
@@ -27,12 +30,11 @@ public class Board {
board = new TileColor[NUM_ROWS][NUM_COLS]; board = new TileColor[NUM_ROWS][NUM_COLS];
for (int r = 0; r < NUM_ROWS; r++) { for (int r = 0; r < NUM_ROWS; r++) {
for (int c = 0; c < NUM_COLS; c++) { for (int c = 0; c < NUM_COLS; c++) {
setTile(new CellPointer(r, c), TileColor.NONE); board[r][c] = TileColor.NONE;
} }
} }
} }
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
@@ -66,7 +68,31 @@ public class Board {
return board[r][c]; 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; board[cp.r][cp.c] = tile;
if (tile != TileColor.NONE) { if (tile != TileColor.NONE) {
@@ -199,5 +225,8 @@ public class Board {
board[cell.r][cell.c] = TileColor.NONE; board[cell.r][cell.c] = TileColor.NONE;
} }
} }
numPlies++;
return true;
} }
} }

View File

@@ -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;
}
}

View File

@@ -3,9 +3,19 @@ package model;
import model.Board.TileColor; import model.Board.TileColor;
public class Move { 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 TileColor color;
private final CellPointer cp; 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) { public Move(CellPointer cllPntr, TileColor tlClr) {
cp = cllPntr; cp = cllPntr;
color = tlClr; color = tlClr;

View File

@@ -1,7 +1,13 @@
package model; package model;
import org.apache.log4j.Logger;
import player.AlphaBetaComPlayer;
import player.HumanPlayer;
import player.Player;
import view.BoardPanel; import view.BoardPanel;
import view.MessagePanel; import view.MessagePanel;
import view.ScorePanel;
import model.Board.TileColor; import model.Board.TileColor;
public class Referee implements Runnable { 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 GAME_OVER = "Game over!";
public static final String PLAYER_TURN = "Waiting for the player's move."; 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 Board board;
private final HumanPlayer humanPlayer = new HumanPlayer(); private final HumanPlayer humanPlayer = new HumanPlayer();
private final Player cc; private final Player computerPlayer;
private boolean playerTurn;
private BoardPanel boardPanel; private BoardPanel boardPanel;
private int score = 0;
private MessagePanel messagePanel; private MessagePanel messagePanel;
private ScorePanel scorePanel;
public Referee() { public Referee() {
board = new Board(); board = new Board();
cc = new RandomComPlayer(); computerPlayer = new AlphaBetaComPlayer();
playerTurn = true;
} }
@Override @Override
public void run() { public void run() {
int plies = 0; int plies = 0;
while (!isOver()) { while (!isOver()) {
if (playerTurn) { scorePanel.updateScore(getPlayerScore());
if (board.isPlayerTurn()) {
boolean wasInterrupted = false; boolean wasInterrupted = false;
while (!humanPlayer.isReady()) { while (!humanPlayer.isReady()) {
try { try {
@@ -39,7 +47,8 @@ public class Referee implements Runnable {
} }
} }
if (wasInterrupted) { if (wasInterrupted) {
System.out.println("Interrupted while waiting for human to move!"); System.out
.println("Interrupted while waiting for human to move!");
} else { } else {
Move mv = humanPlayer.getMove(board); Move mv = humanPlayer.getMove(board);
if (board.getTile(mv.getCell().r, mv.getCell().c) == TileColor.NONE) { if (board.getTile(mv.getCell().r, mv.getCell().c) == TileColor.NONE) {
@@ -49,23 +58,24 @@ public class Referee implements Runnable {
} }
} }
} else { } else {
Move mv = cc.getMove(board); Move mv = computerPlayer.getMove(board);
playToken(mv); playToken(mv);
} }
messagePanel.updateMessage(getMessage()); messagePanel.updateMessage(getMessage());
boardPanel.updateIcons(); boardPanel.updateIcons();
System.out.println("plies: " + plies++); LOGGER.info("plies: " + plies++);
try { try {
Thread.sleep(1000L); Thread.sleep(1000L);
} catch (InterruptedException ie) { } 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() { public Player getComputerPlayer() {
return cc; return computerPlayer;
} }
public HumanPlayer getHumanPlayer() { public HumanPlayer getHumanPlayer() {
@@ -75,46 +85,27 @@ public class Referee implements Runnable {
public String getMessage() { public String getMessage() {
if (isOver()) { if (isOver()) {
return GAME_OVER; return GAME_OVER;
} else if (isPlayersTurn()) { } else if (board.isPlayerTurn()) {
return PLAYER_TURN; return PLAYER_TURN;
} else { } else {
return COM_TURN; return COM_TURN;
} }
} }
public boolean isOver() { public int getPlayerScore() {
for (int r = 0; r < Board.NUM_ROWS; r++) { return board.getTurn();
for (int c = 0; c < Board.NUM_COLS; c++) {
if (board.getTile(r, c) == TileColor.NONE) {
return false;
}
}
}
return true;
}
private boolean isPlayersTurn() {
return playerTurn;
}
public int getScore() {
return score;
} }
public TileColor getTile(int r, int c) { public TileColor getTile(int r, int c) {
return board.getTile(r, c); return board.getTile(r, c);
} }
public boolean isOver() {
return board.isTerminalState();
}
public void playToken(Move move) { public void playToken(Move move) {
board.playTile(move.getCell(), move.getColor());
board.setTile(move.getCell(), move.getColor());
if (playerTurn) {
score++;
}
playerTurn = !playerTurn;
} }
public void setBoardPanel(BoardPanel boardPanel) { public void setBoardPanel(BoardPanel boardPanel) {
@@ -124,4 +115,8 @@ public class Referee implements Runnable {
public void setMessagePanel(MessagePanel messagePanel) { public void setMessagePanel(MessagePanel messagePanel) {
this.messagePanel = messagePanel; this.messagePanel = messagePanel;
} }
public void setScorePanel(ScorePanel scorePanel) {
this.scorePanel = scorePanel;
}
} }

View File

@@ -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
}
}

View File

@@ -1,5 +1,8 @@
package model; package player;
import model.Board;
import model.CellPointer;
import model.Move;
import model.Board.TileColor; import model.Board.TileColor;

View File

@@ -0,0 +1,5 @@
package player;
public class MinimaxComPlayer {
}

View File

@@ -1,7 +1,10 @@
package model; package player;
import java.util.Random; import java.util.Random;
import model.Board;
import model.CellPointer;
import model.Move;
import model.Board.TileColor; import model.Board.TileColor;

View File

@@ -1,4 +1,7 @@
package model; package player;
import model.Board;
import model.Move;
public interface Player { public interface Player {

View File

@@ -1,7 +1,10 @@
package model; package player;
import java.util.Random; import java.util.Random;
import model.Board;
import model.CellPointer;
import model.Move;
import model.Board.TileColor; import model.Board.TileColor;

View File

@@ -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<Move> 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<Move> 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<Move> genMoves(Board board, boolean asHuman, int nMoves) {
Move[] doNothing = new Move[] { Move.NONE };
LOGGER.info("Minimax genMoves() stub returning []");
return Arrays.asList(doNothing);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<Move> genMoves(Board board, boolean asHuman, int nMoves);
}

View File

@@ -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<Move> genMoves(Board board, boolean asHuman, int nMoves) {
List<Move> validMoves = new ArrayList<Move>();
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;
}
}

View File

@@ -35,8 +35,9 @@ public class MainFrame extends JFrame {
private void init() { private void init() {
ScorePanel sp = new ScorePanel(referee); 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); BoardPanel bp = new BoardPanel(referee,tp);
referee.setBoardPanel(bp); referee.setBoardPanel(bp);
@@ -82,7 +83,7 @@ public class MainFrame extends JFrame {
add(vWrapper,BorderLayout.CENTER); add(vWrapper,BorderLayout.CENTER);
//To ensure correct size, pre-populate the score and message panels with text. //To ensure correct size, pre-populate the score and message panels with text.
sp.updateScore(); sp.updateScore(0);
mp.updateMessage("Loading new game..."); mp.updateMessage("Loading new game...");
pack(); pack();

View File

@@ -11,17 +11,15 @@ public class ScorePanel extends JPanel {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final JLabel message = new JLabel(); private final JLabel message = new JLabel();
private final Referee referee;
public ScorePanel(Referee ref) { public ScorePanel(Referee ref) {
referee = ref;
add(message); add(message);
} }
public void updateScore() { public void updateScore(final int score) {
SwingUtilities.invokeLater(new Runnable() { SwingUtilities.invokeLater(new Runnable() {
public void run() { public void run() {
message.setText("Score: " + referee.getScore()); message.setText("Score: " + score);
} }
}); });
} }

View File

@@ -9,9 +9,10 @@ import javax.swing.JLabel;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import player.HumanPlayer;
import controller.TSPMouseListener; import controller.TSPMouseListener;
import model.HumanPlayer;
import model.Board.TileColor; import model.Board.TileColor;
public class TileSelectionPanel extends JPanel { public class TileSelectionPanel extends JPanel {

View File

@@ -11,7 +11,9 @@ public class BoardTest {
@Test @Test
public void testConstructor() { public void testConstructor() {
new Board(); Board board = new Board();
assertTrue(board.isPlayerTurn());
assertFalse(board.isTerminalState());
} }
@Test @Test
@@ -19,10 +21,10 @@ public class BoardTest {
Board board = new Board(); Board board = new Board();
Board copy = new Board(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)); assertFalse(board.equals(copy));
copy.setTile(new CellPointer(1,2),TileColor.BLUE); copy.playTile(new CellPointer(1,2),TileColor.BLUE);
assertTrue(board.equals(copy)); assertTrue(board.equals(copy));
} }
} }