/**
* 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";
}
}