/**
* 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 javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
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.Game;
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.Location;
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.NetworkConstants;
import net.sf.freecol.common.util.Utils;
import net.sf.freecol.server.ai.mission.BuildColonyMission;
import net.sf.freecol.server.ai.mission.DefendSettlementMission;
import net.sf.freecol.server.ai.mission.IdleAtSettlementMission;
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.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;
// Useful things for the colony.
private final List<Wish> wishes;
// Plans to improve neighbouring tiles.
private final List<TileImprovementPlan> tileImprovementPlans;
// 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 in that role,
// then least skillful.
private static final Comparator<Unit> scoutComparator
= new Comparator<Unit>() {
public int compare(Unit u1, Unit u2) {
boolean a1 = u1.hasAbility(Ability.EXPERT_SCOUT);
boolean a2 = u2.hasAbility(Ability.EXPERT_SCOUT);
if (a1 != a2) return (a1) ? -1 : 1;
a1 = u1.hasAbility("model.ability.scoutIndianSettlement");
a2 = u2.hasAbility("model.ability.scoutIndianSettlement");
if (a1 != a2) return (a1) ? -1 : 1;
return u1.getType().getSkill() - u2.getType().getSkill();
}
};
/**
* Creates a new uninitialized <code>AIColony</code>.
*
* @param aiMain The main AI-object.
* @param id The identifier of this colony.
*/
public AIColony(AIMain aiMain, String id) {
super(aiMain, id);
colony = null;
colonyPlan = null;
aiGoods = new ArrayList<AIGoods>();
wishes = new ArrayList<Wish>();
tileImprovementPlans = new ArrayList<TileImprovementPlan>();
}
/**
* 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) {
this(aiMain, colony.getId());
this.colony = colony;
colonyPlan = new ColonyPlan(aiMain, colony);
colony.addPropertyChangeListener(Colony.REARRANGE_WORKERS, this);
uninitialized = false;
}
/**
* Creates a new <code>AIColony</code> from the given
* XML-representation.
*
* @param aiMain The main AI-object.
* @param element The root element for the XML-representation
* of a <code>Wish</code>.
*/
public AIColony(AIMain aiMain, Element element) {
this(aiMain, (String)null);
readFromXMLElement(element);
addAIObjectWithId();
uninitialized = getColony() == null;
}
/**
* Creates a new <code>AIColony</code> from the given
* XML-representation.
*
* @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 {
this(aiMain, (String)null);
readFromXML(in);
addAIObjectWithId();
uninitialized = getColony() == null;
}
/**
* 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();
colonyPlan = null;
// Do not clear this.colony, the id is still required.
super.dispose();
}
/**
* Gets this AI object's identifier.
*
* @return The id of the colony.
*/
@Override
public String getId() {
if (colony == null) {
logger.warning("Uninitialized AI colony");
return null;
}
return colony.getId();
}
/**
* 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());
}
/**
* Is a colony badly defended?
* Deliberately does not require defenders for 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.getWorkLocationUnitCount() - 2.5f;
}
/**
* Is this colony badly defended?
*
* @return True if this colony needs more defenders.
*/
public boolean isBadlyDefended() {
return isBadlyDefended(colony);
}
/**
* 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() {
// Skip this colony if it does not yet need rearranging, but
// first check if it is collapsing.
final int turn = getGame().getTurn().getNumber();
if (colony.getWorkLocationUnitCount() <= 0) {
avertAutoDestruction();
} else if (rearrangeTurn.getNumber() > turn) {
if (colony.getCurrentlyBuilding() == null
&& colonyPlan.getBestBuildableType() != null) {
logger.warning(colony.getName() + " could be building but"
+ " is asleep until turn: " + rearrangeTurn.getNumber()
+ "( > " + turn + ")");
}
return false;
}
final Tile tile = colony.getTile();
final AIMain aiMain = getAIMain();
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(t, this, 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() || getAIUnit(u) == null) continue;
Mission mission = getAIUnit(u).getMission();
if (mission == null
|| mission instanceof IdleAtSettlementMission
|| 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 unless it failed, in
// which case restore original state.
AIPlayer aiPlayer = getAIOwner();
boolean preferScouts = ((EuropeanAIPlayer)aiPlayer).scoutsNeeded() > 0;
Colony scratch = colonyPlan.assignWorkers(workers, preferScouts);
if (scratch == null) {
if (!UnitWas.revertAll(was)) {
String complain = "Failed to revert:";
for (UnitWas w : was) complain += " " + w.getUnit() + ",";
logger.warning(complain.substring(0, complain.length()-1));
}
rearrangeTurn = new Turn(turn + 1);
return false;
}
// 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()) {
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.getWorkLocationUnitCount() <= 0) {
String destruct = "Autodestruct at " + colony.getName()
+ " in " + turn + ":";
for (UnitWas uw : was) destruct += "\n" + 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
+ " " + colony.getNoBuildReason(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.getWorkLocationUnitCount() + ")"
+ " 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);
if (aiU.getMission() instanceof WorkInsideColonyMission
&& ((WorkInsideColonyMission)aiU.getMission()).getAIColony()
== this) {
;// Do nothing
} else {
aiU.setMission(new WorkInsideColonyMission(aiMain, aiU, this));
}
}
EuropeanAIPlayer aip = (EuropeanAIPlayer)aiMain.getAIPlayer(player);
boolean pioneersWanted = aip.pioneersNeeded() > 0;
Tile pioneerTile;
for (Unit u : tile.getUnitList()) {
AIUnit aiU = getAIUnit(u);
if (aiU == null || aiU.getMission() != null) continue;
switch (u.getRole()) {
case SOLDIER: case DRAGOON:
aiU.setMission(new DefendSettlementMission(aiMain, aiU,
colony));
break;
case SCOUT:
if (preferScouts
&& ScoutingMission.invalidReason(aiU) == null) {
aiU.setMission(new ScoutingMission(aiMain, aiU));
}
break;
case PIONEER:
if (PioneeringMission.invalidReason(aiU) == null
&& ((pioneerTile = aip.getBestPlanTile(colony)) != null
|| pioneersWanted)) {
aiU.setMission(new PioneeringMission(aiMain, aiU,
pioneerTile));
}
break;
// TODO: case MISSIONARY:
default:
break;
}
}
// Change the export settings when required.
resetExports();
createTileImprovementPlans();
updateWishes();
// 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
* updateAIGoods needs to know what to export by transport.
* TODO: consider market prices?
*/
private void resetExports() {
final Specification spec = getSpecification();
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()) {
partExport.add(g);
} else {
fullExport.add(g);
}
}
}
for (EquipmentType e : spec.getEquipmentTypeList()) {
for (AbstractGoods ag : e.getGoodsRequired()) {
if (fullExport.contains(ag.getType())) {
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.getTotalProductionOf(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(t, this, 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(steal, this, 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().getUnitCount() + " available";
for (Unit u : colony.getTile().getUnitList()) {
msg += ", " + u.toString();
}
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(type, u.getType()) > 0
&& AIMessage.askWork(getAIUnit(u), wl)
&& u.getLocation() == wl) {
AIMessage.askChangeWorkType(getAIUnit(u), type);
msg += ". Autodestruct averted with " + u + ".";
logger.warning(msg);
break;
}
}
}
}
// No good, no choice but to fail.
if (colony.getWorkLocationUnitCount() <= 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);
}
removeAIGoods(ag);
ag.dispose();
}
/**
* Emits a standard message regarding the state of AIGoods.
*
* @param ag The <code>AIGoods</code> to log.
* @param action The state of the goods.
*/
private void goodsLog(AIGoods ag, String action) {
Goods goods = (ag == null) ? null : ag.getGoods();
int amount = (goods == null) ? -1 : goods.getAmount();
String type = (goods == null) ? "(null)"
: Utils.lastPart(ag.getGoods().getType().getId(), ".");
logger.finest(String.format("%-20s %-10s %s %s %s",
colony.getName(), action,
((ag == null) ? "(null)" : ag.getId()),
((amount >= GoodsContainer.CARGO_SIZE) ? "full"
: Integer.toString(amount)), type));
}
/**
* Creates a list of the goods which should be shipped out of this colony.
*/
public void updateAIGoods() {
if (colony.hasAbility(Ability.EXPORT)) {
while (!aiGoods.isEmpty()) {
AIGoods ag = aiGoods.remove(0);
goodsLog(ag, "customizes");
dropGoods(ag);
}
return;
}
int i = 0;
while (i < aiGoods.size()) {
AIGoods ag = aiGoods.get(i);
if (ag == null) {
aiGoods.remove(i);
} else if (!ag.checkIntegrity()) {
goodsLog(ag, "reaps");
dropGoods(ag);
} else if (ag.getGoods().getLocation() != colony) {
// On its way, no longer of interest here, but do not dispose
// as that will happen when delivered.
goodsLog(ag, "sends");
aiGoods.remove(i);
} else if (colony.getAdjustedNetProductionOf(ag.getGoods()
.getType()) < 0) {
goodsLog(ag, "needs");
dropGoods(ag);
} else {
i++;
}
}
final Europe europe = colony.getOwner().getEurope();
final int capacity = colony.getWarehouseCapacity();
List<AIGoods> newAIGoods = 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)
? Transportable.IMPORTANT_DELIVERY
: (exportAmount >= GoodsContainer.CARGO_SIZE)
? Transportable.FULL_DELIVERY
: 0;
// Find all existing AI goods of type g
// update amount of goods to export
// reduce exportAmount at each step, dropping excess exports
i = 0;
while (i < aiGoods.size()) {
AIGoods ag = aiGoods.get(i);
Goods goods = ag.getGoods();
if (goods.getType() != g) {
i++;
continue;
}
int amount = goods.getAmount();
if (amount <= exportAmount) {
if (amount < GoodsContainer.CARGO_SIZE) {
amount = Math.min(exportAmount,
GoodsContainer.CARGO_SIZE);
goods.setAmount(amount);
ag.setTransportPriority(priority);
}
goodsLog(ag, "exports");
} else if (exportAmount >= EXPORT_MINIMUM) {
goods.setAmount(exportAmount);
goodsLog(ag, "clamps");
} else {
goodsLog(ag, "unexports");
dropGoods(ag);
continue;
}
exportAmount -= amount;
i++;
}
// Export new goods, to Europe if possible.
Location destination = (colony.getOwner().canTrade(g)) ? europe
: null;
while (exportAmount >= EXPORT_MINIMUM) {
int amount = Math.min(exportAmount, GoodsContainer.CARGO_SIZE);
AIGoods newGoods = new AIGoods(getAIMain(), colony, g,
amount, destination);
newGoods.setTransportPriority(priority);
newAIGoods.add(newGoods);
goodsLog(newGoods, "makes");
exportAmount -= amount;
}
}
aiGoods.addAll(newAIGoods);
Collections.sort(aiGoods, Transportable.transportableComparator);
}
/**
* 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);
}
/**
* Tries to complete a supplied wish.
*
* @param wish The <code>Wish</code> to complete.
* @return True if this wish was successfully completed.
*/
public boolean completeWish(Wish wish, String reason) {
if (!wishes.remove(wish)) return false;
logger.finest(colony.getName() + " completes " + reason
+ " wish: " + wish);
wish.dispose();
return true;
}
/**
* Tries to complete any wishes for some goods that have just arrived.
*
* @param goods Some <code>Goods</code> that are arriving in this colony.
* @return True if a wish was successfully completed.
*/
public boolean completeWish(Goods goods) {
boolean ret = false;
int i = 0;
while (i < wishes.size()) {
if (wishes.get(i) instanceof GoodsWish) {
GoodsWish gw = (GoodsWish)wishes.get(i);
if (gw.satisfiedBy(goods)
&& completeWish(gw, "satisfied(" + goods + ")")) {
ret = true;
continue;
}
}
i++;
}
return ret;
}
/**
* Tries to complete any wishes for a unit that has just arrived.
*
* @param unit A <code>Unit</code> that is arriving in this colony.
* @return True if a wish was successfully completed.
*/
public boolean completeWish(Unit unit) {
boolean ret = false;
int i = 0;
while (i < wishes.size()) {
if (wishes.get(i) instanceof WorkerWish) {
WorkerWish ww = (WorkerWish)wishes.get(i);
if (ww.satisfiedBy(unit)
&& completeWish(ww, "satisfied (" + unit.getId() + ")")) {
ret = true;
continue;
}
}
i++;
}
return ret;
}
/**
* Gets the wishes this colony has.
*
* @return A copy of the wishes list.
*/
public List<Wish> getWishes() {
return new ArrayList<Wish>(wishes);
}
/**
* Gets the goods wishes this colony has.
*
* @return A copy of the wishes list with non-goods wishes removed.
*/
public List<GoodsWish> getGoodsWishes() {
List<GoodsWish> result = new ArrayList<GoodsWish>();
for (Wish wish : wishes) {
if (wish instanceof GoodsWish) {
result.add((GoodsWish) wish);
}
}
return result;
}
/**
* 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;
}
/**
* Requires a goods wish for a specified goods type be present at this
* AI colony. If one is already present, the amount specified here
* takes precedence as it is more likely to be up to date. The value
* is treated as a minimum requirement.
*
* @param type The <code>GoodsType</code> to wish for.
* @param amount The amount of goods wished for.
* @param value The urgency of the wish.
*/
public void requireGoodsWish(GoodsType type, int amount, int value) {
GoodsWish gw = null;
for (Wish w : wishes) {
if (w instanceof GoodsWish
&& ((GoodsWish)w).getGoodsType() == type) {
gw = (GoodsWish)w;
break;
}
}
if (gw != null) {
gw.setGoodsAmount(amount);
gw.setValue(value);
} else {
gw = new GoodsWish(getAIMain(), colony, value, amount, type);
wishes.add(gw);
logger.finest(colony.getName() + " makes new goods wish: " + gw);
}
}
/**
* Requires a worker wish for a unit type to be provided to this AIColony.
* If a suitable wish is already present, the expert and value parameters
* take precedence as they are more likely to be up to date.
*
* @param type The <code>UnitType</code> to wish for.
* @param expertNeeded Is an expert unit required?
* @param value The urgency of the wish.
*/
public void requireWorkerWish(UnitType type, boolean expertNeeded,
int value) {
WorkerWish ww = null;
for (Wish w : wishes) {
if (w instanceof WorkerWish
&& ((WorkerWish)w).getUnitType() == type) {
ww = (WorkerWish)w;
break;
}
}
if (ww != null) {
ww.update(type, expertNeeded, value);
} else {
ww = new WorkerWish(getAIMain(), colony, value, type, expertNeeded);
wishes.add(ww);
logger.finest(colony.getName() + " makes new worker wish: "
+ ww);
}
}
/**
* Updates the wishes for the <code>Colony</code>.
*/
private void updateWishes() {
updateWorkerWishes();
updateGoodsWishes();
Collections.sort(wishes);
}
/**
* Updates the worker wishes.
*/
private void updateWorkerWishes() {
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);
requireWorkerWish(expert, true, value);
}
// Request population increase if no worker wishes and the bonus
// can take it.
if (experts.isEmpty()
&& colony.governmentChange(colony.getWorkLocationUnitCount() + 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;
}
requireWorkerWish(expert, false, 50);
}
// TODO: check for students
// TODO: add missionaries
// Improve defence.
if (isBadlyDefended()) {
UnitType bestDefender = colony.getBestDefenderType();
if (bestDefender != null) {
requireWorkerWish(bestDefender, true, 100);
}
}
}
/**
* Updates the goods wishes.
*/
private void updateGoodsWishes() {
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()) {
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;
}
}
}
// Drop wishes that are no longer needed.
int i = 0;
while (i < wishes.size()) {
if (wishes.get(i) instanceof GoodsWish) {
GoodsWish g = (GoodsWish)wishes.get(i);
GoodsType t = g.getGoodsType();
if (required.getCount(t) < colony.getGoodsCount(t)) {
completeWish(g, "redundant");
continue;
}
}
i++;
}
// Require wishes for what is missing.
for (GoodsType type : required.keySet()) {
GoodsType requiredType = type;
while (requiredType != null) {
if (requiredType.isStorable()) break;
requiredType = requiredType.getRawMaterial();
}
if (requiredType == null) continue;
int amount = Math.min(colony.getWarehouseCapacity(),
(required.getCount(type)
- colony.getGoodsCount(requiredType)));
if (amount > 0) {
int value = goodsWishValue;
if (colonyCouldProduce(requiredType)) value /= 10;
requireGoodsWish(requiredType, amount, value);
}
}
}
/**
* 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.
*
* @param event The <code>PropertyChangeEvent</code>.
*/
public void propertyChange(PropertyChangeEvent event) {
logger.finest("Property change REARRANGE_WORKERS fired.");
requestRearrange();
}
/**
* Sets the rearrangeTurn variable such that rearrangeWorkers will
* run fully next time it is invoked.
*/
public void requestRearrange() {
rearrangeTurn = new Turn(0);
}
/**
* Checks the integrity of a this AIColony
*
* @return True if the colony is intact.
*/
public boolean checkIntegrity() {
return super.checkIntegrity()
&& colony != null
&& !colony.isDisposed();
}
// 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 {
out.writeStartElement(getXMLElementTagName());
out.writeAttribute(ID_ATTRIBUTE, getId());
for (AIGoods ag : aiGoods) {
if (!ag.checkIntegrity()) continue;
out.writeStartElement(AIGoods.getXMLElementTagName() + LIST_ELEMENT);
out.writeAttribute(ID_ATTRIBUTE, ag.getId());
out.writeEndElement();
}
for (Wish w : wishes) {
String tag = (w instanceof GoodsWish)
? GoodsWish.getXMLElementTagName()
: (w instanceof WorkerWish)
? WorkerWish.getXMLElementTagName()
: null;
if (!w.checkIntegrity() || !w.shouldBeStored()
|| tag == null) continue;
out.writeStartElement(tag + LIST_ELEMENT);
out.writeAttribute(ID_ATTRIBUTE, w.getId());
out.writeEndElement();
}
for (TileImprovementPlan tip : tileImprovementPlans) {
if (!tip.checkIntegrity()) continue;
out.writeStartElement(TileImprovementPlan.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 readAttributes(XMLStreamReader in) throws XMLStreamException {
final AIMain aiMain = getAIMain();
final Game game = aiMain.getGame();
String str = in.getAttributeValue(null, ID_ATTRIBUTE);
if ((colony = game.getFreeColGameObject(str, Colony.class)) == null) {
throw new IllegalStateException("Not a Colony: " + str);
}
aiGoods.clear();
tileImprovementPlans.clear();
wishes.clear();
colonyPlan = new ColonyPlan(aiMain, colony);
}
protected void readChild(XMLStreamReader in) throws XMLStreamException {
final AIMain aiMain = getAIMain();
String tag = in.getLocalName();
String str;
if (tag.equals(AIGoods.getXMLElementTagName() + LIST_ELEMENT)) {
str = in.getAttributeValue(null, ID_ATTRIBUTE);
AIGoods ag = (AIGoods)aiMain.getAIObject(str);
if (ag == null) ag = new AIGoods(aiMain, str);
aiGoods.add(ag);
} else if (tag.equals(TileImprovementPlan.getXMLElementTagName()
+ LIST_ELEMENT)
// @compat 0.10.3
|| in.getLocalName().equals("tileimprovementplan"
+ LIST_ELEMENT)
// end compatibility code
) {
str = in.getAttributeValue(null, ID_ATTRIBUTE);
TileImprovementPlan ti = (TileImprovementPlan)aiMain
.getAIObject(str);
if (ti == null) ti = new TileImprovementPlan(aiMain, str);
tileImprovementPlans.add(ti);
} else if (tag.equals(GoodsWish.getXMLElementTagName()
+ LIST_ELEMENT)
// @compat 0.10.3
|| in.getLocalName().equals(GoodsWish.getXMLElementTagName()
+ "Wish" + LIST_ELEMENT)
// end compatibility code
) {
str = in.getAttributeValue(null, ID_ATTRIBUTE);
GoodsWish w = (GoodsWish)aiMain.getAIObject(str);
if (w == null) w = new GoodsWish(aiMain, str);
wishes.add(w);
} else if (tag.equals(WorkerWish.getXMLElementTagName()
+ LIST_ELEMENT)
// @compat 0.10.3
|| in.getLocalName().equals(WorkerWish.getXMLElementTagName()
+ "Wish" + LIST_ELEMENT)
// end compatibility code
) {
str = in.getAttributeValue(null, ID_ATTRIBUTE);
Wish w = (Wish)aiMain.getAIObject(str);
if (w == null) w = new WorkerWish(aiMain, str);
wishes.add(w);
} else {
logger.warning("Unknown tag name: " + in.getLocalName());
}
in.nextTag();
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "aiColony"
*/
public static String getXMLElementTagName() {
return "aiColony";
}
}