diff --git a/src/main/java/com/github/sandorw/mocabogaso/games/hex/FirstLineHexHeuristic.java b/src/main/java/com/github/sandorw/mocabogaso/games/hex/FirstLineHeuristic.java similarity index 90% rename from src/main/java/com/github/sandorw/mocabogaso/games/hex/FirstLineHexHeuristic.java rename to src/main/java/com/github/sandorw/mocabogaso/games/hex/FirstLineHeuristic.java index 1290fee..d38714d 100644 --- a/src/main/java/com/github/sandorw/mocabogaso/games/hex/FirstLineHexHeuristic.java +++ b/src/main/java/com/github/sandorw/mocabogaso/games/hex/FirstLineHeuristic.java @@ -6,10 +6,15 @@ import com.github.sandorw.mocabogaso.games.defaults.DefaultGameResult; import com.github.sandorw.mocabogaso.games.hex.HexGameState.BoardStatus; -public class FirstLineHexHeuristic implements Heuristic { +/** + * Heuristic to discourage exploring unforced or unnecessary moves on the perimeter of the board. + * + * @author sandorw + */ +public class FirstLineHeuristic implements Heuristic { private int weight; - public FirstLineHexHeuristic(int weight) { + public FirstLineHeuristic(int weight) { this.weight = weight; } diff --git a/src/main/java/com/github/sandorw/mocabogaso/games/hex/HexGameState.java b/src/main/java/com/github/sandorw/mocabogaso/games/hex/HexGameState.java index 16eab22..5e23dc0 100644 --- a/src/main/java/com/github/sandorw/mocabogaso/games/hex/HexGameState.java +++ b/src/main/java/com/github/sandorw/mocabogaso/games/hex/HexGameState.java @@ -50,10 +50,15 @@ public String toString() { } } + public static final List neighborRowDelta = ImmutableList.of(1, 1, 0, -1, -1, 0); + public static final List neighborColDelta = ImmutableList.of(0, 1, 1, 0, -1, -1); + public static HexGameState of(int boardSize) { List> heuristics = Lists.newArrayList(); heuristics.add(new InitialStateHeuristic(5)); - heuristics.add(new FirstLineHexHeuristic(10)); + heuristics.add(new FirstLineHeuristic(15)); + heuristics.add(new OneSpaceHopHeuristic(10)); + heuristics.add(new SecureConnectionHeuristic(15)); return new HexGameState(boardSize, new MNKZobristHashService(boardSize, boardSize), heuristics); } @@ -152,12 +157,16 @@ public String getHumanReadableMoveString(DefaultGameMove move) { public boolean isValidMove(DefaultGameMove move) { int i = getRowNumber(move.getLocation()); int j = getColNumber(move.getLocation()); - if ((i < 0) || (j < 0) || (i >= boardSize) || (j >= boardSize) || + if (!isIndexInBounds(i) || !isIndexInBounds(j) || (!move.getPlayerName().equals(nextPlayerName))) return false; return boardLocation[i][j] == BoardStatus.EMPTY; } + public boolean isIndexInBounds(int index) { + return (index >= 0) && (index < boardSize); + } + protected int getRowNumber(int location) { return location/boardSize; } @@ -173,20 +182,16 @@ public void applyMove(DefaultGameMove move) { BoardStatus newStatus = (move.getPlayerName().equals("X") ? BoardStatus.X : BoardStatus.O); boardLocation[i][j] = newStatus; groups[i][j] = new Group(i,j,newStatus); - checkAndJoinNeighboringGroups(i,j,i+1,j); - checkAndJoinNeighboringGroups(i,j,i+1,j+1); - checkAndJoinNeighboringGroups(i,j,i,j-1); - checkAndJoinNeighboringGroups(i,j,i,j+1); - checkAndJoinNeighboringGroups(i,j,i-1,j-1); - checkAndJoinNeighboringGroups(i,j,i-1,j); + for (int k=0; k < 6; ++k) { + checkAndJoinNeighboringGroups(i,j,i+neighborRowDelta.get(k),j+neighborColDelta.get(k)); + } zobristHash ^= zobristHashService.getLocationHash(i,j,newStatus.getIndex()); toggleCurrentPlayer(); } private void checkAndJoinNeighboringGroups(int i, int j, int neighborRow, int neighborCol) { - if ((neighborRow >= 0) && (neighborRow < boardSize) && - (neighborCol >= 0) && (neighborCol < boardSize) && - (boardLocation[i][j] == boardLocation[neighborRow][neighborCol])) { + if (isIndexInBounds(neighborRow) && isIndexInBounds(neighborCol) + && (boardLocation[i][j] == boardLocation[neighborRow][neighborCol])) { addAllToGroup(groups[i][j], groups[neighborRow][neighborCol]); } } @@ -280,7 +285,7 @@ public int hashCode() { return (int) ((zobristHash >>> 32) ^ ((zobristHash & 0xFFFF0000) >>> 32)); } - private final class Group { + protected final class Group { private int minBound; private int maxBound; diff --git a/src/main/java/com/github/sandorw/mocabogaso/games/hex/InitialStateHeuristic.java b/src/main/java/com/github/sandorw/mocabogaso/games/hex/InitialStateHeuristic.java index f2dce5e..534e2f5 100644 --- a/src/main/java/com/github/sandorw/mocabogaso/games/hex/InitialStateHeuristic.java +++ b/src/main/java/com/github/sandorw/mocabogaso/games/hex/InitialStateHeuristic.java @@ -6,7 +6,8 @@ import com.github.sandorw.mocabogaso.games.defaults.DefaultGameResult; /** - * Heuristic for + * Heuristic for setting the initial state of NodeResults to make sure the first few simulations + * don't skew the value too significantly. * * @author sandorw */ diff --git a/src/main/java/com/github/sandorw/mocabogaso/games/hex/OneSpaceHopHeuristic.java b/src/main/java/com/github/sandorw/mocabogaso/games/hex/OneSpaceHopHeuristic.java new file mode 100644 index 0000000..2e49ebe --- /dev/null +++ b/src/main/java/com/github/sandorw/mocabogaso/games/hex/OneSpaceHopHeuristic.java @@ -0,0 +1,85 @@ +package com.github.sandorw.mocabogaso.games.hex; + +import com.github.sandorw.mocabogaso.ai.mcts.Heuristic; +import com.github.sandorw.mocabogaso.games.GameState; +import com.github.sandorw.mocabogaso.games.defaults.DefaultGameMove; +import com.github.sandorw.mocabogaso.games.defaults.DefaultGameResult; +import com.github.sandorw.mocabogaso.games.hex.HexGameState.BoardStatus; + +/** + * Heuristic to encourage one space jump moves that can still be securely connected if needed. + * + * @author sandorw + */ +public class OneSpaceHopHeuristic implements Heuristic { + int weight; + + public OneSpaceHopHeuristic(int weight) { + this.weight = weight; + } + + @Override + public int getWeight() { + return weight; + } + + @Override + public > DefaultGameResult + evaluateMove(DefaultGameMove move, GS initialGameState) { + if (move == null) { + return null; + } + HexGameState hexGameState = (HexGameState) initialGameState; + int rowIndex = hexGameState.getRowNumber(move.getLocation()); + int colIndex = hexGameState.getColNumber(move.getLocation()); + int boardSize = hexGameState.boardSize; + BoardStatus movingPlayer = (move.getPlayerName().equals("X") ? BoardStatus.X : BoardStatus.O); + if ((rowIndex < boardSize-2) && (colIndex < boardSize-1) + && (hexGameState.boardLocation[rowIndex+2][colIndex+1] == movingPlayer) + && (hexGameState.boardLocation[rowIndex+1][colIndex] == BoardStatus.EMPTY) + && (hexGameState.boardLocation[rowIndex+1][colIndex+1] == BoardStatus.EMPTY)) { + return new DefaultGameResult(move.getPlayerName(), false); + } + if (rowIndex < boardSize-1) { + if ((colIndex < boardSize-2) + && (hexGameState.boardLocation[rowIndex+1][colIndex+2] == movingPlayer) + && (hexGameState.boardLocation[rowIndex+1][colIndex+1] == BoardStatus.EMPTY) + && (hexGameState.boardLocation[rowIndex][colIndex+1] == BoardStatus.EMPTY)) { + return new DefaultGameResult(move.getPlayerName(), false); + } + if ((colIndex > 0) + && (hexGameState.boardLocation[rowIndex+1][colIndex-1] == movingPlayer) + && (hexGameState.boardLocation[rowIndex][colIndex-1] == BoardStatus.EMPTY) + && (hexGameState.boardLocation[rowIndex+1][colIndex] == BoardStatus.EMPTY)) { + return new DefaultGameResult(move.getPlayerName(), false); + } + } + if (rowIndex > 0) { + if ((colIndex > 1) + && (hexGameState.boardLocation[rowIndex-1][colIndex-2] == movingPlayer) + && (hexGameState.boardLocation[rowIndex-1][colIndex-1] == BoardStatus.EMPTY) + && (hexGameState.boardLocation[rowIndex][colIndex-1] == BoardStatus.EMPTY)) { + return new DefaultGameResult(move.getPlayerName(), false); + } + if ((colIndex < boardSize-1) + && (hexGameState.boardLocation[rowIndex-1][colIndex+1] == movingPlayer) + && (hexGameState.boardLocation[rowIndex][colIndex+1] == BoardStatus.EMPTY) + && (hexGameState.boardLocation[rowIndex-1][colIndex] == BoardStatus.EMPTY)) { + return new DefaultGameResult(move.getPlayerName(), false); + } + } + if ((rowIndex > 1) && (colIndex > 0) + && (hexGameState.boardLocation[rowIndex-2][colIndex-1] == movingPlayer) + && (hexGameState.boardLocation[rowIndex-1][colIndex] == BoardStatus.EMPTY) + && (hexGameState.boardLocation[rowIndex-1][colIndex-1] == BoardStatus.EMPTY)) { + return new DefaultGameResult(move.getPlayerName(), false); + } + return null; + } + + @Override + public > DefaultGameMove suggestPlayoutMove(GS gameState) { + return null; + } + +} diff --git a/src/main/java/com/github/sandorw/mocabogaso/games/hex/SecureConnectionHeuristic.java b/src/main/java/com/github/sandorw/mocabogaso/games/hex/SecureConnectionHeuristic.java new file mode 100644 index 0000000..ae766f3 --- /dev/null +++ b/src/main/java/com/github/sandorw/mocabogaso/games/hex/SecureConnectionHeuristic.java @@ -0,0 +1,101 @@ +package com.github.sandorw.mocabogaso.games.hex; + +import com.github.sandorw.mocabogaso.ai.mcts.Heuristic; +import com.github.sandorw.mocabogaso.games.GameState; +import com.github.sandorw.mocabogaso.games.defaults.DefaultGameMove; +import com.github.sandorw.mocabogaso.games.defaults.DefaultGameResult; +import com.github.sandorw.mocabogaso.games.hex.HexGameState.BoardStatus; + +/** + * Heuristic to incentivize linking groups together when threatened. + * + * @author sandorw + */ +public class SecureConnectionHeuristic implements Heuristic { + private int weight; + + public SecureConnectionHeuristic(int weight) { + this.weight = weight; + } + + @Override + public int getWeight() { + return weight; + } + + @Override + public > DefaultGameResult + evaluateMove(DefaultGameMove move, GS initialGameState) { + if (move == null) { + return null; + } + HexGameState hexGameState = (HexGameState) initialGameState; + int rowIndex = hexGameState.getRowNumber(move.getLocation()); + int colIndex = hexGameState.getColNumber(move.getLocation()); + BoardStatus movingPlayer = (move.getPlayerName().equals("X") ? BoardStatus.X : BoardStatus.O); + boolean nonUrgentConnectionExists = false; + boolean directOppositeAllies = false; + int numAlliedGroups = 0; + int numAlliedPieces = 0; + int firstAllyIndex = -4; + HexGameState.Group[] alliedGroups = {null, null, null}; + for (int i=0; i < 6; ++i) { + int neighborRow = rowIndex + HexGameState.neighborRowDelta.get(i); + int neighborCol = colIndex + HexGameState.neighborColDelta.get(i); + if (hexGameState.isIndexInBounds(neighborRow) && hexGameState.isIndexInBounds(neighborCol)) { + BoardStatus neighborStatus = hexGameState.boardLocation[neighborRow][neighborCol]; + if (neighborStatus == movingPlayer) { + ++numAlliedPieces; + HexGameState.Group neighborGroup = hexGameState.groups[neighborRow][neighborCol]; + boolean matchingGroup = false; + for (int j=0; j < numAlliedGroups; ++j) { + if (neighborGroup == alliedGroups[j]) { + matchingGroup = true; + break; + } + } + if (!matchingGroup) { + alliedGroups[numAlliedGroups] = neighborGroup; + ++numAlliedGroups; + if (numAlliedGroups == 1) { + firstAllyIndex = i; + } else if (firstAllyIndex == i-3) { + directOppositeAllies = true; + } + } + } else if (neighborStatus == BoardStatus.EMPTY) { + int prevIndex = (i == 0 ? 5 : i-1); + int nextIndex = (i == 5 ? 0 : i+1); + int prevRow = rowIndex + HexGameState.neighborRowDelta.get(prevIndex); + int prevCol = colIndex + HexGameState.neighborColDelta.get(prevIndex); + int nextRow = rowIndex + HexGameState.neighborRowDelta.get(nextIndex); + int nextCol = colIndex + HexGameState.neighborColDelta.get(nextIndex); + if (hexGameState.isIndexInBounds(prevRow) + && hexGameState.isIndexInBounds(prevCol) + && hexGameState.isIndexInBounds(nextRow) + && hexGameState.isIndexInBounds(nextCol)) { + BoardStatus prevStatus = hexGameState.boardLocation[prevRow][prevCol]; + BoardStatus nextStatus = hexGameState.boardLocation[nextRow][nextCol]; + HexGameState.Group prevGroup = hexGameState.groups[prevRow][prevCol]; + HexGameState.Group nextGroup = hexGameState.groups[nextRow][nextCol]; + if ((prevStatus == movingPlayer) && (nextStatus == movingPlayer) + && (prevGroup != nextGroup)) { + nonUrgentConnectionExists = true; + } + } + } + } + } + if ((numAlliedGroups == 3) || ((numAlliedGroups == 2) + && ((directOppositeAllies && (numAlliedPieces == 2)) + || !nonUrgentConnectionExists))) { + return new DefaultGameResult(move.getPlayerName(), false); + } + return null; + } + + @Override + public > DefaultGameMove suggestPlayoutMove(GS gameState) { + return null; + } +} diff --git a/src/main/java/com/github/sandorw/mocabogaso/players/AIBuilder.java b/src/main/java/com/github/sandorw/mocabogaso/players/AIBuilder.java new file mode 100644 index 0000000..8319266 --- /dev/null +++ b/src/main/java/com/github/sandorw/mocabogaso/players/AIBuilder.java @@ -0,0 +1,87 @@ +package com.github.sandorw.mocabogaso.players; + +import com.github.sandorw.mocabogaso.ai.AIService; +import com.github.sandorw.mocabogaso.ai.mcts.MonteCarloSearchService; +import com.github.sandorw.mocabogaso.ai.mcts.NodeResultsFactory; +import com.github.sandorw.mocabogaso.ai.mcts.NodeResultsService; +import com.github.sandorw.mocabogaso.ai.mcts.PlayoutPolicy; +import com.github.sandorw.mocabogaso.ai.mcts.amaf.AMAFHeuristicNodeResults; +import com.github.sandorw.mocabogaso.ai.mcts.amaf.AMAFHeuristicNodeResultsFactory; +import com.github.sandorw.mocabogaso.ai.mcts.amaf.AMAFMonteCarloSearchService; +import com.github.sandorw.mocabogaso.ai.mcts.amaf.AMAFNodeResultsService; +import com.github.sandorw.mocabogaso.ai.mcts.amaf.DefaultAMAFNodeResults; +import com.github.sandorw.mocabogaso.ai.mcts.amaf.DefaultAMAFNodeResultsFactory; +import com.github.sandorw.mocabogaso.ai.mcts.defaults.DefaultNodeResults; +import com.github.sandorw.mocabogaso.ai.mcts.defaults.DefaultNodeResultsFactory; +import com.github.sandorw.mocabogaso.ai.mcts.defaults.DefaultNodeResultsService; +import com.github.sandorw.mocabogaso.ai.mcts.policies.RandomMovePlayoutPolicy; +import com.github.sandorw.mocabogaso.games.GameMove; +import com.github.sandorw.mocabogaso.games.GameResult; +import com.github.sandorw.mocabogaso.games.GameState; + +public class AIBuilder> { + private GS initialGameState; + private boolean withAMAF; + private boolean withHeuristics; + private int timePerMoveMs; + private int numThreads; + + public AIBuilder(GS initialGameState) { + this.initialGameState = initialGameState; + withAMAF = false; + withHeuristics = false; + timePerMoveMs = 1000; + numThreads = 1; + } + + public AIBuilder withAMAF() { + withAMAF = true; + return this; + } + + public AIBuilder withHeuristics() { + withHeuristics = true; + return this; + } + + public AIBuilder withTimePerMove(int timePerMoveMs) { + this.timePerMoveMs = timePerMoveMs; + return this; + } + + public AIBuilder multithreaded(int numThreads) { + this.numThreads = numThreads; + return this; + } + + public Player build() { + PlayoutPolicy policy = new RandomMovePlayoutPolicy(); + AIService aiService = null; + if (withHeuristics) { + NodeResultsFactory nodeResultsFactory = new AMAFHeuristicNodeResultsFactory(); + if (withAMAF) { + AMAFNodeResultsService nodeResultsService + = new AMAFNodeResultsService<>(nodeResultsFactory); + aiService = new AMAFMonteCarloSearchService<>(nodeResultsService, policy, initialGameState); + } else { + DefaultNodeResultsService nodeResultsService + = new DefaultNodeResultsService<>(nodeResultsFactory); + aiService = new MonteCarloSearchService<>(nodeResultsService, policy, initialGameState); + } + } else if (withAMAF) { + NodeResultsFactory nodeResultsFactory = new DefaultAMAFNodeResultsFactory(); + AMAFNodeResultsService nodeResultsService + = new AMAFNodeResultsService<>(nodeResultsFactory); + aiService = new AMAFMonteCarloSearchService<>(nodeResultsService, policy, initialGameState); + } else { + NodeResultsFactory nodeResultsFactory = new DefaultNodeResultsFactory(); + NodeResultsService nodeResultsService + = new DefaultNodeResultsService<>(nodeResultsFactory); + aiService = new MonteCarloSearchService<>(nodeResultsService, policy, initialGameState); + } + if (numThreads > 1) { + return new MultiThreadedAIPlayer<>(aiService, timePerMoveMs, numThreads); + } + return new AIPlayer<>(aiService, timePerMoveMs); + } +} diff --git a/src/main/java/com/github/sandorw/mocabogaso/players/PlayerFactory.java b/src/main/java/com/github/sandorw/mocabogaso/players/PlayerFactory.java index 71a76e1..63daf6c 100644 --- a/src/main/java/com/github/sandorw/mocabogaso/players/PlayerFactory.java +++ b/src/main/java/com/github/sandorw/mocabogaso/players/PlayerFactory.java @@ -1,16 +1,12 @@ package com.github.sandorw.mocabogaso.players; import com.github.sandorw.mocabogaso.ai.AIService; -import com.github.sandorw.mocabogaso.ai.mcts.MonteCarloSearchService; import com.github.sandorw.mocabogaso.ai.mcts.NodeResultsFactory; import com.github.sandorw.mocabogaso.ai.mcts.PlayoutPolicy; import com.github.sandorw.mocabogaso.ai.mcts.amaf.AMAFMonteCarloSearchService; import com.github.sandorw.mocabogaso.ai.mcts.amaf.AMAFNodeResultsService; import com.github.sandorw.mocabogaso.ai.mcts.amaf.DefaultAMAFNodeResults; import com.github.sandorw.mocabogaso.ai.mcts.amaf.DefaultAMAFNodeResultsFactory; -import com.github.sandorw.mocabogaso.ai.mcts.defaults.DefaultNodeResults; -import com.github.sandorw.mocabogaso.ai.mcts.defaults.DefaultNodeResultsFactory; -import com.github.sandorw.mocabogaso.ai.mcts.defaults.DefaultNodeResultsService; import com.github.sandorw.mocabogaso.ai.mcts.policies.RandomMovePlayoutPolicy; import com.github.sandorw.mocabogaso.games.GameMove; import com.github.sandorw.mocabogaso.games.GameResult; @@ -25,29 +21,25 @@ public final class PlayerFactory { public static > Player getNewAIPlayer(GS initialGameState, int timePerMoveMs) { - NodeResultsFactory nodeResultsFactory = new DefaultNodeResultsFactory(); - DefaultNodeResultsService nodeResultsService = new DefaultNodeResultsService<>(nodeResultsFactory); - PlayoutPolicy policy = new RandomMovePlayoutPolicy(); - AIService aiService = new MonteCarloSearchService<>(nodeResultsService, policy, initialGameState); - return new AIPlayer<>(aiService, timePerMoveMs); + return new AIBuilder<>(initialGameState) + .withTimePerMove(timePerMoveMs) + .build(); } public static > Player getNewAMAFAIPlayer(GS initialGameState, int timePerMoveMs) { - NodeResultsFactory nodeResultsFactory = new DefaultAMAFNodeResultsFactory(); - AMAFNodeResultsService nodeResultsService = new AMAFNodeResultsService<>(nodeResultsFactory); - PlayoutPolicy policy = new RandomMovePlayoutPolicy(); - AIService aiService = new AMAFMonteCarloSearchService<>(nodeResultsService, policy, initialGameState); - return new AIPlayer<>(aiService, timePerMoveMs); + return new AIBuilder<>(initialGameState) + .withTimePerMove(timePerMoveMs) + .withAMAF() + .build(); } public static > Player getNewMultiThreadedAMAFAIPlayer(GS initialGameState, int timePerMoveMs, int numThreads) { - NodeResultsFactory nodeResultsFactory = new DefaultAMAFNodeResultsFactory(); - AMAFNodeResultsService nodeResultsService = new AMAFNodeResultsService<>(nodeResultsFactory); - PlayoutPolicy policy = new RandomMovePlayoutPolicy(); - AIService aiService = new AMAFMonteCarloSearchService<>(nodeResultsService, policy, initialGameState); - return new MultiThreadedAIPlayer<>(aiService, timePerMoveMs, numThreads); + return new AIBuilder<>(initialGameState) + .withTimePerMove(timePerMoveMs) + .multithreaded(numThreads) + .build(); } public static > diff --git a/src/test/java/com/github/sandorw/mocabogaso/games/hex/HexGameStateTest.java b/src/test/java/com/github/sandorw/mocabogaso/games/hex/HexGameStateTest.java index 8321040..1c5b6f1 100644 --- a/src/test/java/com/github/sandorw/mocabogaso/games/hex/HexGameStateTest.java +++ b/src/test/java/com/github/sandorw/mocabogaso/games/hex/HexGameStateTest.java @@ -181,13 +181,13 @@ public void initialStateHeuristic_isTieTest() { @Test public void firstLineHeuristic_weightTest() { - FirstLineHexHeuristic heuristic = new FirstLineHexHeuristic(5); + FirstLineHeuristic heuristic = new FirstLineHeuristic(5); assertEquals(heuristic.getWeight(), 5); } @Test public void firstLineHeuristic_badFirstLinePlayTest() { - FirstLineHexHeuristic heuristic = new FirstLineHexHeuristic(5); + FirstLineHeuristic heuristic = new FirstLineHeuristic(5); HexGameState gameState = HexGameState.of(5); for (int i=0; i < 5; ++i) { DefaultGameMove move = new DefaultGameMove("X", i); @@ -217,7 +217,7 @@ public void firstLineHeuristic_badFirstLinePlayTest() { @Test public void firstLineHeuristic_goodFirstLinePlayTest() { - FirstLineHexHeuristic heuristic = new FirstLineHexHeuristic(5); + FirstLineHeuristic heuristic = new FirstLineHeuristic(5); HexGameState gameState = HexGameState.of(5); DefaultGameMove move = new DefaultGameMove("X", 6); gameState.applyMove(move); @@ -230,10 +230,184 @@ public void firstLineHeuristic_goodFirstLinePlayTest() { @Test public void firstLineHeuristic_interiorPlayTest() { - FirstLineHexHeuristic heuristic = new FirstLineHexHeuristic(5); + FirstLineHeuristic heuristic = new FirstLineHeuristic(5); HexGameState gameState = HexGameState.of(5); DefaultGameMove move = new DefaultGameMove("X", 6); DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); assertNull(gameResult); } + + @Test + public void oneSpaceHopHeuristic_weightTest() { + OneSpaceHopHeuristic heuristic = new OneSpaceHopHeuristic(5); + assertEquals(heuristic.getWeight(), 5); + } + + @Test + public void oneSpaceHopHeuristic_positiveTest() { + OneSpaceHopHeuristic heuristic = new OneSpaceHopHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 12); + gameState.applyMove(move); + move = new DefaultGameMove("X", 1); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + move = new DefaultGameMove("X", 8); + gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + move = new DefaultGameMove("X", 19); + gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + move = new DefaultGameMove("X", 23); + gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + move = new DefaultGameMove("X", 16); + gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + move = new DefaultGameMove("X", 5); + gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + } + + @Test + public void oneSpaceHopHeuristic_negativeTest() { + OneSpaceHopHeuristic heuristic = new OneSpaceHopHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 12); + gameState.applyMove(move); + move = new DefaultGameMove("O", 6); + gameState.applyMove(move); + move = new DefaultGameMove("X", 1); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertNull(gameResult); + } + + @Test + public void secureConnectionHeuristic_weightTest() { + SecureConnectionHeuristic heuristic = new SecureConnectionHeuristic(5); + assertEquals(heuristic.getWeight(), 5); + } + + @Test + public void secureConnectionHeuristic_directOppositeLinkTest() { + SecureConnectionHeuristic heuristic = new SecureConnectionHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 6); + gameState.applyMove(move); + move = new DefaultGameMove("X", 18); + gameState.applyMove(move); + move = new DefaultGameMove("X", 12); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + } + + @Test + public void secureConnectionHeuristic_threeAlliesTest() { + SecureConnectionHeuristic heuristic = new SecureConnectionHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 6); + gameState.applyMove(move); + move = new DefaultGameMove("X", 18); + gameState.applyMove(move); + move = new DefaultGameMove("X", 17); + gameState.applyMove(move); + move = new DefaultGameMove("X", 12); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertNull(gameResult); + } + + @Test + public void secureConnectionHeuristic_noEnemyLinkTest() { + SecureConnectionHeuristic heuristic = new SecureConnectionHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 6); + gameState.applyMove(move); + move = new DefaultGameMove("X", 17); + gameState.applyMove(move); + move = new DefaultGameMove("X", 12); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertNull(gameResult); + } + + @Test + public void secureConnectionHeuristic_forcedLinkTest() { + SecureConnectionHeuristic heuristic = new SecureConnectionHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 6); + gameState.applyMove(move); + move = new DefaultGameMove("X", 17); + gameState.applyMove(move); + move = new DefaultGameMove("O", 11); + gameState.applyMove(move); + move = new DefaultGameMove("X", 12); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + } + + @Test + public void secureConnectionHeuristic_unforcedLinkTest() { + SecureConnectionHeuristic heuristic = new SecureConnectionHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 6); + gameState.applyMove(move); + move = new DefaultGameMove("X", 17); + gameState.applyMove(move); + move = new DefaultGameMove("X", 18); + gameState.applyMove(move); + move = new DefaultGameMove("O", 13); + gameState.applyMove(move); + move = new DefaultGameMove("X", 12); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertNull(gameResult); + } + + @Test + public void secureConnectionHeuristic_wrapAroundGroupTest() { + SecureConnectionHeuristic heuristic = new SecureConnectionHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 6); + gameState.applyMove(move); + move = new DefaultGameMove("X", 13); + gameState.applyMove(move); + move = new DefaultGameMove("X", 17); + gameState.applyMove(move); + move = new DefaultGameMove("X", 23); + gameState.applyMove(move); + move = new DefaultGameMove("X", 24); + gameState.applyMove(move); + move = new DefaultGameMove("X", 19); + gameState.applyMove(move); + move = new DefaultGameMove("O", 11); + gameState.applyMove(move); + move = new DefaultGameMove("O", 7); + gameState.applyMove(move); + move = new DefaultGameMove("X", 12); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + } + + @Test + public void secureConnectionHeuristic_cornerTest() { + SecureConnectionHeuristic heuristic = new SecureConnectionHeuristic(5); + HexGameState gameState = HexGameState.of(5); + DefaultGameMove move = new DefaultGameMove("X", 1); + gameState.applyMove(move); + move = new DefaultGameMove("X", 5); + gameState.applyMove(move); + move = new DefaultGameMove("O", 6); + gameState.applyMove(move); + move = new DefaultGameMove("X", 0); + DefaultGameResult gameResult = heuristic.evaluateMove(move, gameState); + assertFalse(gameResult.isTie()); + assertEquals(gameResult.getWinningPlayer(), "X"); + } }