/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * FreeCol is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol.server.ai; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.logging.Logger; import net.sf.freecol.common.model.Ability; import net.sf.freecol.common.model.Colony; import net.sf.freecol.common.model.ColonyTradeItem; import net.sf.freecol.common.model.CombatModel; import net.sf.freecol.common.model.DiplomaticTrade; import net.sf.freecol.common.model.Europe; import net.sf.freecol.common.model.FeatureContainer; import net.sf.freecol.common.model.FoundingFather; import net.sf.freecol.common.model.GoldTradeItem; import net.sf.freecol.common.model.Goods; import net.sf.freecol.common.model.GoodsTradeItem; import net.sf.freecol.common.model.GoodsType; import net.sf.freecol.common.model.IndianSettlement; import net.sf.freecol.common.model.Location; import net.sf.freecol.common.model.Market; import net.sf.freecol.common.model.Modifier; import net.sf.freecol.common.model.Ownable; import net.sf.freecol.common.model.PathNode; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.Player.PlayerType; import net.sf.freecol.common.model.Player.Stance; import net.sf.freecol.common.model.Settlement; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.StanceTradeItem; import net.sf.freecol.common.model.Tension; import net.sf.freecol.common.model.Tile; import net.sf.freecol.common.model.TradeItem; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.model.Unit.UnitState; import net.sf.freecol.common.model.UnitTradeItem; import net.sf.freecol.common.model.UnitType; import net.sf.freecol.common.model.WorkLocation; import net.sf.freecol.common.model.pathfinding.CostDeciders; import net.sf.freecol.common.model.pathfinding.GoalDecider; import net.sf.freecol.common.networking.NetworkConstants; import net.sf.freecol.common.util.Utils; import net.sf.freecol.server.ai.mission.BuildColonyMission; import net.sf.freecol.server.ai.mission.CashInTreasureTrainMission; import net.sf.freecol.server.ai.mission.DefendSettlementMission; import net.sf.freecol.server.ai.mission.IdleAtColonyMission; import net.sf.freecol.server.ai.mission.Mission; import net.sf.freecol.server.ai.mission.PioneeringMission; import net.sf.freecol.server.ai.mission.PrivateerMission; import net.sf.freecol.server.ai.mission.ScoutingMission; import net.sf.freecol.server.ai.mission.TransportMission; import net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission; import net.sf.freecol.server.ai.mission.UnitWanderHostileMission; import net.sf.freecol.server.ai.mission.WishRealizationMission; import net.sf.freecol.server.ai.mission.WorkInsideColonyMission; import net.sf.freecol.server.model.ServerPlayer; import org.freecolandroid.xml.stream.XMLStreamException; import org.freecolandroid.xml.stream.XMLStreamReader; import org.w3c.dom.Element; /** * Objects of this class contains AI-information for a single {@link * Player} and is used for controlling this player. * * The method {@link #startWorking} gets called by the * {@link AIInGameInputHandler} when it is this player's turn. */ public class EuropeanAIPlayer extends AIPlayer { private static final Logger logger = Logger.getLogger(EuropeanAIPlayer.class.getName()); /** * Stores temporary information for sessions (trading with another player * etc). */ private HashMap<String, Integer> sessionRegister = new HashMap<String, Integer>(); /** * Creates a new <code>AIPlayer</code>. * * @param aiMain The main AI-class. * @param player The player that should be associated with this * <code>AIPlayer</code>. */ public EuropeanAIPlayer(AIMain aiMain, ServerPlayer player) { super(aiMain, player.getId()); setPlayer(player); } /** * Creates a new <code>AIPlayer</code> and reads the information from the * given <code>Element</code>. * * @param aiMain The main AI-class. * @param element The XML-element containing information. */ public EuropeanAIPlayer(AIMain aiMain, Element element) { super(aiMain, element.getAttribute(ID_ATTRIBUTE)); readFromXMLElement(element); } /** * Creates a new <code>AIPlayer</code>. * * @param aiMain The main AI-object. * @param in The input stream containing the XML. * @throws XMLStreamException if a problem was encountered during parsing. */ public EuropeanAIPlayer(AIMain aiMain, XMLStreamReader in) throws XMLStreamException { super(aiMain, in.getAttributeValue(null, ID_ATTRIBUTE)); readFromXML(in); } /* IMPLEMENTATION (AIPlayer interface) ****************************************/ /** * Tells this <code>AIPlayer</code> to make decisions. The * <code>AIPlayer</code> is done doing work this turn when this method * returns. */ public void startWorking() { final Player player = getPlayer(); logger.finest("Entering method startWorking: " + player + ", year " + getGame().getTurn()); sessionRegister.clear(); clearAIUnits(); cheat(); determineStances(); rearrangeWorkersInColonies(); abortInvalidAndOneTimeMissions(); ensureColonyMissions(); giveNormalMissions(); bringGifts(); demandTribute(); createAIGoodsInColonies(); createTransportLists(); doMissions(); rearrangeWorkersInColonies(); abortInvalidMissions(); // Some of the mission might have been invalidated by a another mission. giveNormalMissions(); doMissions(); rearrangeWorkersInColonies(); abortInvalidMissions(); ensureColonyMissions(); clearAIUnits(); } /** * Gets a list of all the player's tile improvement plans required by * the colonies. * * @return A list of tile improvements. * @see net.sf.freecol.common.model.TileImprovement */ public List<TileImprovementPlan> getTileImprovementPlans() { List<TileImprovementPlan> tileImprovements = new ArrayList<TileImprovementPlan>(); for (AIColony aic : getAIColonies()) { tileImprovements.addAll(aic.getTileImprovementPlans()); } return tileImprovements; } /** * Remove a <code>TileImprovementPlan</code> from the relevant colony. */ public void removeTileImprovementPlan(TileImprovementPlan plan) { for (AIColony aic : getAIColonies()) { if (aic.removeTileImprovementPlan(plan)) return; } logger.warning("Not found given TileImprovementPlan to remove"); } /** * Asks the server to recruit a unit in Europe on behalf of the AIPlayer. * * TODO: Move this to a specialized Handler class (AIEurope?) * TODO: Give protected access? * * @param index The index of the unit to recruit in the recruitables list, * (if not a valid index, recruit a random unit). * @return The new AIUnit created by this action or null on failure. */ public AIUnit recruitAIUnitInEurope(int index) { AIUnit aiUnit = null; Europe europe = getPlayer().getEurope(); int n = europe.getUnitCount(); final String selectAbility = "model.ability.selectRecruit"; int slot = (index >= 0 && index < Europe.RECRUIT_COUNT && getPlayer().hasAbility(selectAbility)) ? (index + 1) : 0; if (AIMessage.askEmigrate(getConnection(), slot) && europe.getUnitCount() == n+1) { aiUnit = getAIUnit(europe.getUnitList().get(n)); } return aiUnit; } /** * Helper function for server communication - Ask the server * to train a unit in Europe on behalf of the AIGetPlayer(). * * TODO: Move this to a specialized Handler class (AIEurope?) * TODO: Give protected access? * * @return the new AIUnit created by this action. May be null. */ public AIUnit trainAIUnitInEurope(UnitType unitType) { if (unitType==null) { throw new IllegalArgumentException("Invalid UnitType."); } AIUnit aiUnit = null; Europe europe = getPlayer().getEurope(); int n = europe.getUnitCount(); if (AIMessage.askTrainUnitInEurope(getConnection(), unitType) && europe.getUnitCount() == n+1) { aiUnit = getAIUnit(europe.getUnitList().get(n)); } return aiUnit; } /** * This is a temporary method which are used for forcing the * computer players into building more colonies. The method will * be removed after the proper code for deciding whether a colony * should be built or not has been implemented. * * @return <code>true</code> if the AI should build more colonies. */ public boolean hasFewColonies() { if (!getPlayer().canBuildColonies()) return false; int numberOfColonies = 0; int numberOfWorkers = 0; for (Settlement settlement : getPlayer().getSettlements()) { numberOfColonies++; numberOfWorkers += settlement.getUnitCount(); } boolean result = numberOfColonies <= 2 || (numberOfColonies >= 3 && numberOfWorkers / numberOfColonies > numberOfColonies - 2); logger.finest("hasFewColonies (" + numberOfColonies + " : " + numberOfWorkers + "): " + result); return result; } /** * Gets the wishes for all this player's colonies, sorted by the * {@link Wish#getValue value}. * * @return A list of wishes. */ public List<Wish> getWishes() { List<Wish> wishes = new ArrayList<Wish>(); for (AIColony aic : getAIColonies()) { wishes.addAll(aic.getWishes()); } Collections.sort(wishes); return wishes; } /** * Selects the most useful founding father offered. * * @param foundingFathers The founding fathers on offer. * @return The founding father selected. */ public FoundingFather selectFoundingFather(List<FoundingFather> foundingFathers) { // TODO: improve choice int age = getGame().getTurn().getAge(); FoundingFather bestFather = null; int bestWeight = Integer.MIN_VALUE; for (FoundingFather father : foundingFathers) { if (father == null) continue; // For the moment, arbitrarily: always choose the one // offering custom houses. Allowing the AI to build CH // early alleviates the complexity problem of handling all // TransportMissions correctly somewhat. if (father.hasAbility("model.ability.buildCustomHouse")) { bestFather = father; break; } int weight = father.getWeight(age); if (weight > bestWeight) { bestWeight = weight; bestFather = father; } } return bestFather; } /** * Decides whether to accept the monarch's tax raise or not. * * @param tax The new tax rate to be considered. * @return <code>true</code> if the tax raise should be accepted. */ public boolean acceptTax(int tax) { Goods toBeDestroyed = getPlayer().getMostValuableGoods(); if (toBeDestroyed == null) { return false; } GoodsType goodsType = toBeDestroyed.getType(); if (goodsType.isFoodType() || goodsType.isBreedable()) { // we should be able to produce food and horses ourselves // TODO: check whether we already have horses! return false; } else if (goodsType.isMilitaryGoods() || goodsType.isTradeGoods() || goodsType.isBuildingMaterial()) { if (getGame().getTurn().getAge() == 3) { // by this time, we should be able to produce // enough ourselves // TODO: check whether we have an armory, at least return false; } else { return true; } } else { int averageIncome = 0; int numberOfGoods = 0; // TODO: consider the amount of goods produced. If we // depend on shipping huge amounts of cheap goods, we // don't want these goods to be boycotted. List<GoodsType> goodsTypes = getSpecification().getGoodsTypeList(); for (GoodsType type : goodsTypes) { if (type.isStorable()) { averageIncome += getPlayer().getIncomeAfterTaxes(type); numberOfGoods++; } } averageIncome = averageIncome / numberOfGoods; if (getPlayer().getIncomeAfterTaxes(toBeDestroyed.getType()) > averageIncome) { // this is a more valuable type of goods return false; } else { return true; } } } /** * Decides whether to accept an Indian demand, or not. * * @param unit The <code>Unit</code> making demands. * @param colony The <code>Colony</code> where demands are being made. * @param goods The <code>Goods</code> demanded. * @param gold The amount of gold demanded. * @return True if this player accepts the demand. */ public boolean indianDemand(Unit unit, Colony colony, Goods goods, int gold) { // TODO: make a better choice, check whether the colony is // well defended return !"conquest".equals(getAIAdvantage()); } /** * Decides to accept an offer of mercenaries or not. * TODO: make a better choice. * * @return True if the mercenaries are accepted. */ public boolean acceptMercenaries() { return getPlayer().isAtWar() || "conquest".equals(getAIAdvantage()); } public boolean acceptDiplomaticTrade(DiplomaticTrade agreement) { boolean validOffer = true; Stance stance = null; int value = 0; Iterator<TradeItem> itemIterator = agreement.iterator(); while (itemIterator.hasNext()) { TradeItem item = itemIterator.next(); if (item instanceof GoldTradeItem) { int gold = ((GoldTradeItem) item).getGold(); if (item.getSource() == getPlayer()) { value -= gold; } else { value += gold; } } else if (item instanceof StanceTradeItem) { // TODO: evaluate whether we want this stance change stance = ((StanceTradeItem) item).getStance(); switch (stance) { case UNCONTACTED: validOffer = false; //never accept invalid stance change break; case WAR: // always accept war without cost break; case CEASE_FIRE: value -= 500; break; case PEACE: if (!agreement.getSender().hasAbility("model.ability.alwaysOfferedPeace")) { // TODO: introduce some kind of counter in order to avoid // Benjamin Franklin exploit value -= 1000; } break; case ALLIANCE: value -= 2000; break; } } else if (item instanceof ColonyTradeItem) { // TODO: evaluate whether we might wish to give up a colony if (item.getSource() == getPlayer()) { validOffer = false; break; } else { value += 1000; } } else if (item instanceof UnitTradeItem) { // TODO: evaluate whether we might wish to give up a unit if (item.getSource() == getPlayer()) { validOffer = false; break; } else { value += 100; } } else if (item instanceof GoodsTradeItem) { Goods goods = ((GoodsTradeItem) item).getGoods(); if (item.getSource() == getPlayer()) { value -= getPlayer().getMarket().getBidPrice(goods.getType(), goods.getAmount()); } else { value += getPlayer().getMarket().getSalePrice(goods.getType(), goods.getAmount()); } } } if (validOffer) { logger.info("Trade value is " + value + ", accept if >=0"); } else { logger.info("Trade offer is considered invalid!"); } return (value>=0)&&validOffer; } /** * Called after another <code>Player</code> sends a <code>trade</code> message * * * @param goods The goods which we are going to offer */ public void registerSellGoods(Goods goods) { String goldKey = "tradeGold#" + goods.getType().getId() + "#" + goods.getAmount() + "#" + goods.getLocation().getId(); sessionRegister.put(goldKey, null); } /** * Called when another <code>Player</code> proposes to buy. * * * @param unit The foreign <code>Unit</code> trying to trade. * @param settlement The <code>Settlement</code> this player owns and * which the given <code>Unit</code> is trading. * @param goods The goods the given <code>Unit</code> is trying to sell. * @param gold The suggested price. * @return The price this <code>AIPlayer</code> suggests or * {@link NetworkConstants#NO_TRADE}. */ // TODO: this obviously applies only to native players. Is there // an European equivalent? public int buyProposition(Unit unit, Settlement settlement, Goods goods, int gold) { logger.finest("Entering method buyProposition"); Player buyer = unit.getOwner(); String goldKey = "tradeGold#" + goods.getType().getId() + "#" + goods.getAmount() + "#" + settlement.getId(); String hagglingKey = "tradeHaggling#" + unit.getId(); Integer registered = sessionRegister.get(goldKey); if (registered == null) { int price = ((IndianSettlement) settlement).getPriceToSell(goods) + getPlayer().getTension(buyer).getValue(); Unit missionary = ((IndianSettlement) settlement).getMissionary(buyer); if (missionary != null && getSpecification() .getBoolean("model.option.enhancedMissionaries")) { // 10% bonus for missionary, 20% if expert int bonus = (missionary.hasAbility(Ability.EXPERT_MISSIONARY)) ? 8 : 9; price = (price * bonus) / 10; } sessionRegister.put(goldKey, new Integer(price)); return price; } else { int price = registered.intValue(); if (price < 0 || price == gold) { return price; } else if (gold < (price * 9) / 10) { logger.warning("Cheating attempt: sending a offer too low"); sessionRegister.put(goldKey, new Integer(-1)); return NetworkConstants.NO_TRADE; } else { int haggling = 1; if (sessionRegister.containsKey(hagglingKey)) { haggling = sessionRegister.get(hagglingKey).intValue(); } if (Utils.randomInt(logger, "Buy gold", getAIRandom(), 3 + haggling) <= 3) { sessionRegister.put(goldKey, new Integer(gold)); sessionRegister.put(hagglingKey, new Integer(haggling + 1)); return gold; } else { sessionRegister.put(goldKey, new Integer(-1)); return NetworkConstants.NO_TRADE_HAGGLE; } } } } /** * Called when another <code>Player</code> proposes a sale. * * @param unit The foreign <code>Unit</code> trying to trade. * @param settlement The <code>Settlement</code> this player owns and * which the given <code>Unit</code> if trying to sell goods. * @param goods The goods the given <code>Unit</code> is trying to sell. * @param gold The suggested price. * @return The price this <code>AIPlayer</code> suggests or * {@link NetworkConstants#NO_TRADE}. */ public int sellProposition(Unit unit, Settlement settlement, Goods goods, int gold) { logger.finest("Entering method sellProposition"); Colony colony = (Colony) settlement; Player otherPlayer = unit.getOwner(); // don't pay for more than fits in the warehouse int amount = colony.getWarehouseCapacity() - colony.getGoodsCount(goods.getType()); amount = Math.min(amount, goods.getAmount()); // get a good price Tension.Level tensionLevel = getPlayer().getTension(otherPlayer).getLevel(); int percentage = (9 - tensionLevel.ordinal()) * 10; // what we could get for the goods in Europe (minus taxes) int netProfits = ((100 - getPlayer().getTax()) * getPlayer().getMarket().getSalePrice(goods.getType(), amount)) / 100; int price = (netProfits * percentage) / 100; return price; } /* Internal methods ***********************************************************/ /** * Cheats for the AI. Please try to centralize cheats here. * * TODO: Remove when the AI is good enough. */ private void cheat() { logger.finest("Entering method cheat"); Specification spec = getSpecification(); Market market = getPlayer().getMarket(); for (GoodsType goodsType : spec.getGoodsTypeList()) { if (market.getArrears(goodsType) > 0 && Utils.randomInt(logger, "Cheat boycott", getAIRandom(), 5) == 0) { market.setArrears(goodsType, 0); // Just remove one goods party modifier (we can not // currently identify which modifier applies to which // goods type, but that is not worth fixing for the // benefit of `temporary' cheat code). If we do not // do this, AI colonies accumulate heaps of party // modifiers because of the cheat boycott removal. findOne: for (Colony c : getPlayer().getColonies()) { FeatureContainer fc = c.getFeatureContainer(); for (Modifier m : fc.getModifiers()) { if ("model.modifier.colonyGoodsParty".equals(m.getSource())) { fc.removeModifier(m); break findOne; } } } } } //TODO: This seems to buy units the AIPlayer can't possibly use (see BR#2566180) if (getAIMain().getFreeColServer().isSingleplayer() && getPlayer().getPlayerType() == PlayerType.COLONIAL) { Europe europe = getPlayer().getEurope(); List<UnitType> unitTypes = spec.getUnitTypeList(); if (Utils.randomInt(logger, "Cheat buy unit", getAIRandom(), 10) == 1) { WorkerWish bestWish = null; int bestValue = Integer.MIN_VALUE; for (AIColony aic : getAIColonies()) { for (WorkerWish ww : aic.getWorkerWishes()) { if (ww.getValue() > bestValue) { bestValue = ww.getValue(); bestWish = ww; } } } UnitType unitType; int unitPrice; if (bestWish != null && (unitType = bestWish.getUnitType()) != null && (unitPrice = europe.getUnitPrice(unitType)) >= 0) { // cheat add the necessary amount of money getPlayer().modifyGold(unitPrice); AIUnit aiUnit = trainAIUnitInEurope(unitType); if (aiUnit != null) { Unit unit = aiUnit.getUnit(); if (unit != null && unit.isColonist()) { aiUnit.equipForRole(Unit.Role.DRAGOON, true); } aiUnit.setMission(new WishRealizationMission(getAIMain(), aiUnit, bestWish)); } } } // TODO: better heuristics to determine which ship to buy if (Utils.randomInt(logger, "Cheat buy ship", getAIRandom(), 40) == 21) { int total = 0; ArrayList<UnitType> navalUnits = new ArrayList<UnitType>(); for (UnitType unitType : unitTypes) { if (unitType.hasAbility(Ability.NAVAL_UNIT) && unitType.hasPrice()) { navalUnits.add(unitType); total += europe.getUnitPrice(unitType); } } UnitType unitToPurchase = null; int r = Utils.randomInt(logger, "Cheat which ship", getAIRandom(), total); total = 0; for (UnitType unitType : navalUnits) { total += unitType.getPrice(); if (r < total) { unitToPurchase = unitType; break; } } getPlayer().modifyGold(europe.getUnitPrice(unitToPurchase)); this.trainAIUnitInEurope(unitToPurchase); } } } /** * Ensures that all workers inside a colony gets a * {@link WorkInsideColonyMission}. */ private void ensureColonyMissions() { logger.finest("Entering method ensureColonyMissions"); for (AIUnit au : getAIUnits()) { if (au.hasMission()) continue; // TODO: Find out why this happens, or even if it still does. Unit u = au.getUnit(); if (u.getLocation() instanceof WorkLocation) { AIColony ac = getAIColony(u.getColony()); if (ac == null) { logger.warning("No AIColony for unit: " + u + " at: " + u.getLocation()); continue; } au.setMission(new WorkInsideColonyMission(getAIMain(), au, ac)); } } } /** * Calls {@link AIColony#rearrangeWorkers} for every colony this player * owns. */ private void rearrangeWorkersInColonies() { logger.finest("Entering method rearrangeWorkersInColonies"); for (AIColony aic : getAIColonies()) aic.rearrangeWorkers(); } /** * Takes the necessary actions to secure a european colony */ /* private void secureColony(Colony colony) { GoodsType musketType = getSpecification().getGoodsType("model.goods.muskets"); GoodsType horsesType = getSpecification().getGoodsType("model.goods.horses"); final EquipmentType muskets = getSpecification().getEquipmentType("model.equipment.muskets"); final EquipmentType horses = getSpecification().getEquipmentType("model.equipment.horses"); Map map = getPlayer().getGame().getMap(); int olddefenders = 0; int defenders = 0; int threat = 0; int worstThreat = 0; Location bestTarget = null; Iterator<Unit> ui = colony.getTile().getUnitIterator(); while (ui.hasNext()) { if ((ui.next()).isDefensiveUnit()) { defenders++; } } Iterator<Position> positionIterator = map.getCircleIterator(colony.getTile().getPosition(), true, 5); while (positionIterator.hasNext()) { Tile t = map.getTile(positionIterator.next()); if (t.getFirstUnit() != null) { if (t.getFirstUnit().getOwner() == getPlayer()) { Iterator<Unit> uit = t.getUnitIterator(); while (uit.hasNext()) { if (uit.next().isOffensiveUnit()) { defenders++; } } } else { int thisThreat = 0; if (getPlayer().getTension(t.getFirstUnit().getOwner()).getValue() >= Tension.TENSION_ADD_MAJOR) { Iterator<Unit> uit = t.getUnitIterator(); while (uit.hasNext()) { if (uit.next().isOffensiveUnit()) { thisThreat += 2; } } } else if (getPlayer().getTension(t.getFirstUnit().getOwner()).getValue() >= Tension.TENSION_ADD_MINOR) { Iterator<Unit> uit = t.getUnitIterator(); while (uit.hasNext()) { if (uit.next().isOffensiveUnit()) { thisThreat++; } } } threat += thisThreat; if (thisThreat > worstThreat) { if (t.getSettlement() != null) { bestTarget = t.getSettlement(); } else { bestTarget = t.getFirstUnit(); } worstThreat = thisThreat; } } } } olddefenders = defenders; if (colony.hasStockade()) { defenders += (defenders * (colony.getStockade().getLevel()) / 2); } if (threat > defenders) { // We're under attack! Man the stockade! ArrayList<Unit> recruits = new ArrayList<Unit>(); ArrayList<Unit> others = new ArrayList<Unit>(); int inColonyCount = 0; // Let's make some more soldiers, if we can. // First, find some people we can recruit. ui = colony.getUnitIterator(); while (ui.hasNext()) { Unit u = (ui.next()); if (u.isOffensiveUnit()) { continue; // don't bother dealing with current // soldiers at the moment } if (u.getLocation() != colony.getTile()) { // If we are not on the tile we are in the colony. inColonyCount++; } if (u.hasAbility(Ability.EXPERT_SOLDIER)) { recruits.add(u); } else if (u.hasAbility(Ability.CAN_BE_EQUIPPED)) { others.add(u); } } // ATTENTION: skill may be Integer.MIN_VALUE! Collections.sort(others, Unit.getSkillLevelComparator()); recruits.addAll(others); // Don't overdo it - leave at least one person behind. int recruitCount = threat - defenders; if (recruitCount > recruits.size() - 1) { recruitCount = recruits.size() - 1; } if (recruitCount > inColonyCount - 1) { recruitCount = inColonyCount - 1; } // Actually go through and arm our people. boolean needMuskets = false; boolean needHorses = false; ui = recruits.iterator(); while (ui.hasNext() && recruitCount > 0) { Unit u = (ui.next()); if (!u.isArmed() && u.canBeEquippedWith(muskets)) { recruitCount--; Element equipUnitElement = Message.createNewRootElement("equipUnit"); equipUnitElement.setAttribute("unit", u.getId()); equipUnitElement.setAttribute("type", muskets.getId()); equipUnitElement.setAttribute("amount", "1"); u.equipWith(muskets); sendAndWaitSafely(equipUnitElement); Element putOutsideColonyElement = Message.createNewRootElement("putOutsideColony"); putOutsideColonyElement.setAttribute("unit", u.getId()); u.putOutsideColony(); sendAndWaitSafely(putOutsideColonyElement); // Check if the unit can fortify before sending the order if (u.checkSetState(UnitState.FORTIFYING)) { Element changeStateElement = Message.createNewRootElement("changeState"); changeStateElement.setAttribute("unit", u.getId()); changeStateElement.setAttribute("state", UnitState.FORTIFYING.toString()); sendAndWaitSafely(changeStateElement); } olddefenders++; if (!u.isMounted() && u.canBeEquippedWith(horses)) { equipUnitElement = Message.createNewRootElement("equipUnit"); equipUnitElement.setAttribute("unit", u.getId()); equipUnitElement.setAttribute("type", horses.getId()); equipUnitElement.setAttribute("amount", "1"); sendAndWaitSafely(equipUnitElement); } else { needHorses = true; } } else { needMuskets = true; break; } } AIColony ac = null; if (needMuskets || needHorses) { Iterator<AIColony> aIterator = getAIColonyIterator(); while (aIterator.hasNext()) { AIColony temp = aIterator.next(); if (temp != null && temp.getColony() == colony) { ac = temp; break; } } } if (needMuskets && ac != null) { // Check and see if we have already made a GoodsWish for // here. Iterator<Wish> wishes = ac.getWishIterator(); boolean made = false; while (wishes.hasNext()) { Wish w = wishes.next(); if (!(w instanceof GoodsWish)) { continue; } GoodsWish gw = (GoodsWish) w; if (gw.getGoodsType() == musketType) { made = true; } } if (made == false) { // Add a new GoodsWish onto the stack. ac .addGoodsWish(new GoodsWish(getAIMain(), colony, (threat - olddefenders) * 50, musketType)); } } if (needHorses && ac != null) { // Check and see if we have already made a GoodsWish for // here. Iterator<Wish> wishes = ac.getWishIterator(); boolean made = false; while (wishes.hasNext()) { Wish w = wishes.next(); if (!(w instanceof GoodsWish)) { continue; } GoodsWish gw = (GoodsWish) w; if (gw.getGoodsType() == horsesType) { made = true; } } if (made == false) { // Add a new GoodsWish onto the stack. ac.addGoodsWish(new GoodsWish(getAIMain(), colony, (threat - defenders) * 50, horsesType)); } } defenders = olddefenders; if (colony.hasStockade()) { defenders += (defenders * (colony.getStockade().getLevel()) / 2); } } if (defenders > (threat * 2)) { // We're so big and tough, we can go wipe out this threat. // Pick someone to go make it happen. Unit u = null; Iterator<Unit> uit = colony.getUnitIterator(); while (uit.hasNext()) { Unit candidate = uit.next(); if (candidate.isOffensiveUnit() && candidate.getState() == UnitState.FORTIFIED) { u = candidate; break; } } if (u != null) { u.setState(UnitState.ACTIVE); u.setLocation(colony.getTile()); AIUnit newDefenderAI = (AIUnit) getAIMain().getAIObject(u); if (bestTarget != null) { newDefenderAI.setMission(new UnitSeekAndDestroyMission(getAIMain(), newDefenderAI, bestTarget)); } else { newDefenderAI.setMission(new UnitWanderHostileMission(getAIMain(), newDefenderAI)); } } } } */ private List<AIUnit>getPlayerPioneers() { List<AIUnit> pioneers = new ArrayList<AIUnit>(); AIMain aiMain = getAIMain(); for (Unit u : getPlayer().getUnits()) { AIUnit aiu = aiMain.getAIUnit(u); if (aiu == null) continue; if (aiu.getMission() instanceof PioneeringMission) { pioneers.add(aiu); } } return pioneers; } /** * Gives a mission to non-naval units. */ protected void giveNormalMissions() { logger.finest("Entering method giveNormalMissions"); // Create a datastructure for the worker wishes: java.util.Map<UnitType, ArrayList<Wish>> workerWishes = new HashMap<UnitType, ArrayList<Wish>>(); for (UnitType unitType : getSpecification().getUnitTypeList()) { workerWishes.put(unitType, new ArrayList<Wish>()); } for (Wish w : getWishes()) { if (w instanceof WorkerWish && w.getTransportable() == null) { workerWishes.get(((WorkerWish) w).getUnitType()).add(w); } } final boolean fewColonies = hasFewColonies(); boolean isPioneerReq = getPlayerPioneers().size() == 0; Iterator<AIUnit> aiUnitsIterator = getAIUnitIterator(); while (aiUnitsIterator.hasNext()) { AIUnit aiUnit = aiUnitsIterator.next(); if (aiUnit.hasMission()) continue; Unit unit = aiUnit.getUnit(); if (unit.isUninitialized()) { logger.warning("Unit is uninitialized: " + unit.getId()); continue; } if (unit.isDisposed()) continue; if (unit.getState() == UnitState.IN_COLONY && unit.getSettlement().getUnitCount() <= 1) { // The unit has its hand full keeping the colony alive. continue; } if (PrivateerMission.isValid(aiUnit)) { aiUnit.setMission(new PrivateerMission(getAIMain(), aiUnit)); continue; } /* TODO: we need a mission for frigates and similar * units to patrol enemy shipping lanes */ if (unit.isCarrier()) { aiUnit.setMission(new TransportMission(getAIMain(), aiUnit)); continue; } if (unit.canCarryTreasure()) { aiUnit.setMission(new CashInTreasureTrainMission(getAIMain(), aiUnit)); continue; } if (ScoutingMission.isValid(aiUnit)) { aiUnit.setMission(new ScoutingMission(getAIMain(), aiUnit)); continue; } if (unit.isOffensiveUnit() || unit.isDefensiveUnit()){ Player owner = unit.getOwner(); boolean isPastStart = getGame().getTurn().getNumber() > 5 && !owner.getSettlements().isEmpty(); if (!unit.isColonist() || isPastStart) { giveMilitaryMission(aiUnit); continue; } } // Setup as a pioneer if unit: // - already has tools // - an expert pioneer // - a non-expert unit and there are no other units assigned as pioneers boolean isPioneer = unit.hasAbility("model.ability.improveTerrain") || unit.hasAbility(Ability.EXPERT_PIONEER); boolean isExpert = unit.getSkillLevel() > 0; if (isPioneer || (isPioneerReq && !isExpert)) { if (PioneeringMission.isValid(aiUnit)) { aiUnit.setMission(new PioneeringMission(getAIMain(), aiUnit)); isPioneerReq = false; continue; } } if (unit.isColonist()) { giveColonistMission(aiUnit, fewColonies, workerWishes); } if (!aiUnit.hasMission()) { if (aiUnit.getUnit().isOffensiveUnit()) { aiUnit.setMission(new UnitWanderHostileMission(getAIMain(), aiUnit)); } else { //non-offensive units should take shelter in a nearby colony, //not try to be hostile aiUnit.setMission(new IdleAtColonyMission(getAIMain(), aiUnit)); } } } } private void giveColonistMission(AIUnit aiUnit, boolean fewColonies, java.util.Map<UnitType, ArrayList<Wish>> workerWishes) { final Unit unit = aiUnit.getUnit(); /* * Motivated by (speed) performance: This map stores the * distance between the unit and the destination of a Wish: */ HashMap<Location, Integer> distances = new HashMap<Location, Integer>(121); for (ArrayList<Wish> al : workerWishes.values()) { for (Wish w : al) { if (w.getDestination() != null && !distances.containsKey(w.getDestination())) { distances.put(w.getDestination(), ((w.getDestination() == unit.getLocation()) ? 0 : unit.getTurnsToReach(w.getDestination()))); } } } // Check if this unit is needed as an expert (using: // "WorkerWish"): ArrayList<Wish> wishList = workerWishes.get(unit.getType()); WorkerWish bestWish = null; int bestTurns = Integer.MAX_VALUE; for (int i = 0; i < wishList.size(); i++) { // TODO: is this necessary? If so, use Iterator? WorkerWish ww = (WorkerWish) wishList.get(i); if (ww.getTransportable() != null) { wishList.remove(i); i--; continue; } int turns = getScaledTurns(distances, ww.getDestination()); if (bestWish == null || ww.getValue() - (turns * 2) > bestWish.getValue() - (bestTurns * 2)) { bestWish = ww; bestTurns = turns; } } if (bestWish != null) { bestWish.setTransportable(aiUnit); aiUnit.setMission(new WishRealizationMission(getAIMain(), aiUnit, bestWish)); return; } // Find a site for a new colony: Tile colonyTile = null; if (getPlayer().canBuildColonies() && BuildColonyMission.isValid(aiUnit)) { colonyTile = BuildColonyMission.findTargetTile(aiUnit, false); if (colonyTile != null) { bestTurns = unit.getTurnsToReach(colonyTile); } } // Check if we can find a better site to work than a new colony: if (!fewColonies || colonyTile == null || bestTurns > 10) { for (List<Wish> wishes : workerWishes.values()) { for (int j = 0; j < wishes.size(); j++) { WorkerWish ww = (WorkerWish) wishes.get(j); if (ww.getTransportable() != null) { wishes.remove(j); j--; continue; } int turns = getScaledTurns(distances, ww.getDestination()); // TODO: Choose to build colony if the value of the // wish is low. if (bestWish == null || ww.getValue() - (turns * 2) > bestWish.getValue() - (bestTurns * 2)) { bestWish = ww; bestTurns = turns; } } } } if (bestWish != null) { bestWish.setTransportable(aiUnit); aiUnit.setMission(new WishRealizationMission(getAIMain(), aiUnit, bestWish)); return; } // Choose to build a new colony: if (colonyTile != null) { aiUnit.setMission(new BuildColonyMission(getAIMain(), aiUnit, colonyTile)); boolean isUnitOnCarrier = aiUnit.getUnit().isOnCarrier(); if (isUnitOnCarrier) { // Verify carrier mission AIUnit carrier = getAIUnit((Unit) aiUnit.getUnit() .getLocation()); Mission carrierMission = carrier.getMission(); if (carrierMission == null) { carrierMission = new TransportMission(getAIMain(), carrier); carrier.setMission(carrierMission); } else if (!(carrierMission instanceof TransportMission)) { throw new IllegalStateException("Carrier carrying unit not on a transport mission"); } // Transport unit to carrier destination (is this what // is truly wanted?) ((TransportMission) carrierMission).addToTransportList(aiUnit); } return; } } private int getScaledTurns(java.util.Map<Location, Integer> distances, Location destination) { int turns = distances.get(destination); // TODO: what do these calcuations mean? if (turns == Integer.MAX_VALUE) { return (destination.getTile() == null) ? 5 : 10; } else { return Math.min(5, turns); } } /** * Brings gifts to nice players with nearby colonies. * * TODO: European players can also bring gifts! However, * this might be folded into a trade mission, since * European gifts are just a special case of trading. */ private void bringGifts() { return; } /** * Demands goods from players with nearby colonies. * * TODO: European players can also demand tribute! */ private void demandTribute() { return; } /** * Calls {@link AIColony#createAIGoods()} for every colony this player owns. */ private void createAIGoodsInColonies() { logger.finest("Entering method createAIGoodsInColonies"); for (AIColony aic : getAIColonies()) aic.createAIGoods(); } /** * Counts the number of defenders in a colony. * * @param colony The <code>Colony</code> to examine. * @return The number of defenders. */ protected int getColonyDefenders(Colony colony) { int defenders = 0; for (AIUnit au : getAIUnits()) { Mission m = au.getMission(); if (m != null && m instanceof DefendSettlementMission && ((DefendSettlementMission) m).getSettlement() == colony && au.getUnit().getColony() == colony) { defenders++; } } return defenders; } /** * Evaluate allocating a unit to the defence of a colony. * Temporary helper method for giveMilitaryMission. * * @param unit The <code>Unit</code> that is to defend. * @param colony The <code>Colony</code> to defend. * @param turns The turns for the unit to reach the colony. * @return A value for such a mission. */ public int getDefendColonyMissionValue(Unit unit, Colony colony, int turns) { // Sanitation if (unit == null || colony == null) return Integer.MIN_VALUE; int value = 10025 - 10 * turns; int defenders = getColonyDefenders(colony); // Reduce value in proportion to the number of defenders. value -= 500 * defenders; // Reduce value if defenders are protected. if (colony.hasStockade()) { if (defenders > colony.getStockade().getLevel() + 1) { value = 20 - defenders; } else { value -= 1000 * colony.getStockade().getLevel(); } } // Do not return negative except for error cases. return Math.max(0, value); } /** * Evaluate a potential seek and destroy mission for a given unit * to a given tile. * TODO: revisit and rebalance the mass of magic numbers. * * @param unit The <code>Unit</code> to do the mission. * @param newTile The <code>Tile</code> to go to. * @param turns How long to travel to the tile. * @return A score for the proposed mission. */ public int getUnitSeekAndDestroyMissionValue(Unit unit, Tile newTile, int turns) { if (!isTargetValidForSeekAndDestroy(unit, newTile)) { return Integer.MIN_VALUE; } Settlement settlement = newTile.getSettlement(); Unit defender = newTile.getDefendingUnit(unit); // Take distance to target into account int value = 10020 - turns * 100; if (settlement != null) { // Do not cheat and look inside the settlement. // Just use visible facts about it. // TODO: if we are the REF and there is a significant Tory // population inside, assume traitors have briefed us. if (settlement instanceof Colony) { // Favour high population and weak fortifications. Colony colony = (Colony) settlement; value += 50 * colony.getUnitCount(); if (colony.hasStockade()) { value -= 1000 * colony.getStockade().getLevel(); } } else if (settlement instanceof IndianSettlement) { // Favour the most hostile settlements IndianSettlement is = (IndianSettlement) settlement; Tension tension = is.getAlarm(unit.getOwner()); if (tension != null) value += tension.getValue(); } } else if (defender != null) { CombatModel combatModel = unit.getGame().getCombatModel(); float off = combatModel.getOffencePower(unit, defender); float def = combatModel.getDefencePower(unit, defender); Unit train = getBestTreasureTrain(newTile); if (train != null) { value += Math.min(train.getTreasureAmount() / 10, 50); } if (defender.getType().getOffence() > 0) { value += 200 - def * 2 - turns * 50; } value += combatModel.getOffencePower(defender, unit) - combatModel.getDefencePower(defender, unit); if (!defender.isNaval() && defender.hasAbility(Ability.EXPERT_SOLDIER) && !defender.isArmed()) { value += 10 - def * 2 - turns * 25; } if (value < 0) value = 0; } logger.finest("getUnitSeekAndDestroyMissionValue " + unit.getId() + " v " + ((settlement != null) ? settlement.getId() : (defender != null) ? defender.getId() : "none") + " = " + value); return value; } /** * Chooses the best target for an AI unit to attack. * * @param aiUnit The <code>AIUnit</code> that will attack. * @return The best target for a military mission. */ public Ownable chooseMilitaryTarget(AIUnit aiUnit) { final AIMain aiMain = getAIMain(); final Unit unit = aiUnit.getUnit(); final Unit carrier = (!unit.isOnCarrier()) ? null : (Unit) unit.getLocation(); Ownable bestTarget = null; // The best target for a mission. int bestValue = Integer.MIN_VALUE; // The value of the target above. // Determine starting tile. Tile startTile = ((carrier != null) ? carrier : unit).getTile(); if (startTile == null) { startTile = ((carrier != null) ? carrier : unit) .getFullEntryLocation(); } if (startTile == null) { logger.warning("chooseMilitaryTarget failed, no tile for: " + unit); return null; } // Check if the unit is located on a Tile with a // Settlement which requires defenders. if (unit.getColony() != null) { bestTarget = unit.getColony(); bestValue = getDefendColonyMissionValue(unit, (Colony) bestTarget, 0); } // Checks if a nearby colony requires additional defence: GoalDecider gd = new GoalDecider() { private PathNode best = null; private int bestValue = 0; public PathNode getGoal() { return best; } public boolean hasSubGoals() { return true; } private int getValue(PathNode pathNode) { // Note: check *unit*, not *u*, which could be // the carrier unit is on. Colony colony = pathNode.getTile().getColony(); return (colony == null || colony.getOwner() != unit.getOwner()) ? Integer.MIN_VALUE : getDefendColonyMissionValue(unit, colony, pathNode.getTurns()); } public boolean check(Unit u, PathNode pathNode) { int value = getValue(pathNode); if (value > bestValue) { bestValue = value; best = pathNode; return true; } return false; } }; final int MAXIMUM_TURNS_TO_SETTLEMENT = 10; PathNode bestPath = unit.search(startTile, gd, CostDeciders.avoidIllegal(), MAXIMUM_TURNS_TO_SETTLEMENT, carrier); if (bestPath != null) { PathNode last = bestPath.getLastNode(); Colony colony = last.getTile().getColony(); int value = getDefendColonyMissionValue(unit, colony, last.getTurns()); if (value > bestValue) { bestTarget = colony; bestValue = value; } } // Search for the closest target of a pre-existing // UnitSeekAndDestroyMission. Location bestExistingTarget = null; int bestDistance = Integer.MAX_VALUE; for (AIUnit au : getAIUnits()) { Unit u = au.getUnit(); if (u.getTile() == null || !(au.getMission() instanceof UnitSeekAndDestroyMission)) continue; UnitSeekAndDestroyMission usdm = (UnitSeekAndDestroyMission) au.getMission(); int distance = unit.getTurnsToReach(startTile, usdm.getTarget().getTile()); if (distance < bestDistance) { bestExistingTarget = usdm.getTarget(); bestDistance = distance; } } if (bestExistingTarget != null) { int value = getUnitSeekAndDestroyMissionValue(unit, bestExistingTarget.getTile(), bestDistance); if (value > bestValue) { bestValue = value; bestTarget = (Ownable) bestExistingTarget; } } // General check for available targets for seek-and-destroy. GoalDecider targetDecider = new GoalDecider() { private PathNode bestTarget = null; private int bestNewTargetValue = Integer.MIN_VALUE; public PathNode getGoal() { return bestTarget; } public boolean hasSubGoals() { return true; } private int getValue(PathNode pathNode) { // Note: check *unit*, not *u*, which could be // the carrier unit is on. Tile tile = pathNode.getTile(); return (isTargetValidForSeekAndDestroy(unit, tile)) ? getUnitSeekAndDestroyMissionValue(unit, tile, pathNode.getTurns()) : Integer.MIN_VALUE; } public boolean check(Unit u, PathNode pathNode) { int value = getValue(pathNode); if (value > bestNewTargetValue) { bestTarget = pathNode; bestNewTargetValue = value; return true; } return false; } }; PathNode newTarget = unit.search(startTile, targetDecider, CostDeciders.avoidIllegal(), INFINITY, carrier); if (newTarget != null) { Tile targetTile = newTarget.getLastNode().getTile(); int value = getUnitSeekAndDestroyMissionValue(unit, targetTile, newTarget.getTotalTurns()); if (value > bestValue) { bestValue = value; if (targetTile.getSettlement() != null) { bestTarget = targetTile.getSettlement(); } else if (getBestTreasureTrain(targetTile) != null) { bestTarget = getBestTreasureTrain(targetTile); } else { bestTarget = targetTile.getDefendingUnit(unit); } } } return (bestValue > 0) ? bestTarget : null; } /** * Gives a military mission to the given unit. * * Old comment: Temporary method for giving a military mission. * This method will be removed when "MilitaryStrategy" and the * "Tactic"-classes has been implemented. * * @param aiUnit The <code>AIUnit</code> to give a mission to. */ public void giveMilitaryMission(AIUnit aiUnit) { logger.finest("Entering giveMilitaryMission for: " + aiUnit.getUnit()); final AIMain aiMain = getAIMain(); final Player player = getPlayer(); Mission mission; Ownable bestTarget = chooseMilitaryTarget(aiUnit); if (bestTarget == null) { // Just wander around if there is nothing better to do. mission = new UnitWanderHostileMission(aiMain, aiUnit); } else if (bestTarget instanceof Settlement && ((Settlement) bestTarget).getOwner() == player) { Settlement settlement = (Settlement) bestTarget; mission = new DefendSettlementMission(aiMain, aiUnit, settlement); } else { mission = new UnitSeekAndDestroyMission(aiMain, aiUnit, (Location) bestTarget); } aiUnit.setMission(mission); logger.finest("giveMilitaryMission found: " + mission + " for unit: " + aiUnit); } private int getTurns(Transportable t, TransportMission tm) { if (t.getTransportDestination() != null && t.getTransportDestination().getTile() == tm.getUnit().getTile()) { return 0; } PathNode path = tm.getTransportPath(t); return (path == null) ? -1 : path.getTotalTurns(); } /** * Assign transportable units and goods to available carriers * transport lists. */ private void createTransportLists() { logger.finest("Entering method createTransportLists"); if (!getPlayer().isEuropean()) return; List<Transportable> transportables = new ArrayList<Transportable>(); // Collect non-naval units needing transport. for (AIUnit au : getAIUnits()) { if (!au.getUnit().isNaval() && au.getTransport() == null && au.getTransportDestination() != null) { transportables.add(au); } } // Add goods to transport for (AIColony aic : getAIColonies()) { for (AIGoods aig : aic.getAIGoods()) { if (aig.getTransportDestination() != null && aig.getTransport() == null) { transportables.add(aig); } } } // Update the priority. for (Transportable t : transportables) { t.increaseTransportPriority(); } // Order the transportables by priority. Collections.sort(transportables, new Comparator<Transportable>() { public int compare(Transportable o1, Transportable o2) { if (o1 == o2) return 0; int result = o2.getTransportPriority() - o1.getTransportPriority(); return (result == 0) ? o1.getId().compareTo(o2.getId()) : result; } }); // Collect current transport missions with space available. ArrayList<TransportMission> availableMissions = new ArrayList<TransportMission>(); for (AIUnit au : getAIUnits()) { if (au.hasMission() && au.getMission() instanceof TransportMission && au.getUnit().getSpaceLeft() > 0) { availableMissions.add((TransportMission) au.getMission()); } } // If a transportable is already on a carrier, just add it to // its transport list. Note however that it may not be // possible to complete such transport, in which case, the // carrier should dump the transportable in the nearest // colony. for (Transportable t : new ArrayList<Transportable>(transportables)) { Location transportableLoc = t.getTransportLocatable().getLocation(); boolean onCarrier = transportableLoc instanceof Unit; if (onCarrier) { AIUnit carrierAI = getAIUnit((Unit) transportableLoc); Mission m = carrierAI.getMission(); if (m instanceof TransportMission) { ((TransportMission) m).addToTransportList(t); } transportables.remove(t); } } // For all remaining transportables, find the best carrier. // That is the one with space available that is closest. // TODO: this concentrates on packing, but ignores destinations // which is definitely going to be inefficient // TODO: be smarter about removing bestTransport from // availableMissions when it is full. while (!transportables.isEmpty() && !availableMissions.isEmpty()) { Transportable t = transportables.remove(0); TransportMission bestTransport = null; int bestTransportSpace = 0; int bestTransportTurns = Integer.MAX_VALUE; for (TransportMission tm : availableMissions) { int transportSpace = tm.getAvailableSpace(t); if (transportSpace <= 0) continue; if (t instanceof AIUnit) { if (!tm.getUnit().canCarryUnits()) continue; } else if (t instanceof AIGoods) { if (!tm.getUnit().canCarryGoods()) continue; } if (t.getTransportSource() != null && (t.getTransportSource().getTile() == tm.getUnit().getTile())) { if (bestTransportTurns > 0 || (bestTransportTurns == 0 && transportSpace > bestTransportSpace)) { bestTransport = tm; bestTransportSpace = transportSpace; bestTransportTurns = 0; } continue; } int totalTurns = getTurns(t, tm); if (totalTurns <= 0) continue; if (totalTurns < bestTransportTurns || (totalTurns == bestTransportTurns && transportSpace > bestTransportSpace)) { bestTransport = tm; bestTransportSpace = transportSpace; bestTransportTurns = totalTurns; } } if (bestTransport == null) { logger.finest("Transport failed for: " + t); continue; } bestTransport.addToTransportList(t); logger.finest("Transport found for: " + t + " using: " + bestTransport + " unit: " + bestTransport.getUnit()); // See if any other transportables are present at the same // location and can fit. for (int i = 0; i < transportables.size(); i++) { Transportable t2 = transportables.get(i); if (t2.getTransportLocatable().getLocation() == t.getTransportLocatable().getLocation() && bestTransport.getAvailableSpace(t2) > 0) { transportables.remove(t2); bestTransport.addToTransportList(t2); logger.finest("Transport piggyback for: " + t2 + " using: " + t); } } } } /** * Finds the treasure train carrying the largest treasure located * on the given <code>Tile</code>. * * @param tile The <code>Tile</code> to check. * @return The best treasure train found or null if none present. */ private Unit getBestTreasureTrain(Tile tile) { Unit best = null; for (Unit unit : tile.getUnitList()) { if (unit.canCarryTreasure() && (best == null || best.getTreasureAmount() < unit.getTreasureAmount())) { best = unit; } } return best; } }