Skip to content

Commit

Permalink
Merge pull request #45 from sandorw/feature/hexheuristics
Browse files Browse the repository at this point in the history
Feature/hexheuristics
  • Loading branch information
sandorw committed Nov 25, 2015
2 parents 4eca0e5 + 693838e commit 05c3568
Show file tree
Hide file tree
Showing 8 changed files with 488 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<DefaultGameMove, DefaultGameResult> {
/**
* Heuristic to discourage exploring unforced or unnecessary moves on the perimeter of the board.
*
* @author sandorw
*/
public class FirstLineHeuristic implements Heuristic<DefaultGameMove, DefaultGameResult> {
private int weight;

public FirstLineHexHeuristic(int weight) {
public FirstLineHeuristic(int weight) {
this.weight = weight;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ public String toString() {
}
}

public static final List<Integer> neighborRowDelta = ImmutableList.of(1, 1, 0, -1, -1, 0);
public static final List<Integer> neighborColDelta = ImmutableList.of(0, 1, 1, 0, -1, -1);

public static HexGameState of(int boardSize) {
List<Heuristic<DefaultGameMove, DefaultGameResult>> 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);
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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]);
}
}
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DefaultGameMove, DefaultGameResult> {
int weight;

public OneSpaceHopHeuristic(int weight) {
this.weight = weight;
}

@Override
public int getWeight() {
return weight;
}

@Override
public <GS extends GameState<DefaultGameMove, DefaultGameResult>> 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 <GS extends GameState<DefaultGameMove, DefaultGameResult>> DefaultGameMove suggestPlayoutMove(GS gameState) {
return null;
}

}
Original file line number Diff line number Diff line change
@@ -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<DefaultGameMove, DefaultGameResult> {
private int weight;

public SecureConnectionHeuristic(int weight) {
this.weight = weight;
}

@Override
public int getWeight() {
return weight;
}

@Override
public <GS extends GameState<DefaultGameMove, DefaultGameResult>> 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 <GS extends GameState<DefaultGameMove, DefaultGameResult>> DefaultGameMove suggestPlayoutMove(GS gameState) {
return null;
}
}
87 changes: 87 additions & 0 deletions src/main/java/com/github/sandorw/mocabogaso/players/AIBuilder.java
Original file line number Diff line number Diff line change
@@ -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<GM extends GameMove, GR extends GameResult, GS extends GameState<GM,GR>> {
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<GM,GR,GS> withAMAF() {
withAMAF = true;
return this;
}

public AIBuilder<GM,GR,GS> withHeuristics() {
withHeuristics = true;
return this;
}

public AIBuilder<GM,GR,GS> withTimePerMove(int timePerMoveMs) {
this.timePerMoveMs = timePerMoveMs;
return this;
}

public AIBuilder<GM,GR,GS> multithreaded(int numThreads) {
this.numThreads = numThreads;
return this;
}

public Player<GM> build() {
PlayoutPolicy policy = new RandomMovePlayoutPolicy();
AIService<GM> aiService = null;
if (withHeuristics) {
NodeResultsFactory<AMAFHeuristicNodeResults> nodeResultsFactory = new AMAFHeuristicNodeResultsFactory();
if (withAMAF) {
AMAFNodeResultsService<AMAFHeuristicNodeResults> nodeResultsService
= new AMAFNodeResultsService<>(nodeResultsFactory);
aiService = new AMAFMonteCarloSearchService<>(nodeResultsService, policy, initialGameState);
} else {
DefaultNodeResultsService<AMAFHeuristicNodeResults> nodeResultsService
= new DefaultNodeResultsService<>(nodeResultsFactory);
aiService = new MonteCarloSearchService<>(nodeResultsService, policy, initialGameState);
}
} else if (withAMAF) {
NodeResultsFactory<DefaultAMAFNodeResults> nodeResultsFactory = new DefaultAMAFNodeResultsFactory();
AMAFNodeResultsService<DefaultAMAFNodeResults> nodeResultsService
= new AMAFNodeResultsService<>(nodeResultsFactory);
aiService = new AMAFMonteCarloSearchService<>(nodeResultsService, policy, initialGameState);
} else {
NodeResultsFactory<DefaultNodeResults> nodeResultsFactory = new DefaultNodeResultsFactory();
NodeResultsService<DefaultNodeResults> nodeResultsService
= new DefaultNodeResultsService<>(nodeResultsFactory);
aiService = new MonteCarloSearchService<>(nodeResultsService, policy, initialGameState);
}
if (numThreads > 1) {
return new MultiThreadedAIPlayer<>(aiService, timePerMoveMs, numThreads);
}
return new AIPlayer<>(aiService, timePerMoveMs);
}
}
Loading

0 comments on commit 05c3568

Please sign in to comment.