/** * 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.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Logger; import net.sf.freecol.common.model.Ability; import net.sf.freecol.common.model.AbstractGoods; import net.sf.freecol.common.model.BuildableType; import net.sf.freecol.common.model.Building; import net.sf.freecol.common.model.Colony; import net.sf.freecol.common.model.ColonyTile; import net.sf.freecol.common.model.EquipmentType; import net.sf.freecol.common.model.Europe; import net.sf.freecol.common.model.Goods; import net.sf.freecol.common.model.GoodsContainer; import net.sf.freecol.common.model.GoodsType; import net.sf.freecol.common.model.Map.Direction; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.ProductionInfo; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.Tile; import net.sf.freecol.common.model.TileImprovementType; import net.sf.freecol.common.model.TileType; import net.sf.freecol.common.model.Turn; import net.sf.freecol.common.model.TypeCountMap; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.model.UnitType; import net.sf.freecol.common.model.UnitWas; import net.sf.freecol.common.model.WorkLocation; import net.sf.freecol.common.networking.Connection; import net.sf.freecol.common.networking.NetworkConstants; import net.sf.freecol.server.ai.mission.BuildColonyMission; 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.ScoutingMission; import net.sf.freecol.server.ai.mission.TransportMission; import net.sf.freecol.server.ai.mission.WorkInsideColonyMission; import org.freecolandroid.xml.stream.XMLStreamConstants; import org.freecolandroid.xml.stream.XMLStreamException; import org.freecolandroid.xml.stream.XMLStreamReader; import org.freecolandroid.xml.stream.XMLStreamWriter; import org.w3c.dom.Element; /** * Objects of this class contains AI-information for a single {@link Colony}. */ public class AIColony extends AIObject implements PropertyChangeListener { private static final Logger logger = Logger.getLogger(AIColony.class.getName()); private static final String LIST_ELEMENT = "ListElement"; // Do not perform tile improvements that would leave less than // this amount of forested work locations available to the colony. private static final int FOREST_MINIMUM = 1; // Do not bother trying to ship out less than this amount of goods. private static final int EXPORT_MINIMUM = 10; // The colony this AIColony is managing. private Colony colony; // The current production plan for the colony. private ColonyPlan colonyPlan; // Goods to export from the colony. private final List<AIGoods> aiGoods = new ArrayList<AIGoods>(); // Useful things for the colony. private final List<Wish> wishes = new ArrayList<Wish>(); // Plans to improve neighbouring tiles. private final List<TileImprovementPlan> tileImprovementPlans = new ArrayList<TileImprovementPlan>(); // When should the workers in this Colony be rearranged? private Turn rearrangeTurn = new Turn(0); // Goods that should be completely exported and only exported to // prevent the warehouse filling. private static final Set<GoodsType> fullExport = new HashSet<GoodsType>(); private static final Set<GoodsType> partExport = new HashSet<GoodsType>(); // Comparator to favour expert scouts, then units with the SCOUT role, // then least skillful. private static final Comparator<Unit> scoutComparator = new Comparator<Unit>() { public int compare(Unit u1, Unit u2) { boolean a1 = u1.hasAbility("model.ability.expertScout"); boolean a2 = u2.hasAbility("model.ability.expertScout"); if (a1 != a2) return (a1) ? -1 : 1; a1 = u1.getRole() == Unit.Role.SCOUT; a2 = u2.getRole() == Unit.Role.SCOUT; if (a1 != a2) return (a1) ? -1 : 1; return u1.getType().getSkill() - u2.getType().getSkill(); } }; /** * Creates a new <code>AIColony</code>. * * @param aiMain The main AI-object. * @param colony The colony to make an {@link AIObject} for. */ public AIColony(AIMain aiMain, Colony colony) { super(aiMain, colony.getId()); this.colony = colony; colonyPlan = new ColonyPlan(aiMain, colony); colony.addPropertyChangeListener(Colony.REARRANGE_WORKERS, this); } /** * Creates a new <code>AIColony</code>. * * @param aiMain The main AI-object. * @param element An <code>Element</code> containing an XML-representation * of this object. */ public AIColony(AIMain aiMain, Element element) { super(aiMain, element.getAttribute(ID_ATTRIBUTE)); readFromXMLElement(element); } /** * Creates a new <code>AIColony</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 AIColony(AIMain aiMain, XMLStreamReader in) throws XMLStreamException { super(aiMain, in.getAttributeValue(null, ID_ATTRIBUTE)); readFromXML(in); } /** * Creates a new <code>AIColony</code>. * * @param aiMain The main AI-object. * @param id */ public AIColony(AIMain aiMain, String id) { this(aiMain, (Colony) aiMain.getGame().getFreeColGameObject(id)); } /** * Gets the <code>Colony</code> this <code>AIColony</code> controls. * * @return The <code>Colony</code>. */ public Colony getColony() { return colony; } /** * Gets the current plan for this colony. * * @return The current <code>ColonyPlan</code>. */ public ColonyPlan getColonyPlan() { return colonyPlan; } protected AIUnit getAIUnit(Unit unit) { return getAIMain().getAIUnit(unit); } protected AIPlayer getAIOwner() { return getAIMain().getAIPlayer(colony.getOwner()); } protected Connection getConnection() { return getAIOwner().getConnection(); } /** * Disposes this <code>AIColony</code>. */ public void dispose() { List<AIObject> disposeList = new ArrayList<AIObject>(); for (AIGoods ag : aiGoods) { if (ag.getGoods().getLocation() == colony) { disposeList.add(ag); } } for (Wish w : wishes) { disposeList.add(w); } for (TileImprovementPlan ti : tileImprovementPlans) { disposeList.add(ti); } for (AIObject o : disposeList) { o.dispose(); } super.dispose(); } /** * Is the colony badly defended? * Deliberately does not waste defenders on small colonies. * * @param colony The <code>Colony</code> to consider. * @return True if the colony needs more defenders. */ public static boolean isBadlyDefended(Colony colony) { return colony.getTotalDefencePower() < 1.25f * colony.getUnitCount() - 2.5f; } /** * Rearranges the workers within this colony using the {@link ColonyPlan}. * TODO: Detect military threats and boost defence. * * @return True if the workers were rearranged. */ public boolean rearrangeWorkers() { if (colony.getUnitCount() <= 0) { throw new IllegalStateException("Empty colony found: " + colony.getName()); } int turn = getGame().getTurn().getNumber(); if (colony.getCurrentlyBuilding() == null && colonyPlan.getBestBuildableType() != null && rearrangeTurn.getNumber() > turn) { logger.warning(colony.getName() + " could be building but" + " is asleep until turn: " + rearrangeTurn.getNumber() + "( > " + turn + ")"); } if (rearrangeTurn.getNumber() > turn) return false; final AIMain aiMain = getAIMain(); final Tile tile = colony.getTile(); final Player player = colony.getOwner(); final Specification spec = getSpecification(); // For now, cap the rearrangement horizon, because confidence // that we are triggering on all relevant changes is low. int nextRearrange = 15; // See if there are neighbouring LCRs to explore, or tiles // to steal, or just unclaimed tiles (a neighbouring settlement // might have disappeared or relinquished a tile). // This needs to be done early so that new tiles can be // included in any new colony plan. exploreLCRs(); stealTiles(); for (Tile t : tile.getSurroundingTiles(1)) { if (!player.owns(t) && player.canClaimForSettlement(t)) { AIMessage.askClaimLand(getConnection(), t, colony, 0); } } // Update the colony plan. colonyPlan.update(); // Now that we know what raw materials are available in the // colony plan, set the current buildable, first backing out // of anything currently being built that is now impossible. // If a buildable is chosen, refine the worker allocation in // the colony plan in case the required building materials // have changed. BuildableType oldBuild = colony.getCurrentlyBuilding(); BuildableType build = colonyPlan.getBestBuildableType(); if (build != oldBuild) { List<BuildableType> queue = new ArrayList<BuildableType>(); if (build != null) queue.add(build); AIMessage.askSetBuildQueue(this, queue); build = colony.getCurrentlyBuilding(); } colonyPlan.refine(build); // Collect all potential workers from the colony and from the tile, // being careful not to disturb existing non-colony missions. // Note the special case of a unit aiming to build a colony on this // tile, which happens regularly with the initial AI colony. // Remember where the units came from. List<Unit> workers = colony.getUnitList(); List<UnitWas> was = new ArrayList<UnitWas>(); for (Unit u : workers) was.add(new UnitWas(u)); for (Unit u : tile.getUnitList()) { if (!u.isPerson()) continue; Mission mission = getAIUnit(u).getMission(); if (mission == null || mission instanceof IdleAtColonyMission || mission instanceof WorkInsideColonyMission || (mission instanceof BuildColonyMission && ((BuildColonyMission)mission).getTarget() == tile) // TODO: drop this when the AI stops building excessive armies || mission instanceof DefendSettlementMission) { workers.add(u); was.add(new UnitWas(u)); } } // Assign the workers according to the colony plan. // ATM we just accept this assignment. Colony scratch = colonyPlan.assignWorkers(workers); // Apply the arrangement, and give suitable missions to all units. // For now, do a soft rearrange (that is, no c-s messaging). // Also change the goods counts as we may have changed equipment. // TODO: Better would be to restore the initial state and use // a special c-s message to execute the rearrangement--- code to // untangle the movement dependencies is non-trivial. for (Unit u : scratch.getUnitList()) { AIUnit aiU = getAIUnit(u); WorkLocation wl = (WorkLocation)u.getLocation(); wl = colony.getCorrespondingWorkLocation(wl); u.setLocation(wl); } for (Unit u : scratch.getTile().getUnitList()) { u.setLocation(tile); } for (GoodsType g : spec.getGoodsTypeList()) { if (!g.isStorable()) continue; int oldCount = colony.getGoodsCount(g); int newCount = scratch.getGoodsCount(g); if (newCount != oldCount) { colony.getGoodsContainer().addGoods(g, newCount - oldCount); } } scratch.disposeScratchColony(); // Emergency recovery if something broke and the colony is empty. if (colony.getUnitCount() <= 0) { String destruct = "Autodestruct at " + colony.getName() + " in " + turn + ":"; for (UnitWas uw : was) destruct += " " + uw.toString(); logger.warning(destruct); avertAutoDestruction(); } // Argh. We may have chosen to build something we can no // longer build due to some limitation. Try to find a // replacement, but do not re-refine/assign as that process is // sufficiently complex that we can not be confident that this // will not loop indefinitely. The compromise is to just // rearrange next turn until we get out of this state. if (build != null && !colony.canBuild(build)) { logger.warning(colony.getName() + " reneged building " + build); List<BuildableType> queue = new ArrayList<BuildableType>(); build = colonyPlan.getBestBuildableType(); if (build != null) queue.add(build); AIMessage.askSetBuildQueue(this, queue); nextRearrange = 1; } // Now that all production has been stabilized, plan to // rearrange when the warehouse hits a limit. if (colony.getNetProductionOf(spec.getPrimaryFoodType()) < 0) { int net = colony.getNetProductionOf(spec.getPrimaryFoodType()); int when = colony.getGoodsCount(spec.getPrimaryFoodType()) / -net; nextRearrange = Math.max(0, Math.min(nextRearrange, when-1)); } int warehouse = colony.getWarehouseCapacity(); for (GoodsType g : spec.getGoodsTypeList()) { if (!g.isStorable() || g.isFoodType()) continue; int have = colony.getGoodsCount(g); int net = colony.getAdjustedNetProductionOf(g); if (net >= 0 && (have >= warehouse || g.limitIgnored())) continue; int when = (net < 0) ? (have / -net - 1) : (net > 0) ? ((warehouse - have) / net - 1) : Integer.MAX_VALUE; nextRearrange = Math.max(1, Math.min(nextRearrange, when)); } // Log the changes. build = colony.getCurrentlyBuilding(); String buildStr = (build != null) ? build.toString() : ((build = colonyPlan.getBestBuildableType()) != null) ? "unexpected-null(" + build.toString() + ")" : "expected-null"; String report = "Rearrange " + colony.getName() + " (" + colony.getUnitCount() + ")" + " build=" + buildStr + " " + getGame().getTurn() + " + " + nextRearrange; for (UnitWas uw : was) report += "\n" + uw.toString(); logger.finest(report); // Give suitable missions to all units. for (Unit u : colony.getUnitList()) { AIUnit aiU = getAIUnit(u); aiU.setMission(new WorkInsideColonyMission(aiMain, aiU, this)); } for (Unit u : tile.getUnitList()) { AIUnit aiU = getAIUnit(u); if (aiU.getMission() != null) continue; switch (u.getRole()) { case SOLDIER: case DRAGOON: aiU.setMission(new DefendSettlementMission(aiMain, aiU, colony)); break; case SCOUT: if (ScoutingMission.isValid(aiU)) { aiU.setMission(new ScoutingMission(aiMain, aiU)); } break; case PIONEER: if (PioneeringMission.isValid(aiU)) { aiU.setMission(new PioneeringMission(aiMain, aiU)); } break; // TODO: case MISSIONARY: default: break; } } // Change the export settings when required. resetExports(); createTileImprovementPlans(); createWishes(); // Set the next rearrangement turn. rearrangeTurn = new Turn(turn + nextRearrange); return true; } /** * Reset the export settings. * This is always needed even when there is no customs house, because * createAIGoods needs to know what to export by transport. * TODO: consider market prices? */ private void resetExports() { final Specification spec = getSpecification(); final List<GoodsType> produce = colonyPlan.getPreferredProduction(); if (fullExport.isEmpty()) { // Initialize the exportable sets. // Luxury goods, non-raw materials (silver), and raw // materials we do not produce (might have been gifted) // should always be fully exported. Other raw and // manufactured goods should be exported only to the // extent of not filling the warehouse. for (GoodsType g : spec.getGoodsTypeList()) { if (g.isStorable() && !g.isFoodType() && !g.isBuildingMaterial() && !g.isMilitaryGoods() && !g.isTradeGoods()) { if (g.isRawMaterial() && produce.contains(g)) { partExport.add(g); } else { fullExport.add(g); } } } for (EquipmentType e : spec.getEquipmentTypeList()) { for (AbstractGoods ag : e.getGoodsRequired()) { fullExport.remove(ag.getType()); partExport.add(ag.getType()); } } } if (colony.getOwner().getMarket() == null) { // Do not export when there is no market! for (GoodsType g : spec.getGoodsTypeList()) { colony.getExportData(g).setExported(false); } } else { int exportLevel = 4 * colony.getWarehouseCapacity() / 5; for (GoodsType g : spec.getGoodsTypeList()) { if (fullExport.contains(g)) { colony.getExportData(g).setExportLevel(0); colony.getExportData(g).setExported(true); } else if (partExport.contains(g)) { colony.getExportData(g).setExportLevel(exportLevel); colony.getExportData(g).setExported(true); } else { colony.getExportData(g).setExported(false); } } } } /** * Explores any neighbouring LCRs. * Choose non-expert persons for the exploration. */ private void exploreLCRs() { final Tile tile = colony.getTile(); List<Unit> explorers = new ArrayList<Unit>(); for (Unit u : tile.getUnitList()) { if (u.isPerson() && (u.getType().getSkill() <= 0 || u.hasAbility("model.ability.expertScout"))) { explorers.add(u); } } Collections.sort(explorers, scoutComparator); for (Tile t : tile.getSurroundingTiles(1)) { if (t.hasLostCityRumour()) { Direction direction = tile.getDirection(t); for (;;) { if (explorers.isEmpty()) return; Unit u = explorers.remove(0); if (!u.getMoveType(t).isProgress()) continue; if (AIMessage.askMove(getAIUnit(u), direction) && !t.hasLostCityRumour()) { u.setDestination(tile); break; } } } } } /** * Steals neighbouring tiles but only if the colony has some * defence. Grab everything if at war with the owner, otherwise * just take the tile that best helps with the currently required * raw building materials, with a lesser interest in food. */ private void stealTiles() { final Specification spec = getSpecification(); final Tile tile = colony.getTile(); final Player player = colony.getOwner(); boolean hasDefender = false; for (Unit u : tile.getUnitList()) { if (u.isDefensiveUnit() && getAIUnit(u).getMission() instanceof DefendSettlementMission) { // TODO: be smarter hasDefender = true; break; } } if (!hasDefender) return; // What goods are really needed? List<GoodsType> needed = new ArrayList<GoodsType>(); for (GoodsType g : spec.getRawBuildingGoodsTypeList()) { if (colony.getProductionOf(g) <= 0) needed.add(g); } // If a tile can be stolen, do so if already at war with the // owner or if it is the best one available. UnitType unitType = spec.getDefaultUnitType(); Tile steal = null; float score = 1.0f; for (Tile t : tile.getSurroundingTiles(1)) { Player owner = t.getOwner(); if (owner == null || owner == player || owner.isEuropean()) continue; if (owner.atWarWith(player)) { if (AIMessage.askClaimLand(getConnection(), t, colony, NetworkConstants.STEAL_LAND) && player.owns(t)) { logger.info(player.getName() + " stole tile " + t + " from hostile " + owner.getName()); } } else { // Pick the best tile to steal, considering mainly the // building goods needed, but including food at a lower // weight. float s = 0.0f; for (GoodsType g : needed) { s += t.potential(g, unitType); } for (GoodsType g : spec.getFoodGoodsTypeList()) { s += 0.1 * t.potential(g, unitType); } if (s > score) { score = s; steal = t; } } } if (steal != null) { Player owner = steal.getOwner(); if (AIMessage.askClaimLand(getConnection(), steal, colony, NetworkConstants.STEAL_LAND) && player.owns(steal)) { logger.info(player.getName() + " stole tile " + steal + " (score = " + score + ") from " + owner.getName()); } } } /** * Something bad happened, there is no remaining unit working in * the colony. * * Throwing an exception stalls the AI and wrecks the colony in a * weird way. Try to recover by hopefully finding a unit outside * the colony and stuffing it into the town hall. */ private void avertAutoDestruction() { String msg = "Colony " + colony.getName() + " rearrangement leaves no units, " + colony.getTile().getUnitList().size() + " available"; for (Unit u : colony.getTile().getUnitList()) { msg += ", " + u.toString(); } logger.warning(msg); List<GoodsType> libertyGoods = getSpecification() .getLibertyGoodsTypeList(); for (Unit u : colony.getTile().getUnitList()) { if (!u.isPerson()) continue; for (WorkLocation wl : colony.getAvailableWorkLocations()) { if (!wl.canAdd(u)) continue; for (GoodsType type : libertyGoods) { if (wl.getPotentialProduction(u.getType(), type) > 0 && AIMessage.askWork(getAIUnit(u), wl) && u.getLocation() == wl) { AIMessage.askChangeWorkType(getAIUnit(u), type); logger.warning("Colony " + colony.getName() + " autodestruct averted."); break; } } } } // No good, no choice but to fail. if (colony.getUnitCount() <= 0) { throw new IllegalStateException(msg); } } /** * Gets the goods to be exported from this AI colony. * * @return A copy of the aiGoods list. */ public List<AIGoods> getAIGoods() { return new ArrayList<AIGoods>(aiGoods); } /** * Removes the given <code>AIGoods</code> from this colony's list. The * <code>AIGoods</code>-object is not disposed as part of this operation. * Use that method instead to remove the object completely (this method * would then be called indirectly). * * @param ag The <code>AIGoods</code> to be removed. * @see AIGoods#dispose() */ public void removeAIGoods(AIGoods ag) { while (aiGoods.remove(ag)) { /* Do nothing here */ } } /** * Drops some goods from the goods list, and cancels any transport. * * @param ag The <code>AIGoods</code> to drop. */ private void dropGoods(AIGoods ag) { if (ag.getTransport() != null && ag.getTransport().getMission() instanceof TransportMission) { ((TransportMission)ag.getTransport().getMission()) .removeFromTransportList(ag); } aiGoods.remove(ag); ag.dispose(); } /** * Creates a list of the goods which should be shipped out of this colony. */ public void createAIGoods() { if (colony.hasAbility(Ability.EXPORT)) { while (!aiGoods.isEmpty()) { AIGoods ag = aiGoods.remove(0); dropGoods(ag); } return; } final Europe europe = colony.getOwner().getEurope(); final int capacity = colony.getWarehouseCapacity(); List<AIGoods> newAIGoods = new ArrayList<AIGoods>(); List<AIGoods> oldAIGoods = new ArrayList<AIGoods>(); for (GoodsType g : getSpecification().getGoodsTypeList()) { if (colony.getAdjustedNetProductionOf(g) < 0) continue; int count = colony.getGoodsCount(g); int exportAmount = (fullExport.contains(g)) ? count : (partExport.contains(g)) ? count - colony.getExportData(g).getExportLevel() : -1; int priority = (exportAmount >= capacity) ? AIGoods.IMPORTANT_DELIVERY : (exportAmount > GoodsContainer.CARGO_SIZE) ? AIGoods.FULL_DELIVERY : 0; //if (exportAmount > 0) { // logger.finest(String.format("%-20s %-8s AIGoods: %d %s\n", // colony.getName(), "finds", // exportAmount, g.toString().substring(12))); //} int i = 0; while (i < aiGoods.size()) { AIGoods ag = aiGoods.get(i); if (ag == null) { aiGoods.remove(i); continue; } if (ag.getGoods() == null || ag.getGoods().getType() == null || ag.getGoods().getAmount() <= 0) { dropGoods(ag); continue; } Goods oldGoods = ag.getGoods(); if (oldGoods.getLocation() != colony) { // Its on its way. No longer of interest to the colony. aiGoods.remove(ag); continue; } if (oldGoods.getType() != g) { i++; continue; } int oldAmount = oldGoods.getAmount(); String msg = null; if (oldAmount < exportAmount) { int goodsAmount = oldAmount; if (oldAmount < GoodsContainer.CARGO_SIZE) { goodsAmount = Math.min(exportAmount, GoodsContainer.CARGO_SIZE); oldGoods.setAmount(goodsAmount); //msg = String.format("%-20s %-8s AIGoods: %s %d %s\n", // colony.getName(), "grows", ag.getId(), // oldGoods.getAmount(), g.toString().substring(12)); ag.setTransportPriority(priority); } else { //msg = String.format("%-20s %-8s AIGoods: %s full %s\n", // colony.getName(), "keeps", ag.getId(), // g.toString().substring(12)); } exportAmount -= goodsAmount; oldAIGoods.add(ag); } else if (oldAmount == exportAmount) { //msg = String.format("%-20s %-8s AIGoods: %s %d %s\n", // colony.getName(), "keeps", ag.getId(), // oldGoods.getAmount(), g.toString().substring(12)); oldAIGoods.add(ag); exportAmount = 0; } else { // oldAmount > exportAmount if (exportAmount <= 0) { msg = String.format("%-20s %-8s AIGoods: %s %d %s\n", colony.getName(), "drops", ag.getId(), oldGoods.getAmount(), g.toString().substring(12)); dropGoods(ag); continue; } oldGoods.setAmount(exportAmount); //msg = String.format("%-20s %-8s AIGoods: %s %d %s\n", // colony.getName(), "shrinks", ag.getId(), // oldGoods.getAmount(), g.toString().substring(12)); oldAIGoods.add(ag); exportAmount = 0; } if (msg != null) logger.finest(msg); i++; } while (exportAmount >= GoodsContainer.CARGO_SIZE) { AIGoods newGoods = new AIGoods(getAIMain(), colony, g, GoodsContainer.CARGO_SIZE, europe); logger.finest(String.format("%-20s %-8s AIGoods: %s full %s\n", colony.getName(), "makes", newGoods.getId(), g.toString().substring(12))); newGoods.setTransportPriority(priority); newAIGoods.add(newGoods); exportAmount -= GoodsContainer.CARGO_SIZE; } if (exportAmount >= EXPORT_MINIMUM) { AIGoods newGoods = new AIGoods(getAIMain(), colony, g, exportAmount, europe); logger.finest(String.format("%-20s %-8s AIGoods: %s %d %s\n", colony.getName(), "makes", newGoods.getId(), exportAmount, g.toString().substring(12))); newAIGoods.add(newGoods); } } aiGoods.clear(); aiGoods.addAll(oldAIGoods); aiGoods.addAll(newAIGoods); Collections.sort(aiGoods, AIGoods.getAIGoodsPriorityComparator()); } /** * Adds a <code>Wish</code> to the wishes list. * * @param wish The <code>Wish</code> to be added. */ public void addWish(Wish wish) { wishes.add(wish); } /** * Removes a wish from the wishes list. * * @param wish The <code>Wish</code> to remove. */ public void removeWish(Wish wish) { wishes.remove(wish); } /** * Tries to complete any wishes for some goods that have just arrived. * * @param goods Some <code>Goods</code> that are arriving in this colony. */ public void completeWish(Goods goods) { int i = 0; while (i < wishes.size()) { if (wishes.get(i) instanceof GoodsWish) { GoodsWish gw = (GoodsWish)wishes.get(i); if (gw.getGoodsType() == goods.getType() && gw.getGoodsAmount() <= goods.getAmount()) { logger.finest(colony.getName() + " completes goods wish: " + gw); wishes.remove(gw); gw.dispose(); continue; } } i++; } } /** * Tries to complete any wishes for a unit that has just arrived. * * @param unit A <code>Unit</code> that is arriving in this colony. */ public void completeWish(Unit unit) { int i = 0; while (i < wishes.size()) { if (wishes.get(i) instanceof WorkerWish) { WorkerWish ww = (WorkerWish)wishes.get(i); if (ww.getUnitType() == unit.getType()) { logger.finest(colony.getName() + " completes worker wish: " + ww); wishes.remove(ww); ww.dispose(); continue; } } i++; } } /** * Gets the wishes this colony has. * * @return A copy of the wishes list. */ public List<Wish> getWishes() { return new ArrayList<Wish>(wishes); } /** * Gets the worker wishes this colony has. * * @return A copy of the wishes list with non-worker wishes removed. */ public List<WorkerWish> getWorkerWishes() { List<WorkerWish> result = new ArrayList<WorkerWish>(); for (Wish wish : wishes) { if (wish instanceof WorkerWish) { result.add((WorkerWish) wish); } } return result; } /** * Creates the wishes for the <code>Colony</code>. */ private void createWishes() { wishes.clear(); createWorkerWishes(); createGoodsWishes(); } /** * Creates the worker wishes. */ private void createWorkerWishes() { final Specification spec = getSpecification(); final int baseValue = 25; final int priorityMax = 50; final int priorityDecay = 5; final int multipleBonus = 5; final int multipleMax = 5; // For every non-expert, request expert replacement. // Prioritize by lowest net production among the goods that are // being produced by units (note that we have to traverse the work // locations/unit-lists, rather than just check for non-zero // production because it could be in balance). // Add some weight when multiple cases of the same expert are // needed, rather than generating heaps of wishes. List<GoodsType> producing = new ArrayList<GoodsType>(); for (WorkLocation wl : colony.getAvailableWorkLocations()) { for (Unit u : wl.getUnitList()) { GoodsType work = u.getWorkType(); if (work != null) { work = work.getStoredAs(); if (!producing.contains(work)) producing.add(work); } } } Collections.sort(producing, new Comparator<GoodsType>() { public int compare(GoodsType g1, GoodsType g2) { return colony.getAdjustedNetProductionOf(g1) - colony.getAdjustedNetProductionOf(g2); } }); TypeCountMap<UnitType> experts = new TypeCountMap<UnitType>(); for (Unit unit : colony.getUnitList()) { GoodsType goods = unit.getWorkType(); UnitType expert = (goods == null || goods == unit.getType().getExpertProduction()) ? null : spec.getExpertForProducing(goods); if (expert != null) { experts.incrementCount(expert, 1); } } for (UnitType expert : experts.keySet()) { GoodsType goods = expert.getExpertProduction(); int value = baseValue + Math.max(0, priorityMax - priorityDecay * producing.indexOf(goods)) + (Math.min(multipleMax, experts.getCount(expert) - 1) * multipleBonus); WorkerWish ww = new WorkerWish(getAIMain(), colony, value, expert, true); wishes.add(ww); logger.finest("New WorkerWish at " + colony.getName() + ": " + ww.getId() + " " + ww); } // Request population increase if no worker wishes and the bonus // can take it. if (experts.isEmpty() && colony.governmentChange(colony.getUnitCount() + 1) >= 0) { boolean needFood = colony.getFoodProduction() <= colony.getFoodConsumption() + colony.getOwner().getMaximumFoodConsumption(); // Choose expert for best work location plan UnitType expert = spec.getDefaultUnitType(); for (WorkLocationPlan plan : (needFood) ? colonyPlan.getFoodPlans() : colonyPlan.getWorkPlans()) { WorkLocation location = plan.getWorkLocation(); if (!location.canBeWorked()) continue; expert = spec.getExpertForProducing(plan.getGoodsType()); break; } WorkerWish ww = new WorkerWish(getAIMain(), colony, 50, expert, false); wishes.add(ww); logger.finest("New WorkerWish at " + colony.getName() + ": " + ww.getId() + " " + ww); } // TODO: check for students // TODO: add missionaries // Improve defence. if (isBadlyDefended(colony)) { UnitType bestDefender = colony.getBestDefenderType(); if (bestDefender != null) { WorkerWish ww = new WorkerWish(getAIMain(), colony, 100, bestDefender, true); wishes.add(ww); logger.finest("New WorkerWish at " + colony.getName() + ": " + ww.getId() + " " + ww); } } } /** * Creates the goods wishes. */ private void createGoodsWishes() { final Specification spec = getSpecification(); int goodsWishValue = 50; // request goods // TODO: improve heuristics TypeCountMap<GoodsType> required = new TypeCountMap<GoodsType>(); // add building materials if (colony.getCurrentlyBuilding() != null) { for (AbstractGoods ag : colony.getCurrentlyBuilding() .getGoodsRequired()) { if (colony.getAdjustedNetProductionOf(ag.getType()) <= 0) { required.incrementCount(ag.getType(), ag.getAmount()); } } } // add materials required to improve tiles for (TileImprovementPlan plan : tileImprovementPlans) { for (AbstractGoods ag : plan.getType().getExpendedEquipmentType() .getGoodsRequired()) { required.incrementCount(ag.getType(), ag.getAmount()); } } // add raw materials for buildings for (WorkLocation workLocation : colony.getCurrentWorkLocations()) { if (workLocation instanceof Building) { Building building = (Building) workLocation; GoodsType inputType = building.getGoodsInputType(); ProductionInfo info = colony.getProductionInfo(building); if (inputType != null && info != null && !info.hasMaximumProduction()) { // TODO: find better heuristics required.incrementCount(inputType, 100); } } } // Add breedable goods for (GoodsType g : spec.getGoodsTypeList()) { if (g.isBreedable() && colony.getGoodsCount(g) < g.getBreedingNumber()) { required.incrementCount(g, g.getBreedingNumber()); } } // Add materials required to build military equipment, // but make sure there is a unit present that can use it. if (isBadlyDefended(colony)) { for (EquipmentType type : spec.getEquipmentTypeList()) { if (!type.isMilitaryEquipment()) continue; for (Unit unit : colony.getTile().getUnitList()) { if (!unit.canBeEquippedWith(type)) continue; for (AbstractGoods ag : type.getGoodsRequired()) { required.incrementCount(ag.getType(), ag.getAmount()); } break; } } } for (GoodsType type : required.keySet()) { GoodsType requiredType = type; while (requiredType != null) { if (requiredType.isStorable()) break; requiredType = requiredType.getRawMaterial(); } if (requiredType != null) { int amount = Math.min(colony.getWarehouseCapacity(), (required.getCount(requiredType) - colony.getGoodsCount(requiredType))); if (amount > 0) { int value = goodsWishValue; if (colonyCouldProduce(requiredType)) value /= 10; GoodsWish gw = new GoodsWish(getAIMain(), colony, value, amount, requiredType); wishes.add(gw); logger.finest("New GoodsWish at " + colony.getName() + ": " + gw.getId() + " " + gw); } } } Collections.sort(wishes); } /** * Can a colony produce certain goods? * * @param goodsType The <code>GoodsType</code> to check production of. * @return True if the colony can produce such goods. */ private boolean colonyCouldProduce(GoodsType goodsType) { if (goodsType.isBreedable()) { return colony.getGoodsCount(goodsType) >= goodsType.getBreedingNumber(); } if (goodsType.isFarmed()) { for (ColonyTile colonyTile : colony.getColonyTiles()) { if (colonyTile.getWorkTile().potential(goodsType, null) > 0) { return true; } } } else { if (!colony.getBuildingsForProducing(goodsType).isEmpty()) { return (goodsType.getRawMaterial() == null) ? true : colonyCouldProduce(goodsType.getRawMaterial()); } } return false; } /** * Gets the tile improvements planned for this colony. * * @return A copy of the tile improvement plan list. */ public List<TileImprovementPlan> getTileImprovementPlans() { return new ArrayList<TileImprovementPlan>(tileImprovementPlans); } /** * Removes a <code>TileImprovementPlan</code> from the list * @return True if it was successfully deleted, false otherwise */ public boolean removeTileImprovementPlan(TileImprovementPlan plan){ return tileImprovementPlans.remove(plan); } /** * Gets the first plan for a specified tile from a list of tile * improvement plans. * * @param tile The <code>Tile</code> to look for. * @param plans A list of <code>TileImprovementPlan</code>s to search. * @return A matching plan, or null if not found. */ private TileImprovementPlan getPlanFor(Tile tile, List<TileImprovementPlan> plans) { for (TileImprovementPlan tip : plans) { if (tip.getTarget() == tile) return tip; } return null; } /** * Creates a list of the <code>Tile</code>-improvements which will * increase the production by this <code>Colony</code>. * * @see TileImprovementPlan */ public void createTileImprovementPlans() { List<TileImprovementPlan> newPlans = new ArrayList<TileImprovementPlan>(); for (WorkLocation wl : colony.getAvailableWorkLocations()) { if (!(wl instanceof ColonyTile)) continue; ColonyTile colonyTile = (ColonyTile) wl; Tile workTile = colonyTile.getWorkTile(); if (workTile.getOwningSettlement() != colony || getPlanFor(workTile, newPlans) != null) continue; // Require food for the center tile, but otherwise insist // the tile is being used, and try to improve the // production that is underway. GoodsType goodsType = null; if (colonyTile.isColonyCenterTile()) { for (AbstractGoods ag : colonyTile.getProduction()) { if (ag.getType().isFoodType()) { goodsType = ag.getType(); break; } } } else { if (colonyTile.isEmpty()) continue; goodsType = colonyTile.getUnitList().get(0).getWorkType(); } if (goodsType == null) continue; TileImprovementPlan plan = getPlanFor(workTile, tileImprovementPlans); if (plan == null) { TileImprovementType type = TileImprovementPlan .getBestTileImprovementType(workTile, goodsType); if (type != null) { plan = new TileImprovementPlan(getAIMain(), workTile, type, type.getImprovementValue(workTile, goodsType)); } } else { if (!plan.update(goodsType)) plan = null; } if (plan != null) { // Defend against clearing the last forested tile, but // otherwise add the plan. TileType change = plan.getType().getChange(workTile.getType()); if (change != null && !change.isForested()) { int forest = 0; for (WorkLocation f : colony.getAvailableWorkLocations()) { if (f instanceof ColonyTile && ((ColonyTile)f).getWorkTile().isForested()) forest++; } if (forest <= FOREST_MINIMUM) continue; } newPlans.add(plan); logger.info(colony.getName() + " new tile improvement plan: " + plan); } } tileImprovementPlans.clear(); tileImprovementPlans.addAll(newPlans); Collections.sort(tileImprovementPlans); } /** * Handle REARRANGE_WORKERS property change events, by setting * the rearrangeTurn variable such that rearrangeWorkers will * run fully next time it is invoked. * * @param event The <code>PropertyChangeEvent</code>. */ public void propertyChange(PropertyChangeEvent event) { logger.finest("Property change REARRANGE_WORKERS fired."); rearrangeTurn = new Turn(0); } // Serialization /** * Writes this object to an XML stream. * * @param out The target stream. * @throws XMLStreamException if there are any problems writing to the * stream. */ protected void toXMLImpl(XMLStreamWriter out) throws XMLStreamException { if (colony == null || colony.isDisposed()) { logger.warning("Dead AIColony: " + colony.getName()); return; } out.writeStartElement(getXMLElementTagName()); out.writeAttribute(ID_ATTRIBUTE, getId()); for (AIGoods ag : aiGoods) { if (ag.getId() == null) { logger.warning("ag.getId() == null"); continue; } out.writeStartElement(ag.getXMLElementTagName() + LIST_ELEMENT); out.writeAttribute(ID_ATTRIBUTE, ag.getId()); out.writeEndElement(); } for (Wish w : wishes) { if (!w.shouldBeStored()) continue; String tag = (w instanceof GoodsWish) ? GoodsWish.getXMLElementTagName() : (w instanceof WorkerWish) ? WorkerWish.getXMLElementTagName() : null; if (tag == null) continue; out.writeStartElement(tag + LIST_ELEMENT); out.writeAttribute(ID_ATTRIBUTE, w.getId()); out.writeEndElement(); } for (TileImprovementPlan tip : tileImprovementPlans) { out.writeStartElement(tip.getXMLElementTagName() + LIST_ELEMENT); out.writeAttribute(ID_ATTRIBUTE, tip.getId()); out.writeEndElement(); } out.writeEndElement(); } /** * Reads information for this object from an XML stream. * * @param in The input stream with the XML. * @throws XMLStreamException if there are any problems reading from the * stream. */ protected void readFromXMLImpl(XMLStreamReader in) throws XMLStreamException { colony = (Colony) getAIMain().getFreeColGameObject(in.getAttributeValue(null, ID_ATTRIBUTE)); if (colony == null) { throw new NullPointerException("Could not find Colony with ID: " + in.getAttributeValue(null, ID_ATTRIBUTE)); } aiGoods.clear(); wishes.clear(); colonyPlan = new ColonyPlan(getAIMain(), colony); while (in.nextTag() != XMLStreamConstants.END_ELEMENT) { if (in.getLocalName().equals(AIGoods.getXMLElementTagName() + LIST_ELEMENT)) { AIGoods ag = (AIGoods) getAIMain().getAIObject(in.getAttributeValue(null, ID_ATTRIBUTE)); if (ag == null) { ag = new AIGoods(getAIMain(), in.getAttributeValue(null, ID_ATTRIBUTE)); } aiGoods.add(ag); in.nextTag(); } else if (in.getLocalName().equals(WorkerWish.getXMLElementTagName() + LIST_ELEMENT) // @compat 0.10.3 || in.getLocalName().equals(WorkerWish.getXMLElementTagName() + "Wish" + LIST_ELEMENT) // end compatibility code ) { Wish w = (Wish) getAIMain().getAIObject(in.getAttributeValue(null, ID_ATTRIBUTE)); if (w == null) { w = new WorkerWish(getAIMain(), in.getAttributeValue(null, ID_ATTRIBUTE)); } wishes.add(w); in.nextTag(); } else if (in.getLocalName().equals(GoodsWish.getXMLElementTagName() + LIST_ELEMENT) // @compat 0.10.3 || in.getLocalName().equals("GoodsWishWish" + LIST_ELEMENT) // end compatibility code ) { Wish w = (Wish) getAIMain().getAIObject(in.getAttributeValue(null, ID_ATTRIBUTE)); if (w == null) { w = new GoodsWish(getAIMain(), in.getAttributeValue(null, ID_ATTRIBUTE)); } wishes.add(w); in.nextTag(); } else if (in.getLocalName().equals(TileImprovementPlan.getXMLElementTagName() + LIST_ELEMENT) // @compat 0.10.3 || in.getLocalName().equals("tileimprovementplan" + LIST_ELEMENT) // end compatibility code ) { TileImprovementPlan ti = (TileImprovementPlan) getAIMain().getAIObject(in.getAttributeValue(null, ID_ATTRIBUTE)); if (ti == null) { ti = new TileImprovementPlan(getAIMain(), in.getAttributeValue(null, ID_ATTRIBUTE)); } tileImprovementPlans.add(ti); in.nextTag(); } else { logger.warning("Unknown tag name: " + in.getLocalName()); } } if (!in.getLocalName().equals(getXMLElementTagName())) { logger.warning("Expected end tag, received: " + in.getLocalName()); } } /** * Returns the tag name of the root element representing this object. * * @return "aiColony" */ public static String getXMLElementTagName() { return "aiColony"; } }