diff --git a/Makefile b/Makefile index 3b45812..251c2f2 100644 --- a/Makefile +++ b/Makefile @@ -11,16 +11,17 @@ DOC=doc CLASSPATH=src LIBARIES= CLASSES=\ -src\game\Main.java \ -src\game\Game.java \ -src\game\GameHistory.java \ -src\game\GameEntry.java \ -src\game\PlayerObject.java \ -src\player\Player.java \ -src\player\malte\MalteAI.java \ -src\player\malte\Pattern.java \ -src\player\malte\Item.java \ -src\player\maurizio\MaurizioAI.java +src/game/Main.java \ +src/game/Game.java \ +src/game/GameHistory.java \ +src/game/GameEntry.java \ +src/game/PlayerObject.java \ +src/player/Player.java \ +src/player/malte/MalteAI.java \ +src/player/malte/Pattern.java \ +src/player/malte/Item.java \ +src/player/malte/PatternGenerator.java \ +src/player/maurizio/MaurizioAI.java OBJECTS=$($(subst $(CLASSPATH),$(BUILDS),$(CLASSES)):.java=.class) diff --git a/src/game/Game.java b/src/game/Game.java index b8afb8b..d019bbc 100644 --- a/src/game/Game.java +++ b/src/game/Game.java @@ -143,6 +143,7 @@ public class Game { return state; // the result of the game } } + System.out.println("BUG!"); return checkState(false); // it is impossible to reach this, but it makes the compiler happy ;) } @@ -169,7 +170,8 @@ public class Game { // Check his choice against the current board. if (choice < 0 || choice > GAME_COLUMNS || this.board[choice][0] != 0) { // If a player makes a false move, the game punishes him. - this.gameOn = false; + // TODO: Fix this, causes -1 return of checkstate in Simulation mode, breaks the game. + // this.gameOn = false; //TODO the game gets stopped, but checkState doesn't know what that means!? if(output){ log(currentP.getP().getName() + " made an illegal move and lost!"); diff --git a/src/player/malte/Item.java b/src/player/malte/Item.java index a27847e..e28e1cb 100644 --- a/src/player/malte/Item.java +++ b/src/player/malte/Item.java @@ -1,15 +1,43 @@ package player.malte; +/** + * An item of a pattern for the game of Connect Four. + */ public class Item { + /** + * The relative horizontal position of the item. + */ private int posX; + + /** + * The relative vertical position of the item. + */ private int posY; + + /** + * The IDs this item recognizes valid. + */ private int[] ids; + /** + * Basic constructor. + * + * @param posX Relative horizontal position. + * @param posY Relative vertical position. + * @param The accepted id. + */ public Item(int posX, int posY, int id) { this(posX, posY, new int[]{id}); } + /** + * Basic constructor for multiple IDs. + * + * @param posX Relative horizontal position. + * @param posY Relative vertical position. + * @param The accepted IDs. + */ public Item(int posX, int posY, int[] ids) { this.posX = posX; this.posY = posY; @@ -36,4 +64,46 @@ public class Item { } return false; } + + /** + * Changes the given oldID to the newID. + * If newID already exists, do nothing. + * IF oldID does not exist, do nothing. + * + * @param oldID The ID to be replaced. + * @param newID The new ID. + */ + public void changeID(int oldID, int newID) { + // If the ID already exists, do nothing. + for (int id: ids) { + if (id == newID) { + return; + } + } + // Otherwise replace it, if oldID exists. + for (int i = 0; i < ids.length; i++) { + if (ids[i] == oldID) { + ids[i] = newID; + return; + } + } + } + + @Override + public String toString() { + String s = String.format("(%d, %d, [", posX, posY); + for (int i: ids) { + s += i + " "; + } + return s + "])"; + } + + /** + * Returns a deep copy of the item. + * + * @return A deep copy of this element. + */ + public Item copy() { + return new Item(posX, posY, ids); + } } diff --git a/src/player/malte/MalteAI.java b/src/player/malte/MalteAI.java index 32ce9a9..6b7b540 100644 --- a/src/player/malte/MalteAI.java +++ b/src/player/malte/MalteAI.java @@ -6,60 +6,201 @@ import java.util.HashSet; import java.util.Arrays; import player.Player; +import game.Game; +/** + * Maltes artificial intelligence for playing a game of Connect Four + */ public class MalteAI implements Player{ + /** + * Name of the player. + */ private String name; + + /** + * Random Object. + */ private Random ran; + + /** + * The players ID. + */ private int id; + + /** + * The enemy's ID. + */ private int enemyID; + /** + * Constructor. + * + * @param name The name for the player. + */ public MalteAI(String name){ this.name = name; this.ran = new Random(); } + @Override public void setPlayerID(int id) { this.id = id; } + @Override public void setEnemyID(int id) { this.enemyID = enemyID; } + @Override public int move(int[][] board){ + // Create a set of all possible options. Set options = new HashSet<>(Arrays.asList(0,1,2,3,4,5,6)); + // Remove impossible options. for (Integer i: copySet(options)) { if (board[i][0] != 0) { options.remove(i); } } - Set instantWins = getWinningOptions(options, board); - // Set preventions = getPreventionOptions(options, board); - + // Get options which would lead to instant win. + Set winningOptions = getRowCompletionOptions(options, board, id); + for (Integer i: winningOptions) { + return i.intValue(); + } + // Get options which would prevent an instant win of the enemy. + enemyID = enemyID == 0 ? 1: enemyID; + Set preventionsOptions = getRowCompletionOptions(options, board, enemyID); + for (Integer i: preventionsOptions) { + return i.intValue(); + } + // Choose a move that will continue a sequence that already exists. + Set twoOfFour = getTwoOfFourOptions(options, board, id); + for (Integer i: twoOfFour) { + return i.intValue(); + } + // Choose a move that will not lead to an instant win of the enemy + Set choosewisely = new HashSet<>(options); + while (choosewisely.size() > 0){ + Integer i = takeRandom(choosewisely); + choosewisely.remove(i); + int[][] fakeBoard = makeMove(copyBoard(board), i, id); + if (getRowCompletionOptions(options, fakeBoard, enemyID).size() == 0) { + return i.intValue(); + } + } + // If nothing applies, take a random valied one. return takeRandom(options).intValue(); } - private Set getWinningOptions(Set options, int[][] board) { - Pattern topOfColumn = new Pattern(new Item(0, 0, 0), - new Item(0, 1, id), - new Item(0, 2, id), - new Item(0, 3, id)); - Pattern leftInRow = new Pattern(new Item(0, 0, 0), - new Item(1, 0, id), - new Item(2, 0, id), - new Item(3, 0, id)); - Pattern rightInRow = new Pattern(new Item(0, 0, 0), - new Item(-1, 0, id), - new Item(-2, 0, id), - new Item(-3, 0, id)); - return null; + private Set getTwoOfFourOptions(Set options, int[][] board, int id) { + Set twoOfFourPattern = PatternGenerator.winInTwoPatterns(id); + // Get patterns, that match anywhere on the board. + Set matches = Pattern.matchingPatterns(twoOfFourPattern, board); + // Create set to be returned. + Set ret = new HashSet<>(); + // Iterate over all matches. + for (Pattern p: matches) { + // Get all positions, this pattern matches. + Set positions = p.matches(board); + // Get empty spaces in the pattern. + for (Item i: p.getZeros()) { + // Add all options to the set. + for(Position pos: positions) { + ret.add(new Integer(i.getPosX() + pos.getPosX())); + } + } + } + return ret; } + /** + * Copies the given board. + * + * @param board The board to be copied. + * @return A deep copy of the board. + */ + private int[][] copyBoard(int[][] board) { + int[][] copy = new int[board.length][board[0].length]; + for (int i = 0; i < board.length; i++) { + for (int j = 0; j < board[i].length; j++) { + copy[i][j] = board[i][j]; + } + } + return copy; + } + + /** + * Makes a move on the given board. + * Makes a move on the given board, no winning check or anything, + * just a fake move on the given board. Necessary for confirming that + * a chosen move will not give the enemy an instant win. + * + * @param board The board on which the move is made. + * @param choice The column to play in. + * @param id The player's id. + * @return The modified board. + */ + private int[][] makeMove(int[][] board, int choice, int id) { + int row = 0; + // If the column is full, do nothing. + if (board[choice][row] != 0) { + return board; + } + // Find the last empty row. + while (row < Game.GAME_ROWS - 1 && board[choice][row + 1] == 0) { + row++; + } + // Add the players piece. + board[choice][row] = id; + return board; + } + + /** + * Instant winning options. + * Returns a set of options for the player to choose from. If any one is choosen and + * played by the player with the given id, a win is certain. + * + * @param options The options, that are valid, a subset is returned. + * @param board The current game's board. + * @param id The players id. + * @return A subset of options which would lead to a win. + */ + private Set getRowCompletionOptions(Set options, int[][] board, int id) { + // Get winning patterns from Generator. + Set pats = PatternGenerator.winningPatterns(id); + // Get patterns, that match anywhere on the board. + Set matches = Pattern.matchingPatterns(pats, board); + // Create set to be returned. + Set ret = new HashSet<>(); + // Iterate over all matches. + for (Pattern p: matches) { + // Get all positions, this pattern matches. + Set positions = p.matches(board); + // Get empty spaces in the pattern. + for (Item i: p.getZeros()) { + // Add all options to the set. + for(Position pos: positions) { + ret.add(new Integer(i.getPosX() + pos.getPosX())); + } + } + } + return ret; + } + + /** + * Copies a set of Integer. + */ private Set copySet(Set s) { return new HashSet(s); } + /** + * Takes a random Integer from a set of Integer. + * + * @param s The set to take from. + * @return A random element of s. + */ private Integer takeRandom(Set s) { int item = ran.nextInt(s.size()); int i = 0; @@ -73,6 +214,9 @@ public class MalteAI implements Player{ return 0; } + /** + * Get the player's name. + */ public String getName(){ return this.name; } diff --git a/src/player/malte/Pattern.java b/src/player/malte/Pattern.java index 156f03f..af3a129 100644 --- a/src/player/malte/Pattern.java +++ b/src/player/malte/Pattern.java @@ -1,10 +1,197 @@ package player.malte; +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; + +import game.Game; + +/** + * A Connect Four Pattern. + * Every part has a relative position and a number of IDs that it matches. + * When Pattern.matches(int[][]) is called, all possible positions on the board + * are checked against every part of the pattern. If all parts match, the whole + * pattern matches. + */ public class Pattern { - private Item[] parts; + /** + * The parts of the pattern. + */ + private Set parts; + /** + * Constructor. + * + * @param parts The parts of the pattern. + */ public Pattern(Item... parts) { + this.parts = new HashSet(Arrays.asList(parts)); + } + + /** + * Constructor. + * + * @param parts The parts of the pattern. + */ + public Pattern(Set parts) { this.parts = parts; } + + /** + * Returns the matching positions. + * Checks the given board for matches. The positions of the matches are returned. + * + * @param board The game's board. + * @return The positions of the matches. + */ + public Set matches(int[][] board) { + // Preparing iteration + int maxLeft = 0, + maxRight = 0, + maxUp = 0, + maxDown = 0; + for (Item i: parts) { + if (i.hasID(-1)) { + continue; + } + if (maxLeft < -i.getPosX()) { + maxLeft = -i.getPosX(); + } + if (maxRight > -i.getPosX()) { + maxRight = -i.getPosX(); + } + if (maxUp < -i.getPosY()) { + maxUp = -i.getPosY(); + } + if (maxDown > -i.getPosY()) { + maxDown = -i.getPosY(); + } + } + // Iteration + Position accumulation + Set set = new HashSet<>(); + for (int i = maxLeft; i < Game.GAME_COLUMNS + maxRight; i++) { + inner:for (int j = maxUp; j < Game.GAME_ROWS + maxDown; j++) { + for (Item k: parts) { + int posX = i + k.getPosX(); + int posY = j + k.getPosY(); + if ((!isOnBoard(new Position(posX, posY)) && + !k.hasID(-1)) || + (isOnBoard(new Position(posX, posY)) && + !k.hasID(board[posX][posY]))) { + continue inner; + } + } + set.add(new Position(i, j)); + } + } + return set; + } + + /** + * Is the given position on the Board. + * + * @param pos The position to check. + * @return Whether the position is on the board. + */ + private boolean isOnBoard(Position pos) { + if (pos.getPosX() < 0 || + pos.getPosX() > Game.GAME_COLUMNS - 1 || + pos.getPosY() < 0 || + pos.getPosY() > Game.GAME_ROWS - 1) { + return false; + } + return true; + } + + /** + * Replaces a part of the Pattern. + * + * @param oldItem The part to be replaced. + * @param newItem The part to added. + */ + public void replaceItem(Item oldItem, Item newItem) { + if (this.parts.contains(oldItem)) { + this.parts.remove(oldItem); + this.parts.add(newItem); + } + } + + /** + * Adds a part to the pattern. + * + * @param part The new part. + */ + public void addItem(Item part) { + this.parts.add(part); + } + + /** + * Returns the zeros in this pattern. + * Returns a set of all parts which contain a zero in their valid IDs. + * + * @return The set of parts + */ + public Set getZeros() { + Set ret = new HashSet<>(); + for (Item i: parts) { + if (i.hasID(0)) { + ret.add(i); + } + } + return ret; + } + + /** + * Returns a deep copy of this pattern. + * + * @return This pattern... only copied. + */ + public Pattern copy() { + Set itemCopy = new HashSet<>(); + for (Item i: parts) { + itemCopy.add(i.copy()); + } + return new Pattern(itemCopy); + } + + /** + * Static matching method for a set of pattern. + * Returns a set of all positions any one pattern matches against. + * + * @param pats The patterns to match. + * @param board The board to match against. + */ + public static Set matches(Set pats, int[][] board) { + Set ret = new HashSet<>(); + for (Pattern p: pats) { + ret.addAll(p.matches(board)); + } + return ret; + } + + /** + * Returns the subset of all patterns, that do have a match on the board. + * + * @param pats The patterns to check. + * @param board The board to match against. + */ + public static Set matchingPatterns(Set pats, int[][] board) { + Set ret = new HashSet<>(); + for (Pattern p: pats) { + if (p.matches(board).size() > 0) { + ret.add(p); + } + } + return ret; + } + + @Override + public String toString() { + String s = ""; + for (Item i: parts) { + s += i + ", "; + } + return s + "\n"; + } } diff --git a/src/player/malte/PatternGenerator.java b/src/player/malte/PatternGenerator.java new file mode 100644 index 0000000..5b34f2b --- /dev/null +++ b/src/player/malte/PatternGenerator.java @@ -0,0 +1,138 @@ +package player.malte; + +import java.util.Set; +import java.util.HashSet; + +/** + * A generator for patterns for the game of Connect Four. + */ +public class PatternGenerator { + + /** + * Do not instanciate. + */ + private PatternGenerator() {} + + /** + * Returns the winning patterns. + * X | X | XXXX | X + * X | X | | X + * X | X | | X + * X | X | | X + * Any of the above with an empty space is considered a winning pattern. + * Except the first one, in which only the topmost space can be empty. + * + * @param id The player's id. + */ + public static Set winningPatterns(int id) { + // Four in a row with one hole + Set pats = PatternGenerator.emptySpaceGenerator(new Item(0, 0, id), + new Item(1, 0, id), + new Item(2, 0, id), + new Item(3, 0, id)); + // Four in a diagonal line from lower left to upper right with one hole + pats.addAll(PatternGenerator.emptySpaceGenerator(new Item(0, 0, id), + new Item(-1, 1, id), + new Item(-2, 2, id), + new Item(-3, 3, id))); + // Four in a diagonal line from upper left to lower right with one hole + pats.addAll(PatternGenerator.emptySpaceGenerator(new Item(0, 0, id), + new Item(1, 1, id), + new Item(2, 2, id), + new Item(3, 3, id))); + // Three on top of each other. + pats.add(new Pattern(new Item(0, 0, 0), + new Item(0, 1, id), + new Item(0, 2, id), + new Item(0, 3, id))); + return pats; + } + + public static Set winInTwoPatterns(int id) { + // Four in a row with two holes + Set pats = PatternGenerator.empty2SpaceGenerator(new Item(0, 0, id), + new Item(1, 0, id), + new Item(2, 0, id), + new Item(3, 0, id)); + // Four in a diagonal line from lower left to upper right with two holes + pats.addAll(PatternGenerator.empty2SpaceGenerator(new Item(0, 0, id), + new Item(-1, 1, id), + new Item(-2, 2, id), + new Item(-3, 3, id))); + // Four in a diagonal line from upper left to lower right with two holes + pats.addAll(PatternGenerator.empty2SpaceGenerator(new Item(0, 0, id), + new Item(1, 1, id), + new Item(2, 2, id), + new Item(3, 3, id))); + // Three on top of each other. + pats.add(new Pattern(new Item(0, 0, 0), + new Item(0, 1, id), + new Item(0, 2, id))); + return pats; + } + + /** + * Creates patterns with one empty space. + * Returns a set of patterns, each one containing an empty space. + * Each element of model is replaced once with an empty space Item. + * Always adds one support Item beneath the hole. + * + * @param model The basis of the pattern. + * @return A set of patterns created by the above rule. + */ + public static Set emptySpaceGenerator(Item... model) { + Set ret = new HashSet<>(); + // Iterate over all models. + for (int i = 0; i < model.length; i++) { + // Create a new pattern. + Pattern newP = new Pattern(model); + Item x = model[i]; + Item newI = new Item(x.getPosX(), x.getPosY(), 0); + // Replace one item with the new 0-one. + newP.replaceItem(x, newI); + // Add the support. + newP.addItem(new Item(x.getPosX(), x.getPosY() + 1, new int[]{-1, 1, 2})); + // Add the pattern to the set. + ret.add(newP); + } + return ret; + } + + /** + * Generates Patterns with two empty holes. + * Generates all possible patterns given the parts, which contain + * two empty spaces supported by two parts. + * + * @param model The pattern's basis. + * @return All possible patterns with two holes. + */ + public static Set empty2SpaceGenerator(Item... model) { + Set ret = new HashSet<>(); + // Iterate over all models. + for (int i = 0; i < model.length; i++) { + for (int j = 0; j < model.length; j++) { + // If we're looking at the same model, skip. + if (i == j) { + continue; + } + // Create a new Pattern with the models. + Pattern newP = new Pattern(model); + Item x = model[i]; + Item y = model[j]; + // Create new Items to replace two items. + Item newXI = new Item(x.getPosX(), x.getPosY(), 0); + Item newYI = new Item(y.getPosX(), y.getPosY(), 0); + // Actually replace them. + newP.replaceItem(x, newXI); + newP.replaceItem(y, newYI); + // Add support items. + newP.addItem(new Item(x.getPosX(), x.getPosY() + 1, new int[]{-1, 1, 2})); + newP.addItem(new Item(y.getPosX(), y.getPosY() + 1, new int[]{-1, 1, 2})); + // Add the pattern to the set. + ret.add(newP); + } + } + return ret; + } + +} diff --git a/src/player/malte/Position.java b/src/player/malte/Position.java new file mode 100644 index 0000000..3f28b12 --- /dev/null +++ b/src/player/malte/Position.java @@ -0,0 +1,27 @@ +package player.malte; + +import java.lang.String; + +public class Position { + + private final int posX; + private final int posY; + + public Position(int posX, int posY) { + this.posX = posX; + this.posY = posY; + } + + public int getPosX() { + return this.posX; + } + + public int getPosY() { + return this.posY; + } + + @Override + public String toString() { + return String.format("(%d, %d)", posX, posY); + } +}