package net.sf.colossus.ai; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.logging.Logger; import net.sf.colossus.ai.helper.LegionMove; import net.sf.colossus.client.Client; import net.sf.colossus.client.CritterMove; import net.sf.colossus.client.LegionClientSide; import net.sf.colossus.client.PlayerClientSide; import net.sf.colossus.common.Constants; import net.sf.colossus.common.Options; import net.sf.colossus.game.Battle; import net.sf.colossus.game.BattleCritter; import net.sf.colossus.game.Dice; import net.sf.colossus.game.EntrySide; import net.sf.colossus.game.Legion; import net.sf.colossus.game.Player; import net.sf.colossus.game.PlayerColor; import net.sf.colossus.game.SummonInfo; import net.sf.colossus.util.Glob; import net.sf.colossus.util.InstanceTracker; import net.sf.colossus.util.PermutationIterator; import net.sf.colossus.util.Probs; import net.sf.colossus.util.ValueRecorder; import net.sf.colossus.variant.BattleHex; import net.sf.colossus.variant.CreatureType; import net.sf.colossus.variant.HazardTerrain; import net.sf.colossus.variant.MasterBoardTerrain; import net.sf.colossus.variant.MasterHex; import net.sf.colossus.xmlparser.TerrainRecruitLoader; /** * Simple implementation of a Titan AI * * TODO somehow we call client.getOwningPlayer() a lot -- there should probably be a better * link between AI and player, after all the AI either IS_A player or PLAYS_FOR a player * * * @author Bruce Sherrod, David Ripton * @author Romain Dolbeau */ public class SimpleAI extends AbstractAI { private static final Logger LOGGER = Logger.getLogger(SimpleAI.class .getName()); /** * Stores the skill and power bonuses for a single terrain. * * For internal use only, so we don't bother with encapsulation. */ private static class TerrainBonuses { final int attackerPower; final int defenderPower; final int attackerSkill; final int defenderSkill; TerrainBonuses(int attackerPower, int defenderPower, int attackerSkill, int defenderSkill) { this.attackerPower = attackerPower; this.defenderPower = defenderPower; this.attackerSkill = attackerSkill; this.defenderSkill = defenderSkill; } } /** * Maps the terrain names to their matching bonuses. * * Only the terrains that have bonuses are in this map, so * users have to expect to retrieve null values. Note that * the terrain names include names for master board and * hazard terrains, so it can be used for lookup up either * type. * * TODO there seems to be some overlap with * {@link HazardTerrain#isNativeBonusTerrain()} and * {@link HazardTerrain#isNonNativePenaltyTerrain()}. * * This is a Map<String,TerrainBonuses>. * * TODO: this shouldn't be here, this is a property of the Variant player * (well, not yet for Hazard, but it should be, someday). * Actually, this doesn't make sense to me (RD). tower has bonus for * both attacker & defender (because of walls, I assume), but is special- * cased for attacker & defender. Brush and Jungle assumes Brushes, but * Jungle has Tree, too. And the comments themselves makes clear that * 'Sand' is actually 'Slope', but mixing up Attacker & Native and Defender * & non-native. * This and calcBonus should be reviewed thoroughly. */ private static final Map<String, TerrainBonuses> TERRAIN_BONUSES = new HashMap<String, TerrainBonuses>(); static { // strike down wall, defender strike up TERRAIN_BONUSES.put("Tower", new TerrainBonuses(0, 0, 1, 1)); // native in bramble has skill to hit increased by 1 TERRAIN_BONUSES.put("Brush", new TerrainBonuses(0, 0, 0, 1)); TERRAIN_BONUSES.put("Jungle", new TerrainBonuses(0, 0, 0, 1)); TERRAIN_BONUSES.put("Brambles", new TerrainBonuses(0, 0, 0, 1)); // native gets an extra die when attack down slope // non-native loses 1 skill when attacking up slope TERRAIN_BONUSES.put("Hills", new TerrainBonuses(1, 0, 0, 1)); // native gets an extra 2 dice when attack down dune // non-native loses 1 die when attacking up dune TERRAIN_BONUSES.put("Desert", new TerrainBonuses(2, 1, 0, 0)); TERRAIN_BONUSES.put("Sand", new TerrainBonuses(2, 1, 0, 0)); // Native gets extra 1 die when attack down slope // non-native loses 1 skill when attacking up slope TERRAIN_BONUSES.put("Mountains", new TerrainBonuses(1, 0, 0, 1)); TERRAIN_BONUSES.put("Volcano", new TerrainBonuses(1, 0, 0, 1)); // the other types have only movement bonuses } protected int timeLimit = Constants.DEFAULT_AI_TIME_LIMIT; // in s boolean timeIsUp; private int splitsDone = 0; private int splitsAcked = 0; private List<String> remainingMarkers = null; public SimpleAI(Client client) { super(client); // small bonus to the 'kill value' of a creature when there's some // terrain bonus to combat. cvc.HAS_NATIVE_COMBAT_BONUS = 3; // initialize the creature info needed by the AI InstanceTracker.register(this, client.getOwningPlayer().getName()); } public PlayerColor pickColor(List<PlayerColor> colors, List<PlayerColor> favoriteColors) { for (PlayerColor preferredColor : favoriteColors) { if (colors.contains(preferredColor)) { return preferredColor; } } // Can't have one of our favorites, so take what's there. for (PlayerColor color : colors) { return color; } return null; } /* prepare a list of markers, first those with preferred color, * then all the others, but inside these groups shuffled. * Caller can then always just take next to get a "random" marker. * @param markerIds list of available markers to prepare * @param preferredShortColor thos with this color first * @returns list of markers */ private List<String> prepareMarkers(Set<String> markerIds, String preferredShortColor) { List<String> myMarkerIds = new ArrayList<String>(); List<String> otherMarkerIds = new ArrayList<String>(); List<String> allMarkerIds = new ArrayList<String>(); // split between own / other for (String markerId : markerIds) { if (preferredShortColor != null && markerId.startsWith(preferredShortColor)) { myMarkerIds.add(markerId); } else { otherMarkerIds.add(markerId); } } if (!(myMarkerIds.isEmpty())) { Collections.shuffle(myMarkerIds, random); allMarkerIds.addAll(myMarkerIds); } if (!(otherMarkerIds.isEmpty())) { Collections.shuffle(otherMarkerIds, random); allMarkerIds.addAll(otherMarkerIds); } return allMarkerIds; } public String pickMarker(Set<String> markerIds, String preferredShortColor) { List<String> myMarkerIds = new ArrayList<String>(); List<String> otherMarkerIds = new ArrayList<String>(); // split between own / other for (String markerId : markerIds) { if (preferredShortColor != null && markerId.startsWith(preferredShortColor)) { myMarkerIds.add(markerId); } else { otherMarkerIds.add(markerId); } } if (!(myMarkerIds.isEmpty())) { Collections.shuffle(myMarkerIds, random); return myMarkerIds.get(0); } if (!(otherMarkerIds.isEmpty())) { Collections.shuffle(otherMarkerIds, random); return otherMarkerIds.get(0); } return null; } public void muster() { client.resetRecruitReservations(); // Do not recruit if this legion is a scooby snack. double scoobySnackFactor = 0.15; int minimumSizeToRecruit = (int)(scoobySnackFactor * client .getGameClientSide().getAverageLegionPointValue()); for (LegionClientSide legion : client.getOwningPlayer().getLegions()) { if (client.canRecruit(legion) && (legion.hasTitan() || legion.getPointValue() >= minimumSizeToRecruit)) { CreatureType recruit = chooseRecruit(legion, legion.getCurrentHex(), true); if (recruit != null) { List<String> recruiters = client.findEligibleRecruiters( legion, recruit); String recruiterName = null; if (!recruiters.isEmpty()) { // Just take the first one. recruiterName = recruiters.get(0); } client.doRecruit(legion, recruit.getName(), recruiterName); client.reserveRecruit(recruit); } } } client.resetRecruitReservations(); } public void reinforce(Legion legion) { CreatureType recruit = chooseRecruit(((LegionClientSide)legion), legion.getCurrentHex(), false); String recruitName = null; String recruiterName = null; if (recruit != null) { recruitName = recruit.getName(); List<String> recruiters = client.findEligibleRecruiters(legion, recruit); if (!recruiters.isEmpty()) { recruiterName = recruiters.get(0); } } // Call regardless to advance past recruiting. client.doRecruit(legion, recruitName, recruiterName); } CreatureType chooseRecruit(LegionClientSide legion, MasterHex hex, boolean considerReservations) { List<CreatureType> recruits = client.findEligibleRecruits(legion, hex, considerReservations); if (recruits.size() == 0) { return null; } CreatureType recruit = getVariantRecruitHint(legion, hex, recruits); /* use the hinted value as the actual recruit */ return recruit; } public boolean split() { Player player = client.getOwningPlayer(); remainingMarkers = prepareMarkers(player.getMarkersAvailable(), player.getShortColor()); splitsDone = 0; splitsAcked = 0; for (Legion legion : player.getLegions()) { if (remainingMarkers.isEmpty()) { break; } splitOneLegion(player, legion); } remainingMarkers.clear(); remainingMarkers = null; // if we did splits, don't signal to client that it's done; // because it would do doneWithSplits immediately; // instead the last didSplit callback will do doneWithSplits // (This is done to avoid the advancePhase illegally messages) return splitsDone <= 0; } /** Unused in this AI; just return true to indicate done. */ public boolean splitCallback(Legion parent, Legion child) { splitsAcked++; return splitsAcked >= splitsDone; } private void splitOneLegion(Player player, Legion legion) { if (legion.getHeight() < 7) { return; } // Do not split if we're likely to be forced to attack and lose // Do not split if we're likely to want to fight and we need to // be 7 high. // Do not split if there's no upwards recruiting or angel // acquiring potential. // TODO: Don't split if we're about to be attacked and we // need the muscle // Only consider this if we're not doing initial game split if (legion.getHeight() == 7) { int forcedToAttack = 0; boolean goodRecruit = false; for (int roll = 1; roll <= 6; roll++) { Set<MasterHex> moves = client.getMovement().listAllMoves( legion, legion.getCurrentHex(), roll); int safeMoves = 0; for (MasterHex hex : moves) { if (client.getGameClientSide() .getEnemyLegions(hex, player).size() == 0) { safeMoves++; if (!goodRecruit && couldRecruitUp(legion, hex, null)) { goodRecruit = true; } } else { Legion enemy = client.getGameClientSide() .getFirstEnemyLegion(hex, player); int result = estimateBattleResults(legion, true, enemy, hex); if (result == WIN_WITH_MINIMAL_LOSSES) { LOGGER .finest("We can safely split AND attack with " + legion); safeMoves++; // Also consider acquiring angel. if (!goodRecruit && couldRecruitUp(legion, hex, enemy)) { goodRecruit = true; } } int result2 = estimateBattleResults(legion, false, enemy, hex); if (result2 == WIN_WITH_MINIMAL_LOSSES && result != WIN_WITH_MINIMAL_LOSSES && roll <= 4) { // don't split so that we can attack! LOGGER.finest("Not splitting " + legion + " because we want the muscle to attack"); forcedToAttack = 999; return; } } } if (safeMoves == 0) { forcedToAttack++; } // If we'll be forced to attack on 2 or more rolls, // don't split. if (forcedToAttack >= 2) { return; } } if (!goodRecruit) { // No point in splitting, since we can't improve. LOGGER.finest("Not splitting " + legion + " because it can't improve from here"); return; } } if (remainingMarkers.isEmpty()) { LOGGER.finest("Not splitting " + legion + " because no markers available"); return; } String newMarkerId = remainingMarkers.get(0); remainingMarkers.remove(0); List<CreatureType> creatures = chooseCreaturesToSplitOut(legion); // increment BEFORE calling client // (instead of: return true and caller increments). // Otherwise we might have a race situation, if callback is quicker // than caller incrementing the splitsDone value... this.splitsDone++; client.sendDoSplitToServer(legion, newMarkerId, creatures); } /** Decide how to split this legion, and return a list of * Creatures to remove. */ private List<CreatureType> chooseCreaturesToSplitOut(Legion legion) { // // split a 7 or 8 high legion somehow // // idea: pick the 2 weakest creatures and kick them // out. if there are more than 2 weakest creatures, // prefer a pair of matching ones. // // For an 8-high starting legion, call helper // method doInitialGameSplit() // // TODO: keep 3 cyclops if we don't have a behemoth // (split out a gorgon instead) // // TODO: prefer to split out creatures that have no // recruiting value (e.g. if we have 1 angel, 2 // centaurs, 2 gargoyles, and 2 cyclops, split out the // gargoyles) // if (legion.getHeight() == 8) { return doInitialGameSplit(legion.getCurrentHex()); } return findWeakestTwoCritters((LegionClientSide)legion); } /** * Find the two weakest creatures in a legion according to * @link {@link #chooseCreaturesToSplitOut(Legion)} * * @param legion * @return List containing the CreatureTypes of the two weakest critters */ List<CreatureType> findWeakestTwoCritters(LegionClientSide legion) { CreatureType weakest1 = null; CreatureType weakest2 = null; for (CreatureType critter : legion.getCreatureTypes()) { // Never split out the titan. if (critter.isTitan()) { continue; } if (weakest1 == null) { weakest1 = critter; } else if (weakest2 == null) { weakest2 = critter; } else if (getHintedRecruitmentValue(critter, legion, hintSectionUsed) < getHintedRecruitmentValue(weakest1, legion, hintSectionUsed)) { weakest1 = critter; } else if (getHintedRecruitmentValue(critter, legion, hintSectionUsed) < getHintedRecruitmentValue(weakest2, legion, hintSectionUsed)) { weakest2 = critter; } else if (getHintedRecruitmentValue(critter, legion, hintSectionUsed) == getHintedRecruitmentValue(weakest1, legion, hintSectionUsed) && getHintedRecruitmentValue(critter, legion, hintSectionUsed) == getHintedRecruitmentValue( weakest2, legion, hintSectionUsed)) { if (critter.getName().equals(weakest1.getName())) { weakest2 = critter; } else if (critter.getName().equals(weakest2.getName())) { weakest1 = critter; } } } List<CreatureType> weakestTwoCritters = new ArrayList<CreatureType>(); weakestTwoCritters.add(weakest1); weakestTwoCritters.add(weakest2); return weakestTwoCritters; } // From Hugh Moore: // // It really depends on how many players there are and how good I // think they are. In a 5 or 6 player game, I will pretty much // always put my gargoyles together in my Titan group. I need the // extra strength, and I need it right away. In 3-4 player // games, I certainly lean toward putting my gargoyles together. // If my opponents are weak, I sometimes split them for a // challenge. If my opponents are strong, but my situation looks // good for one reason or another, I may split them. I never // like to split them when I am in tower 3 or 6, for obvious // reasons. In two player games, I normally split the gargoyles, // but two player games are messed up. // /** Return a list of exactly four creatures (including one lord) to * split out. */ List<CreatureType> doInitialGameSplit(MasterHex hex) { List<CreatureType> hintSuggestedSplit = getInitialSplitHint(hex); /* Log.debug("HINT: suggest splitting " + hintSuggestedSplit + " in " + label); */ if (!((hintSuggestedSplit == null) || (hintSuggestedSplit.size() != 4))) { return hintSuggestedSplit; } CreatureType[] startCre = TerrainRecruitLoader .getStartingCreatures(hex); // in CMU style splitting, we split centaurs in even towers, // ogres in odd towers. final boolean oddTower = "100".equals(hex.getLabel()) || "300".equals(hex.getLabel()) || "500".equals(hex.getLabel()); final CreatureType splitCreature = oddTower ? startCre[2] : startCre[0]; final CreatureType nonsplitCreature = oddTower ? startCre[0] : startCre[2]; // XXX Hardcoded to default board. // don't split gargoyles in tower 3 or 6 (because of the extra jungles) if ("300".equals(hex.getLabel()) || "600".equals(hex.getLabel())) { return CMUsplit(false, splitCreature, nonsplitCreature, hex); } // // new idea due to David Ripton: split gargoyles in tower 2 or // 5, because on a 5 we can get to brush and jungle and get 2 // gargoyles. I'm not sure if this is really better than recruiting // a cyclops and leaving the other group in the tower, but it's // interesting so we'll try it. // else if ("200".equals(hex.getLabel()) || "500".equals(hex.getLabel())) { return MITsplit(true, splitCreature, nonsplitCreature, hex); } // // otherwise, mix it up for fun else { if (Dice.rollDie() <= 3) { return MITsplit(true, splitCreature, nonsplitCreature, hex); } else { return CMUsplit(true, splitCreature, nonsplitCreature, hex); } } } /** Keep the gargoyles together. */ private List<CreatureType> CMUsplit(boolean favorTitan, CreatureType splitCreature, CreatureType nonsplitCreature, MasterHex hex) { CreatureType[] startCre = TerrainRecruitLoader .getStartingCreatures(hex); List<CreatureType> splitoffs = new LinkedList<CreatureType>(); if (favorTitan) { if (Dice.rollDie() <= 3) { splitoffs.add(variant.getCreatureByName(Constants.titan)); splitoffs.add(startCre[1]); splitoffs.add(startCre[1]); splitoffs.add(splitCreature); } else { splitoffs.add(variant.getCreatureByName(variant .getPrimaryAcquirable())); splitoffs.add(nonsplitCreature); splitoffs.add(nonsplitCreature); splitoffs.add(splitCreature); } } else { if (Dice.rollDie() <= 3) { splitoffs.add(variant.getCreatureByName(Constants.titan)); } else { splitoffs.add(variant.getCreatureByName(variant .getPrimaryAcquirable())); } if (Dice.rollDie() <= 3) { splitoffs.add(startCre[1]); splitoffs.add(startCre[1]); splitoffs.add(splitCreature); } else { splitoffs.add(nonsplitCreature); splitoffs.add(nonsplitCreature); splitoffs.add(splitCreature); } } return splitoffs; } /** Split the gargoyles. */ private List<CreatureType> MITsplit(boolean favorTitan, CreatureType splitCreature, CreatureType nonsplitCreature, MasterHex hex) { CreatureType[] startCre = TerrainRecruitLoader .getStartingCreatures(hex); List<CreatureType> splitoffs = new LinkedList<CreatureType>(); if (favorTitan) { if (Dice.rollDie() <= 3) { splitoffs.add(variant.getCreatureByName(Constants.titan)); splitoffs.add(nonsplitCreature); splitoffs.add(nonsplitCreature); splitoffs.add(startCre[1]); } else { splitoffs.add(variant.getCreatureByName(variant .getPrimaryAcquirable())); splitoffs.add(splitCreature); splitoffs.add(splitCreature); splitoffs.add(startCre[1]); } } else { if (Dice.rollDie() <= 3) { splitoffs.add(variant.getCreatureByName(Constants.titan)); } else { splitoffs.add(variant.getCreatureByName(variant .getPrimaryAcquirable())); } if (Dice.rollDie() <= 3) { splitoffs.add(nonsplitCreature); splitoffs.add(nonsplitCreature); splitoffs.add(startCre[1]); } else { splitoffs.add(splitCreature); splitoffs.add(splitCreature); splitoffs.add(startCre[1]); } } return splitoffs; } /** Do a masterboard move (or consider taking mulligan, if feasible). * * Returns true if we need to run this method again after the server * updates the client with the results of a move or mulligan. */ public boolean masterMove() { boolean moved = false; PlayerClientSide player = client.getOwningPlayer(); // consider mulligans if (handleMulligans(player)) { return true; } /** cache all places enemies can move to, for use in risk analysis. */ Map<MasterHex, List<Legion>>[] enemyAttackMap = buildEnemyAttackMap(player); // A mapping from Legion to List of MoveInfo objects, // listing all moves that we've evaluated. We use this if // we're forced to move. Map<Legion, List<MoveInfo>> moveMap = new HashMap<Legion, List<MoveInfo>>(); moved = handleVoluntaryMoves(player, moveMap, enemyAttackMap); if (moved) { return true; } // make sure we move splits (when forced) moved = handleForcedSplitMoves(player, moveMap); if (moved) { return true; } // make sure we move at least one legion if (!player.hasMoved()) { moved = handleForcedSingleMove(player, moveMap); // Earlier here was a comment: // "always need to retry" and hardcoded returned true. // In [ 1748718 ] Game halt in Abyssal9 this lead to a deadlock; // - so, if here is returned "false" as for "I won't do any more // move", that problem does not occur (server recognizes that // there is no legal move and accepts it) // -- does this cause negative side effects elsewhere?? // Let's try ;-) return moved; } return false; } /** * Take a mulligan if roll is 2 or 5 in first turn, and can still take * a mulligan. * Returns true if AI took a mulligan, false otherwise. */ boolean handleMulligans(Player player) { // TODO: This is really stupid. Do something smart here. if (client.getTurnNumber() == 1 && player.getMulligansLeft() > 0 && (client.getGame().getMovementRoll() == 2 || client.getGame() .getMovementRoll() == 5) && !client.tookMulligan()) { client.mulligan(); // TODO Need to wait for new movement roll. return true; } return false; } /** Return true if we moved something. */ private boolean handleVoluntaryMoves(PlayerClientSide player, Map<Legion, List<MoveInfo>> moveMap, Map<MasterHex, List<Legion>>[] enemyAttackMap) { boolean moved = false; // TODO this is still List<LegionClientSide> to get the Comparable // -> use a Comparator instead since we are the only ones needing this List<LegionClientSide> legions = player.getLegions(); // Sort markerIds in descending order of legion importance. Collections.sort(legions, Legion.ORDER_TITAN_THEN_POINTS); for (LegionClientSide legion : legions) { if (legion.hasMoved() || legion.getCurrentHex() == null) { continue; } // compute the value of sitting still List<MoveInfo> moveList = new ArrayList<MoveInfo>(); moveMap.put(legion, moveList); ValueRecorder why = new ValueRecorder(); MoveInfo sitStillMove = new MoveInfo(legion, null, evaluateMove( legion, legion.getCurrentHex(), false, enemyAttackMap, why), 0, why); moveList.add(sitStillMove); // find the best move (1-ply search) MasterHex bestHex = null; MoveInfo bestMove = null; int bestValue = Integer.MIN_VALUE; Set<MasterHex> set = client.getMovement().listAllMoves(legion, legion.getCurrentHex(), client.getGame().getMovementRoll()); for (MasterHex hex : set) { // TODO // Do not consider moves onto hexes where we already have a // legion. This is sub-optimal since the legion in this hex // may be able to move and "get out of the way" if (client.getGameClientSide().getFriendlyLegions(hex, player) .size() > 0) { continue; } ValueRecorder whyR = new ValueRecorder(); final int value = evaluateMove(legion, hex, true, enemyAttackMap, whyR); MoveInfo move = new MoveInfo(legion, hex, value, value - sitStillMove.value, whyR); if (value > bestValue || bestHex == null) { bestValue = value; bestHex = hex; bestMove = move; } moveList.add(move); } // if we found a move that's better than sitting still, move if (bestValue > sitStillMove.value && bestHex != null) { moved = doMove(legion, bestHex); if (moved) { if (bestMove != null) { LOGGER.finer("Moved " + legion + " to " + bestHex + " after evaluating: " + bestMove.why.toString() + " is better than sitting tight: " + sitStillMove.why.toString()); } else { LOGGER.warning("Moved " + legion + " to " + bestHex + " after evaluating: " + " BEST MOVE IS NULL! RUN FOR COVER! " + " is better than sitting tight: " + sitStillMove.why.toString()); } return true; } } } return false; } /** Return true if we moved something. */ private boolean handleForcedSplitMoves(Player player, Map<Legion, List<MoveInfo>> moveMap) { for (Legion legion : player.getLegions()) { List<Legion> friendlyLegions = client.getGameClientSide() .getFriendlyLegions(legion.getCurrentHex(), player); if (friendlyLegions.size() > 1 && !client .getMovement() .listNormalMoves(legion, legion.getCurrentHex(), client.getGame().getMovementRoll()).isEmpty()) { // Pick the legion in this hex whose best move has the // least difference with its sitStillValue, scaled by // the point value of the legion, and force it to move. LOGGER.finest("Ack! forced to move a split group"); // first, concatenate all the moves for all the // legions that are here, and sort them by their // difference from sitting still multiplied by // the value of the legion. List<MoveInfo> allmoves = new ArrayList<MoveInfo>(); for (Legion friendlyLegion : friendlyLegions) { List<MoveInfo> moves = moveMap.get(friendlyLegion); if (moves != null) { allmoves.addAll(moves); } } Collections.sort(allmoves, new Comparator<MoveInfo>() { public int compare(MoveInfo m1, MoveInfo m2) { return m2.difference * (m2.legion).getPointValue() - m1.difference * (m1.legion).getPointValue(); } }); // now, one at a time, try applying moves until we // have handled our split problem. for (MoveInfo move : allmoves) { if (move.hex == null) { continue; // skip the sitStill moves } LOGGER.finest("forced to move split legion " + move.legion + " to " + move.hex + " taking penalty " + move.difference + " in order to handle illegal legion " + legion); boolean moved = doMove(move.legion, move.hex); if (moved) { LOGGER.finer("Moved " + move.legion + " to " + move.hex + " after evaluating: " + move.why.toString() + " as we couldn't sit tight"); return true; } } } } return false; } private boolean handleForcedSingleMove(Player player, Map<Legion, List<MoveInfo>> moveMap) { LOGGER.finest("Ack! forced to move someone"); // Pick the legion whose best move has the least // difference with its sitStillValue, scaled by the // point value of the legion, and force it to move. // first, concatenate all the moves all legions, and // sort them by their difference from sitting still List<MoveInfo> allmoves = new ArrayList<MoveInfo>(); for (Legion friendlyLegion : player.getLegions()) { List<MoveInfo> moves = moveMap.get(friendlyLegion); if (moves != null) { allmoves.addAll(moves); } } Collections.sort(allmoves, new Comparator<MoveInfo>() { public int compare(MoveInfo m1, MoveInfo m2) { return m2.difference * (m2.legion).getPointValue() - m1.difference * (m1.legion).getPointValue(); } }); // now, one at a time, try applying moves until we have moved a legion for (MoveInfo move : allmoves) { if (move.hex == null) { continue; // skip the sitStill moves } LOGGER.finest("forced to move " + move.legion + " to " + move.hex + " taking penalty " + move.difference + " in order to handle illegal legion " + move.legion); boolean moved = doMove(move.legion, move.hex); if (moved) { LOGGER.finer("Moved " + move.legion + " to " + move.hex + " after evaluating: " + move.why.toString() + " as we must move somebody."); return true; } } LOGGER.warning("handleForcedSingleMove() didn't move anyone - " + "probably no legion can move?? " + "(('see [ 1748718 ] Game halt in Abyssal9'))"); // Let's hope the server sees it the same way, otherwise // we'll loop forever... return false; } private boolean doMove(Legion legion, MasterHex hex) { return client.doMove(legion, hex); } /** cheap, inaccurate evaluation function. Returns a value for * moving this legion to this hex. The value defines a distance * metric over the set of all possible moves. * * TODO: should be parameterized with weights * TODO: the hex parameter is probably not needed anymore now that we * pass the legion instead of just the marker [RD: actually, * handleVoluntaryMove sees to call this one with several different * hexes, so we probably can't remove it] */ private int evaluateMove(LegionClientSide legion, MasterHex hex, boolean moved, Map<MasterHex, List<Legion>>[] enemyAttackMap, ValueRecorder value) { // Avoid using MIN_VALUE and MAX_VALUE because of possible overflow. final int WIN_GAME = Integer.MAX_VALUE / 2; final int LOSE_LEGION = -10000; //int value = 0; // consider making an attack final Legion enemyLegion = client.getGameClientSide() .getFirstEnemyLegion(hex, legion.getPlayer()); if (enemyLegion != null) { final int enemyPointValue = enemyLegion.getPointValue(); final int result = estimateBattleResults(legion, enemyLegion, hex); switch (result) { case WIN_WITH_MINIMAL_LOSSES: LOGGER.finest("legion " + legion + " can attack " + enemyLegion + " in " + hex + " and WIN_WITH_MINIMAL_LOSSES"); // we score a fraction of a basic acquirable value .add( ((variant.getCreatureByName(variant .getPrimaryAcquirable())).getPointValue() * enemyPointValue) / getAcqStepValue(), "Fraction Basic Acquirable"); // plus a fraction of a titan strength // TODO Should be by variant value.add( (6 * enemyPointValue) / variant.getTitanImprovementValue(), "Fraction Titan Strength"); // plus some more for killing a group (this is arbitrary) value.add((10 * enemyPointValue) / 100, "Arbitrary For Killing"); // TODO if enemy titan, we also score half points // (this may make the AI unfairly gun for your titan) break; case WIN_WITH_HEAVY_LOSSES: LOGGER.finest("legion " + legion + " can attack " + enemyLegion + " in " + hex + " and WIN_WITH_HEAVY_LOSSES"); // don't do this with our titan unless we can win the game boolean haveOtherSummonables = false; Player player = legion.getPlayer(); for (Legion l : player.getLegions()) { if (l.equals(legion)) { continue; } if (!l.hasSummonable()) { continue; } haveOtherSummonables = true; break; } if (legion.hasTitan()) { // unless we can win the game with this attack if ((enemyLegion).hasTitan() && client.getGameClientSide() .getNumLivingPlayers() == 2) { // do it and win the game value.add(enemyPointValue, "Killing Last Enemy Titan"); } else { // ack! we'll mess up our titan group value.add(LOSE_LEGION + 10, "<Profanity> Our Titan Group"); } } // don't do this if we'll lose our only summonable group // and won't score enough points to make up for it else if (legion.hasSummonable() && !haveOtherSummonables && enemyPointValue < getAcqStepValue() * .88) { value.add(LOSE_LEGION + 5, "Lose Legion"); } else { // we score a fraction of a basic acquirable value .add( ((variant.getCreatureByName(variant .getPrimaryAcquirable())).getPointValue() * enemyPointValue) / getAcqStepValue(), "Fraction Basic Acquirable 2"); // plus a fraction of a titan strength value.add( (6 * enemyPointValue) / variant.getTitanImprovementValue(), "Fraction Titan Strength 2"); // but we lose this group value.add(-(20 * legion.getPointValue()) / 100, "Lost This Group"); // TODO: if we have no other angels, more penalty here // TODO: if enemy titan, we also score half points // (this may make the AI unfairly gun for your titan) } break; case DRAW: LOGGER.finest("legion " + legion + " can attack " + enemyLegion + " in " + hex + " and DRAW"); // If this is an unimportant group for us, but // is enemy titan, do it. This might be an // unfair use of information for the AI if (legion.numLords() == 0 && enemyLegion.hasTitan()) { // Arbitrary value for killing a player but // scoring no points: it's worth a little // If there are only 2 players, we should do this. if (client.getGameClientSide().getNumLivingPlayers() == 2) { value.resetTo(WIN_GAME, "WinGame"); } else { value.add(enemyPointValue / 6, "Unfairly Assault Titan"); } } else { // otherwise no thanks value.add(LOSE_LEGION + 2, "Lose Legion 2"); } break; case LOSE_BUT_INFLICT_HEAVY_LOSSES: LOGGER.finest("legion " + legion + " can attack " + enemyLegion + " in " + hex + " and LOSE_BUT_INFLICT_HEAVY_LOSSES"); // TODO: how important is it that we damage his group? value.add(LOSE_LEGION + 1, "Lose Legion 3"); break; case LOSE: LOGGER.finest("legion " + legion + " can attack " + enemyLegion + " in " + hex + " and LOSE"); value.add(LOSE_LEGION, "Lose Legion 4"); break; default: LOGGER.severe("Bogus battle result case"); } } // consider what we can recruit CreatureType recruit = null; if (moved) { recruit = chooseRecruit(legion, hex, false); if (recruit != null) { int oldval = value.getValue(); if (legion.getHeight() <= 5) { value.add( getHintedRecruitmentValue(recruit, legion, hintSectionUsed), "Hinted Recruitment Value"); } else if (legion.getHeight() == 6) { // if we're 6-high, then the value of a recruit is // equal to the improvement in the value of the // pieces that we'll have after splitting. // TODO this should call our splitting code to see // what split decision we would make // If the legion would never split, then ignore // this special case. // This special case was overkill. A 6-high stack // with 3 lions, or a 6-high stack with 3 clopses, // sometimes refused to go to a safe desert/jungle, // and 6-high stacks refused to recruit colossi, // because the value of the recruit was toned down // too much. So the effect has been reduced. LOGGER.finest("--- 6-HIGH SPECIAL CASE"); List<CreatureType> weakestTwo = findWeakestTwoCritters(legion); CreatureType weakest1 = weakestTwo.get(0); CreatureType weakest2 = weakestTwo.get(1); int minCreaturePV = Math.min( getHintedRecruitmentValue(weakest1, legion, hintSectionUsed), getHintedRecruitmentValue(weakest2, legion, hintSectionUsed)); int maxCreaturePV = Math.max( getHintedRecruitmentValue(weakest1, legion, hintSectionUsed), getHintedRecruitmentValue(weakest2, legion, hintSectionUsed)); // point value of my best 5 pieces right now int oldPV = legion.getPointValue() - minCreaturePV; // point value of my best 5 pieces after adding this // recruit and then splitting off my 2 weakest int newPV = legion.getPointValue() - getHintedRecruitmentValue(weakest1, legion, hintSectionUsed) - getHintedRecruitmentValue(weakest2, legion, hintSectionUsed) + Math.max( maxCreaturePV, getHintedRecruitmentValue(recruit, legion, hintSectionUsed)); value .add( (newPV - oldPV) + getHintedRecruitmentValue(recruit, legion, hintSectionUsed), "Hinted Recruitment Value 2"); } else if (legion.getHeight() == 7) { // Cannot recruit, unless we have an angel to summon out, // and we're not fighting, and someone else is, and that // other stack summons out our angel. // Since we don't have enough information about other // stacks to be sure that someone will summon from us, // just give a small bonus for the possible recruit, if // we're not fighting and have a summonable. if (enemyLegion == null && legion.hasSummonable()) { // This is total fudge. Removing an angel may hurt // this legion, or may help it if the recruit is even // better. But it'll help someone else. And we don't // know which legion is more important. So just give // a small bonus for possibly being able to summon out // an angel and recruit. double POSSIBLE_SUMMON_FACTOR = 0.1; value.add( (int)Math.round(POSSIBLE_SUMMON_FACTOR * getHintedRecruitmentValue(recruit, legion, hintSectionUsed)), "Hinted Recruitment Value 3"); } } else { LOGGER.severe("Bogus legion height " + (legion).getHeight() + " in legion " + legion.getMarkerId() + "; content: " + Glob.glob(",", legion.getCreatures())); } LOGGER.finest("--- if " + legion + " moves to " + hex + " then recruit " + recruit.toString() + " (adding " + (value.getValue() - oldval) + ")"); } } // consider what we might be able to recruit next turn, from here for (int roll = 1; roll <= 6; roll++) { int nextTurnValue = 0; // XXX Should ignore friends. Set<MasterHex> moves = client.getMovement().listAllMoves(legion, hex, roll); int bestRecruitVal = 0; for (MasterHex nextHex : moves) { // if we have to fight in that hex and we can't // WIN_WITH_MINIMAL_LOSSES, then assume we can't // recruit there. IDEA: instead of doing any of this // work, perhaps we could recurse here to get the // value of being in _that_ hex next turn... and then // maximize over choices and average over die rolls. // this would be essentially minimax but ignoring the // others players ability to move. Legion enemy = client.getGameClientSide().getFirstEnemyLegion( nextHex, legion.getPlayer()); if (enemy != null && estimateBattleResults(legion, enemy, nextHex) != WIN_WITH_MINIMAL_LOSSES) { continue; } List<CreatureType> nextRecruits = client.findEligibleRecruits( legion, nextHex); if (nextRecruits.size() == 0) { continue; } CreatureType nextRecruit = nextRecruits.get(nextRecruits .size() - 1); // Reduced val by 5 to make current turn recruits more // valuable than next turn's recruits int val = nextRecruit.getPointValue() - 5; if (val > bestRecruitVal) { bestRecruitVal = val; } } nextTurnValue += bestRecruitVal; nextTurnValue /= 6; // 1/6 chance of each happening value .add(nextTurnValue, "Next Turn Value (for roll " + roll + ")"); } // consider risk of being attacked if (enemyAttackMap != null) { if (moved) { LOGGER.finest("considering risk of moving " + legion + " to " + hex); } else { LOGGER.finest("considering risk of leaving " + legion + " in " + hex); } Map<MasterHex, List<Legion>>[] enemiesThatCanAttackOnA = enemyAttackMap; int roll; for (roll = 1; roll <= 6; roll++) { List<Legion> enemies = enemiesThatCanAttackOnA[roll].get(hex); if (enemies == null) { continue; } for (Legion enemy : enemies) { final int result = estimateBattleResults(enemy, false, legion, hex, recruit); if (result == WIN_WITH_MINIMAL_LOSSES || result == DRAW && (legion).hasTitan()) { break; // break on the lowest roll from which we can // be attacked and killed } } } // Ignore all fear of attack on turn 1. Not perfect, // but a pretty good rule of thumb. if (roll < 7 && client.getTurnNumber() > 1) { final double chanceToAttack = (7.0 - roll) / 6.0; final double risk; if (legion.hasTitan()) { risk = LOSE_LEGION * chanceToAttack; } else { risk = -legion.getPointValue() / 2.0 * chanceToAttack; } value.add((int)Math.round(risk), "Risk (not the trademark)"); } } // TODO: consider mobility. e.g., penalty for suckdown // squares, bonus if next to tower or under the top // TODO: consider what we can attack next turn from here // TODO: consider nearness to our other legions // TODO: consider being a scooby snack (if so, everything // changes: we want to be in a location with bad mobility, we // want to be at risk of getting killed, etc) // TODO: consider risk of being scooby snacked (this might be inherent) // TODO: consider splitting up our good recruitment rolls // (i.e. if another legion has warbears under the top that // recruit on 1,3,5, and we have a behemoth with choice of 3/5 // to jungle or 4/6 to jungle, prefer the 4/6 location). LOGGER.finest("EVAL " + legion + (moved ? " move to " : " stay in ") + hex + " = " + value); return value.getValue(); } private static final int WIN_WITH_MINIMAL_LOSSES = 0; private static final int WIN_WITH_HEAVY_LOSSES = 1; private static final int DRAW = 2; private static final int LOSE_BUT_INFLICT_HEAVY_LOSSES = 3; private static final int LOSE = 4; /* can be overloaded by subclass -> not final */ // TODO turn into some more Javaish code, particularly in terms of naming conventions, // ideally this should be all encapsulated in a configuration object double RATIO_WIN_MINIMAL_LOSS() { return 1.30; } double RATIO_WIN_HEAVY_LOSS() { return 1.15; } double RATIO_DRAW() { return 0.85; } double RATIO_LOSE_HEAVY_LOSS() { return 0.70; } private int estimateBattleResults(Legion attacker, Legion defender, MasterHex hex) { return estimateBattleResults(attacker, false, defender, hex, null); } private int estimateBattleResults(Legion attacker, boolean attackerSplitsBeforeBattle, Legion defender, MasterHex hex) { return estimateBattleResults(attacker, attackerSplitsBeforeBattle, defender, hex, null); } private int estimateBattleResults(Legion attacker, boolean attackerSplitsBeforeBattle, Legion defender, MasterHex hex, CreatureType recruit) { MasterBoardTerrain terrain = hex.getTerrain(); double attackerPointValue = getCombatValue(attacker, terrain); if (attackerSplitsBeforeBattle) { // remove PV of the split List<CreatureType> creaturesToRemove = chooseCreaturesToSplitOut(attacker); for (CreatureType creature : creaturesToRemove) { attackerPointValue -= getCombatValue(creature, terrain); } } if (recruit != null) { // Log.debug("adding in recruited " + recruit + // " when evaluating battle"); attackerPointValue += getCombatValue(recruit, terrain); } // TODO: add angel call double defenderPointValue = getCombatValue(defender, terrain); // TODO: add in enemy's most likely turn 4 recruit if (hex.getTerrain().isTower()) { // defender in the tower! ouch! defenderPointValue *= 1.2; } else if (hex.getTerrain().getDisplayName().equals("Abyss")) // The Abyss, in variants { // defender in the abyss! Kill! defenderPointValue *= 0.8; } // really dumb estimator double ratio = attackerPointValue / defenderPointValue; if (ratio >= RATIO_WIN_MINIMAL_LOSS()) { return WIN_WITH_MINIMAL_LOSSES; } else if (ratio >= RATIO_WIN_HEAVY_LOSS()) { return WIN_WITH_HEAVY_LOSSES; } else if (ratio >= RATIO_DRAW()) { return DRAW; } else if (ratio >= RATIO_LOSE_HEAVY_LOSS()) { return LOSE_BUT_INFLICT_HEAVY_LOSSES; } else // ratio less than 0.70 { return LOSE; } } // This is a really dumb placeholder. TODO Make it smarter. // In particular, the AI should pick a side that will let it enter // as many creatures as possible. public EntrySide pickEntrySide(MasterHex hex, Legion legion, Set<EntrySide> entrySides) { // Default to bottom to simplify towers. if (entrySides.contains(EntrySide.BOTTOM)) { return EntrySide.BOTTOM; } if (entrySides.contains(EntrySide.RIGHT)) { return EntrySide.RIGHT; } if (entrySides.contains(EntrySide.LEFT)) { return EntrySide.LEFT; } return null; } public MasterHex pickEngagement() { Set<MasterHex> hexes = client.getGameClientSide().findEngagements(); // Bail out early if we have no real choice. int numChoices = hexes.size(); if (numChoices == 0) { return null; } if (numChoices == 1) { return hexes.iterator().next(); } MasterHex bestChoice = null; int bestScore = Integer.MIN_VALUE; for (MasterHex hex : hexes) { int score = evaluateEngagement(hex); if (score > bestScore) { bestScore = score; bestChoice = hex; } } return bestChoice; } private int evaluateEngagement(MasterHex hex) { // Fight losing battles last, so that we don't give away // points while they may be used against us this turn. // Fight battles with angels first, so that those angels // can be summoned out. // Try not to lose potential angels and recruits by having // scooby snacks flee to 7-high stacks (or 6-high stacks // that could recruit later this turn) and push them // over 100-point boundaries. Player player = client.getActivePlayer(); Legion attacker = client.getGameClientSide().getFirstFriendlyLegion( hex, player); Legion defender = client.getGameClientSide().getFirstEnemyLegion(hex, player); int value = 0; final int result = estimateBattleResults(attacker, defender, hex); // The worse we expect to do, the more we want to put off this // engagement, either to avoid strengthening an enemy titan that // we may fight later this turn, or to increase our chances of // being able to call an angel. value -= result; // Avoid losing angels and recruits. boolean wouldFlee = flee(defender, attacker); if (wouldFlee) { int currentScore = player.getScore(); int fleeValue = ((LegionClientSide)defender).getPointValue() / 2; if (((currentScore + fleeValue) / getAcqStepValue()) > (currentScore / getAcqStepValue())) { if (attacker.getHeight() == 7 || attacker.getHeight() == 6 && client.canRecruit(attacker)) { value -= 10; } else { // Angels go best in Titan legions. if ((attacker).hasTitan()) { value += 6; } else { // A bird in the hand... value += 2; } } } } // Fight early with angel legions, so that others can summon. if (result <= WIN_WITH_HEAVY_LOSSES && ((LegionClientSide)attacker).hasSummonable()) { value += 5; } return value; } public boolean flee(Legion legion, Legion enemy) { if ((legion).hasTitan()) { return false; } // Titan never flee ! int result = estimateBattleResults(enemy, legion, legion.getCurrentHex()); switch (result) { case WIN_WITH_HEAVY_LOSSES: case DRAW: case LOSE_BUT_INFLICT_HEAVY_LOSSES: case LOSE: LOGGER.finest("Legion " + legion.getMarkerId() + " doesn't flee " + " before " + enemy.getMarkerId() + " with result " + result + " (" + ((LegionClientSide)legion).getPointValue() + " vs. " + ((LegionClientSide)enemy).getPointValue() + " in " + legion.getCurrentHex().getTerrainName() + ")"); return false; case WIN_WITH_MINIMAL_LOSSES: // don't bother unless we can try to weaken the titan stack // and we aren't going to help him by removing cruft // also, 7-height stack never flee and wimpy stack always flee if ((legion).getHeight() < 6) { LOGGER.finest("Legion " + legion.getMarkerId() + " flee " + " as they are just " + (legion).getHeight() + " wimps !"); return true; } if ((((LegionClientSide)enemy).getPointValue() * 0.5) > ((LegionClientSide)legion) .getPointValue()) { LOGGER.finest("Legion " + legion.getMarkerId() + " flee " + " as they are less than half as strong as " + enemy.getMarkerId()); return true; } if ((enemy).getHeight() == 7) { List<CreatureType> recruits = client.findEligibleRecruits( enemy, legion.getCurrentHex()); if (recruits.size() > 0) { CreatureType best = recruits.get(recruits.size() - 1); int lValue = ((LegionClientSide)enemy).getPointValue(); if (best.getPointValue() > (lValue / (enemy) .getHeight())) { LOGGER.finest("Legion " + legion + " flee " + " to prevent " + enemy + " to be able to recruit " + best); return true; } } } if ((enemy).hasTitan()) { LOGGER.finest("Legion " + legion.getMarkerId() + " doesn't flee " + " to fight the Titan in " + enemy.getMarkerId()); } if ((legion).getHeight() == 7) { LOGGER.finest("Legion " + legion.getMarkerId() + " doesn't flee " + " they are the magnificent 7 !"); } return !((enemy).hasTitan() || ((legion).getHeight() == 7)); } return false; } public boolean concede(Legion legion, Legion enemy) { // Never concede titan legion. if ((legion).hasTitan()) { return false; } // Wimpy legions should concede if it costs the enemy an // angel or good recruit. MasterBoardTerrain terrain = legion.getCurrentHex().getTerrain(); int height = (enemy).getHeight(); if (getCombatValue(legion, terrain) < 0.5 * getCombatValue(enemy, terrain) && height >= 6) { int currentScore = enemy.getPlayer().getScore(); int pointValue = ((LegionClientSide)legion).getPointValue(); boolean canAcquireAngel = ((currentScore + pointValue) / getAcqStepValue() > (currentScore / getAcqStepValue())); // Can't use Legion.getRecruit() because it checks for // 7-high legions. boolean canRecruit = !client.findEligibleRecruits(enemy, enemy.getCurrentHex()).isEmpty(); if (height == 7 && (canAcquireAngel || canRecruit)) { return true; } if (canAcquireAngel && canRecruit) // know height == 6 { return true; } } return false; } // should be correct for most variants. public CreatureType acquireAngel(Legion legion, List<CreatureType> angels) { // TODO If the legion is a tiny scooby snack that's about to get // smooshed, turn down the angel. CreatureType bestAngel = getBestCreature(angels); if (bestAngel == null) { return null; } // Don't take an angel if 6 high and a better recruit is available. // TODO Make this also work for post-battle reinforcements if (legion.getHeight() == 6 && client.canRecruit(legion)) { List<CreatureType> recruits = client.findEligibleRecruits(legion, legion.getCurrentHex()); CreatureType bestRecruit = recruits.get(recruits.size() - 1); if (getKillValue(bestRecruit) > getKillValue(bestAngel)) { LOGGER.finest("AI declines acquiring to recruit " + bestRecruit.getName()); return null; } } return bestAngel; } /** * Return the most important Creature in the list of Creatures. */ private CreatureType getBestCreature(List<CreatureType> creatures) { if (creatures == null || creatures.isEmpty()) { return null; } CreatureType best = null; for (CreatureType creature : creatures) { if (best == null || getKillValue(creature) > getKillValue(best)) { best = creature; } } return best; } /** * Return a SummonInfo object, containing the summoner, donor and unittype. */ public SummonInfo summonAngel(Legion summoner, List<Legion> donors) { // Always summon the biggest possible angel, from the least // important legion that has one. // // TODO Sometimes leave room for recruiting. Legion bestLegion = null; CreatureType bestAngel = null; for (Legion legion : donors) { // Do not summon an angel out of a small Titan stack // (Late game it could be desirable, but this is a good start) if (legion.hasTitan() && legion.getHeight() <= 5) { continue; } for (CreatureType candidate : legion.getCreatureTypes()) { if (candidate.isSummonable()) { if (bestAngel == null || bestLegion == null || candidate.getPointValue() > bestAngel .getPointValue() || Legion.ORDER_TITAN_THEN_POINTS.compare(legion, bestLegion) > 0 && candidate.getPointValue() == bestAngel .getPointValue()) { bestLegion = legion; bestAngel = candidate; } } } } return bestAngel == null || bestLegion == null ? new SummonInfo() : new SummonInfo(summoner, bestLegion, bestAngel); } private BattleCritter findBestTarget() { BattleCritter bestTarget = null; MasterBoardTerrain terrain = client.getBattleSite().getTerrain(); // Create a map containing each target and the likely number // of hits it would take if all possible creatures attacked it. Map<BattleCritter, Double> map = generateDamageMap(); for (Entry<BattleCritter, Double> entry : map.entrySet()) { BattleCritter target = entry.getKey(); double h = entry.getValue().doubleValue(); if (h + target.getHits() >= target.getPower()) { // We can probably kill this target. if (bestTarget == null || getKillValue(target, terrain) > getKillValue( bestTarget, terrain)) { bestTarget = target; } } else { // We probably can't kill this target. // But if it is a Titan it may be more valuable to do fractional damage if (bestTarget == null || (0.5 * ((h + target.getHits()) / target.getPower()) * getKillValue(target, terrain) > getKillValue( bestTarget, terrain))) { bestTarget = target; } } } return bestTarget; } // TODO Have this actually find the best one, not the first one. private BattleCritter findBestAttacker(BattleCritter target) { for (BattleCritter critter : client.getActiveBattleUnits()) { if (client.getBattleCS().canStrike(critter, target)) { return critter; } } return null; } /** Apply carries first to the biggest creature that could be killed * with them, then to the biggest creature. carryTargets are * hexLabel description strings. */ public void handleCarries(int carryDamage, Set<String> carryTargets) { MasterBoardTerrain terrain = client.getBattleSite().getTerrain(); BattleCritter bestTarget = null; for (String desc : carryTargets) { String targetHexLabel = desc.substring(desc.length() - 2); BattleHex targetHex = terrain.getHexByLabel(targetHexLabel); BattleCritter target = getBattleUnit(targetHex); if (target.wouldDieFrom(carryDamage)) { if (bestTarget == null || !bestTarget.wouldDieFrom(carryDamage) || getKillValue(target, terrain) > getKillValue( bestTarget, terrain)) { bestTarget = target; } } else { if (bestTarget == null || (!bestTarget.wouldDieFrom(carryDamage) && getKillValue( target, terrain) > getKillValue(bestTarget, terrain))) { bestTarget = target; } } } if (bestTarget == null) { LOGGER.warning("No carry target but " + carryDamage + " points of available carry damage"); client.leaveCarryMode(); } else { LOGGER.finest("Best carry target is " + bestTarget.getDescription()); client.applyCarries(bestTarget.getCurrentHex()); } } /** Pick one of the list of String strike penalty options. */ public String pickStrikePenalty(List<String> choices) { // XXX Stupid placeholder. return choices.get(choices.size() - 1); } /** Simple one-ply group strike algorithm. Return false if there were * no strike targets. */ public boolean strike(Legion legion) { LOGGER.finest("Called ai.strike() for " + legion); // PRE: Caller handles forced strikes before calling this. // Pick the most important target that can likely be killed this // turn. If none can, pick the most important target. // TODO If none can, and we're going to lose the battle this turn, // pick the easiest target to kill. BattleCritter bestTarget = findBestTarget(); if (bestTarget == null) { LOGGER.finest("Best target is null, aborting"); return false; } // LOGGER.finest("Best target is " + bestTarget.getDescription()); // Having found the target, pick an attacker. The // first priority is finding one that does not need // to worry about carry penalties to hit this target. // The second priority is using the weakest attacker, // so that more information is available when the // stronger attackers strike. BattleCritter bestAttacker = findBestAttacker(bestTarget); if (bestAttacker == null) { return false; } // LOGGER.finest("Best attacker is " + bestAttacker.getDescription()); // Having found the target and attacker, strike. // Take a carry penalty if there is still a 95% // chance of killing this target. client.strike(bestAttacker.getTag(), bestTarget.getCurrentHex()); return true; } private static int getCombatValue(BattleCritter battleUnit, MasterBoardTerrain terrain) { int val = battleUnit.getPointValue(); CreatureType creature = battleUnit.getType(); if (creature.isFlier()) { val++; } if (creature.isRangestriker()) { val++; } if (terrain.hasNativeCombatBonus(creature)) { val++; } return val; } /** XXX Inaccurate for titans. */ private int getCombatValue(CreatureType creature, MasterBoardTerrain terrain) { if (creature.isTitan()) { // Don't know the power, so just estimate. LOGGER.warning("Called SimpleAI.getCombatValue() for Titan"); return 6 * variant.getCreatureByName("Titan").getSkill(); } int val = (creature).getPointValue(); if ((creature).isFlier()) { val++; } if ((creature).isRangestriker()) { val++; } if (terrain.hasNativeCombatBonus(creature)) { val++; } return val; } private int getTitanCombatValue(int power) { int titan_skill = variant.getCreatureByName("Titan").getSkill(); int val = power * titan_skill; // Weak Titans do not contribute much to a stack (in fact they're a // liability because they need to be protected), so reduce their value. // Formula chosen to scale from 0 at power 6, to full strength (48) at // power 12 for a 4 skill factor Titan. 5 skill factor Titans are not // as vulnerable so reduce them a little less. if (power < 12) { val -= (8 - titan_skill) * (12 - power); } return val; } private int getCombatValue(Legion legion, MasterBoardTerrain terrain) { int val = 0; for (CreatureType creature : legion.getCreatureTypes()) { if (creature.isTitan()) { val += getTitanCombatValue(legion.getPlayer().getTitanPower()); } else { val += getCombatValue(creature, terrain); } } return val; } protected class PowerSkill { private final String name; private final int power_attack; private final int power_defend; // how many dice attackers lose private final int skill_attack; private final int skill_defend; private double hp; // how many hit points or power left private final double value; public PowerSkill(String nm, int p, int pa, int pd, int sa, int sd) { name = nm; power_attack = pa; power_defend = pd; skill_attack = sa; skill_defend = sd; hp = p; // may not be the same as power_attack! value = p * Math.min(sa, sd); } public PowerSkill(String nm, int pa, int sa) { this(nm, pa, pa, 0, sa, sa); } public int getPowerAttack() { return power_attack; } public int getPowerDefend() { return power_defend; } public int getSkillAttack() { return skill_attack; } public int getSkillDefend() { return skill_defend; } public double getHP() { return hp; } public void setHP(double h) { hp = h; } public void addDamage(double d) { hp -= d; } public double getPointValue() { return value; } public String getName() { return name; } @Override public String toString() { return name + "(" + hp + ")"; } } // return power and skill of a given creature given the terrain // terrain here is either a board hex label OR // a Hex terrain label // TODO this either or is dangerous and forces us to use the label // instead of the objects private PowerSkill calcBonus(CreatureType creature, String terrain, boolean defender) { int power = creature.getPower(); int skill = creature.getSkill(); TerrainBonuses bonuses = TERRAIN_BONUSES.get(terrain); if (bonuses == null) { // terrain has no special bonuses return new PowerSkill(creature.getName(), power, skill); } else if (terrain.equals("Tower") && defender == false) { // no attacker bonus for tower return new PowerSkill(creature.getName(), power, skill); } else if ((terrain.equals("Mountains") || terrain.equals("Volcano")) && defender == true && creature.getName().equals("Dragon")) { // Dragon gets an extra 3 die when attack down slope // non-native loses 1 skill when attacking up slope return new PowerSkill(creature.getName(), power, power + 3, bonuses.defenderPower, skill + bonuses.attackerSkill, skill + bonuses.defenderSkill); } else { return new PowerSkill(creature.getName(), power, power + bonuses.attackerPower, bonuses.defenderPower, skill + bonuses.attackerSkill, skill + bonuses.defenderSkill); } } // return power and skill of a given creature given the terrain // board hex label // *WARNING* CreatureType.getPower doesn't work for Titan. protected PowerSkill getNativeValue(CreatureType creature, MasterBoardTerrain terrain, boolean defender) { // TODO checking the tower via string is unsafe -- maybe terrain.isTower() // is meant anyway if (!(terrain.hasNativeCombatBonus(creature) || (terrain.getId() .equals("Tower") && defender == true))) { return new PowerSkill(creature.getName(), creature.getPower(), creature.getSkill()); } return calcBonus(creature, terrain.getId(), defender); } /** Return a list of critter moves, in best move order. */ public List<CritterMove> battleMove() { LOGGER.finest("Called battleMove()"); // Defer setting time limit until here where it's needed, to // avoid initialization timing issues. timeLimit = client.getOptions().getIntOption(Options.aiTimeLimit); // Consider one critter at a time, in order of importance. // Examine all possible moves for that critter not already // taken by a more important one. // TODO Handle summoned/recruited critters, in particular // getting stuff out of the way so that a reinforcement // has room to enter. Collection<LegionMove> legionMoves = findBattleMoves(); LegionMove bestLegionMove = findBestLegionMove(legionMoves); List<CritterMove> bestMoveOrder = findMoveOrder(bestLegionMove); return bestMoveOrder; } /** Try another move for creatures whose moves failed. */ public void retryFailedBattleMoves(List<CritterMove> bestMoveOrder) { if (bestMoveOrder == null) { return; } for (CritterMove cm : bestMoveOrder) { BattleCritter critter = cm.getCritter(); // LOGGER.finest(critter.getDescription() + " failed to move"); List<CritterMove> moveList = findBattleMovesOneCritter(critter); if (!moveList.isEmpty()) { CritterMove cm2 = moveList.get(0); /* LOGGER.finest("Moving " + critter.getDescription() + " to " + cm2.getEndingHex().getLabel() + " (startingHexLabel was " + cm.getStartingHex().getLabel() + ")"); */ client.tryBattleMove(cm2); } } } private List<CritterMove> findMoveOrder(LegionMove lm) { if (lm == null) { return null; } int perfectScore = 0; List<CritterMove> critterMoves = new ArrayList<CritterMove>(); critterMoves.addAll(lm.getCritterMoves()); Iterator<CritterMove> itCrit = critterMoves.iterator(); while (itCrit.hasNext()) { CritterMove cm = itCrit.next(); if (cm.getStartingHex().equals(cm.getEndingHex())) { // Prune non-movers itCrit.remove(); } else { perfectScore += cm.getCritter().getPointValue(); } } if (perfectScore == 0) { // No moves, so exit. return null; } // Figure the order in which creatures should move to get in // each other's way as little as possible. // Iterate through all permutations of critter move orders, // tracking how many critters get their preferred hex with each // order, until we find an order that lets every creature reach // its preferred hex. If none does, take the best we can find. int bestScore = 0; List<CritterMove> bestOrder = null; boolean bestAllOK = false; int count = 0; Timer findMoveTimer = setupTimer(); Iterator<List<CritterMove>> it = new PermutationIterator<CritterMove>( critterMoves); while (it.hasNext()) { List<CritterMove> order = it.next(); count++; boolean allOK = true; int score = testMoveOrder(order, null); if (score < 0) { allOK = false; score = -score; } if (score > bestScore) { bestOrder = new ArrayList<CritterMove>(order); bestScore = score; bestAllOK = allOK; if (score >= perfectScore) { break; } } // Bail out early, if there is at least some valid move. if (timeIsUp) { if (bestScore > 0) { break; } else { LOGGER .warning("Time is up figuring move order, but we ignore " + "it (no valid moveOrder found yet... " + " - buggy break)"); timeIsUp = false; } } } findMoveTimer.cancel(); if (!bestAllOK) { List<CritterMove> newOrder = new ArrayList<CritterMove>(); testMoveOrder(bestOrder, newOrder); bestOrder = new ArrayList<CritterMove>(newOrder); } LOGGER.finest("Got score " + bestScore + " in " + count + " permutations"); return bestOrder; } /** Try each of the moves in order. Return the number that succeed, * scaled by the importance of each critter. * In newOrder, if not null, place the moves that are valid. */ private int testMoveOrder(List<CritterMove> order, List<CritterMove> newOrder) { boolean allOK = true; int val = 0; for (CritterMove cm : order) { BattleCritter critter = cm.getCritter(); BattleHex hex = cm.getEndingHex(); if (client.testBattleMove(critter, hex)) { // XXX Use kill value instead? val += critter.getPointValue(); if (newOrder != null) { newOrder.add(cm); } } else { allOK = false; } } // Move them all back where they started. for (CritterMove cm : order) { BattleCritter critter = cm.getCritter(); BattleHex hex = cm.getStartingHex(); critter.setCurrentHex(hex); } if (!allOK) { val = -val; } return val; } private final int MAX_LEGION_MOVES = 10000; /** Find the maximum number of moves per creature to test, such that * numMobileCreaturesInLegion ^ N <= LEGION_MOVE_LIMIT, but we must * have at least as many moves as mobile creatures to ensure that * every creature has somewhere to go. */ protected int getCreatureMoveLimit() // NO_UCD { int mobileCritters = client.findMobileBattleUnits().size(); if (mobileCritters <= 1) { // Avoid infinite logs and division by zero, and just try // all possible moves. return Constants.BIGNUM; } int max = (int)Math.floor(Math.log(MAX_LEGION_MOVES) / Math.log(mobileCritters)); return (Math.min(max, mobileCritters)); } private Collection<LegionMove> findBattleMoves() { LOGGER.finest("Called findBattleMoves()"); // Consider one critter at a time in isolation. // Find the best N moves for each critter. // TODO Do not consider immobile critters. Also, do not allow // non-flying creatures to move through their hexes. // TODO Handle summoned/recruited critters, in particular // getting stuff out of the way so that a reinforcement // has room to enter. // The caller is responsible for actually making the moves. final List<List<CritterMove>> allCritterMoves = new ArrayList<List<CritterMove>>(); for (BattleCritter critter : client.getActiveBattleUnits()) { List<CritterMove> moveList = findBattleMovesOneCritter(critter); // Add this critter's moves to the list. allCritterMoves.add(moveList); // Put all critters back where they started. Iterator<List<CritterMove>> it2 = allCritterMoves.iterator(); while (it2.hasNext()) { moveList = it2.next(); CritterMove cm = moveList.get(0); BattleCritter critter2 = cm.getCritter(); critter2.moveToHex(cm.getStartingHex()); } } Collection<LegionMove> legionMoves = findLegionMoves(allCritterMoves); return legionMoves; } private List<CritterMove> findBattleMovesOneCritter(BattleCritter critter) { BattleHex currentHex = critter.getCurrentHex(); // moves is a list of hex labels where one critter can move. // Sometimes friendly critters need to get out of the way to // clear a path for a more important critter. We consider // moves that the critter could make, disregarding mobile allies. // XXX Should show moves including moving through mobile allies. Set<BattleHex> moves = client.showBattleMoves(critter); // TODO Make less important creatures get out of the way. // Not moving is also an option. moves.add(currentHex); List<CritterMove> moveList = new ArrayList<CritterMove>(); for (BattleHex hex : moves) { ValueRecorder why = new ValueRecorder(); CritterMove cm = new CritterMove(critter, currentHex, hex); // Need to move the critter to evaluate. critter.moveToHex(hex); // Compute and save the value for each CritterMove. cm.setValue(evaluateCritterMove(critter, null, why)); moveList.add(cm); // Move the critter back where it started. critter.moveToHex(critter.getStartingHex()); } // Sort critter moves in descending order of score. Collections.sort(moveList, new Comparator<CritterMove>() { public int compare(CritterMove cm1, CritterMove cm2) { return cm2.getValue() - cm1.getValue(); } }); // Show the moves considered. StringBuilder buf = new StringBuilder("Considered " + moveList.size() + " moves for " + critter.getTag() + " " + critter.getType().getName() + " in " + currentHex.getLabel() + ":"); for (CritterMove cm : moveList) { buf.append(" " + cm.getEndingHex().getLabel()); } LOGGER.finest(buf.toString()); return moveList; } Timer setupTimer() { // java.util.Timer, not Swing Timer Timer timer = new Timer(); timeIsUp = false; final int MS_PER_S = 1000; if (timeLimit < Constants.MIN_AI_TIME_LIMIT || timeLimit > Constants.MAX_AI_TIME_LIMIT) { timeLimit = Constants.DEFAULT_AI_TIME_LIMIT; } timer.schedule(new TriggerTimeIsUp(), MS_PER_S * timeLimit); return timer; } protected final static int MIN_ITERATIONS = 50; /** Evaluate all legion moves in the list, and return the best one. * Break out early if the time limit is exceeded. */ protected LegionMove findBestLegionMove(Collection<LegionMove> legionMoves) { int bestScore = Integer.MIN_VALUE; LegionMove best = null; if (legionMoves instanceof List) Collections.shuffle((List<LegionMove>)legionMoves, random); Timer findBestLegionMoveTimer = setupTimer(); int count = 0; for (LegionMove lm : legionMoves) { int score = evaluateLegionBattleMove(lm); if (score > bestScore) { bestScore = score; best = lm; LOGGER.finest("INTERMEDIATE Best legion move: " + lm.getStringWithEvaluation() + " (" + score + ")"); } else { LOGGER.finest("INTERMEDIATE legion move: " + lm.getStringWithEvaluation() + " (" + score + ")"); } count++; if (timeIsUp) { if (count >= MIN_ITERATIONS) { LOGGER.finest("findBestLegionMove() time up after " + count + " iterations"); break; } else { LOGGER.finest("findBestLegionMove() time up after " + count + " iterations, but we keep searching until " + MIN_ITERATIONS); } } } findBestLegionMoveTimer.cancel(); LOGGER.finer("Best legion move of " + count + " checked (turn " + client.getBattleTurnNumber() + "): " + ((best == null) ? "none " : best.getStringWithEvaluation()) + " (" + bestScore + ")"); return best; } /** allCritterMoves is a List of sorted MoveLists. A MoveList is a * sorted List of CritterMoves for one critter. Return a sorted List * of LegionMoves. A LegionMove is a List of one CritterMove per * mobile critter in the legion, where no two critters move to the * same hex. */ Collection<LegionMove> findLegionMoves( final List<List<CritterMove>> allCritterMoves) { return generateLegionMoves(allCritterMoves, false); } @SuppressWarnings("unused") protected int evaluateLegionBattleMoveAsAWhole(LegionMove lm, Map<BattleHex, Integer> strikeMap, ValueRecorder value) { // This is empty, to be overidden by subclasses. return 0; } /** this compute the special case of the Titan critter */ protected void evaluateCritterMove_Titan(final BattleCritter critter, ValueRecorder value, final MasterBoardTerrain terrain, final BattleHex hex, final Legion legion, final int turn) { if (hex.isEntrance()) { return; } // Reward titans sticking to the edges of the back row // surrounded by allies. We need to relax this in the // last few turns of the battle, so that attacking titans // don't just sit back and wait for a time loss. BattleHex entrance = terrain.getEntrance(legion.getEntrySide()); if (!critter.isTitan()) { LOGGER .warning("evaluateCritterMove_Titan called on non-Titan critter"); return; } if (terrain.isTower() && legion.equals(client.getDefender())) { // Stick to the center of the tower. value.add(bec.TITAN_TOWER_HEIGHT_BONUS * hex.getElevation(), "TitanTowerHeightBonus"); } else { if (turn <= 4) { value.add( bec.TITAN_FORWARD_EARLY_PENALTY * Battle.getRange(hex, entrance, true), "TitanForwardEarlyPenalty"); for (int i = 0; i < 6; i++) { BattleHex neighbor = hex.getNeighbor(i); if (neighbor == null /* Edge of the map */ || neighbor.getTerrain().blocksGround() || (neighbor.getTerrain().isGroundNativeOnly() && !hasOpponentNativeCreature(neighbor .getTerrain()))) { value.add(bec.TITAN_BY_EDGE_OR_BLOCKINGHAZARD_BONUS, "TitanByEdgeOrBlockingHazard (" + i + ")"); } } } } // Treat damage to Titan as 4 times worse than for a normal critter value.add( 4 * bec.PENALTY_DAMAGE_TERRAIN * hex.damageToCreature(critter.getType()), "TerrainDamageToTitanIsExtraBad"); } /** This compute the influence of terrain */ private void evaluateCritterMove_Terrain( final BattleCritter critter, // NO_UCD ValueRecorder value, final MasterBoardTerrain terrain, final BattleHex hex, final int power, final int skill) { if (hex.isEntrance()) { // Staying offboard to die is really bad. value.add( bec.OFFBOARD_DEATH_SCALE_FACTOR * getCombatValue(critter, terrain), "StayingOffboard"); return; } PowerSkill ps = calcBonus(critter.getType(), hex.getTerrain() .getName(), true); int native_power = ps.getPowerAttack() + (ps.getPowerDefend() + power); int native_skill = ps.getSkillAttack() + ps.getSkillDefend(); // Add for sitting in favorable terrain. // Subtract for sitting in unfavorable terrain. if (hex.isNativeBonusTerrain() && critter.getType().isNativeIn(hex.getTerrain())) { value.add(bec.NATIVE_BONUS_TERRAIN, "NativeBonusTerrain"); // Above gives a small base value. // Scale remaining bonus to size of benefit if (hex.getElevation() > 0) { native_skill += 1; // guess at bonus } int bonus = (native_power - 2 * power) * skill + (native_skill - 2 * skill) * power; value.add(3 * bonus, "More NativeBonusTerrain"); // We want marsh natives to slightly prefer moving to bog hexes, // even though there's no real bonus there, to leave other hexes // clear for non-native allies. if (hex.getTerrain().equals(HazardTerrain.getTerrainByName("Bog"))) { value.add(bec.NATIVE_BOG, "NativeBog"); } } else // Critter is not native or the terrain is not beneficial { if (hex.isNonNativePenaltyTerrain() && (!critter.getType().isNativeIn(hex.getTerrain()))) { value.add(bec.NON_NATIVE_PENALTY_TERRAIN, "NonNativePenalty"); // Above gives a small base value. // Scale remaining bonus to size of benefit int bonus = (native_power - 2 * power) * skill + (native_skill - 2 * skill) * power; value.add(3 * bonus, "More NonNativePenalty"); } } /* damage is positive, healing is negative, so we can always add */ value.add( bec.PENALTY_DAMAGE_TERRAIN * hex.damageToCreature(critter.getType()), "PenaltyDamageTerrain"); } /** this compute for non-titan attacking critter */ @SuppressWarnings({ "unused", "deprecation" }) private void evaluateCritterMove_Attacker( final BattleCritter critter, // NO_UCD ValueRecorder value, final MasterBoardTerrain terrain, final BattleHex hex, final LegionClientSide legion, final int turn) { if (hex.isEntrance()) { return; } // Attacker, non-titan, needs to charge. // Head for enemy creatures. value.add(bec.ATTACKER_DISTANCE_FROM_ENEMY_PENALTY * client.getBattleCS().minRangeToEnemy(critter), "AttackerDistanceFromEnemyPenalty"); } /** this compute for non-titan defending critter */ @SuppressWarnings("unused") protected void evaluateCritterMove_Defender(final BattleCritter critter, ValueRecorder value, final MasterBoardTerrain terrain, final BattleHex hex, final LegionClientSide legion, final int turn) { if (hex.isEntrance()) { return; } // Encourage defending critters to hang back. BattleHex entrance = terrain.getEntrance(legion.getEntrySide()); if (terrain.isTower()) { // Stick to the center of the tower. value.add(bec.DEFENDER_TOWER_HEIGHT_BONUS * hex.getElevation(), "DefenderTowerHeightBonus"); } else { int range = Battle.getRange(hex, entrance, true); // To ensure that defending legions completely enter // the board, prefer the second row to the first. The // exception is small legions early in the battle, // when trying to survive long enough to recruit. int preferredRange = 3; if (legion.getHeight() <= 3 && turn < 4) { preferredRange = 2; } if (range != preferredRange) { value.add( bec.DEFENDER_FORWARD_EARLY_PENALTY * Math.abs(range - preferredRange), "DefenderForwardEarlyPenalty"); } for (int i = 0; i < 6; i++) { BattleHex neighbor = hex.getNeighbor(i); if (neighbor == null /* Edge of the map */ || neighbor.getTerrain().blocksGround() || (neighbor.getTerrain().isGroundNativeOnly() && !hasOpponentNativeCreature(neighbor .getTerrain()))) { value.add(bec.DEFENDER_BY_EDGE_OR_BLOCKINGHAZARD_BONUS, "DefenderByEdgeOrBlockingHazard (" + i + ")"); } } } } @SuppressWarnings("unused") protected void evaluateCritterMove_Rangestrike( final BattleCritter critter, final Map<BattleHex, Integer> strikeMap, ValueRecorder value, final MasterBoardTerrain terrain, final BattleHex hex, final int power, final int skill, final LegionClientSide legion, final int turn, final Set<BattleHex> targetHexes) { if (hex.isEntrance()) { return; } int numTargets = targetHexes.size(); // Rangestrikes. value.add(bec.FIRST_RANGESTRIKE_TARGET, "FirstRangestrikeTarget"); // Having multiple targets is good, in case someone else // kills one. if (numTargets >= 2) { value.add(bec.EXTRA_RANGESTRIKE_TARGET, "ExtraRangestrikeTarget"); } // Non-warlock skill 4 rangestrikers should slightly prefer // range 3 to range 4. Non-brush rangestrikers should // prefer strikes not through bramble. Warlocks should // try to rangestrike titans. boolean penalty = true; for (BattleHex targetHex : targetHexes) { BattleCritter target = getBattleUnit(targetHex); if (target.isTitan()) { value.add(bec.RANGESTRIKE_TITAN, "RangestrikeTitan"); } int strikeNum = getBattleStrike().getStrikeNumber(critter, target); if (strikeNum <= 4 - skill + target.getSkill()) { penalty = false; } // Reward ganging up on enemies. if (strikeMap != null) { int numAttackingThisTarget = strikeMap.get(targetHex) .intValue(); if (numAttackingThisTarget > 1) { value.add(bec.GANG_UP_ON_CREATURE, "GangUpOnCreature RangeStrike"); } } } if (!penalty) { value.add(bec.RANGESTRIKE_WITHOUT_PENALTY, "RangestrikeWithoutPenalty"); } } @SuppressWarnings("unused") protected void evaluateCritterMove_Strike(final BattleCritter critter, final Map<BattleHex, Integer> strikeMap, ValueRecorder value, final MasterBoardTerrain terrain, final BattleHex hex, final int power, final int skill, final LegionClientSide legion, final int turn, final Set<BattleHex> targetHexes) { if (hex.isEntrance()) { return; } // Normal strikes. If we can strike them, they can strike us. // Reward being adjacent to an enemy if attacking. if (legion.equals(client.getAttacker())) { value.add(bec.ATTACKER_ADJACENT_TO_ENEMY, "AttackerAdjacentToEnemy"); } // Slightly penalize being adjacent to an enemy if defending. else { value.add(bec.DEFENDER_ADJACENT_TO_ENEMY, "DefenderAdjacentToEnemy"); } int killValue = 0; int numKillableTargets = 0; int hitsExpected = 0; for (BattleHex targetHex : targetHexes) { BattleCritter target = getBattleUnit(targetHex); // Reward being next to enemy titans. (Banzai!) if (target.isTitan()) { value.add(bec.ADJACENT_TO_ENEMY_TITAN, "AdjacentToEnemyTitan"); } // Reward being next to a rangestriker, so it can't hang // back and plink us. if (target.isRangestriker() && !critter.isRangestriker()) { value.add(bec.ADJACENT_TO_RANGESTRIKER, "AdjacenttoRangestriker"); } // Attack Warlocks so they don't get Titan if (target.getType().getName().equals("Warlock")) { value.add(bec.ADJACENT_TO_BUDDY_TITAN, "AdjacentToBuddyTitan"); } // Reward being next to an enemy that we can probably // kill this turn. int dice = getBattleStrike().getDice(critter, target); int strikeNum = getBattleStrike().getStrikeNumber(critter, target); double meanHits = Probs.meanHits(dice, strikeNum); if (meanHits + target.getHits() >= target.getPower()) { numKillableTargets++; int targetValue = getKillValue(target, terrain); killValue = Math.max(targetValue, killValue); } else { // reward doing damage to target - esp. titan. int targetValue = getKillValue(target, terrain); killValue = (int)(0.5 * (meanHits / target.getPower()) * Math .max(targetValue, killValue)); } // Reward ganging up on enemies. if (strikeMap != null) { int numAttackingThisTarget = strikeMap.get(targetHex) .intValue(); if (numAttackingThisTarget > 1) { value.add(bec.GANG_UP_ON_CREATURE, "GangUpOnCreature Strike"); } } // Penalize damage that we can take this turn, { dice = getBattleStrike().getDice(target, critter); strikeNum = getBattleStrike().getStrikeNumber(target, critter); hitsExpected += Probs.meanHits(dice, strikeNum); } } if (legion.equals(client.getAttacker())) { value.add(bec.ATTACKER_KILL_SCALE_FACTOR * killValue, "AttackerKillValueScaled"); value.add(bec.KILLABLE_TARGETS_SCALE_FACTOR * numKillableTargets, "AttackerNumKillable"); } else { value.add(bec.DEFENDER_KILL_SCALE_FACTOR * killValue, "DefenderKillValueScaled"); value.add(bec.KILLABLE_TARGETS_SCALE_FACTOR * numKillableTargets, "DefenderNumKillable"); } int hits = critter.getHits(); // XXX Attacking legions late in battle ignore damage. // the isTitan() here should be moved to _Titan function above ? if (legion.equals(client.getDefender()) || critter.isTitan() || turn <= 4) { if (hitsExpected + hits >= power) { if (legion.equals(client.getAttacker())) { value.add(bec.ATTACKER_GET_KILLED_SCALE_FACTOR * getKillValue(critter, terrain), "AttackerGetKilled"); } else { value.add(bec.DEFENDER_GET_KILLED_SCALE_FACTOR * getKillValue(critter, terrain), "DefenderGetKilled"); } } else { if (legion.equals(client.getAttacker())) { value.add(bec.ATTACKER_GET_HIT_SCALE_FACTOR * getKillValue(critter, terrain), "AttackerGetHit"); } else { value.add(bec.DEFENDER_GET_HIT_SCALE_FACTOR * getKillValue(critter, terrain), "DefendergetHit"); } } } } /** strikeMap is optional */ private int evaluateCritterMove(BattleCritter critter, Map<BattleHex, Integer> strikeMap, ValueRecorder value) { final MasterBoardTerrain terrain = client.getBattleSite().getTerrain(); final LegionClientSide legion = (LegionClientSide)client .getMyEngagedLegion(); final int skill = critter.getSkill(); final int power = critter.getPower(); final BattleHex hex = critter.getCurrentHex(); final int turn = client.getBattleTurnNumber(); PowerSkill ps = calcBonus(critter.getType(), hex.getTerrain() .getName(), true); int native_power = ps.getPowerAttack() + (ps.getPowerDefend() + power); int native_skill = ps.getSkillAttack() + ps.getSkillDefend(); evaluateCritterMove_Terrain(critter, value, terrain, hex, power, skill); if (hex.isEntrance()) { return value.getValue(); } Set<BattleHex> targetHexes = client.findStrikes(critter.getTag()); int numTargets = targetHexes.size(); if (numTargets >= 1) { if (!client.isInContact(critter, true)) { evaluateCritterMove_Rangestrike(critter, strikeMap, value, terrain, hex, power, skill, legion, turn, targetHexes); } else { evaluateCritterMove_Strike(critter, strikeMap, value, terrain, hex, power, skill, legion, turn, targetHexes); } } if (critter.isTitan()) { evaluateCritterMove_Titan(critter, value, terrain, hex, legion, turn); } else if (legion.equals(client.getDefender())) { evaluateCritterMove_Defender(critter, value, terrain, hex, legion, turn); } else { evaluateCritterMove_Attacker(critter, value, terrain, hex, legion, turn); } // Adjacent buddies for (int i = 0; i < 6; i++) { if (!hex.isCliff(i)) { BattleHex neighbor = hex.getNeighbor(i); if (neighbor != null && client.getGame().getBattle().isOccupied(neighbor)) { BattleCritter other = getBattleUnit(neighbor); if (other.isDefender() == critter.isDefender()) { // Buddy if (other.isTitan()) { value.add(bec.ADJACENT_TO_BUDDY_TITAN, "AdjacentToBuddyTitan 2"); value.add( native_skill * (native_power - critter.getHits()), "More AdjacentToBuddyTitan 2"); } else { value .add(bec.ADJACENT_TO_BUDDY, "AdjacentToBuddy"); } } } } } return value.getValue(); } protected int evaluateLegionBattleMove(LegionMove lm) { lm.resetEvaluate(); // First we need to move all critters into position. for (CritterMove cm : lm.getCritterMoves()) { cm.getCritter().moveToHex(cm.getEndingHex()); } Map<BattleHex, Integer> strikeMap = findStrikeMap(); // Then find the sum of all critter evals. int sum = 0; for (CritterMove cm : lm.getCritterMoves()) { ValueRecorder why = new ValueRecorder(); int val = evaluateCritterMove(cm.getCritter(), strikeMap, why); lm.setEvaluate(cm, why.toString()); sum += val; } // whole position evaluation { ValueRecorder why = new ValueRecorder(); int val = evaluateLegionBattleMoveAsAWhole(lm, strikeMap, why); lm.setEvaluate(why.toString()); sum += val; } // Then move them all back. for (CritterMove cm : lm.getCritterMoves()) { cm.getCritter().moveToHex(cm.getStartingHex()); } lm.setValue(sum); return sum; } protected class TriggerTimeIsUp extends TimerTask { @Override public void run() { timeIsUp = true; } } }