package com.hearthsim.player.playercontroller; import com.hearthsim.card.minion.Minion; import com.hearthsim.exception.HSException; import com.hearthsim.exception.HSInvalidCardException; import com.hearthsim.exception.HSInvalidParamFileException; import com.hearthsim.exception.HSParamNotFoundException; import com.hearthsim.io.ParamFile; import com.hearthsim.model.BoardModel; import com.hearthsim.model.PlayerModel; import com.hearthsim.util.CardFactory; import com.hearthsim.util.HearthActionBoardPair; import com.hearthsim.util.factory.BoardStateFactoryBase; import com.hearthsim.util.factory.DepthBoardStateFactory; import com.hearthsim.util.factory.SparseBoardStateFactory; import com.hearthsim.util.tree.HearthTreeNode; import com.hearthsim.util.tree.StopNode; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Set; public class BruteForceSearchAI implements ArtificialPlayer { private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this.getClass()); private final static int MAX_THINK_TIME = 20000; private boolean useSparseBoardStateFactory_ = true; private boolean useDuplicateNodePruning = true; public WeightedScorer scorer = new WeightedScorer(); protected BruteForceSearchAI() { } // TODO: come up with more meaningful names for these different AI 'styles' public static BruteForceSearchAI buildStandardAI2() { BruteForceSearchAI artificialPlayer = buildStandardAI1(); artificialPlayer.scorer.setTauntWeight(0); artificialPlayer.scorer.setSpellDamageAddWeight(0.9); artificialPlayer.scorer.setSpellDamageMultiplierWeight(1); artificialPlayer.scorer.setMyDivineShieldWeight(1); artificialPlayer.scorer.setEnemyDivineShieldWeight(1); return artificialPlayer; } public static BruteForceSearchAI buildStandardAI1() { BruteForceSearchAI artificialPlayer = new BruteForceSearchAI(); artificialPlayer.scorer.setMyAttackWeight(0.9); artificialPlayer.scorer.setMyHealthWeight(0.9); artificialPlayer.scorer.setEnemyAttackWeight(1.0); artificialPlayer.scorer.setEnemyHealthWeight(1.0); artificialPlayer.scorer.setTauntWeight(1.0); artificialPlayer.scorer.setMyHeroHealthWeight(0.1); artificialPlayer.scorer.setEnemyHeroHealthWeight(0.1); artificialPlayer.scorer.setManaWeight(0.1); artificialPlayer.scorer.setMyNumMinionsWeight(0.5); artificialPlayer.scorer.setEnemyNumMinionsWeight(0.5); artificialPlayer.scorer.setSpellDamageAddWeight(0.0); artificialPlayer.scorer.setSpellDamageMultiplierWeight(0.5); artificialPlayer.scorer.setMyDivineShieldWeight(0.0); artificialPlayer.scorer.setEnemyDivineShieldWeight(0.0); artificialPlayer.scorer.setMyWeaponWeight(0.5); artificialPlayer.scorer.setEnemyWeaponWeight(0.5); return artificialPlayer; } /** * Constructor * This is the preferred (non-deprecated) way to instantiate this class * * @param aiParamFile The path to the input parameter file * @throws IOException * @throws HSInvalidParamFileException */ public BruteForceSearchAI(Path aiParamFile) throws IOException, HSInvalidParamFileException { ParamFile pFile = new ParamFile(aiParamFile); try { this.scorer.setMyAttackWeight(pFile.getDouble("w_a")); this.scorer.setMyHealthWeight(pFile.getDouble("w_h")); this.scorer.setEnemyAttackWeight(pFile.getDouble("wt_a")); this.scorer.setEnemyHealthWeight(pFile.getDouble("wt_h")); this.scorer.setTauntWeight(pFile.getDouble("w_taunt")); this.scorer.setMyHeroHealthWeight(pFile.getDouble("w_health")); this.scorer.setEnemyHeroHealthWeight(pFile.getDouble("wt_health")); this.scorer.setMyNumMinionsWeight(pFile.getDouble("w_num_minions")); this.scorer.setEnemyNumMinionsWeight(pFile.getDouble("wt_num_minions")); // The following two have default values for now... // These are rather arcane parameters, so please understand // them before attempting to change them. this.scorer.setSpellDamageMultiplierWeight(pFile.getDouble("w_sd_mult", 1.0)); this.scorer.setSpellDamageAddWeight(pFile.getDouble("w_sd_add", 0.9)); // Divine Shield defualts to 0 for now this.scorer.setMyDivineShieldWeight(pFile.getDouble("w_divine_shield", 0.0)); this.scorer.setEnemyDivineShieldWeight(pFile.getDouble("wt_divine_shield", 0.0)); // weapon score for the hero this.scorer.setMyWeaponWeight(pFile.getDouble("w_weapon", 0.5)); this.scorer.setEnemyWeaponWeight(pFile.getDouble("wt_weapon", 0.5)); // charge model score this.scorer.setMyChargeWeight(pFile.getDouble("w_charge", 0.0)); this.scorer.setManaWeight(pFile.getDouble("w_mana", 0.1)); useSparseBoardStateFactory_ = pFile.getBoolean("use_sparse_board_state_factory", true); useDuplicateNodePruning = pFile.getBoolean("use_duplicate_node_pruning", true); // Look for a pattern: card_in_hand_value_* Set<String> keys = pFile.getKeysContaining("card_in_hand_score_"); for (String key : keys) { String cardName = key.substring(19); try { this.scorer.putCardInHandExtraScore(CardFactory.getCard(cardName).getClass(), pFile.getDouble(key)); } catch (HSInvalidCardException e) { throw new HSInvalidParamFileException("Invalid key: " + key); } } // Look for a pattern: minion_on_board_value_* keys = pFile.getKeysContaining("minion_on_board_value_"); for (String key : keys) { String minionName = key.substring(22); try { Minion minion = (Minion) CardFactory.getCard(minionName); this.scorer.putMinionOnBoardExtraScore(minion.getClass(), pFile.getDouble(key)); } catch (HSInvalidCardException e) { throw new HSInvalidParamFileException("Invalid key: " + key); } } } catch(HSParamNotFoundException e) { log.error(e.getMessage()); System.exit(1); } } public boolean getUseSparseBoardStateFactory() { return useSparseBoardStateFactory_; } public void setUseSparseBoardStateFactory(boolean value) { useSparseBoardStateFactory_ = value; } public boolean getUseDuplicateNodePruning() { return useDuplicateNodePruning; } public void setUseDuplicateNodePruning(boolean value) { useDuplicateNodePruning = value; } @Override public List<HearthActionBoardPair> playTurn(int turn, BoardModel board) throws HSException { PlayerModel playerModel0 = board.getCurrentPlayer(); PlayerModel playerModel1 = board.getWaitingPlayer(); BoardStateFactoryBase factory; if (useSparseBoardStateFactory_) { factory = new SparseBoardStateFactory(playerModel0.getDeck(), playerModel1.getDeck(), MAX_THINK_TIME, useDuplicateNodePruning); } else { factory = new DepthBoardStateFactory(playerModel0.getDeck(), playerModel1.getDeck(), MAX_THINK_TIME, useDuplicateNodePruning); } return this.playTurn(turn, board, factory); } @Override public List<HearthActionBoardPair> playTurn(int turn, BoardModel board, BoardStateFactoryBase factory) throws HSException { PlayerModel playerModel0 = board.getCurrentPlayer(); log.debug("playing turn for " + playerModel0.getName()); // The goal of this ai is to maximize his board score log.debug("start turn board state is {}", board); HearthTreeNode toRet = new HearthTreeNode(board); HearthTreeNode allMoves = factory.doMoves(toRet, this.scorer); ArrayList<HearthActionBoardPair> retList = new ArrayList<>(); HearthTreeNode curMove = allMoves; while(curMove.getChildren() != null) { curMove = curMove.getChildren().get(0); if (curMove instanceof StopNode) { // Add the initial step that created the StopNode retList.add(new HearthActionBoardPair(curMove.getAction(), curMove.data_.deepCopy())); // Force the step to resolve HearthTreeNode allEffectsDone = ((StopNode)curMove).finishAllEffects(); // Add the resolution to action list retList.add(new HearthActionBoardPair(allEffectsDone.getAction(), allEffectsDone.data_.deepCopy())); // Continue the turn List<HearthActionBoardPair> nextMoves = this.playTurn(turn, allEffectsDone.data_); if (nextMoves.size() > 0) { for ( HearthActionBoardPair actionBoard : nextMoves) { retList.add(actionBoard); } } break; } else { retList.add(new HearthActionBoardPair(curMove.getAction(), curMove.data_)); } } return retList; } @Override public ArtificialPlayer deepCopy() { BruteForceSearchAI copied = new BruteForceSearchAI(); copied.scorer = this.scorer.deepCopy(); copied.useSparseBoardStateFactory_ = useSparseBoardStateFactory_; copied.useDuplicateNodePruning = useDuplicateNodePruning; return copied; } @Override public int getMaxThinkTime() { return MAX_THINK_TIME; } }