package com.hearthsim.util.factory; import com.hearthsim.card.Deck; import com.hearthsim.exception.HSException; import com.hearthsim.model.BoardModel; import com.hearthsim.player.playercontroller.BoardScorer; import com.hearthsim.util.tree.CardDrawNode; import com.hearthsim.util.tree.HearthTreeNode; import com.hearthsim.util.tree.RandomEffectNode; import com.hearthsim.util.tree.StopNode; import java.util.ArrayList; import java.util.HashSet; public class DepthBoardStateFactory extends BoardStateFactoryBase { private boolean lethal_; private boolean timedOut_; private final long maxTime_; private long startTime_; private final boolean useDuplicateNodePruning; private int numNodes; private int numDuplicates; private HashSet<BoardModel> boardsAlreadySeen; /** * Constructor * maxThinkTime defaults to 10000 milliseconds (10 seconds) */ public DepthBoardStateFactory(Deck deckPlayer0, Deck deckPlayer1, boolean useDuplicateNodePruning) { this(deckPlayer0, deckPlayer1, 10000, useDuplicateNodePruning); } /** * Constructor * * @param deckPlayer0 * @param deckPlayer1 * @param maxThinkTime The maximum amount of time in milliseconds the factory is allowed to spend on generating the simulation tree. */ public DepthBoardStateFactory(Deck deckPlayer0, Deck deckPlayer1, long maxThinkTime, boolean useDuplicateNodePruning) { super(deckPlayer0, deckPlayer1); lethal_ = false; startTime_ = System.currentTimeMillis(); maxTime_ = maxThinkTime; timedOut_ = false; this.useDuplicateNodePruning = useDuplicateNodePruning; if (useDuplicateNodePruning) boardsAlreadySeen = new HashSet<>(500000); } public DepthBoardStateFactory(Deck deckPlayer0, Deck deckPlayer1, long maxThinkTime, boolean useDuplicateNodePruning, SparseChildNodeCreator sparseChildNodeCreator) { super(deckPlayer0, deckPlayer1, sparseChildNodeCreator); lethal_ = false; startTime_ = System.currentTimeMillis(); maxTime_ = maxThinkTime; timedOut_ = false; this.useDuplicateNodePruning = useDuplicateNodePruning; if (useDuplicateNodePruning) boardsAlreadySeen = new HashSet<>(500000); } /** * Recursively generate all possible moves * This function recursively generates all possible moves that can be done starting from a given BoardState. * While generating the moves, it applies the scoring function to each BoardState generated, and it will only keep the * highest scoring branch. * The results are stored in a tree structure and returned as a tree of BoardState class. * * @param boardStateNode The initial BoardState wrapped in a HearthTreeNode. * @param scoreFunc The scoring function for AI. * @return boardStateNode manipulated such that all subsequent actions are children of the original boardStateNode input. */ @Override public HearthTreeNode doMoves(HearthTreeNode boardStateNode, BoardScorer ai) throws HSException { log.trace("recursively performing moves"); if (lethal_) { log.debug("found lethal"); // if it's lethal, we don't have to do anything ever. Just play the lethal. return null; } if (System.currentTimeMillis() - startTime_ > maxTime_) { log.debug("setting think time over"); timedOut_ = true; } if (timedOut_) { log.debug("think time is already over"); // Time's up! no more thinking... return null; } boolean lethalFound = false; if (boardStateNode.data_.isLethalState()) { // one of the players is dead, no reason to keep playing lethal_ = true; lethalFound = true; } boardStateNode.setScore(ai.boardScore(boardStateNode.data_)); // We can end up with children at this state, for example, after a battle cry. If we don't have children yet, create them. ++numNodes; if (!lethalFound && boardStateNode.numChildren() <= 0) { if (useDuplicateNodePruning) { if (boardsAlreadySeen.contains(boardStateNode.data_)) { boardStateNode.setBestChildScore(boardStateNode.getScore()); ++numDuplicates; return null; } else { boardsAlreadySeen.add(boardStateNode.data_); } } ArrayList<HearthTreeNode> nodes = this.createChildren(boardStateNode); boardStateNode.addChildren(nodes); } if (boardStateNode.isLeaf()) { // If at this point the node has no children, it is a leaf node. Set its best child score to its own score. boardStateNode.setBestChildScore(boardStateNode.getScore()); } else { // If it is not a leaf, set the score as the maximum score of its children. // We can also throw out any children that don't have the highest score (boy, this sounds so wrong...) double tmpScore; double bestScore = 0; HearthTreeNode bestBranch = null; for (HearthTreeNode child : boardStateNode.getChildren()) { // try { this.doMoves(child, ai); // Don't need to check lethal because lethal states shouldn't get children. Even if they do, doMoves resolves the issue. // } catch (StackOverflowError e) { // e.printStackTrace(); // } tmpScore = child.getBestChildScore(); // We need to add the card score after child scoring because CardDrawNode children // do not inherit the value of drawn cards // TODO Children of CardDrawNodes should be able to track this on their own. Doing // it this way "breaks" the best score chain and makes it harder to isolate and test // scoring. It effectively "bubbles down" the tree but it isn't sent through when // creating children. if (child instanceof CardDrawNode) { tmpScore += ((CardDrawNode)child).cardDrawScore(deckPlayer0_, ai); } if (bestBranch == null || tmpScore > bestScore) { bestBranch = child; bestScore = tmpScore; } } // TODO this should be automatically handled else where... // Cannot continue past a StopNode except RNG nodes. The children of RNG nodes will be used // during resolution so if we clear them out it can get awkward if (bestBranch instanceof StopNode && !(bestBranch instanceof RandomEffectNode)) { bestBranch.clearChildren(); } if (boardStateNode instanceof RandomEffectNode) { // Set best child score according to the random effect score. We need to set this after all descendants have been calculated double boardScore = ((RandomEffectNode)boardStateNode).weightedAverageBestChildScore(); boardStateNode.setBestChildScore(boardScore); // TODO do we want to override boardStateNode.score here? } else { boardStateNode.clearChildren(); boardStateNode.addChild(bestBranch); if (bestBranch != null) { boardStateNode.setBestChildScore(bestBranch.getBestChildScore()); } } } return boardStateNode; } }