package net.sf.colossus.ai; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.logging.Logger; import net.sf.colossus.ai.helper.BattleEvalConstants; 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.common.Constants; import net.sf.colossus.game.Battle; import net.sf.colossus.game.BattleCritter; import net.sf.colossus.game.BattleStrike; import net.sf.colossus.game.BattleUnit; import net.sf.colossus.game.Caretaker; import net.sf.colossus.game.Creature; import net.sf.colossus.game.Legion; import net.sf.colossus.game.Player; import net.sf.colossus.server.VariantSupport; import net.sf.colossus.util.DevRandom; 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.IHintOracle; import net.sf.colossus.variant.IVariantHint; import net.sf.colossus.variant.MasterBoardTerrain; import net.sf.colossus.variant.MasterHex; import net.sf.colossus.variant.Variant; /** * Abstract implementation of the Colossus AI interface. * This class should hold most of the helper functions. * Ideally, most (all) "data-gathering" functions from the AIs * should be here, mostly as "final protected". AIs should mostly * only use information gathered from here to make decisions. * There's still a LOT of work to do... * * @author Romain Dolbeau * Also contains extracted code from SimpleAI: * @author Bruce Sherrod, David Ripton, Romain Dolbeau * Also contains extracted code from RationalAI: * @author Bruce Sherrod, David Ripton, Romain Dolbeau, Corwin Joy */ public abstract class AbstractAI implements AI { final static private Logger LOGGER = Logger.getLogger(AbstractAI.class .getName()); final public BattleEvalConstants bec = new BattleEvalConstants(); final protected CreatureValueConstants cvc = new CreatureValueConstants(); /** The Client we're working for. */ final protected Client client; protected Variant variant; /** Our random source. */ final protected Random random = new DevRandom(); /** * For the Oracle Hint stuff, the play style we use. * * This can be replaced by AI implementation. */ protected List<IVariantHint.AIStyle> hintSectionUsed = Collections .singletonList(IVariantHint.AIStyle.Offensive); protected AbstractAI(Client client) { this.client = client; } public void setVariant(Variant v) { this.variant = v; } final public CreatureType getVariantRecruitHint(LegionClientSide legion, MasterHex hex, List<CreatureType> recruits) { return VariantSupport.getRecruitHint(hex.getTerrain(), legion, recruits, new AbstractAIOracle(legion, hex, recruits), hintSectionUsed); } /** * arrays and generics don't work well together -- TODO replace the * array with a list or model some intermediate classes */ @SuppressWarnings(value = "unchecked") final protected Map<MasterHex, List<Legion>>[] buildEnemyAttackMap( Player player) { Map<MasterHex, List<Legion>>[] enemyMap = (Map<MasterHex, List<Legion>>[])new HashMap<?, ?>[7]; for (int i = 1; i <= 6; i++) { enemyMap[i] = new HashMap<MasterHex, List<Legion>>(); } // for each enemy player for (Player enemyPlayer : client.getGameClientSide().getPlayers()) { if (enemyPlayer == player) { continue; } // for each legion that player controls for (Legion legion : enemyPlayer.getLegions()) { // for each movement roll he might make for (int roll = 1; roll <= 6; roll++) { // count the moves he can get to Set<MasterHex> set; // Only allow Titan teleport // Remember, tower teleports cannot attack if (legion.hasTitan() && legion.getPlayer().canTitanTeleport() && client.getMovement().titanTeleportAllowed()) { set = client.getMovement().listAllMoves(legion, legion.getCurrentHex(), roll); } else { set = client.getMovement().listNormalMoves(legion, legion.getCurrentHex(), roll); } for (MasterHex hex : set) { for (int effectiveRoll = roll; effectiveRoll <= 6; effectiveRoll++) { // legion can attack to hexlabel on a effectiveRoll List<Legion> list = enemyMap[effectiveRoll] .get(hex); if (list == null) { list = new ArrayList<Legion>(); } if (list.contains(legion)) { continue; } list.add(legion); enemyMap[effectiveRoll].put(hex, list); } } } } } return enemyMap; } final protected int getNumberOfWaysToTerrain(Legion legion, MasterHex hex, String terrainTypeName) { int total = 0; for (int roll = 1; roll <= 6; roll++) { Set<MasterHex> tempset = client.getMovement().listAllMoves(legion, hex, roll, true); if (doesSetContainHexWithTerrain(tempset, terrainTypeName)) { total++; } } return total; } final private boolean doesSetContainHexWithTerrain(Set<MasterHex> set, String terrainTypeName) { for (MasterHex hex : set) { if (hex.getTerrain().getDisplayName().equals(terrainTypeName)) { return true; } } return false; } /** * Return a map of target hex label to number * of friendly creatures that can strike it */ final public Map<BattleHex, Integer> findStrikeMap() { Map<BattleHex, Integer> map = new HashMap<BattleHex, Integer>(); for (BattleCritter critter : client.getActiveBattleUnits()) { Set<BattleHex> targets = client.findStrikes(critter.getTag()); for (BattleHex targetHex : targets) { Integer old = map.get(targetHex); if (old == null) { map.put(targetHex, Integer.valueOf(1)); } else { map.put(targetHex, Integer.valueOf(old.intValue() + 1)); } } } return map; } /** * Create a map containing each target and the number of hits it would * likely take if all possible creatures attacked it. */ final protected Map<BattleCritter, Double> generateDamageMap() { Map<BattleCritter, Double> map = new HashMap<BattleCritter, Double>(); for (BattleCritter critter : client.getActiveBattleUnits()) { // Offboard critters can't strike. if (critter.getCurrentHex().getLabel().startsWith("X")) { continue; } Set<BattleHex> set = client.findStrikes(critter.getTag()); for (BattleHex targetHex : set) { BattleCritter target = getBattleUnit(targetHex); int dice = getBattleStrike().getDice(critter, target); int strikeNumber = getBattleStrike().getStrikeNumber(critter, target); double h = Probs.meanHits(dice, strikeNumber); if (map.containsKey(target)) { double d = map.get(target).doubleValue(); h += d; } map.put(target, new Double(h)); } } return map; } /** Return which creature the variant suggest splitting at turn 1 when * starting in a specific hex. * @param hex The masterboard hex where the split occurs. * @return The List of Creaturetype to split. */ final protected List<CreatureType> getInitialSplitHint(MasterHex hex) { return VariantSupport.getInitialSplitHint(hex, hintSectionUsed); } /** Get the 'kill value' of a creature on a specific terrain. * @param battleCritter The BattleCritter whose value is requested. * @param terrain The terrain on which the value is requested, or null. * @return The 'kill value' value of the critter, on terrain if non-null */ public int getKillValue(final BattleCritter battleCritter, final MasterBoardTerrain terrain) { return getKillValue(battleCritter.getType(), terrain); } /** Get the 'kill value' of a creature on an unspecified terrain. * @param creature The CreatureType whose value is requested. * @return The 'kill value' value of creature. */ protected int getKillValue(final CreatureType creature) { return getKillValue(creature, null); } /** Get the 'kill value' of a creature on a specific terrain. * @param creature The CreatureType whose value is requested. * @param terrain The terrain on which the value is requested, or null * @return The 'kill value' value of chit, on terrain if non-null */ private int getKillValue(final CreatureType creature, MasterBoardTerrain terrain) { int val; if (creature == null) { LOGGER.warning("Called getKillValue with null creature"); return 0; } // get non-terrain modified part of kill value val = creature.getKillValue(); // modify with terrain if (terrain != null && terrain.hasNativeCombatBonus(creature)) { val += cvc.HAS_NATIVE_COMBAT_BONUS; } return val; } /** * Shortcut to ask for the acquirables basic value from the variant * @link #Variant.getAcquirableRecruitmentsValue() * * @return The acquirableRecruitmentsValue */ protected int getAcqStepValue() { return variant.getAcquirableRecruitmentsValue(); } /** * Return true if the legion could recruit or acquire something * better than its worst creature in hexLabel. */ final protected boolean couldRecruitUp(Legion legion, MasterHex hex, Legion enemy) { CreatureType weakest = legion.getCreatureTypes().get( legion.getHeight() - 1); // Consider recruiting. List<CreatureType> recruits = client.findEligibleRecruits(legion, hex); if (!recruits.isEmpty()) { CreatureType bestRecruit = recruits.get(recruits.size() - 1); if (bestRecruit != null && getHintedRecruitmentValue(bestRecruit, legion, hintSectionUsed) > getHintedRecruitmentValue(weakest, legion, hintSectionUsed)) { return true; } } // Consider acquiring angels. if (enemy != null) { int pointValue = enemy.getPointValue(); boolean wouldFlee = flee(enemy, legion); if (wouldFlee) { pointValue /= 2; } // should work with all variants int currentScore = legion.getPlayer().getScore(); int arv = getAcqStepValue(); int nextScore = ((currentScore / arv) + 1) * arv; CreatureType bestRecruit = null; while ((currentScore + pointValue) >= nextScore) { List<String> ral = variant.getRecruitableAcquirableList( hex.getTerrain(), nextScore); for (String creatureName : ral) { CreatureType tempRecruit = variant .getCreatureByName(creatureName); if ((bestRecruit == null) || (getHintedRecruitmentValue(tempRecruit, legion, hintSectionUsed) >= getHintedRecruitmentValue( bestRecruit, legion, hintSectionUsed))) { bestRecruit = tempRecruit; } } nextScore += arv; } if (bestRecruit != null && getHintedRecruitmentValue(bestRecruit, legion, hintSectionUsed) > getHintedRecruitmentValue(weakest, legion, hintSectionUsed)) { return true; } } return false; } // The NonTitan forms moved here from variant.CreatureType so that variant // does not (just for those two calls from VariantSupport) depend on // server any more. AIs are the only users of that functionality anyway. public int getHintedRecruitmentValueNonTitan(CreatureType creature) { return creature.getPointValue() + VariantSupport.getHintedRecruitmentValueOffset(creature); } public int getHintedRecruitmentValueNonTitan(CreatureType creature, List<IVariantHint.AIStyle> styles) { return creature.getPointValue() + VariantSupport.getHintedRecruitmentValueOffset(creature, styles); } protected final int getHintedRecruitmentValue(CreatureType creature, Legion legion, List<IVariantHint.AIStyle> styles) { if (!creature.isTitan()) { return getHintedRecruitmentValueNonTitan(creature, styles); } Player player = legion.getPlayer(); int power = player.getTitanPower(); int skill = creature.getSkill(); return power * skill * VariantSupport.getHintedRecruitmentValueOffset(creature, styles); } /** Various constants used by the AIs code for creature evaluation. * Each specific AI should be able to override them * to tweak the evaluation results w/o rewriting the code. */ protected class CreatureValueConstants { /** Bonus to the 'kill value' when the terrain offer a bonus * in combat to the creature. * 0 by default, so the default 'kill value' is the 'kill value' * returned by the creature type. * SimpleAI (and all its subclasses) override this to 3. */ int HAS_NATIVE_COMBAT_BONUS = 0; } /** Test whether a Legion belongs to a Human player */ final protected boolean isHumanLegion(Legion legion) { return !legion.getPlayer().isAI(); } final protected boolean hasOpponentNativeCreature(HazardTerrain terrain) { boolean honc = false; for (BattleCritter critter : client.getInactiveBattleUnits()) { if (critter.getType().isNativeIn(terrain)) { honc = true; break; } } LOGGER.finest("Opponent " + (honc ? "has" : "doesn't have") + " native(s) from " + terrain.getName()); return honc; } final protected int rangeToClosestOpponent(final BattleHex hex) { int range = Constants.BIGNUM; for (BattleCritter critter : client.getInactiveBattleUnits()) { BattleHex hex2 = critter.getCurrentHex(); int r = Battle.getRange(hex, hex2, false); if (r < range) range = r; } return range; } /** 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. * This implementation try to build a near-exhaustive List of all * possible moves. It will be fully exhaustive if forceAll is true. * Otherwise, it will try to limit to a reasonable number (the exact * algorithm is in nestForLoop) */ final protected Collection<LegionMove> generateLegionMoves( final List<List<CritterMove>> allCritterMoves, boolean forceAll) { List<List<CritterMove>> critterMoves = new ArrayList<List<CritterMove>>( allCritterMoves); while (trimCritterMoves(critterMoves)) {// Just trimming } // Now that the list is as small as possible, start finding combos. List<LegionMove> legionMoves = new ArrayList<LegionMove>(); int[] indexes = new int[critterMoves.size()]; nestForLoop(indexes, indexes.length, critterMoves, legionMoves, forceAll); LOGGER.finest("generateLegionMoves got " + legionMoves.size() + " legion moves"); return legionMoves; } /** Set of hex name, to check for duplicates. * I assume the reason it is a class variable and not a local variable * inside the function is performance (avoiding creation/recreation). */ final private Set<BattleHex> duplicateHexChecker = new HashSet<BattleHex>(); /** Private helper for generateLegionMoves * If forceAll is true, generate all possible moves. Otherwise, * this function tries to limit the number of moves. * This function uses an intermediate array of indexes (called * and this is not a surprise, 'indexes') using recursion. * The minimum number of try should be the level (level one * is the most important creature and should always get his * own favorite spot, higher levels need to be able to fall back * on a not-so-good choice). */ final private void nestForLoop(int[] indexes, final int level, final List<List<CritterMove>> critterMoves, List<LegionMove> legionMoves, boolean forceAll) { // TODO See if doing the set test at every level is faster than // always going down to level 0 then checking. if (level == 0) { duplicateHexChecker.clear(); boolean offboard = false; for (int j = 0; j < indexes.length; j++) { List<CritterMove> moveList = critterMoves.get(j); if (indexes[j] >= moveList.size()) { return; } CritterMove cm = moveList.get(indexes[j]); BattleHex endingHex = cm.getEndingHex(); if (endingHex.getLabel().startsWith("X")) { offboard = true; } else if (duplicateHexChecker.contains(endingHex)) { // Need to allow duplicate offboard moves, in case 2 or // more creatures cannot enter. return; } duplicateHexChecker.add(cm.getEndingHex()); } LegionMove lm = makeLegionMove(indexes, critterMoves); // Put offboard moves last, so they'll be skipped if the AI // runs out of time. if (offboard) { legionMoves.add(lm); } else { legionMoves.add(0, lm); } } else { int howmany = critterMoves.get(level - 1).size(); int size = critterMoves.size(); // try and limit combinatorial explosion // criterions here: // 1) at least level moves per creatures (if possible!) // 2) not too many moves in total... int thresh = level + 1; // default: a bit more than the minimum if (size < 5) { thresh = level + 16; } else if (size < 6) { thresh = level + 8; } else if (size < 7) { thresh = level + 3; } if (thresh < level) // safety belt... for older codes. { thresh = level; } if (!forceAll && (howmany > thresh)) { howmany = thresh; } for (int i = 0; i < howmany; i++) { indexes[level - 1] = i; nestForLoop(indexes, level - 1, critterMoves, legionMoves, forceAll); } } } /** critterMoves is a List of sorted MoveLists. indexes is * a list of indexes, one per MoveList. * This return a LegionMove, made of one CritterMove per * MoveList. The CritterMove is selected by the index. */ final public static LegionMove makeLegionMove(int[] indexes, List<List<CritterMove>> critterMoves) { LegionMove lm = new LegionMove(); for (int i = 0; i < indexes.length; i++) { List<CritterMove> moveList = critterMoves.get(i); CritterMove cm = moveList.get(indexes[i]); lm.add(cm); } return lm; } /** Modify allCritterMoves in place, and return true if it changed. */ final private boolean trimCritterMoves( List<List<CritterMove>> allCritterMoves) { Set<BattleHex> takenHexes = new HashSet<BattleHex>(); // XXX reuse? boolean changed = false; // First trim immobile creatures from the list, and add their // hexes to takenHexes. Iterator<List<CritterMove>> it = allCritterMoves.iterator(); while (it.hasNext()) { List<CritterMove> moveList = it.next(); if (moveList.size() == 1) { // This critter is not mobile, and its hex is taken. CritterMove cm = moveList.get(0); takenHexes.add(cm.getStartingHex()); it.remove(); changed = true; } } // Now trim all moves to taken hexes from all movelists. it = allCritterMoves.iterator(); while (it.hasNext()) { List<CritterMove> moveList = it.next(); for (CritterMove cm : moveList) { if (takenHexes.contains(cm.getEndingHex())) { it.remove(); changed = true; } } } return changed; } protected class AbstractAIOracle implements IHintOracle { private final LegionClientSide legion; private final MasterHex hex; private final List<CreatureType> recruits; private Map<MasterHex, List<Legion>>[] enemyAttackMap = null; AbstractAIOracle(LegionClientSide legion, MasterHex hex, List<CreatureType> recruits) { this.legion = legion; this.hex = hex; this.recruits = recruits; } public boolean canReach(String terrainTypeName) { int now = getNumberOfWaysToTerrain(legion, hex, terrainTypeName); return (now > 0); } public int creatureAvailable(String name) { return creatureAvailable(variant.getCreatureByName(name)); } public int creatureAvailable(CreatureType creatureType) { return client.getReservedRemain(creatureType); } public boolean canRecruit(String name) { return recruits.contains(variant.getCreatureByName(name)); } public String hexLabel() { return hex.getLabel(); } public int biggestAttackerHeight() { if (enemyAttackMap == null) { enemyAttackMap = buildEnemyAttackMap(client.getOwningPlayer()); } int worst = 0; for (int i = 1; i < 6; i++) { List<Legion> enemyList = enemyAttackMap[i].get(legion .getCurrentHex()); if (enemyList != null) { for (Legion enemy : enemyList) { if ((enemy).getHeight() > worst) { worst = (enemy).getHeight(); } } } } return worst; } } /** little helper to store info about possible moves */ protected class MoveInfo { final Legion legion; /** hex to move to. if hex == null, then this means sit still. */ final MasterHex hex; final int value; final int difference; // difference from sitting still final ValueRecorder why; // explain value MoveInfo(Legion legion, MasterHex hex, int value, int difference, ValueRecorder why) { this.legion = legion; this.hex = hex; this.value = value; this.difference = difference; this.why = why; } } public void initBattle() { LOGGER.finer("A battle started."); } public void cleanupBattle() { LOGGER.finer("A battle is finished."); } // TODO get directly, not via Client protected BattleUnit getBattleUnit(BattleHex hex) { return client.getBattleCS().getBattleUnit(hex); } public BattleStrike getBattleStrike() { return client.getGame().getBattleStrike(); } final public int countCreatureAccrossAllLegionFromPlayer(Creature creature) { Player player = creature.getPlayer(); int count = 0; for (Legion legion : player.getLegions()) { for (Creature creature2 : legion.getCreatures()) { if (creature2.getType().equals(creature.getType())) { count++; } } } return count; } public Caretaker getCaretaker() { return client.getGame().getCaretaker(); } }