Commit c0d437fc authored by mva021's avatar mva021
Browse files

Added AI players, one for random move and one using minimax strategy.

parent 83390478
......@@ -7,6 +7,14 @@ import inf101.v20.sem2.games.AbstractPlayer;
import inf101.v20.sem2.games.IGame;
import inf101.v20.sem2.games.Player;
/**
* This Player should be used if one wants input from GUI.
* The game loop will stop when reaching an instance of GuiPlayer
* and when a mouse click is detected the game loop will resume.
*
* @author Martin Vatshelle - martin.vatshelle@uib.no
*
*/
public class GuiPlayer extends AbstractPlayer {
public GuiPlayer(String piece, String name) {
......@@ -14,7 +22,7 @@ public class GuiPlayer extends AbstractPlayer {
}
public GuiPlayer(String piece) {
super(piece, getPlayerName(piece));
super(piece, readPlayerName(piece));
}
@Override
......@@ -22,10 +30,15 @@ public class GuiPlayer extends AbstractPlayer {
throw new IllegalStateException("This method should not be called.");
}
public static String getPlayerName(String piece) {
/**
* Asks player to type in name in a GUI pop up
* @param symbol - The symbol representing this player
* @return the name chosen by the player
*/
public static String readPlayerName(String symbol) {
String name = null;
while(!Player.isValidName(name)) {
name = JOptionPane.showInputDialog("Player "+piece+". Type in your name.");
name = JOptionPane.showInputDialog("Player "+symbol+". Type in your name.");
}
return name;
}
......
......@@ -28,7 +28,7 @@ public class MNKGameGUI extends JPanel{
public MNKGameGUI(Game game) {
this.game = game;
statusMessage = new JLabel();
statusMessage.setText("Welcome to this game! " + game.getCurrentPlayerName() + " begins.");
statusMessage.setText("Welcome to this game! " + game.getCurrentPlayer().getName() + " begins.");
clickablePanels = new ClickableGrid(game, MNKGameGUI.getColors());
initialize();
}
......
package inf101.v20.sem2.games;
/**
* This class takes care of the name and symbol handling in Player classes.
* Using an abstract class here means we avoid duplicating this code in each
* class of Players.
*
* @author Martin Vatshelle - martin.vatshelle@uib.no
*
*/
public abstract class AbstractPlayer implements Player {
protected String symbol;
protected String name;
/**
* This Constructor can not be called directly, but classes extending (inheriting from)
* AbstractPlayer will be able to call this constructor
* @param piece
* @param name
*/
public AbstractPlayer(String piece, String name) {
if(piece == null)
throw new IllegalArgumentException("Each player need to have a Symbol to play");
......@@ -14,7 +28,7 @@ public abstract class AbstractPlayer implements Player {
@Override
public String toString() {
return name;
return getName()+" with symbol "+getSymbol();
}
@Override
......
......@@ -13,13 +13,17 @@ public class FourinRow extends Game {
super(new GameBoard(6, 7),players);
}
public FourinRow(GameBoard board, Iterable<Player> players) {
super(board,players);
}
@Override
public String getName() {
return "Connect Four";
}
@Override
protected boolean isWinner(Player player) {
public boolean isWinner(Player player) {
return board.countNumInRow(player)==4;
}
......@@ -42,13 +46,8 @@ public class FourinRow extends Game {
* @param loc
* @return
*/
private Location drop(Location loc) {
Location below = loc.move(GridDirection.SOUTH);
while(board.validLocation(below) && board.isEmpty(below)){
loc = below;
below = loc.move(GridDirection.SOUTH);
}
return null;
Location drop(Location loc) {
return drop(loc.getCol());
}
/**
......@@ -57,7 +56,22 @@ public class FourinRow extends Game {
* @return the location to place at, if there is a valid location that one will be returned, otherwise the top location will be returned.
*/
public Location drop(int column) {
return drop(new Location(0, column));
return dropFrom(new Location(0, column));
}
Location dropFrom(Location loc) {
Location below = loc.move(GridDirection.SOUTH);
while(board.validLocation(below) && board.isEmpty(below)){
loc = below;
below = loc.move(GridDirection.SOUTH);
}
return loc;
}
@Override
public Game copy() {
Game newGame = new FourinRow(board.copy(),players);
newGame.setPlayer(getCurrentPlayer());
return newGame;
}
}
......@@ -15,9 +15,9 @@ import inf101.v20.sem2.grid.Location;
*/
public abstract class Game implements IGame {
protected GameBoard board;
protected PlayerList players;
protected MNKGameGUI gui = null;
GameBoard board;
PlayerList players;
MNKGameGUI gui = null;
//********** CONSTRUCTORS **********//
/**
......@@ -167,12 +167,6 @@ public abstract class Game implements IGame {
return board.isEmpty(loc);
}
/**
* Checks the win conditions of the game
* @param player
* @return
*/
protected abstract boolean isWinner(Player player);
/**
* If the game is over a message is printed
......@@ -209,11 +203,8 @@ public abstract class Game implements IGame {
* and no player has won.
* @return
*/
protected boolean isDraw() {
if(board.isFull()) {
return !hasWinner();
}
return false;
public boolean isDraw() {
return board.isFull() && !hasWinner();
}
/**
......@@ -234,12 +225,13 @@ public abstract class Game implements IGame {
public List<Location> possibleMoves(Player p){
ArrayList<Location> moves = new ArrayList<Location>();
for(Location loc : getGameBoard().locations()) {
for(Location loc : board.locations()) {
if(canPlace(loc, p))
moves.add(loc);
}
return moves;
}
/**
* @return The name of the game
*/
......@@ -247,7 +239,7 @@ public abstract class Game implements IGame {
@Override
public GameBoard getGameBoard() {
return board;
return board.copy();
}
public void printBoard() {
......@@ -255,8 +247,8 @@ public abstract class Game implements IGame {
}
@Override
public String getCurrentPlayerName() {
return players.currentPlayer().getName();
public Player getCurrentPlayer() {
return players.currentPlayer();
}
@Override
......@@ -267,5 +259,19 @@ public abstract class Game implements IGame {
start();
}
/**
* This method is needed for the AI players to try out different moves without
* actually changing the game.
*
* @return
*/
public abstract Game copy();
public void setPlayer(Player player) {
while(player != getCurrentPlayer()) {
players.nextPlayer();
}
}
}
......@@ -2,13 +2,12 @@ package inf101.v20.sem2.games;
import inf101.v20.sem2.grid.Grid;
import inf101.v20.sem2.grid.GridDirection;
import inf101.v20.sem2.grid.GridLocationIterator;
import inf101.v20.sem2.grid.Location;
import inf101.v20.sem2.tools.Range;
/**
* Keeps track of a grid and where on this grid different Pieces are placed.
* It is only allowed to place a Piece if the position is empty
* Keeps track of a grid and where on this grid different Players have placed.
* It is only allowed to place if the position is empty
* @author mva021
*
*/
......@@ -20,9 +19,10 @@ public class GameBoard extends Grid<Player>{
* @param cols - number of columns on the board
*/
public GameBoard(int rows, int cols){
super(rows, cols,null);
super(rows, cols,null); //we fill with null and let null indicate empty
}
@Override
public void set(Location loc, Player p) {
if(isEmpty(loc))
super.set(loc, p);
......@@ -31,9 +31,9 @@ public class GameBoard extends Grid<Player>{
}
/**
* Checks if there is a Piece on a given position.
* @param row - row of position to check
* @param col - column of position to check
* Checks if a Player has placed on a given position or if it is free.
* @param currentRow - row of position to check
* @param currentCol - column of position to check
* @return true if the position contains the empty Piece, false otherwise
*/
public boolean isEmpty(Location loc) {
......@@ -63,8 +63,7 @@ public class GameBoard extends Grid<Player>{
/**
* Counts to check how many in a row a given Piece has from a given start location.
*
* @param row - row of start location
* @param col - column of start location
* @param loc - start location
* @param player - the Piece we are counting
* @return
*/
......@@ -77,14 +76,12 @@ public class GameBoard extends Grid<Player>{
}
/**
* Counts to check how many in a row a given Piece has
* Counts to check how many in a row a given Player has
* from a given start location and in a given direction.
*
* @param row - row of start location
* @param col - column of start location
* @param dx - dx and dy describes the direction to count in
* @param dy - dx and dy describes the direction to count in
* @param player - the Piece we are counting
* @param loc - start location
* @param dir - describes the direction to count in
* @param player - the Player we are counting for
* @return
*/
public int count(Location loc, GridDirection dir, Player player) {
......@@ -146,4 +143,11 @@ public class GameBoard extends Grid<Player>{
public Iterable<Integer> colIndices(){
return Range.range(numCols());
}
@Override
public GameBoard copy() {
GameBoard newBoard = new GameBoard(numRows(), numCols());
copy(newBoard);
return newBoard;
}
}
package inf101.v20.sem2.games;
import java.util.List;
import inf101.v20.sem2.grid.Location;
public interface IGame {
......@@ -42,6 +44,29 @@ public interface IGame {
/**
* @return The name of the current player
*/
public String getCurrentPlayerName();
public Player getCurrentPlayer();
/**
* Will find all the possible moves the current player can make
* @return list of all possible Locations for next move
*/
public List<Location> possibleMoves();
/**
* Will find all the possible moves the given player can make
* @return
*/
public List<Location> possibleMoves(Player p);
/**
* Checks the win conditions of the game
* @param player
* @return
*/
public boolean isWinner(Player p);
public boolean isDraw();
public abstract Game copy();
}
package inf101.v20.sem2.games;
import inf101.v20.sem2.grid.Location;
public class MiniMaxPlayer extends AbstractPlayer {
int depth; //defines how many steps ahead the search should continue;
public MiniMaxPlayer(String piece, int level) {
super(piece, "MiniMax");
depth = level;
}
@Override
public Location getMove(IGame game) {
Strategy best = bestMove(game,depth);
return best.move;
}
/**
* Chooses the move that maximizes the players score
* @param game
* @param depth
* @return
*/
private Strategy bestMove(IGame game,int depth){
Strategy best = null;
//try each possible strategy
for (Location loc : game.possibleMoves()) {
//make a copy of the game and try the move
Game newGame = game.copy();
newGame.makeMove(loc); //note that this changes the current player in the copy but not the real game
int score = 0;
if(newGame.isDraw() || depth==1) { //No more moves can be made
score = score(newGame,game.getCurrentPlayer());
}else {
//call recursively such that the opponent makes the move that is best for him
//change the sign since this is the score of the best move for opponent
score = -bestMove(newGame, depth-1).score;
}
//keep the best Strategy
if(best==null) {
best = new Strategy(loc,score);
}else {
if(score>best.score)
best = new Strategy(loc, score);
}
}
return best;
}
/**
* Computes a score for the player p in the current game
* to be used when choosing the best move.
* @param game
* @param p
* @return
*/
private int score(Game game, Player p) {
if(game.isWinner(p)) {
return 1;
}
if(game.isWinner(getOpponent(game, p))) {
return -1;
}
return 0;
}
/**
* @param player - the current player
* @param game - the game
* @return An opponent in the game, this is only uniquely defined in a 2 player game
*/
public static Player getOpponent(IGame game, Player player) {
for(Player p : game.players()) {
if(p != player) {
return p;
}
}
throw new IllegalStateException("There must be 2 different players in the game");
}
}
/**
* A inner class only to be used by MiniMax player
* This class keeps track of a move and a score associated with that move
* @author mva021
*
*/
class Strategy{
Location move;
int score;
public Strategy(Location move, int score) {
this.move = move;
this.score = score;
}
}
......@@ -18,16 +18,35 @@ public interface Player {
*/
boolean usesSameSymbol(Player player);
/**
* @return The name of the Player
*/
public String getName();
/**
* The 1 char symbol unique to each Player to use when printing the GameBoard.
* The return type is String to allow for EMOJIs but should only be 1 character.
* TODO: this only works for certain font types so make sure monospace font is used.
* @return a 1 character symbol unique to each player
*/
public String getSymbol();
/**
* Checks if a given String is a valid player name
* @param name - the name to check
* @return the name if it is valid, otherwise an Exception will be thrown
*/
public static String validateName(String name) {
if(!isValidName(name))
throw new IllegalArgumentException("Name can not be blank");
return name;
}
/**
* Checks if a given string is a valid player name
* @param name - the name to check
* @return true if the name is valid, false otherwise.
*/
public static boolean isValidName(String name) {
return !name.isBlank();
}
......
package inf101.v20.sem2.games;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import inf101.v20.sem2.grid.Location;
......@@ -8,22 +8,19 @@ import inf101.v20.sem2.grid.Location;
public class RandomPlayer extends AbstractPlayer {
public RandomPlayer(String piece, String name) {
super(piece, name);
}
public RandomPlayer(String piece) {
super(piece, "Random player");
}
@Override
public Location getMove(IGame game) {
List<Location> moves = possibleMoves(game);
return null;
List<Location> moves = game.possibleMoves();
if(moves.isEmpty())
throw new IllegalStateException("No possible moves to choose, game should have ended!");
Collections.shuffle(moves);
return moves.get(0);
}
private List<Location> possibleMoves(IGame game){
ArrayList<Location> moves = new ArrayList<Location>();
for(Location loc : game.getGameBoard().locations()) {
if(game.canPlace(loc, this))
moves.add(loc);
}
return moves;
}
}
......@@ -25,11 +25,15 @@ public class TicTacToe extends Game {
}
public TicTacToe(int n, Iterable<Player> players) {
super(new GameBoard(n,n),players);
this(new GameBoard(n,n),players);
}
public TicTacToe(GameBoard board, Iterable<Player> players) {
super(board,players);
}
@Override
protected boolean isWinner(Player player) {
public boolean isWinner(Player player) {
return board.countNumInRow(player)==board.numRows();
}
......@@ -37,4 +41,11 @@ public class TicTacToe extends Game {
public String getName() {
return "TicTacToe";
}
@Override
public Game copy() {
Game newGame = new TicTacToe(board.copy(),players());
newGame.setPlayer(getCurrentPlayer());
return newGame;
}
}
......@@ -85,10 +85,14 @@ public class Grid<T> implements IGrid<T> {
@Override
public IGrid<T> copy() {
Grid<T> newGrid = new Grid<>(numRows(), numCols(), null);
copy(newGrid);
return newGrid;
}
protected void copy(IGrid<T> newGrid) {
for(Location loc : locations())
newGrid.set(loc, get(loc));
return newGrid;
}
/**
......@@ -103,6 +107,11 @@ public class Grid<T> implements IGrid<T> {
return true;
}
/**
* Checks if a given location is within the limits of the grid
* @param loc
* @return
*/
public boolean validLocation(Location loc) {
return validCoordinate(loc.getCol(), loc.getRow());
}
......@@ -118,12 +127,18 @@ public class Grid<T> implements IGrid<T> {
throw new IndexOutOfBoundsException();
}
@Override
public boolean contains(Object obj) {
return this.cells.contains(obj);
}