diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..15d4ca2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/out/
+/lib/
+/.idea/
diff --git a/DomineeringAI.iml b/DomineeringAI.iml
new file mode 100644
index 0000000..c198dd9
--- /dev/null
+++ b/DomineeringAI.iml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..bddc46f
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,21 @@
+# Domineering AI
+
+The game [domineering](https://en.wikipedia.org/wiki/Domineering) has very few rules and is therefore easy to learn.
+However, the strategies which evolve can be more complex than one might think at first glance.
+
+Domineering can be played on a chess board in any rectangular shape, increasing the complexity with the overall size of
+the board. Two players, here called "V" for vertical and "H" for horizontal, each place a domino pieces onto the board.
+The pieces may not overlap each other and can only be placed fully on the board.
+
+The first player who is unable to legally place another piece has lost the game.
+
+To compute the winning strategy for the game, one has to compute a score for the possible board configurations which
+result in the currently possible moves, the player could make. Because the number of possible moves and board
+configurations eventually rises exponentially with bigger boards, we need a good strategy to sort out bad moves quickly.
+
+For this AI, I chose to use a combination of a min-max-algorithm with alpha-beta-pruning. For move ordering, I based my
+board scoring and move evaluation algorithm on methods for move counting and scanning from Nathan Bullocks master thesis.
+
+Depending on how long the AI is allowed to "think" about the next move, the "depthForBoardState" function should be
+altered. It is responsible for evaluating the search depth, aka. the number of next moves, the ai should take into
+consideration.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..bac4996
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,16 @@
+
+
+ 4.0.0
+
+ groupId
+ DomineeringAI
+ 1.0-SNAPSHOT
+
+
+ 15
+ 15
+
+
+
\ No newline at end of file
diff --git a/src/main/java/ai/AI.java b/src/main/java/ai/AI.java
new file mode 100644
index 0000000..01b6c78
--- /dev/null
+++ b/src/main/java/ai/AI.java
@@ -0,0 +1,5 @@
+package ai;
+
+public abstract class AI {
+ public abstract Coordinate playMove(char[][] board, Player player);
+}
diff --git a/src/main/java/ai/Area.java b/src/main/java/ai/Area.java
new file mode 100644
index 0000000..51d8b85
--- /dev/null
+++ b/src/main/java/ai/Area.java
@@ -0,0 +1,24 @@
+package ai;
+
+public abstract class Area {
+ protected final Coordinate cornerUL; // "upper-left"-corner
+ protected final Coordinate cornerLR; // "lower-right"-corner
+
+ public Area(int startX, int startY, int endX, int endY) {
+ cornerUL = new Coordinate(startX, startY);
+ cornerLR = new Coordinate(endX, endY);
+ }
+
+ public Coordinate getCornerUL() {
+ return cornerUL;
+ }
+
+ public Coordinate getCornerLR() {
+ return cornerLR;
+ }
+
+ // return true if the given point is not contained in the area
+ public boolean contains(int x, int y) {
+ return x >= cornerLR.getX() && x <= cornerLR.getX() && y >= cornerUL.getY() && y <= cornerLR.getY();
+ }
+}
diff --git a/src/main/java/ai/BoardAnalyser.java b/src/main/java/ai/BoardAnalyser.java
new file mode 100644
index 0000000..8147e03
--- /dev/null
+++ b/src/main/java/ai/BoardAnalyser.java
@@ -0,0 +1,358 @@
+package ai;
+
+import java.util.Arrays;
+import java.util.Random;
+
+public final class BoardAnalyser {
+ private final Random generator;
+
+ public final BoardLayout vertical;
+ public final BoardLayout horizontal;
+
+ private final char[][] board;
+
+ public BoardAnalyser(char[][] board, boolean noBounds) {
+ this.generator = new Random();
+ this.board = board;
+
+ this.vertical = new BoardLayout(Player.V);
+ this.horizontal = new BoardLayout(Player.H);
+
+ // initialize the internal variables
+ analyseBoard();
+ if (!noBounds) {
+ calcUnplayable();
+ calcLowerBounds();
+ calcUpperBounds();
+ }
+ }
+
+ /*
+ We try to play one of four openings for domineering:
+ ------------------
+ | # |
+ |## # |
+ | |
+ | |
+ | |
+ | # ##|
+ | # |
+ ------------------
+ */
+ // to be able to play an opening without the performance loss of analyzing the entire board, this is a static method
+ public static Coordinate trySimpleOpening(char[][] board, Player player) {
+ // Horizontal
+ if (player == Player.H) {
+ if (board[board.length - 1][board[0].length - 2] == 'E'
+ && board[board.length - 2][board[0].length - 2] == 'E') {
+ return new Coordinate(board.length - 2, board[0].length - 2);
+ }
+ if (board[0][1] == 'E' && board[1][1] == 'E') {
+ return new Coordinate(0, 1);
+ }
+ } else {
+ // Vertical
+ if (board[board.length - 2][0] == 'E' && board[board.length - 2][1] == 'E') {
+ return new Coordinate(board.length - 2, 0);
+ }
+ if (board[1][board[0].length - 2] == 'E' && board[1][board[0].length - 1] == 'E') {
+ return new Coordinate(1, board[0].length - 2);
+ }
+ }
+ // no simple opening possible
+ return null;
+ }
+
+ // scan board for special "areas" which contain all playable moves
+ public void analyseBoard() {
+ // copy board
+ char[][] boardCloneVertical = new char[board.length][];
+ char[][] boardCloneHorizontal = new char[board.length][];
+
+ for (int i = 0; i < board.length; i++) {
+ boardCloneVertical[i] = Arrays.copyOf(board[i], board[i].length);
+ boardCloneHorizontal[i] = Arrays.copyOf(board[i], board[i].length);
+ }
+
+ /*
+ Go through board and scan for "safe areas" and "protective areas":
+
+ Safe areas are spots where the current player could place his tile, but the other player can never obstruct.
+ We should keep these areas as they can't be blocked by the other player.
+
+ Protective areas are 2x2 areas which can be converted to "safe areas" by placing a tile on the internally saved
+ "protectSpot". This move converts a protective area to a safe area. No two protective areas are allowed to be
+ adjacent to each other. Otherwise the other player could destroy two protective areas by placing one tile.
+ */
+ for (int x = 0; x < board.length; x++) {
+ for (int y = 0; y < board[0].length; y++) {
+ if (y < board[0].length - 1 && SafeArea.isSafeArea(boardCloneVertical, Player.V, x, y)) {
+ vertical.safeAreas.add(new SafeArea(x, y, x, y + 1));
+
+ // mark the "used spaces" in the board clone so they aren't reused for other areas
+ boardCloneVertical[x][y] = 'S';
+ boardCloneVertical[x][y + 1] = 'S';
+ }
+ if (x < board.length - 1 && SafeArea.isSafeArea(boardCloneHorizontal, Player.H, x, y)) {
+ horizontal.safeAreas.add(new SafeArea(x, y, x + 1, y));
+
+ // mark the "used spaces" in the board clone so they aren't reused for other areas
+ boardCloneHorizontal[x][y] = 'S';
+ boardCloneHorizontal[x + 1][y] = 'S';
+ }
+
+ if (y < board[0].length - 1
+ && ProtectiveArea
+ .isProtectiveArea(boardCloneVertical, Player.V, x, y, vertical.protectiveAreas)) {
+ vertical.protectiveAreas.add(new ProtectiveArea(x, y, x + 1, y + 1, board, Player.V));
+
+ // mark the 2x2 protective area on the board copy
+ boardCloneVertical[x][y] = 'P';
+ boardCloneVertical[x + 1][y] = 'P';
+ boardCloneVertical[x][y + 1] = 'P';
+ boardCloneVertical[x + 1][y + 1] = 'P';
+ }
+
+ if (x < board.length - 1
+ && ProtectiveArea
+ .isProtectiveArea(boardCloneHorizontal, Player.H, x, y, horizontal.protectiveAreas)) {
+ horizontal.protectiveAreas.add(new ProtectiveArea(x, y, x + 1, y + 1, board, Player.H));
+
+ // mark the 2x2 protective area on the board copy
+ boardCloneHorizontal[x][y] = 'P';
+ boardCloneHorizontal[x + 1][y] = 'P';
+ boardCloneHorizontal[x][y + 1] = 'P';
+ boardCloneHorizontal[x + 1][y + 1] = 'P';
+ }
+ }
+ }
+
+ // scan all safe areas for the so called "option areas". This means player could place the piece into one
+ // square contained in the safe area and one square which is outside. This mustn't reduce the number of safe
+ // moves or destroy another protective area. It is really useful to reduce the opponent's possible moves.
+ for (SafeArea safeArea : vertical.safeAreas) {
+ int x = safeArea.getCornerUL().getX();
+ int y = safeArea.getCornerUL().getY();
+
+ if (y + 2 < board[0].length) {
+ OptionArea oaLower = OptionArea
+ .getOptionArea(boardCloneVertical, Player.V, x, y + 2);
+ if (oaLower != null) {
+ vertical.optionAreas.add(oaLower);
+ safeArea.addOptionAreaLower(oaLower);
+ }
+ }
+ if (y - 1 > 0) {
+ OptionArea oaHigher = OptionArea
+ .getOptionArea(boardCloneVertical, Player.V, x, y - 1);
+ if (oaHigher != null) {
+ vertical.optionAreas.add(oaHigher);
+ safeArea.addOptionAreaHigher(oaHigher);
+ }
+ }
+ }
+
+ for (SafeArea safeArea : horizontal.safeAreas) {
+ int x = safeArea.getCornerUL().getX();
+ int y = safeArea.getCornerUL().getY();
+
+ if (x + 2 < board.length) {
+ OptionArea oaLower = OptionArea
+ .getOptionArea(boardCloneHorizontal, Player.H, x + 2, y);
+ if (oaLower != null) {
+ horizontal.optionAreas.add(oaLower);
+ safeArea.addOptionAreaLower(oaLower);
+ }
+ }
+ if (x - 1 > 0) {
+ OptionArea oaHigher = OptionArea
+ .getOptionArea(boardCloneHorizontal, Player.H, x - 1, y);
+ if (oaHigher != null) {
+ horizontal.optionAreas.add(oaHigher);
+ safeArea.addOptionAreaHigher(oaHigher);
+ }
+ }
+ }
+
+ // used for temporary storage for the vulnerable area type
+ int type;
+
+ // all places which are left and aren't single 1x1 fields are vulnerable areas
+ for (int x = 0; x < board.length; x++) {
+ for (int y = 0; y < board[0].length; y++) {
+ if (y < board[0].length - 1 && (type = VulnArea.isVulnArea(boardCloneVertical, x, y, Player.V)) != 0) {
+ if (type == 1) {
+ vertical.vulnAreasOne.add(VulnArea.getVulnArea(x, y, Player.V));
+ boardCloneVertical[x][y] = 'D';
+ boardCloneVertical[x][y + 1] = 'D';
+ } else if (type == 2) {
+ vertical.vulnAreasTwo.add(VulnArea.getVulnArea(x, y, Player.V));
+ boardCloneVertical[x][y] = 'D';
+ boardCloneVertical[x][y + 1] = 'D';
+ } else if (type == 3) {
+ // the vulnerable areas which contain a protected square are counted twice
+ vertical.vulnAreasOne.add(VulnArea.getVulnArea(x, y, Player.V));
+ vertical.vulnAreasProtectedOne.add(VulnArea.getVulnArea(x, y, Player.V));
+ boardCloneVertical[x][y] = 'D';
+ boardCloneVertical[x][y + 1] = 'D';
+ } else if (type == 4) {
+ vertical.vulnAreasTwo.add(VulnArea.getVulnArea(x, y, Player.V));
+ vertical.vulnAreasProtectedTwo.add(VulnArea.getVulnArea(x, y, Player.V));
+ boardCloneVertical[x][y] = 'D';
+ boardCloneVertical[x][y + 1] = 'D';
+ }
+ }
+
+ if (x < board.length - 1
+ && (type = VulnArea.isVulnArea(boardCloneHorizontal, x, y, Player.H)) != 0) {
+ if (type == 1) {
+ horizontal.vulnAreasOne.add(VulnArea.getVulnArea(x, y, Player.H));
+ boardCloneHorizontal[x][y] = 'D';
+ boardCloneHorizontal[x + 1][y] = 'D';
+ } else if (type == 2) {
+ horizontal.vulnAreasTwo.add(VulnArea.getVulnArea(x, y, Player.H));
+ boardCloneHorizontal[x][y] = 'D';
+ boardCloneHorizontal[x + 1][y] = 'D';
+ } else if (type == 3) {
+ // the vulnerable areas which contain a protected square are counted twice
+ horizontal.vulnAreasOne.add(VulnArea.getVulnArea(x, y, Player.H));
+ horizontal.vulnAreasProtectedOne.add(VulnArea.getVulnArea(x, y, Player.H));
+ boardCloneHorizontal[x][y] = 'D';
+ boardCloneHorizontal[x + 1][y] = 'D';
+ } else if (type == 4) {
+ horizontal.vulnAreasTwo.add(VulnArea.getVulnArea(x, y, Player.H));
+ horizontal.vulnAreasProtectedTwo.add(VulnArea.getVulnArea(x, y, Player.H));
+ boardCloneHorizontal[x][y] = 'D';
+ boardCloneHorizontal[x + 1][y] = 'D';
+ }
+ }
+ }
+ }
+
+ // the last thing to do is to sum up all squares which were available from the start and also which could be
+ // played by either player.
+ for (int x = 0; x < board.length; x++) {
+ for (int y = 0; y < board[0].length; y++) {
+ if (board[x][y] == 'E') {
+ horizontal.startAvailableSquares++;
+ vertical.startAvailableSquares++;
+ }
+
+ if ((boardCloneVertical[x][y] == 'E')
+ && ((x <= 0 || board[x - 1][y] != 'E') && (x + 1 >= board.length || board[x + 1][y] != 'E'))) {
+ horizontal.unavailableSquares++;
+ }
+
+ if ((boardCloneHorizontal[x][y] == 'E')
+ && ((y <= 0 || board[x][y - 1] != 'E') && (y + 1 >= board[0].length
+ || board[x][y + 1] != 'E'))) {
+ vertical.unavailableSquares++;
+ }
+ }
+ }
+ }
+
+ // this lower bound denotes the minimum number of moves the current player is able to play
+ // the main strategy and calculation is based on Nathan Bullock's master thesis (theorem 3.5.1)
+ private void calcLowerBounds() {
+ if (vertical.numProtectiveAreas() % 2 != 0) {
+ ProtectiveArea convertibleArea =
+ vertical.protectiveAreas.get(generator.nextInt(vertical.numProtectiveAreas()));
+ vertical.protectiveAreas.remove(convertibleArea);
+
+ VulnArea[] newVulnAreas = convertibleArea.splitIntoVulnTwo(Player.V);
+
+ vertical.vulnAreasTwo.add(newVulnAreas[0]);
+ vertical.vulnAreasTwo.add(newVulnAreas[1]);
+ }
+
+ int addMove = (vertical.numVulnAreasTwo() % 3 != 0 && vertical.numVulnAreasOne() % 2 != 0) ? 1 : 0;
+
+ vertical.lowerBound = (vertical.numProtectiveAreas()
+ + vertical.numVulnAreasTwo() / 3
+ + vertical.numVulnAreasOne() / 2
+ + vertical.numSafeAreas()
+ + addMove
+ );
+
+ if (horizontal.numProtectiveAreas() % 2 != 0) {
+ ProtectiveArea convertibleArea =
+ horizontal.protectiveAreas.get(generator.nextInt(horizontal.numProtectiveAreas()));
+ horizontal.protectiveAreas.remove(convertibleArea);
+
+ VulnArea[] newVulnAreas = convertibleArea.splitIntoVulnTwo(Player.H);
+
+ horizontal.vulnAreasTwo.add(newVulnAreas[0]);
+ horizontal.vulnAreasTwo.add(newVulnAreas[1]);
+ }
+
+ addMove = (horizontal.numVulnAreasTwo() % 3 != 0 && horizontal.numVulnAreasOne() % 2 != 0) ? 1 : 0;
+
+ horizontal.lowerBound = (horizontal.numProtectiveAreas()
+ + horizontal.numVulnAreasTwo() / 3
+ + horizontal.numVulnAreasOne() / 2
+ + horizontal.numSafeAreas()
+ + addMove
+ );
+ }
+
+ // get the upper bound of moves which could be played by the corresponding player.
+ private void calcUpperBounds() {
+ // first, calculate the upper bound for vertical
+ // number of playable squares after the opponent has played his lower bound of moves
+ int squaresCurr = (vertical.startAvailableSquares - 2 * horizontal.lowerBound);
+ // the upper bound is the number of playable squares divided by 2 -> minimum number of playable tiles
+ vertical.upperBound = (squaresCurr - vertical.unavailableSquares - vertical.unplayableSquares) / 2;
+
+ // calculate the upper bound for horizontal
+ squaresCurr = (horizontal.startAvailableSquares - 2 * vertical.lowerBound);
+ horizontal.upperBound = (squaresCurr - horizontal.unavailableSquares - horizontal.unplayableSquares) / 2;
+ }
+
+ private void calcUnplayable() {
+ // first, calculate the unplayable squares for vertical (use the data from horizontal player)
+ BoardLayout current = vertical;
+ BoardLayout opponent = horizontal;
+
+ for (int i = 0; i < 2; i++) {
+ int o1 = 0;
+ int o2 = 0;
+ int o3 = 0;
+
+ for (OptionArea o : opponent.optionAreas) {
+ switch (o.getWeight()) {
+ case 1 -> o1++;
+ case 2 -> o2++;
+ case 3 -> o3++;
+ }
+ }
+
+ int addMove1 = (opponent.numVulnAreasTwo() % 3 != 0
+ && opponent.numVulnAreasOne() % 2 != 0
+ && (opponent.numVulnAreasProtectedTwo() > 0 || opponent.numVulnAreasProtectedOne() > 0)
+ ) ? -1 : 0;
+
+ int addMove2 = 0;
+ if (!(opponent.numVulnAreasTwo() % 3 != 0 && opponent.numVulnAreasOne() % 2 != 0)
+ && !(opponent.numVulnAreasTwo() % 3 == 0 && opponent.numVulnAreasOne() % 2 == 0)) {
+ if (o3 % 2 == 1) {
+ addMove2 = 3;
+ } else if (o2 % 2 == 1) {
+ addMove2 = 2;
+ } else if (o1 % 2 == 1) addMove2 = 1;
+ }
+
+ current.unplayableSquares = (opponent.numVulnAreasProtectedTwo()
+ - (opponent.numVulnAreasTwo() / 3
+ - (opponent.numVulnAreasTwo() - opponent.numVulnAreasProtectedTwo()) / 3))
+ + (opponent.numVulnAreasProtectedOne()
+ - (opponent.numVulnAreasOne() / 2
+ - (opponent.numVulnAreasOne() - opponent.numVulnAreasProtectedOne()) / 2))
+ + 3 * (o3 / 2) + 2 * (o2 / 2) + o1 / 2 + addMove1 + addMove2;
+
+ // then, do the same thing for horizontal
+ current = horizontal;
+ opponent = vertical;
+ }
+ }
+}
diff --git a/src/main/java/ai/BoardLayout.java b/src/main/java/ai/BoardLayout.java
new file mode 100644
index 0000000..3848bd0
--- /dev/null
+++ b/src/main/java/ai/BoardLayout.java
@@ -0,0 +1,69 @@
+package ai;
+
+import java.util.ArrayList;
+
+/*
+ A BoardLayout object contains all the calculated board areas for either the vertical or the horizontal player. It
+ is only used for storing these objects and values.
+ */
+
+public class BoardLayout {
+ public final ArrayList protectiveAreas;
+ public final ArrayList safeAreas;
+ public final ArrayList vulnAreasOne;
+ public final ArrayList vulnAreasTwo;
+ public final ArrayList vulnAreasProtectedOne;
+ public final ArrayList vulnAreasProtectedTwo;
+ public final ArrayList optionAreas;
+
+ public final Player player;
+
+ public int lowerBound;
+ public int upperBound;
+
+ public int unavailableSquares;
+ public int startAvailableSquares;
+ public int unplayableSquares;
+
+ public BoardLayout(Player player) {
+ this.player = player;
+ this.protectiveAreas = new ArrayList<>(20);
+ this.safeAreas = new ArrayList<>(15);
+ this.vulnAreasTwo = new ArrayList<>(50);
+ this.vulnAreasOne = new ArrayList<>(15);
+ this.vulnAreasProtectedTwo = new ArrayList<>(10);
+ this.vulnAreasProtectedOne = new ArrayList<>(10);
+ this.optionAreas = new ArrayList<>(15);
+
+ this.lowerBound = Integer.MIN_VALUE;
+ this.upperBound = Integer.MIN_VALUE;
+ this.unplayableSquares = Integer.MIN_VALUE;
+
+ this.unavailableSquares = 0;
+ this.startAvailableSquares = 0;
+ }
+
+ public int numProtectiveAreas() {
+ return protectiveAreas.size();
+ }
+
+ public int numSafeAreas() {
+ return safeAreas.size();
+ }
+
+ public int numVulnAreasOne() {
+ return vulnAreasOne.size();
+ }
+
+ public int numVulnAreasTwo() {
+ return vulnAreasTwo.size();
+ }
+
+ public int numVulnAreasProtectedOne() {
+ return vulnAreasProtectedOne.size();
+ }
+
+ public int numVulnAreasProtectedTwo() {
+ return vulnAreasProtectedTwo.size();
+ }
+}
diff --git a/src/main/java/ai/BoardStorage.java b/src/main/java/ai/BoardStorage.java
new file mode 100644
index 0000000..14a7b1b
--- /dev/null
+++ b/src/main/java/ai/BoardStorage.java
@@ -0,0 +1,37 @@
+package ai;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+// simple wrapper for java hashmap structure
+public class BoardStorage {
+ private final HashMap scoreStorage = new HashMap<>();
+
+ public void put(char[][] board, StateInfo info) {
+ scoreStorage.put(Arrays.hashCode(flatten(board)), info);
+ }
+
+ // get the score for the given board, returns null if no score is found
+ public StateInfo get(char[][] board) {
+ return scoreStorage.get(Arrays.hashCode(flatten(board)));
+ }
+
+ // small storage class for the values which should be stored for each registered board
+ public static class StateInfo {
+ // the calculated score
+ public float score;
+ // either the score is a final one, only one for the alpha, or only one for the beta value
+ public char type;
+ // at which depth was the score determined?
+ public int depth;
+ }
+
+ // convert 2d-array to 1d
+ private char[] flatten(char[][] board) {
+ char[] output = new char[board.length * board[0].length];
+ for (int x = 0; x < board.length; x++) {
+ System.arraycopy(board[x], 0, output, x * board[0].length, board[0].length);
+ }
+ return output;
+ }
+}
diff --git a/src/main/java/ai/Coordinate.java b/src/main/java/ai/Coordinate.java
new file mode 100644
index 0000000..6c264ed
--- /dev/null
+++ b/src/main/java/ai/Coordinate.java
@@ -0,0 +1,43 @@
+package ai;
+
+import java.util.Objects;
+
+public class Coordinate {
+ private int x;
+ private int y;
+
+ public Coordinate(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public int getY() {
+ return y;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + x + "," + y + ")";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(x, y);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Coordinate)) {
+ return false;
+ }
+ Coordinate other = (Coordinate) obj;
+ return x == other.x && y == other.y;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ai/Game.java b/src/main/java/ai/Game.java
new file mode 100644
index 0000000..54240c6
--- /dev/null
+++ b/src/main/java/ai/Game.java
@@ -0,0 +1,119 @@
+package ai;
+
+import java.util.Arrays;
+
+public class Game {
+ private static final int BOARD_LENGTH = 13;
+ private final AI verticalAI;
+ private final AI horizontalAI;
+ private final boolean visual;
+ private Player winner;
+
+ public Game(AI verticalAI, AI horizontalAI) {
+ this.verticalAI = verticalAI;
+ this.horizontalAI = horizontalAI;
+ this.visual = false;
+ }
+
+ // a visual game prints the current state of the board to the console after each move
+ public Game(AI verticalAI, AI horizontalAI, boolean visual) {
+ this.verticalAI = verticalAI;
+ this.horizontalAI = horizontalAI;
+ this.visual = visual;
+ }
+
+ public void runGame() {
+ // start by initializing a new game, this way, runGame() could potentially be run more than once
+ char[][] board = generateEmptyBoard();
+ Coordinate move;
+
+ // starting player -> always the vertical player for our game version
+ Player currentPlayer = Player.V;
+
+ if (visual) GameVisualizer.printBoard(board);
+ while (true) {
+ // is there another valid move possible for the current player
+ if (!canPlay(board, currentPlayer)) {
+ if (visual) System.out.println("" + currentPlayer + " can't place another piece, he lost!");
+ break;
+ }
+ if (visual) System.out.println("" + currentPlayer + "'s move:");
+ // ask the AI for a new move (coordinates for a new piece)
+ if (currentPlayer == Player.V) {
+ move = verticalAI.playMove(board, currentPlayer);
+ } else {
+ move = horizontalAI.playMove(board, currentPlayer);
+ }
+
+ if (visual) System.out.println("Places a piece on " + move);
+
+ // is the returned move actually valid
+ if (checkInvalidMoveSimple(board, move, currentPlayer)) {
+ if (visual) System.out.println("!!!! INVALID MOVE BY " + currentPlayer + " !!!!");
+ break;
+ }
+
+ makeMove(board, move, currentPlayer);
+ if (visual) GameVisualizer.printBoard(board);
+
+ // change the play for the next round
+ currentPlayer = currentPlayer.getOtherPlayer();
+ }
+ winner = currentPlayer.getOtherPlayer();
+ }
+
+ // generate an empty board filled with 'E'
+ private char[][] generateEmptyBoard() {
+ char[][] board = new char[BOARD_LENGTH][BOARD_LENGTH];
+ for (char[] column : board) {
+ Arrays.fill(column, 'E');
+ }
+ return board;
+ }
+
+ // fulfill a returned move if it is valid
+ private void makeMove(char[][] board, Coordinate move, Player p) {
+ board[move.getX()][move.getY()] = (p == Player.H ? 'H' : 'V');
+ board[move.getX() + (p == Player.H ? 1 : 0)][move.getY() + (p == Player.H ? 0 : 1)] =
+ (p == Player.H ? 'H' : 'V');
+ }
+
+ // test if a returned move is valid
+ private boolean checkInvalidMoveSimple(char[][] board, Coordinate move, Player p) {
+ return move.getX() < 0
+ || move.getX() >= BOARD_LENGTH
+ || move.getY() < 0
+ || move.getY() >= BOARD_LENGTH
+ || (p == Player.H && move.getX() + 1 >= BOARD_LENGTH)
+ || (p == Player.V && move.getY() + 1 >= BOARD_LENGTH)
+ || board[move.getX()][move.getY()] != 'E'
+ || (p == Player.H && board[move.getX() + 1][move.getY()] != 'E')
+ || (p == Player.V && board[move.getX()][move.getY() + 1] != 'E');
+ }
+
+ // is there any move left for the current player? If not, the other player has one!
+ private boolean canPlay(char[][] board, Player p) {
+ if (p == Player.H) {
+ for (int y = 0; y < board[0].length; y++) {
+ for (int x = 0; x < board.length - 1; x++) {
+ if (board[x][y] == 'E' && board[x + 1][y] == 'E') {
+ return true;
+ }
+ }
+ }
+ } else {
+ for (char[] column : board) {
+ for (int y = 0; y < column.length - 1; y++) {
+ if (column[y] == 'E' && column[y + 1] == 'E') {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public AI getWinner() {
+ return winner == Player.V ? verticalAI : horizontalAI;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ai/GameVisualizer.java b/src/main/java/ai/GameVisualizer.java
new file mode 100644
index 0000000..61565ee
--- /dev/null
+++ b/src/main/java/ai/GameVisualizer.java
@@ -0,0 +1,42 @@
+package ai;
+
+public class GameVisualizer {
+
+ public static void printBoard(char[][] board) {
+ System.out.print(" ");
+ for (int x = 0; x < board.length; x++) {
+ System.out.print(" " + Integer.toHexString(x));
+ }
+ System.out.println();
+
+ System.out.print(" ┏");
+ for (int x = 0; x < board.length - 1; x++) {
+ System.out.print("━┳");
+ }
+ System.out.println("━┓");
+
+ for (int y = 0; y < board[0].length; y++) {
+ System.out.print(Integer.toHexString(y) + "┃");
+ for (char[] chars : board) {
+ if (chars[y] != 'E') {
+ System.out.print(chars[y] + "┃");
+ } else {
+ System.out.print(" ┃");
+ }
+ }
+ System.out.println();
+ if (y != board[0].length - 1) {
+ System.out.print(" ┣");
+ for (int x = 0; x < board.length - 1; x++) {
+ System.out.print("━╋");
+ }
+ System.out.println("━┫");
+ }
+ }
+ System.out.print(" ┗");
+ for (int x = 0; x < board.length - 1; x++) {
+ System.out.print("━┻");
+ }
+ System.out.println("━┛");
+ }
+}
diff --git a/src/main/java/ai/HardMinMax.java b/src/main/java/ai/HardMinMax.java
new file mode 100644
index 0000000..3e249a2
--- /dev/null
+++ b/src/main/java/ai/HardMinMax.java
@@ -0,0 +1,369 @@
+package ai;
+
+import java.util.Arrays;
+
+public class HardMinMax extends AI {
+ public float[] factors = null;
+
+ // store the already calculated scores for each board configuration for the ultimate performance boost
+ static BoardStorage scoreMapHorizontalStarter = new BoardStorage();
+ static BoardStorage scoreMapVerticalStarter = new BoardStorage();
+
+ @Override
+ public synchronized Coordinate playMove(char[][] board, Player player) {
+ // play an opening -> for better performance (an empty board is pretty expensive to calculate)
+ Coordinate opening = BoardAnalyser.trySimpleOpening(board, player);
+ if (opening != null) {
+ return opening;
+ }
+
+ // for medium and hard mode, we use our minimax-algorithm
+ return findBestMove(anonymizeBoard(board), player);
+ }
+
+ /*
+ Because of the nature of our board layout, we only take into consideration whether a square is occupied (X) or
+ empty (E). This greatly increases the performance!
+ */
+ private char[][] anonymizeBoard(char[][] board) {
+ char[][] boardCopy = new char[board.length][board[0].length];
+ for (int x = 0; x < board.length; x++) {
+ for (int y = 0; y < board[0].length; y++) {
+ boardCopy[x][y] = (board[x][y] == 'V' || board[x][y] == 'H' ? 'X' : 'E');
+ }
+ }
+ return boardCopy;
+ }
+
+ // tests how far the game has already commenced and adjusts the depth limit accordingly
+ private int depthForBoardState(char[][] board) {
+ int blocked = 0;
+ for (char[] row : board) {
+ for (char c : row) {
+ if (c != 'E') blocked++;
+ }
+ }
+
+ // for the dynamic depth adjustment, I used a sigmoid-like curve which was fitted by probing values in geogebra
+ return (int) (20 / (1 + Math.pow(1.035, (-blocked + 95)))) + 1;
+ }
+
+ /*
+ Recursively tests all possible moves (really slow for early board stages)
+
+ The approach is based on the concept of a minimax tree. The root configuration is the current board. Each branch
+ is a possible move for either us or the opponent. As soon as one of the players looses or the depth limit is
+ reached, the current board configuration is evaluated. Each player (stage in the tree) tries to play the best
+ strategy. This means is the "score" is how well we (Player A) are doing, than Player B tries to play the move
+ which has the lowest score associated with it. Player A tries to play the move with the maximum score and thus
+ the resulting tree is composed of alternating minimum and maximum phases.
+ */
+ private Coordinate findBestMove(char[][] board, Player player) {
+ Coordinate[] possibleMoves = generateNextPossibleMoves(board, player, null, false);
+ Coordinate currentBestMove = null;
+
+ // this "currentBestScore" should be updated as soon as a score higher than the lowest possible score is found
+ float currentBestScore = Float.NEGATIVE_INFINITY;
+
+ int maxDepth = depthForBoardState(board);
+
+ // just try these possibleMoves in their natural order
+ for (Coordinate move : possibleMoves) {
+ applyMove(board, move, player);
+ float nextBestScore = minimaxAlphaBeta(
+ board,
+ player.getOtherPlayer(),
+ player,
+ maxDepth,
+ currentBestScore,
+ Float.POSITIVE_INFINITY
+ );
+ undoMove(board, move, player);
+ // because the current player is always the maximizing player and we can't prune, we have to go through
+ // each entry and always update the current maximum score and the associated move
+ if (nextBestScore > currentBestScore) {
+ currentBestScore = nextBestScore;
+ currentBestMove = move;
+ }
+ }
+ if (currentBestMove == null) {
+ // if all fails and all scores are somehow equal to Integer.MIN_VALUE, we take the first item from the
+ // generated possible moves and return it.
+ return generateNextPossibleMoves(board, player, null, true)[0];
+ }
+ return currentBestMove;
+ }
+
+ /*
+ The key in efficient solution finding is reducing the unnecessary calculations in our tree. The "scoreSituation"
+ method always tests if winning is still possible and otherwise returns a low score.
+
+ Alpha-Beta-Pruning:
+ -> alpha: currently the highest score (reference for the maximising player)
+ -> beta: currently the lowest score (reference for the minimising player)
+
+ Case 1:
+ -------
+ We are currently the maximizing player. The previous player (minimizing) gave us his updated "beta"-score.
+ Only if our score is lower than the given beta score, our branch is of interest, otherwise there exists
+ another branch with a lower score and because the previous player was minimizing, he would then take the other
+ branch. This means that if our current score is higher than the beta score, we can break the loop and return.
+
+ Case 2:
+ -------
+ We are the minimizing player. The previous player was maximizing and gave the current maximum score in the
+ alpha parameter. If our "best score" (the minimum) drops below the alpha score, we can break the search and
+ return, because even though this situation would be better for us, the previous player wouldn't take our
+ branch into consideration.
+
+ This special implementation of the minimax algorithm which negates the next call with changed alpha and beta
+ values is described in the doctoral thesis of Prof. dr. H.J. van den Herik: "Memory versus Search in Games".
+ It also involves storing the values which were calculated for even higher performance. Some more ideas came
+ from the already often cited master thesis of Nathan Bullock about the game domineering. Both papers didn't
+ contain actual code or the code was not reviewed by me.
+ */
+ private float minimaxAlphaBeta(char[][] board, Player currentPlayer, Player startingPlayer, int depth,
+ float alpha, float beta) {
+ float oldAlpha = alpha;
+ float oldBeta = beta;
+
+ // first, try to load the score from the scoreMap
+ BoardStorage.StateInfo saveState = loadScore(board, startingPlayer);
+
+ if (saveState != null && saveState.depth >= depth) {
+ if (saveState.type == '-' && saveState.score > alpha) {
+ alpha = saveState.score;
+ } else if (saveState.type == '+' && saveState.score < beta) {
+ beta = saveState.score;
+ }
+
+ if (saveState.type == '=' || alpha >= beta) {
+ return saveState.score;
+ }
+ }
+
+ // The new BoardAnalyser object is used by both the scoring function and the possible moves generator. For
+ // performance improvement, it is only created once.
+ BoardAnalyser bA = new BoardAnalyser(board, false);
+ float score = scoreSituation(
+ depth,
+ (startingPlayer == Player.V) ? bA.vertical : bA.horizontal,
+ (startingPlayer == Player.V) ? bA.horizontal : bA.vertical
+ );
+
+ if (score != Float.NEGATIVE_INFINITY) {
+ return score;
+ }
+
+ Coordinate[] possibleMoves = generateNextPossibleMoves(board, currentPlayer, bA, false);
+ float nextBestScore;
+
+ // if true -> current player tries to maximize the score
+ boolean max = (currentPlayer == startingPlayer);
+
+ // never go down / up with the score, but set the given best to be the lowest score for the "current best"
+ float currentBestScore = max ? alpha : beta;
+
+ // goes through all possible moves which are basically just placing tiles in all calculated board cover
+ // regions. These regions are generated using a BoardAnalyser object.
+ for (Coordinate move : possibleMoves) {
+ applyMove(board, move, currentPlayer);
+ nextBestScore = minimaxAlphaBeta(
+ board,
+ currentPlayer.getOtherPlayer(),
+ startingPlayer,
+ depth - 1,
+ max ? currentBestScore : alpha,
+ max ? beta : currentBestScore
+ );
+ undoMove(board, move, currentPlayer);
+
+ // either we try to maximize the score, then update if the new score is higher than the current best
+ // or we try to minimize the score and therefore only update if the new score is lower than the current best
+ if ((max && nextBestScore > currentBestScore) || (!max && nextBestScore < currentBestScore)) {
+ currentBestScore = nextBestScore;
+ }
+
+ // this is the important alpha-beta-pruning improvement over the classic minimax-algorithm. The details
+ // of the implementations are written in the comment above this method.
+ if ((max && currentBestScore >= beta) || (!max && currentBestScore <= alpha)) {
+ break;
+ }
+ }
+
+ BoardStorage.StateInfo boardState = new BoardStorage.StateInfo();
+ boardState.score = currentBestScore;
+ boardState.depth = depth;
+
+ if (currentBestScore <= oldAlpha) {
+ boardState.type = '+';
+ } else if (currentBestScore >= oldBeta) {
+ boardState.type = '-';
+ } else {
+ boardState.type = '=';
+ }
+
+ saveScore(board, boardState, startingPlayer);
+ return currentBestScore;
+ }
+
+ private BoardStorage.StateInfo loadScore(char[][] board, Player starter) {
+ return (starter == Player.V ? scoreMapVerticalStarter.get(board) : scoreMapHorizontalStarter.get(board));
+ }
+
+ private void saveScore(char[][] board, BoardStorage.StateInfo state, Player starter) {
+ if (starter == Player.V) {
+ scoreMapVerticalStarter.put(board, state);
+ } else {
+ scoreMapHorizontalStarter.put(board, state);
+ }
+ }
+
+ /*
+ This "exit-function" calculates the current score if a tree leaf is reached. The current method call
+ represents a tree leaf if either the maximum recursion depth is reached or either one of the players can't
+ play another tile or the current player already won the game because he could always place more tiles than
+ the other player.
+
+ Most switches contained in this function are based on the calculated board cover (BoardAnalyser) and the
+ findings of Nathan Bullock's master thesis "Domineering: Solving Large Combinatorial Search Spaces".
+
+ The current approach is to assign weights to each parameter we have available and figure out the optimal values.
+ */
+ private float scoreSituation(int currentDepth, BoardLayout starter, BoardLayout opponent) {
+ if (currentDepth <= 0
+ || starter.lowerBound <= 0 // we already lost
+ || opponent.lowerBound <= 0 // the opponent already lost
+ || starter.lowerBound > opponent.upperBound // win for the starting player (NB)
+ || opponent.lowerBound >= starter.upperBound // win for the opponent (NB)
+ ) {
+ /*
+ Available values and proposed weights / factors
+
+ (these are just first guesses and still have to be refined)
+ Basically we "punish" the ai / give worse scores for good situations of the opponent than for good
+ situations for the current player.
+
+ The concrete values were determined using an evolutional approach where two instances of
+ the HardMinMax each with different factors played against each other over multiple round, with the winner
+ proceeding.
+ */
+
+ if (factors == null) {
+ factors = new float[]{
+ 6.141892f, // lower bound
+ 3.323705f,
+ 1.5304062f, // upper bound
+ 2.5675583f,
+ 10.425653f, // safe areas
+ -15.922241f,
+ 2.0729046f, // vuln areas
+ -3.497818f,
+ -3.3107972f, // protective areas
+ -11.012934f,
+ -0.85778457f, // unavailable areas
+ 5.7875576f,
+ 0.80485183f, // unplayable areas
+ 1.9488539f
+ };
+ }
+
+ return (starter.lowerBound * factors[0]
+ + opponent.lowerBound * factors[1]
+ + starter.upperBound * factors[2]
+ + opponent.upperBound * factors[3]
+ + starter.numSafeAreas() * factors[4]
+ + opponent.numSafeAreas() * factors[5]
+ + (starter.numVulnAreasOne() + starter.numVulnAreasTwo()) * factors[6]
+ + (opponent.numVulnAreasOne() + opponent.numVulnAreasTwo()) * factors[7]
+ + starter.numProtectiveAreas() * factors[8]
+ + opponent.numProtectiveAreas() * factors[9]
+ + starter.unavailableSquares * factors[10]
+ + opponent.unavailableSquares * factors[11]
+ + starter.unplayableSquares * factors[12]
+ + opponent.unplayableSquares * factors[13]
+ );
+ }
+ // if the recursion should not be stopped, return a score that would never be calculated
+ // I don't know if NEGATIVE_INFINITY is better than MIN_VALUE
+ return Float.NEGATIVE_INFINITY;
+ }
+
+ // returns the entered board configuration with the given move applied
+ private void applyMove(char[][] board, Coordinate move, Player player) {
+ // set the first square occupied
+ board[move.getX()][move.getY()] = 'X';
+
+ // based on the player, set the second square occupied
+ if (player == Player.V) {
+ board[move.getX()][move.getY() + 1] = 'X';
+ } else {
+ board[move.getX() + 1][move.getY()] = 'X';
+ }
+ }
+
+ private void undoMove(char[][] board, Coordinate move, Player player) {
+ // set the first square unoccupied
+ board[move.getX()][move.getY()] = 'E';
+
+ // based on the player, set the second square unoccupied
+ if (player == Player.V) {
+ board[move.getX()][move.getY() + 1] = 'E';
+ } else {
+ board[move.getX() + 1][move.getY()] = 'E';
+ }
+ }
+
+ // runs an entered board analyzer or creates a new one. Returns the concatenated board cover areas.
+ private Coordinate[] generateNextPossibleMoves(char[][] board, Player player, BoardAnalyser bA, boolean include) {
+ if (bA == null) {
+ bA = new BoardAnalyser(board, true);
+ }
+ // select the required BoardLayout object
+ BoardLayout bL = (player == Player.V ? bA.vertical : bA.horizontal);
+
+ // generate a new Array with approximately the maximum size required
+ Coordinate[] outputMoves = new Coordinate[2 * bL.numProtectiveAreas()
+ + bL.numVulnAreasOne()
+ + bL.numVulnAreasTwo()
+ + bL.numSafeAreas() * 3];
+ /*
+ The areas are concatenated to be filled in the following order:
+
+ 1. Protect spots of protective areas -> placing a tile here converts the protective area to a safe area
+ 2. Vulnerable areas type II
+ 3. Vulnerable areas type I
+ 4. The part of protective areas which is not the protect spot
+ 5. Safe areas: first the option area possibilities and then the normal safe areas
+ */
+ int index = 0;
+ for (ProtectiveArea area : bL.protectiveAreas) {
+ outputMoves[index++] = area.getProtectSpot().getCornerUL();
+ }
+ for (VulnArea area : bL.vulnAreasTwo) {
+ outputMoves[index++] = area.getCornerUL();
+ }
+ for (VulnArea area : bL.vulnAreasOne) {
+ outputMoves[index++] = area.getCornerUL();
+ }
+ for (ProtectiveArea area : bL.protectiveAreas) {
+ VulnArea[] split = area.splitIntoVulnTwo(player);
+ // always only add the part of the protective are which is NOT the protect spot
+ if (include) {
+ outputMoves[index++] = split[0].getCornerUL().equals(area.getProtectSpot().getCornerUL())
+ ? split[1].getCornerUL()
+ : split[0].getCornerUL();
+ }
+ }
+ for (SafeArea area : bL.safeAreas) {
+ if (area.getOptionSafeLower() != null) {
+ outputMoves[index++] = area.getOptionSafeLower().getCornerUL();
+ }
+ if (area.getOptionSafeUpper() != null) {
+ outputMoves[index++] = area.getOptionSafeUpper().getCornerUL();
+ }
+ if (include) outputMoves[index++] = area.getCornerUL();
+ }
+ return Arrays.copyOf(outputMoves, index);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ai/OptionArea.java b/src/main/java/ai/OptionArea.java
new file mode 100644
index 0000000..8e0e16d
--- /dev/null
+++ b/src/main/java/ai/OptionArea.java
@@ -0,0 +1,65 @@
+package ai;
+
+public class OptionArea extends Area {
+ private final int weight;
+
+ public OptionArea(int startX, int startY, int endX, int endY, int type) {
+ super(startX, startY, endX, endY);
+ this.weight = type;
+ }
+
+ /*
+ An option area is a spot below or above a safe area which can be occupied by vertical. It could potentially decrease
+ the opponent's squares (squares where he could place another tile) by 0-3. We won't count the ones which only reduce
+ the square count by 0. The other's could be in one of 2 possible configurations:
+
+ S
+ S
+ # E O E #
+ +1 1 +1
+ ----------------
+ S
+ S
+ # # O E #
+ -> 2
+ */
+
+ public static OptionArea getOptionArea(char[][] board, Player player, int x, int y) {
+ // minimum count for optionArea
+ int count = 1;
+
+ if (player == Player.V) {
+ // a 0-OptionArea
+ if (board[x][y] != 'E'
+ || ((x - 1 < 0 || board[x - 1][y] != 'E') && (x + 1 >= board.length || board[x + 1][y] != 'E'))) {
+ return null;
+ }
+ // minimum count for optionArea
+ if ((x - 2 < 0 || board[x - 2][y] == 'X') && (x > 0 && board[x - 1][y] == 'E')) {
+ count++;
+ }
+ if ((x + 2 >= board.length || board[x + 2][y] == 'X')
+ && (x < board.length - 1 && board[x + 1][y] == 'E')) {
+ count++;
+ }
+ return new OptionArea(x, y, x, y, count);
+ }
+ // a 0-OptionArea
+ if (board[x][y] != 'E'
+ || ((y - 1 < 0 || board[x][y - 1] != 'E') && (y + 1 >= board[0].length || board[x][y + 1] != 'E'))) {
+ return null;
+ }
+ if ((y - 2 < 0 || board[x][y - 2] == 'X') && (y > 0 && board[x][y - 1] == 'E')) {
+ count++;
+ }
+ if ((y + 2 >= board[0].length || board[x][y + 2] == 'X')
+ && (y < board[0].length - 1 && board[x][y + 1] == 'E')) {
+ count++;
+ }
+ return new OptionArea(x, y, x, y, count);
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+}
diff --git a/src/main/java/ai/Player.java b/src/main/java/ai/Player.java
new file mode 100644
index 0000000..0624269
--- /dev/null
+++ b/src/main/java/ai/Player.java
@@ -0,0 +1,9 @@
+package ai;
+
+public enum Player {
+ H, V;
+
+ public Player getOtherPlayer() {
+ return this == H ? V : H;
+ }
+}
diff --git a/src/main/java/ai/ProtectiveArea.java b/src/main/java/ai/ProtectiveArea.java
new file mode 100644
index 0000000..1430279
--- /dev/null
+++ b/src/main/java/ai/ProtectiveArea.java
@@ -0,0 +1,133 @@
+package ai;
+
+import java.util.ArrayList;
+
+public class ProtectiveArea extends Area {
+
+ private final VulnArea protectSpot;
+
+ public ProtectiveArea(int startX, int startY, int endX, int endY, char[][] board, Player p) {
+ super(startX, startY, endX, endY);
+
+ // switch for the player to position the protect spot correctly (horizontally or vertically)
+ if (p == Player.V) {
+ if (endX + 1 >= board.length || (board[endX + 1][startY] != 'E' && board[endX + 1][endY] != 'E')) {
+ protectSpot = new VulnArea(startX, startY, startX, startY + 1);
+ } else {
+ protectSpot = new VulnArea(endX, startY, endX, startY + 1);
+ }
+ } else if (endY + 1 >= board[0].length || (board[startX][endY + 1] != 'E' && board[endX][endY + 1] != 'E')) {
+ protectSpot = new VulnArea(startX, startY, startX + 1, startY);
+ } else {
+ protectSpot = new VulnArea(startX, endY, endX, endY);
+ }
+ }
+
+ /*
+ The input coordinates describe a protective area if both sides (left and right for Player.V and up and down for Player.H)
+ are contained with other tiles or the borders. These conditions are tested in the overlong expressions below.
+ */
+ public static boolean isProtectiveArea(char[][] board, Player p, int x, int y,
+ ArrayList addedAreas) {
+ if (p == Player.V) {
+ return (isNotOccupied(board, p, x, y)
+ && noAdjacentProtectiveArea(p, x, y, addedAreas)
+
+ && (((x - 1 < 0)
+ || ((board[x - 1][y] == 'X')
+ && (board[x - 1][y + 1] == 'X')))
+
+ || ((x + 2 >= board.length)
+ || ((board[x + 2][y] == 'X')
+ && (board[x + 2][y + 1] == 'X')))));
+ } else {
+ return (isNotOccupied(board, p, x, y)
+ && noAdjacentProtectiveArea(p, x, y, addedAreas)
+
+ && (((y - 1 < 0)
+ || ((board[x][y - 1] == 'X')
+ && (board[x + 1][y - 1] == 'X')))
+
+ || ((y + 2 >= board[0].length)
+ || ((board[x][y + 2] == 'X')
+ && (board[x + 1][y + 2] == 'X')))));
+ }
+ }
+
+ private static boolean noAdjacentProtectiveArea(Player p, int x, int y, ArrayList addedAreas) {
+ // an area is considered adjacent if the other player can occupy both areas by placing one piece on the board
+ if (p == Player.V) {
+ for (ProtectiveArea protectiveArea : addedAreas) {
+ if (protectiveArea.getCornerUL().getY() >= y - 1 && protectiveArea.getCornerLR().getY() <= y + 2) {
+ if (x - 1 == protectiveArea.getCornerLR().getX() || x + 2 == protectiveArea.getCornerUL().getX()) {
+ return false;
+ }
+ }
+ }
+ } else {
+ for (ProtectiveArea protectiveArea : addedAreas) {
+ if (protectiveArea.getCornerUL().getX() >= x - 1 && protectiveArea.getCornerLR().getX() <= x + 2) {
+ if (y - 1 == protectiveArea.getCornerLR().getY() || y + 2 == protectiveArea.getCornerUL().getY()) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /*
+ Is the given x/y - coordinate already occupied:
+
+ - part of the area is outside of the board
+ - the area is not empty
+
+ */
+ public static boolean isNotOccupied(char[][] board, Player player, int x, int y) {
+ return board[x][y] == 'E'
+ && (!(x + 1 >= board.length) && board[x + 1][y] == 'E')
+ && (!(y + 1 >= board[0].length) && board[x][y + 1] == 'E')
+ && board[x + 1][y + 1] == 'E';
+// return (board[x][y] == 'E' || board[x][y] == 'P')
+// && (!(x + 1 >= board.length) && (board[x + 1][y] == 'E' || board[x + 1][y] == 'P'))
+// && (!(y + 1 >= board[0].length) && (board[x][y + 1] == 'E' || board[x][y + 1] == 'P'))
+// && (board[x + 1][y + 1] == 'E' || board[x + 1][y + 1] == 'P');
+ }
+
+ // Split the protective area into two vulnerable areas
+ public VulnArea[] splitIntoVulnTwo(Player player) {
+ if (player == Player.H) {
+ if (protectSpot.getCornerLR().getY() == cornerLR.getY()) {
+ return new VulnArea[]{
+ protectSpot, new VulnArea(
+ cornerUL.getX(), cornerUL.getY(), cornerLR.getX(), cornerUL.getY()
+ )
+ };
+ } else {
+ return new VulnArea[]{
+ protectSpot, new VulnArea(
+ cornerUL.getX(), cornerLR.getY(), cornerLR.getX(), cornerLR.getY()
+ )
+ };
+ }
+ } else if (protectSpot.getCornerLR().getX() == cornerLR.getX()) {
+ return new VulnArea[]{
+ protectSpot, new VulnArea(
+ cornerUL.getX(), cornerUL.getY(), cornerUL.getX(), cornerLR.getY()
+ )
+ };
+ } else {
+ return new VulnArea[]{
+ protectSpot, new VulnArea(
+ cornerLR.getX(), cornerUL.getY(), cornerLR.getX(), cornerLR.getY()
+ )
+ };
+ }
+ }
+
+ // the protect spot is the half of the protective area which should be occupied first in order to convert the
+ // protective area to a safe area!
+ public VulnArea getProtectSpot() {
+ return protectSpot;
+ }
+}
diff --git a/src/main/java/ai/SafeArea.java b/src/main/java/ai/SafeArea.java
new file mode 100644
index 0000000..8967817
--- /dev/null
+++ b/src/main/java/ai/SafeArea.java
@@ -0,0 +1,56 @@
+package ai;
+
+public class SafeArea extends Area {
+
+ private SafeArea optionSafeUpper;
+ private SafeArea optionSafeLower;
+
+ public SafeArea(int xOne, int yOne, int xTwo, int yTwo) {
+ super(xOne, yOne, xTwo, yTwo);
+ }
+
+ // same as for the other areas, tests if the given coordinates qualify for being a safe area
+ public static boolean isSafeArea(char[][] board, Player p, int x, int y) {
+ if (p == Player.V) {
+ return ((board[x][y] == 'E' && board[x][y + 1] == 'E')
+
+ && ((x - 1 < 0)
+ || ((board[x - 1][y] == 'X')
+ && (board[x - 1][y + 1] == 'X')))
+
+ && ((x + 1 >= board.length)
+ || ((board[x + 1][y] == 'X')
+ && (board[x + 1][y + 1] == 'X'))));
+ } else {
+ return ((board[x][y] == 'E' && board[x + 1][y] == 'E')
+
+ && ((y - 1 < 0)
+ || ((board[x][y - 1] == 'X')
+ && (board[x + 1][y - 1] == 'X')))
+
+ && ((y + 1 >= board[0].length)
+ || ((board[x][y + 1] == 'X')
+ && (board[x + 1][y + 1] == 'X'))));
+ }
+ }
+
+ public void addOptionAreaHigher(OptionArea oaHigher) {
+ optionSafeUpper = new SafeArea(oaHigher.getCornerUL().getX(), oaHigher.getCornerUL().getY(), cornerUL.getX(),
+ cornerUL.getY()
+ );
+ }
+
+ public void addOptionAreaLower(OptionArea oaLower) {
+ optionSafeLower = new SafeArea(cornerLR.getX(), cornerLR.getY(), oaLower.getCornerUL().getX(),
+ oaLower.getCornerUL().getY()
+ );
+ }
+
+ public SafeArea getOptionSafeUpper() {
+ return optionSafeUpper;
+ }
+
+ public SafeArea getOptionSafeLower() {
+ return optionSafeLower;
+ }
+}
diff --git a/src/main/java/ai/Tester.java b/src/main/java/ai/Tester.java
new file mode 100644
index 0000000..03f6333
--- /dev/null
+++ b/src/main/java/ai/Tester.java
@@ -0,0 +1,12 @@
+package ai;
+
+// small test class for running games
+public class Tester {
+ public static void main(String[] args) {
+ AI vertical = new HardMinMax();
+ AI horizontal = new HardMinMax();
+ Game g = new Game(vertical, horizontal, true);
+ g.runGame();
+ System.out.println(g.getWinner() == vertical ? "Vertical wins!" : "Horizontal wins!");
+ }
+}
diff --git a/src/main/java/ai/VulnArea.java b/src/main/java/ai/VulnArea.java
new file mode 100644
index 0000000..6a7cc6d
--- /dev/null
+++ b/src/main/java/ai/VulnArea.java
@@ -0,0 +1,68 @@
+package ai;
+
+/*
+ Vulnerable areas are all spots which are not protective areas or safe areas
+ The weight is determined by the neighbourhood. If another special area is adjacent to the vulnerable area, the other
+ player could place a tile in a way which would obstruct both areas. We give these vulnerable areas a weight of 2. All
+ others get a weight of one.
+*/
+public class VulnArea extends Area {
+
+ public VulnArea(int xOne, int yOne, int xTwo, int yTwo) {
+ super(xOne, yOne, xTwo, yTwo);
+ }
+
+ public static int isVulnArea(char[][] board, int x, int y, Player player) {
+ // Horizontal and vertical modifier
+ int vM = (player == Player.V ? 1 : 0);
+ int hM = (player == Player.H ? 1 : 0);
+
+ int type = 0;
+
+ if (board[x][y] == 'E' && board[x + hM][y + vM] == 'E') {
+ // if other already existing areas are adjacent to the vulnerable area, it is considered a type II
+ if (player == Player.V && (
+ (x - 1 >= 0 && (board[x - 1][y] == 'P' || board[x - 1][y] == 'S' || board[x - 1][y] == 'D'))
+ || (x + 1 < board.length && y < board.length
+ && (board[x + 1][y] == 'P' || board[x + 1][y] == 'S' || board[x + 1][y] == 'D'))
+ || (x - 1 >= 0 && y + 1 <= board.length
+ && (board[x - 1][y + 1] == 'P' || board[x - 1][y + 1] == 'S' || board[x - 1][y + 1] == 'D'))
+ || (x + 1 < board.length && y + 1 < board.length
+ && (board[x + 1][y + 1] == 'P' || board[x + 1][y + 1] == 'S'
+ || board[x + 1][y + 1] == 'D')))) {
+ type = 2;
+ } else if (player == Player.H && ((y > 0 && (board[x][y - 1] == 'P' || board[x][y - 1] == 'S'))
+ || (y + 1 < board[0].length && (board[x][y + 1] == 'P' || board[x][y + 1] == 'S'
+ || board[x][y + 1] == 'D'))
+ || (x + 1 < board.length && y > 0
+ && (board[x + 1][y - 1] == 'P' || board[x + 1][y - 1] == 'S' || board[x + 1][y - 1] == 'D'))
+ || (x + 1 < board.length && y + 1 < board[0].length
+ && (board[x + 1][y + 1] == 'P' || board[x + 1][y + 1] == 'S' || board[x + 1][y + 1] == 'D')))) {
+ type = 2;
+ } else {
+ // Vulnerable Area Type I if no other area is adjacent to it
+ type = 1;
+ }
+ }
+
+ if (type > 0) {
+ if (player == Player.V && (((x <= 0 || board[x - 1][y] == 'X')
+ && (x >= board.length - 1 || board[x + 1][y] == 'X'))
+ || ((x <= 0 || board[x - 1][y + 1] == 'X')
+ && (x >= board.length - 1 || board[x + 1][y + 1] == 'X')))) {
+ type += 2;
+ }
+ if (player == Player.H && (((y <= 0 || board[x][y - 1] == 'X')
+ && (y >= board[0].length - 1 || board[x][y + 1] == 'X'))
+ || ((y <= 0 || board[x + 1][y - 1] == 'X')
+ && (y >= board[0].length - 1 || board[x + 1][y + 1] == 'X')))) {
+ type += 2;
+ }
+ }
+ return type;
+ }
+
+ public static VulnArea getVulnArea(int x, int y, Player player) {
+ return new VulnArea(x, y, x + (player == Player.H ? 1 : 0), y + (player == Player.V ? 1 : 0));
+ }
+}
diff --git a/src/main/java/frontend/BoardVisualizer.java b/src/main/java/frontend/BoardVisualizer.java
new file mode 100644
index 0000000..89a3e79
--- /dev/null
+++ b/src/main/java/frontend/BoardVisualizer.java
@@ -0,0 +1,111 @@
+package frontend;
+
+import ai.Coordinate;
+import ai.HardMinMax;
+import ai.Player;
+import processing.core.PApplet;
+
+import java.util.Arrays;
+
+public class BoardVisualizer extends PApplet {
+
+ char[][] board;
+ int rectLength;
+
+ HardMinMax ai;
+ HardMinMax ai2;
+
+ Player curr;
+ Coordinate move;
+
+ // The argument passed to main must match the class name
+ public static void main(String[] args) {
+ PApplet.main("frontend.BoardVisualizer");
+ }
+
+ // method for setting the size of the window
+ public void settings() {
+ size(800, 800);
+ }
+
+ // identical use to setup in Processing IDE except for size()
+ public void setup() {
+ background(120);
+ board = new char[13][13];
+ for (char[] b : board) {
+ Arrays.fill(b, 'E');
+ }
+
+ rectLength = width / 13;
+ frameRate(60);
+ ai = new HardMinMax();
+ ai2 = new HardMinMax();
+
+ strokeWeight(2);
+ curr = Player.V;
+ }
+
+ // identical use to draw in Processing IDE
+ public void draw() {
+ if (curr == Player.V) {
+ move = ai.playMove(board, Player.V);
+
+ // is the returned move actually valid
+ if (checkInvalidMoveSimple(board, move, curr)) {
+ noLoop();
+ }
+
+ makeMove(board, move, curr);
+
+ drawBoard();
+
+ curr = curr.getOtherPlayer();
+ noLoop();
+ }
+ }
+
+ @Override
+ public void mouseClicked() {
+ move = new Coordinate(mouseX / (width / 13), mouseY / (width / 13));
+ if (!checkInvalidMoveSimple(board, move, curr)) {
+ makeMove(board, move, curr);
+ curr = curr.getOtherPlayer();
+ loop();
+ }
+ drawBoard();
+ }
+
+ private void drawBoard() {
+ for (int x = 0; x < board.length; x++) {
+ for (int y = 0; y < board[0].length; y++) {
+ if (board[x][y] == 'E') {
+ fill(255);
+ rect(x * rectLength, y * rectLength, rectLength, rectLength);
+ } else if (board[x][y] == 'V') {
+ fill(200, 0, 0);
+ rect(x * rectLength, y * rectLength, rectLength, rectLength);
+ } else if (board[x][y] == 'H') {
+ fill(0, 0, 200);
+ rect(x * rectLength, y * rectLength, rectLength, rectLength);
+ }
+ }
+ }
+ }
+
+ private void makeMove(char[][] board, Coordinate move, Player p) {
+ board[move.getX()][move.getY()] = (p == Player.H ? 'H' : 'V');
+ board[move.getX() + (p == Player.H ? 1 : 0)][move.getY() + (p == Player.H ? 0 : 1)] =
+ (p == Player.H ? 'H' : 'V');
+ }
+
+ // test if a returned move is valid
+ private boolean checkInvalidMoveSimple(char[][] board, Coordinate move, Player p) {
+ return move.getX() < 0
+ || move.getX() > 13
+ || move.getY() < 0
+ || move.getY() > 13
+ || board[move.getX()][move.getY()] != 'E'
+ || (p == Player.H && board[move.getX() + 1][move.getY()] != 'E')
+ || (p == Player.V && board[move.getX()][move.getY() + 1] != 'E');
+ }
+}
\ No newline at end of file