From ae6fdf67b7e5623ab5be176660cfc22aba315798 Mon Sep 17 00:00:00 2001 From: Nabil Alsharif Date: Mon, 25 Feb 2013 17:10:32 +0200 Subject: [PATCH] First versio. Wrote it for fun so I didn't use version control --- .../tictactoe/TicTacToeAIPlayer.java | 107 ++++++++++ .../tictactoe/TicTacToeBoard.java | 182 ++++++++++++++++++ .../tictactoe/TicTacToeException.java | 8 + .../tictactoe/TicTacToeGame.java | 85 ++++++++ .../tictactoe/TicTacToeHumanPlayer.java | 89 +++++++++ .../tictactoe/TicTacToePlayer.java | 6 + .../tictactoe/TicTacToePlayerFactory.java | 27 +++ .../tictactoe/ui/TicTacToeBoardView.java | 160 +++++++++++++++ .../tictactoe/ui/TicTacToeGameView.java | 19 ++ .../ui/TicTacToePlayerSelectionView.java | 87 +++++++++ 10 files changed, 770 insertions(+) create mode 100644 src/com/circuitsofimagination/tictactoe/TicTacToeAIPlayer.java create mode 100644 src/com/circuitsofimagination/tictactoe/TicTacToeBoard.java create mode 100644 src/com/circuitsofimagination/tictactoe/TicTacToeException.java create mode 100644 src/com/circuitsofimagination/tictactoe/TicTacToeGame.java create mode 100644 src/com/circuitsofimagination/tictactoe/TicTacToeHumanPlayer.java create mode 100644 src/com/circuitsofimagination/tictactoe/TicTacToePlayer.java create mode 100644 src/com/circuitsofimagination/tictactoe/TicTacToePlayerFactory.java create mode 100644 src/com/circuitsofimagination/tictactoe/ui/TicTacToeBoardView.java create mode 100644 src/com/circuitsofimagination/tictactoe/ui/TicTacToeGameView.java create mode 100644 src/com/circuitsofimagination/tictactoe/ui/TicTacToePlayerSelectionView.java diff --git a/src/com/circuitsofimagination/tictactoe/TicTacToeAIPlayer.java b/src/com/circuitsofimagination/tictactoe/TicTacToeAIPlayer.java new file mode 100644 index 0000000..2f440a2 --- /dev/null +++ b/src/com/circuitsofimagination/tictactoe/TicTacToeAIPlayer.java @@ -0,0 +1,107 @@ +package com.circuitsofimagination.tictactoe; + +import com.circuitsofimagination.tictactoe.TicTacToeBoard.*; + +import java.util.*; + +public class TicTacToeAIPlayer implements TicTacToePlayer { + + private TicTacToeGame game; + private Cell team; + + @Override + public void initialize(Cell team, TicTacToeGame game) { + this.game = game; + this.team = team; + } + + @Override + public Location makeMove() throws TicTacToeException { + List possibleLocations = game.board.emptySlots(); + + float thisScore; + float bestScore = -1; + Location bestMove = null; + TicTacToeBoard currentBoard; + /* + * Find the best move based on the minimax algorithm + * http://en.wikipedia.org/wiki/Minimax + * This is the first iteration, where on Max + * We implement the first iteration here + * so we can find the best move + * instead of having min and max + * return both a score and location + */ + for(Location l : possibleLocations) { + currentBoard = game.board.copy(); + currentBoard.makeMove(team, l); + thisScore = min(currentBoard); + if(thisScore >= bestScore) { + bestScore = thisScore; + bestMove = l; + } + } + + return bestMove; + } + + + + private float min(TicTacToeBoard board)throws TicTacToeException { + float minScore = 1; + + Cell winner = board.winner(); + if(winner != null) { + return evaluateWinner(winner); + } + + List possibleMoves = board.emptySlots(); + float thisScore; + TicTacToeBoard currentBoard; + //TODO: We have three copies of almost the same loop + // Find a way to combine them + // aka: DRY + for(Location l : possibleMoves) { + currentBoard = board.copy(); + currentBoard.makeMove(TicTacToeBoard.oppositePlayer(team), l); + thisScore = max(currentBoard); + if(thisScore <= minScore) { + minScore = thisScore; + } + } + return minScore; + } + + private float max(TicTacToeBoard board) throws TicTacToeException { + float maxScore = -1; + + Cell winner = board.winner(); + if(winner != null) { + return evaluateWinner(winner); + } + + List possibleMoves = board.emptySlots(); + float thisScore; + TicTacToeBoard currentBoard; + for(Location l : possibleMoves) { + currentBoard = board.copy(); + currentBoard.makeMove(team, l); + thisScore = min(currentBoard); + if(thisScore >= maxScore) { + maxScore = thisScore; + } + } + return maxScore; + } + + private float evaluateWinner(Cell winner) { + if((winner == null) || (winner == Cell.EMPTY)) { + return 0; + } else if(winner == team) { + return 1; + } else { + return -1; + } + } + +} diff --git a/src/com/circuitsofimagination/tictactoe/TicTacToeBoard.java b/src/com/circuitsofimagination/tictactoe/TicTacToeBoard.java new file mode 100644 index 0000000..048d0e5 --- /dev/null +++ b/src/com/circuitsofimagination/tictactoe/TicTacToeBoard.java @@ -0,0 +1,182 @@ +package com.circuitsofimagination.tictactoe; + +import java.util.*; + +public class TicTacToeBoard { + + public enum Cell { + EMPTY, X, O + } + + public static class Location { + public int x; + public int y; + + public Location(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return "("+x+", "+y+")"; + } + } + + public static final int WIDTH = 3; + public static final int HEIGHT=3; + private static final int NUM_CELLS=WIDTH*HEIGHT; + + private Cell[] board; + + public TicTacToeBoard() { + board = new Cell[NUM_CELLS]; + for(int i=0; i emptySlots() { + ArrayList slots = new ArrayList(); + for(int y=0; y clickBuffer; + + Semaphore bufferLock; + Semaphore emptyLock; + + @Override + public void initialize(Cell team, TicTacToeGame game) { + this.game = game; + this.team = team; + } + + @Override + public Location makeMove() throws TicTacToeException { + // makeMove is called by the game thread + // But the event listener is called from Swing's + // event dispatch thread + // so we have to do a little cross-thread syncronization + clickBuffer = new LinkedList(); //Clear any previous + bufferLock = new Semaphore(1); // The buffer is ready for use + emptyLock = new Semaphore(0); // And there are 0 items in the buffer + + // Add the listener when we start + // waiting for the human to make a move + game.boardView.addMouseListener(moveListener); + Location move = null; + + // Keep trying until the user click on a valid location + while(move == null) { + move = getMove(); + } + + // And remove the listener once were found a valid move + game.boardView.removeMouseListener(moveListener); + return move; + } + + private Location getMove() { + Location l = null; + try { + // It is necessary to acquire the emptyLock + // before the bufferLock so we don't end up with a + // deadlock + emptyLock.acquire(); + bufferLock.acquire(); + Point thePoint = clickBuffer.poll(); + if(thePoint != null) { + l = game.boardView.translatePoint(thePoint); + } + } catch (InterruptedException exc) { + exc.printStackTrace(); + } finally { + bufferLock.release(); + emptyLock.release(); + } + return l; + } + + private class MoveMouseListener extends MouseAdapter { + public void mouseClicked(MouseEvent evt) { + Point clickPoint = evt.getPoint(); + try { + bufferLock.acquire(); + clickBuffer.add(clickPoint); + emptyLock.release(); + } catch (InterruptedException exc) { + exc.printStackTrace(); + } finally { + bufferLock.release(); + } + } + } + +} diff --git a/src/com/circuitsofimagination/tictactoe/TicTacToePlayer.java b/src/com/circuitsofimagination/tictactoe/TicTacToePlayer.java new file mode 100644 index 0000000..af0a4ab --- /dev/null +++ b/src/com/circuitsofimagination/tictactoe/TicTacToePlayer.java @@ -0,0 +1,6 @@ +package com.circuitsofimagination.tictactoe; + +public interface TicTacToePlayer { + public void initialize(TicTacToeBoard.Cell team, TicTacToeGame game); + public TicTacToeBoard.Location makeMove() throws TicTacToeException; +} diff --git a/src/com/circuitsofimagination/tictactoe/TicTacToePlayerFactory.java b/src/com/circuitsofimagination/tictactoe/TicTacToePlayerFactory.java new file mode 100644 index 0000000..01254f8 --- /dev/null +++ b/src/com/circuitsofimagination/tictactoe/TicTacToePlayerFactory.java @@ -0,0 +1,27 @@ +package com.circuitsofimagination.tictactoe; + +public class TicTacToePlayerFactory { + public static final String AI_PLAYER_NAME = "Positronic Brain"; + public static final String HUMAN_PLAYER_NAME = "Human Brain"; + + public static final String[] PLAYER_NAMES = { + AI_PLAYER_NAME, + HUMAN_PLAYER_NAME + }; + + public static TicTacToePlayer createPlayer(String name) throws TicTacToeException { + TicTacToePlayer thePlayer = null; + switch(name) { + case AI_PLAYER_NAME: + thePlayer = new TicTacToeAIPlayer(); + break; + case HUMAN_PLAYER_NAME: + thePlayer = new TicTacToeHumanPlayer(); + break; + default: + throw new TicTacToeException("Invalid player type"); + } + return thePlayer; + } + +} diff --git a/src/com/circuitsofimagination/tictactoe/ui/TicTacToeBoardView.java b/src/com/circuitsofimagination/tictactoe/ui/TicTacToeBoardView.java new file mode 100644 index 0000000..70975bd --- /dev/null +++ b/src/com/circuitsofimagination/tictactoe/ui/TicTacToeBoardView.java @@ -0,0 +1,160 @@ +package com.circuitsofimagination.tictactoe.ui; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; + +import javax.swing.JPanel; + +import com.circuitsofimagination.tictactoe.*; +import com.circuitsofimagination.tictactoe.TicTacToeBoard.Cell; +import com.circuitsofimagination.tictactoe.TicTacToeBoard.Location; + +public class TicTacToeBoardView extends JPanel { + private TicTacToeBoard theBoard; + + /* + * Variables used for drawing the board + * and translating between locations on the screen + * to cells on the board + */ + private double padding_height; + private double padding_width; + + private double board_width; + private double board_height; + + private double cell_width; + private double cell_height; + + private double cell_padding_width; + private double cell_padding_height; + + public TicTacToeBoardView(TicTacToeBoard board) { + super(); + theBoard = board; + + setBackground(Color.white); + setDoubleBuffered(true); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + Graphics2D g2 = (Graphics2D) g; + + RenderingHints rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHints(rh); + + Dimension size = getSize(); + double width = size.getWidth(); + double height = size.getHeight(); + + g2.setBackground(Color.white); + + padding_height = height/8; + padding_width = width/8; + + board_width = width - (2*padding_width); + board_height = height - (2*padding_height); + + cell_width = board_width/TicTacToeBoard.WIDTH; + cell_height = board_height/TicTacToeBoard.HEIGHT; + + cell_padding_height = cell_height/8; + cell_padding_width = cell_width/8; + + // Temp variables used for drawing the board + int i; + Line2D line; + + // Draw the vertical lines.... + for(i=1; i (padding_width + board_width) + || y < padding_height + || y > (padding_height + board_height) + ) { + return null; + } + + x = x - padding_width; + y = y - padding_height; + x = Math.floor(x/cell_width); + y = Math.floor(y/cell_height); + return TicTacToeBoard.newLocation((new Double(x)).intValue(), (new Double(y)).intValue()); + } + + /* + * Translate a location to the upper left corder of the cell on the screen it belongs to + */ + public Point translateLocation(Location l) { + double x = padding_width + (cell_width * l.x); + double y = padding_height + (cell_height * l.y); + return new Point((new Double(x)).intValue(), (new Double(y)).intValue()); + } + + private void drawX(Graphics2D g, Location l) { + Point where = translateLocation(l); + Line2D l1 = new Line2D.Double( + where.getX() + cell_padding_width, + where.getY() + cell_padding_height, + cell_width+where.getX() - cell_padding_width, + cell_height+where.getY() - cell_padding_height); + Line2D l2 = new Line2D.Double( + where.getX() + cell_width - cell_padding_width, + where.getY() + cell_padding_height, + where.getX() + cell_padding_width, + where.getY() + cell_height - cell_padding_height); + g.draw(l1); + g.draw(l2); + } + + private void drawO(Graphics2D g, Location l) { + Point where = translateLocation(l); + Ellipse2D circle = new Ellipse2D.Double( + where.getX() + cell_padding_width, + where.getY() + cell_padding_height, + cell_width - (2*cell_padding_width), + cell_height - (2*cell_padding_height)); + g.draw(circle); + } +} diff --git a/src/com/circuitsofimagination/tictactoe/ui/TicTacToeGameView.java b/src/com/circuitsofimagination/tictactoe/ui/TicTacToeGameView.java new file mode 100644 index 0000000..15b1e47 --- /dev/null +++ b/src/com/circuitsofimagination/tictactoe/ui/TicTacToeGameView.java @@ -0,0 +1,19 @@ +package com.circuitsofimagination.tictactoe.ui; + +import javax.swing.JFrame; + +public class TicTacToeGameView extends JFrame { + private TicTacToeBoardView board; + + public TicTacToeGameView(TicTacToeBoardView b) { + board = b; + add(board); + setTitle("Tic Tac Toe"); + setDefaultCloseOperation(EXIT_ON_CLOSE); + setSize(300, 280); + setLocationRelativeTo(null); + setVisible(true); + setResizable(false); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} diff --git a/src/com/circuitsofimagination/tictactoe/ui/TicTacToePlayerSelectionView.java b/src/com/circuitsofimagination/tictactoe/ui/TicTacToePlayerSelectionView.java new file mode 100644 index 0000000..d1d6009 --- /dev/null +++ b/src/com/circuitsofimagination/tictactoe/ui/TicTacToePlayerSelectionView.java @@ -0,0 +1,87 @@ +package com.circuitsofimagination.tictactoe.ui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import java.awt.Container; +import java.awt.Dialog; + +import com.circuitsofimagination.tictactoe.*; + +public class TicTacToePlayerSelectionView extends JDialog implements ActionListener { + + private JComboBox xPlayerCombo; + private JComboBox oPlayerCombo; + private JButton playButton; + private JButton quitButton; + + public TicTacToePlayerSelectionView(Dialog owner, String title, boolean modal) { + super(owner, title, modal); + } + + public void execute() { + addComponents(); + setVisible(true); + } + + private void addComponents() { + + Container contentPane = this.getContentPane(); + xPlayerCombo = new JComboBox(TicTacToePlayerFactory.PLAYER_NAMES); + oPlayerCombo = new JComboBox(TicTacToePlayerFactory.PLAYER_NAMES); + + playButton = new JButton("Play!"); + playButton.addActionListener(this); + + quitButton = new JButton("Quit :-("); + quitButton.addActionListener(this); + + contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS)); + + JPanel xPlayerPane = new JPanel(); + xPlayerPane.setLayout(new BoxLayout(xPlayerPane, BoxLayout.X_AXIS)); + xPlayerPane.add(new JLabel("Player X:")); + xPlayerPane.add(xPlayerCombo); + contentPane.add(xPlayerPane); + contentPane.add(Box.createVerticalGlue()); + + JPanel oPlayerPane = new JPanel(); + oPlayerPane.setLayout(new BoxLayout(oPlayerPane, BoxLayout.X_AXIS)); + oPlayerPane.add(new JLabel("Player O:")); + oPlayerPane.add(oPlayerCombo); + contentPane.add(oPlayerPane); + contentPane.add(Box.createVerticalGlue()); + + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.X_AXIS)); + buttonPane.add(playButton); + buttonPane.add(quitButton); + contentPane.add(buttonPane); + + setSize(300, 200); + } + + @Override + public void actionPerformed(ActionEvent evt) { + if(evt.getSource() == playButton) { + this.setVisible(false); + + String xPlayerName = (String)xPlayerCombo.getSelectedItem(); + String oPlayerName = (String)oPlayerCombo.getSelectedItem(); + + TicTacToeGame.startGame(xPlayerName, oPlayerName); + } + + if(evt.getSource() == quitButton) { + this.setVisible(false); + } + } +}