For a project in my AI class, we are making AIs for the game ingenious and are having trouble creating a game-state tree. Our tree is represented by nodes, which contain GameStates, which themselves contain a list of the players (and all their corresponding data, such as their score, the tiles they hold in their hand, and their player number), the current state of the board, and the bag of remaining tiles to choose from. The root node is the current state, and it is connected to child nodes by Edge objects, which contain a parent and child node, and the Action that leads the parent to the child. The action holds a tile that is placed on the board, the colors the tile contains, and the coordinates on the hexagonal grid that the tile will be placed upon.
Our problem is that when creating a new game state, we are updating the data contained in the state, instead of creating a copy of the game state. Normally, we would just make a deep copy of the game state for each child node of the root, etc.
This is where things get tricky. Our hexagonal grid library is from the Java library Hexameter, which is a hexagonal grid containing hexagon objects that can contain satelliteData, which is what we're using to represent the tiles/colors that we place. The problem is that because the satelliteData refers to the original objects, and isn't actually containing the objects, we cannot deep clone our GameState (as far as I can tell).
Since our deadline is fast approaching in less than a week, and we've been banging our heads on the table for the past few days trying to fix our problems, we finally resorted to asking StackOverFlow for help. Take a look at our GameState structure: (note that all below code is pruned of getters, setters, and unrelated methods)
public class GameState {
private Player[] players;
private Board currentBoard;
private Bag currentBag;
private Player gamingPlayer;
public GameState() {
players = new Player[2];
currentBoard = new Board();
currentBag = new Bag(Pieces.createBagPieces());
players[0] = new Player(1, currentBag.pickSix(), true);
for (Tile t: players[0].getHand().getPieces()){
players[0].addToVisibleTiles(t);
}
players[1] = new Player(2, currentBag.pickSix(), true);
for (Tile t: players[1].getHand().getPieces()){
players[1].addToVisibleTiles(t);
}
gamingPlayer = players[0];
}
private GameState(Player[] players, Board currentBoard, Bag currentBag,
Player gamingPlayer) {
this.players = players;
this.currentBoard = currentBoard;
this.currentBag = currentBag;
this.gamingPlayer = gamingPlayer;
}
}
See our two constructors - our first one instantiates the initial game state, and our second one takes the parameters we want to pass when updating the game. Thing is, we realize that
this.players = players;
this.currentBoard = currentBoard;
this.currentBag = currentBag;
this.gamingPlayer = gamingPlayer;
changes the original state, and not a new state.
What we originally looked into doing was in the second constructor, passing the state we want to change, and then create new variables for the new state and instantiate them based on the values of the previous state. Thing is, we don't have any way to access these now.
We looked into utilizing flyweights for our children nodes in the game tree, that represent the changes that we've made based on the action:
public class Action {
private Hexagon h1;
private Hexagon h2;
private Tile tile;
public Action(Hexagon h1, Hexagon h2, Tile t){
this.h1 = h1;
this.h2 = h2;
this.tile = t;
}
public Action(){
this.h1 = null;
this.h2 = null;
this.tile = null;
}
}
And we apply the action in such a way to create the next GameState:
public GameState applyAction(Action a){
HexagonActor first = null;
GameState nextState;
if (a.getH1().getSatelliteData().isPresent()){
// create a link for the actor and hex of the next hex from current
Link hexLink = (Link) a.getH1().getSatelliteData().get();
HexagonActor currentHexActor = hexLink.getActor();
currentHexActor.setHexColor(a.getTileColors()[0]);
first = currentHexActor;
Player.updateScore(Player.scoreGain(currentHexActor, currentBoard.getGrid(), currentHexActor), gamingPlayer);
}
if (a.getH2().getSatelliteData().isPresent()){
// create a link for the actor and hex of the next hex from current
Link hexLink = (Link) a.getH2().getSatelliteData().get();
HexagonActor currentHexActor = hexLink.getActor();
currentHexActor.setHexColor(a.getTileColors()[1]);
if (first != null){
Player.updateScore(Player.scoreGain(currentHexActor, currentBoard.getGrid(), first), gamingPlayer);
}
}
gamingPlayer.getHand().removeFromHand(a.getTile());
Tile newTile = currentBag.pickTile();
gamingPlayer.getHand().pickFromBag(newTile);
gamingPlayer.addToVisibleTiles(newTile);
nextState = new GameState(players, currentBoard, currentBag, changeGamingPlayer());
return nextState;
}
As you can see, this doesn't properly "clone" the game state. After thorough research on StackOverFlow, we came up with 3 good options:
- Deep cloning by manually refactoring our code to have multiple constructors, getters, setters, and methods for creating copies of each class.
- Using the Serializable interface, however we've never used this and due to deadlines are loathe to jump into this to try and figure it out.
- Use the Flyweight pattern, however we also have never used this although it seems like our best option (and is apparently complex to implement).
I am asking if anyone has any "quick fixes" that we can use to have some form of "working" tree that we can run basic AI on (MCTS, A*, ExpectiMax, Greedy, etc) to have some form of results.