/**
* Copyright (C) 2002-2012 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* FreeCol is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.server.ai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.BuildingType;
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.FreeColObject;
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.Market;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.NationType;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Scope;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.Tile;
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.Unit.Role;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.model.UnitTypeChange.ChangeType;
import net.sf.freecol.common.model.WorkLocation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Objects of this class describes the plan the AI has for a
* <code>Colony</code>.
*
* A <code>ColonyPlan</code> contains a list of {@link
* WorkLocationPlan}s which suggests the food and non-food production
* of each {@link WorkLocation}, and a list of {@link BuildableType}s
* to build. It takes account of the available tiles and building
* production, but does not make decisions to claim tiles or change
* the current buildable. It does takes account of goods present in
* the colony, and overall colony size but not the exact composition
* of the units involved. However there is extensive structure for
* making a trial assignment of workers in {@link assignWorkers}.
*
* {@link AIColony.rearrangeWorkers} is responsible for making
* the real decisions.
*
* @see Colony
*/
public class ColonyPlan {
@SuppressWarnings("unused")
private static final Logger logger = Logger.getLogger(ColonyPlan.class.getName());
// Require production plans to always produce an amount exceeding this.
private static final int LOW_PRODUCTION_THRESHOLD = 1;
// Number of turns to require production of without exhausting the
// input goods.
private static final int PRODUCTION_TURNOVER_TURNS = 5;
// The profile of the colony (a sort of general flavour).
private static enum ProfileType {
OUTPOST,
SMALL,
MEDIUM,
LARGE,
CAPITAL;
/**
* Chooses a suitable profile type given a size of colony.
*
* @param size A proposed colony size.
*/
public static ProfileType getProfileTypeFromSize(int size) {
return (size <= 1) ? ProfileType.OUTPOST
: (size <= 2) ? ProfileType.SMALL
: (size <= 4) ? ProfileType.MEDIUM
: (size <= 8) ? ProfileType.LARGE
: ProfileType.CAPITAL;
}
};
private ProfileType profileType;
// Private copy of the AIMain.
private AIMain aiMain;
// The colony this AIColony manages.
private Colony colony;
// The things to build, and their priority.
private class BuildPlan {
public BuildableType type;
public double weight;
public double support;
public double difficulty;
public BuildPlan(BuildableType type, double weight, double support) {
this.type = type;
this.weight = weight;
this.support = support;
this.difficulty = 1.0f;
}
public double getValue() {
return weight * support / difficulty;
}
public String toString() {
String t = type.toString();
return String.format("%s (%1.3f * %1.3f / %1.3f = %1.3f)",
t.substring(t.lastIndexOf(".")+1),
weight, support, difficulty, getValue());
}
};
private final List<BuildPlan> buildPlans = new ArrayList<BuildPlan>();
// Comparator to sort buildable types on their priority in the
// buildPlan map.
private static final Comparator<BuildPlan> buildPlanComparator
= new Comparator<BuildPlan>() {
public int compare(BuildPlan b1, BuildPlan b2) {
double d = b1.getValue() - b2.getValue();
return (d > 0.0) ? -1 : (d < 0.0) ? 1 : 0;
}
};
// Plans for work locations available to this colony.
private final List<WorkLocationPlan> workPlans
= new ArrayList<WorkLocationPlan>();
// The goods types to produce.
private final List<GoodsType> produce = new ArrayList<GoodsType>();
// Lists of goods types to be produced in this colony.
// Temporary variables that do not need to be serialized.
private final List<GoodsType> foodGoodsTypes
= new ArrayList<GoodsType>();
private final List<GoodsType> libertyGoodsTypes
= new ArrayList<GoodsType>();
private final List<GoodsType> immigrationGoodsTypes
= new ArrayList<GoodsType>();
private final List<GoodsType> militaryGoodsTypes
= new ArrayList<GoodsType>();
private final List<GoodsType> rawBuildingGoodsTypes
= new ArrayList<GoodsType>();
private final List<GoodsType> buildingGoodsTypes
= new ArrayList<GoodsType>();
private final List<GoodsType> rawLuxuryGoodsTypes
= new ArrayList<GoodsType>();
private final List<GoodsType> luxuryGoodsTypes
= new ArrayList<GoodsType>();
private final List<GoodsType> otherRawGoodsTypes
= new ArrayList<GoodsType>();
/**
* Creates a new <code>ColonyPlan</code>.
*
* @param aiMain The main AI-object.
* @param colony The colony to make a <code>ColonyPlan</code> for.
*/
public ColonyPlan(AIMain aiMain, Colony colony) {
if (aiMain == null) throw new IllegalArgumentException("Null AIMain");
if (colony == null) throw new IllegalArgumentException("Null colony");
this.aiMain = aiMain;
this.colony = colony;
this.profileType = ProfileType
.getProfileTypeFromSize(colony.getUnitCount());
}
/**
* Creates a new <code>ColonyPlan</code>.
*
* @param aiMain The main AI-object.
* @param element An <code>Element</code> containing an XML-representation
* of this object.
*/
public ColonyPlan(AIMain aiMain, Element element) {
this.aiMain = aiMain;
readFromXMLElement(element);
}
/**
* Gets the main AI-object.
*
* @return The main AI-object.
*/
private AIMain getAIMain() {
return aiMain;
}
/**
* Gets the <code>Colony</code> this <code>ColonyPlan</code> controls.
*
* @return The <code>Colony</code>.
*/
private Colony getColony() {
return colony;
}
/**
* Gets the specification.
*
* @return The specification.
*/
private Specification spec() {
return aiMain.getGame().getSpecification();
}
/**
* Gets the production for a work location of a specified goods type,
* using the default unit type to avoid considering which unit is
* to be allocated. This is thus an approximation to what will
* finally occur when units are assigned, but it serves for planning
* purposes.
*
* @param wl The <code>WorkLocation</code> where production is to occur.
* @param goodsType The <code>GoodsType</code> to produce.
* @return The work location production.
*/
private int getWorkLocationProduction(WorkLocation wl,
GoodsType goodsType) {
return wl.getPotentialProduction(spec().getDefaultUnitType(),
goodsType);
}
// Public functionality.
/**
* Gets the preferred goods to produce.
*
* @return A copy of the preferred goods production list in this plan.
*/
public List<GoodsType> getPreferredProduction() {
return new ArrayList<GoodsType>(produce);
}
/**
* Gets a copy of the current list of buildable types associated
* with this <code>ColonyPlan</code>.
*
* @return A copy of the of <code>BuildableType</code>s list.
*/
public List<BuildableType> getBuildableTypes() {
List<BuildableType> build = new ArrayList<BuildableType>();
for (BuildPlan b : buildPlans) build.add(b.type);
return build;
}
/**
* Gets the best buildable type from this plan that can currently
* be built by the colony.
*
* @return The best current <code>BuildableType</code>.
*/
public BuildableType getBestBuildableType() {
for (BuildPlan b : buildPlans) {
if (colony.canBuild(b.type)) return b.type;
}
return null;
}
/**
* Gets the food-producing and non-autoproducing work location
* plans associated with this <code>ColonyPlan</code>.
*
* @return A list of food producing plans.
*/
public List<WorkLocationPlan> getFoodPlans() {
List<WorkLocationPlan> plans = new ArrayList<WorkLocationPlan>();
for (WorkLocationPlan wlp : workPlans) {
if (wlp.getGoodsType().isFoodType()
&& !wlp.getWorkLocation().canAutoProduce()) plans.add(wlp);
}
return plans;
}
/**
* Gets the non-food-producing/non-autoproducing work location
* plans associated with this <code>ColonyPlan</code>.
*
* @return A list of nonfood producing plans.
*/
public List<WorkLocationPlan> getWorkPlans() {
List<WorkLocationPlan> plans = new ArrayList<WorkLocationPlan>();
for (WorkLocationPlan wlp : workPlans) {
if (!wlp.getGoodsType().isFoodType()
&& !wlp.getWorkLocation().canAutoProduce()) plans.add(wlp);
}
return plans;
}
/**
* Recreates the buildables and work location plans for this
* colony.
*/
public void update() {
UnitType defaultUnitType = spec().getDefaultUnitType();
GoodsType goodsType;
// Update the profile type.
profileType = ProfileType
.getProfileTypeFromSize(colony.getUnitCount());
// Build the total map of all possible production with standard units.
Map<GoodsType, Map<WorkLocation, Integer>> production
= createProductionMap();
// Set the goods type lists, and prune production of manufactured
// goods that are missing raw materials and other non-interesting.
updateGoodsTypeLists(production);
// Set the preferred raw materials. Prune production and
// goods lists further removing the non-preferred new world
// raw and refined materials.
updateRawMaterials(production);
// The buildables depend on the profile type, the goods type lists
// and/or goods-to-produce list.
updateBuildableTypes();
// Make plans for each valid <goods, location> production and
// complete the list of goods to produce.
updatePlans(production);
}
/**
* Gets the goods required to complete a build. The list includes
* the prerequisite raw materials as well as the direct
* requirements (i.e. hammers, tools). If enough of a required
* goods is present in the colony, then that type is not returned.
* Take care to order types with raw materials first so that we
* can prioritize gathering what is required before manufacturing.
*
* Public for the benefit of the test suite.
*
* @param buildable The <code>BuildableType</code> to consider.
* @return A list of required abstract goods.
*/
public List<AbstractGoods> getRequiredGoods(BuildableType buildable) {
List<AbstractGoods> required = new ArrayList<AbstractGoods>();
if (buildable != null && buildable.getGoodsRequired() != null) {
for (AbstractGoods ag : buildable.getGoodsRequired()) {
int amount = ag.getAmount();
GoodsType type = ag.getType();
while (type != null) {
if (amount <= colony.getGoodsCount(type)) break; // Shortcut
required.add(0, new AbstractGoods(type,
amount - colony.getGoodsCount(type)));
type = type.getRawMaterial();
}
}
}
return required;
}
/**
* Refines this plan given the colony choice of what to build.
*
* @param build The <code>BuildableType</code> to be built (may be null).
*/
public void refine(BuildableType build) {
List<GoodsType> required = new ArrayList<GoodsType>();
for (AbstractGoods ag : getRequiredGoods(build)) {
required.add(ag.getType());
}
Map<GoodsType, List<WorkLocationPlan>> suppressed
= new HashMap<GoodsType, List<WorkLocationPlan>>();
// Examine a copy of the work plans, but operate on the
// original list. Maintain a offset between the index in the
// copied list and the original to aid reinsertion.
//
// Remove any work plans to make raw/building goods that are
// not required to complete the current buildable, but take
// care to put such plans back again if a plan is encountered
// that makes goods that are made from a type that was removed
// and there is less than CARGO_SIZE/2 of that type in stock.
// Note though in such cases the position of the
// building-goods plans in the work plans list will have moved
// from their usual high priority to immediately before the
// position of the manufactured goods.
//
// So, for example, we should suppress tool building when a
// colony is building a warehouse, unless we find a plan to
// make muskets and the tool stock is low.
//
// TODO: generalize this further to make tools for pioneers.
//
List<WorkLocationPlan> plans
= new ArrayList<WorkLocationPlan>(workPlans);
int offset = 0;
for (int i = 0; i < plans.size(); i++) {
List<WorkLocationPlan> wls;
WorkLocationPlan wlp = plans.get(i);
GoodsType g = wlp.getGoodsType();
if ((rawBuildingGoodsTypes.contains(g)
&& !required.contains(g.getProducedMaterial()))
|| (buildingGoodsTypes.contains(g)
&& !required.contains(g))) {
workPlans.remove(i - offset);
offset++;
wls = suppressed.get(g);
if (wls == null) wls = new ArrayList<WorkLocationPlan>();
wls.add(0, wlp); // reverses list
suppressed.put(g, wls);
produce.remove(g);
logger.finest("At " + colony.getName()
+ " suppress production of " + g);
} else if (g.isRefined()
&& (rawBuildingGoodsTypes.contains(g.getRawMaterial())
|| buildingGoodsTypes.contains(g.getRawMaterial()))) {
int n = 0, idx = produce.indexOf(g);
for (GoodsType type = g.getRawMaterial(); type != null;
type = type.getRawMaterial()) {
if ((wls = suppressed.get(type)) == null) break;
if (colony.getGoodsCount(type)
>= GoodsContainer.CARGO_SIZE/2) break;
n += wls.size();
while (!wls.isEmpty()) {
// reverses again when adding, cancelling reversal above
workPlans.add(i - offset, wls.remove(0));
}
produce.add(idx, type);
logger.finest("At " + colony.getName()
+ " restore production of " + type);
}
offset -= n;
}
}
}
/**
* Creates a map of potential production of all goods types
* from all available work locations using the default unit type.
* Includes non-workable locations (e.g. chapel, colony-center-tile)
* as their production can influence the choice of goods to produce.
*
* @return The map of potential production.
*/
private Map<GoodsType, Map<WorkLocation, Integer>> createProductionMap() {
Map<GoodsType, Map<WorkLocation, Integer>> production
= new HashMap<GoodsType, Map<WorkLocation, Integer>>();
for (WorkLocation wl : colony.getAvailableWorkLocations()) {
for (GoodsType g : spec().getGoodsTypeList()) {
int p = getWorkLocationProduction(wl, g);
if (p > 0) {
Map<WorkLocation, Integer> m = production.get(g);
if (m == null) {
m = new HashMap<WorkLocation, Integer>();
production.put(g, m);
}
m.put(wl, new Integer(p));
}
}
}
return production;
}
/**
* Updates the goods type lists. The categories are:<UL>
* <LI>food</LI>
* <LI>liberty</LI>
* <LI>immigration</LI>
* <LI>military</LI>
* <LI>raw building</LI>
* <LI>building</LI>
* <LI>raw luxury</LI>
* <LI>luxury</LI>
* <LI>raw other</LI>
* </UL>
*
* Ignore raw materials which can not be refined and refined goods
* that have no raw materials available. Also ignore other goods
* that do not fit these categories (e.g. trade goods).
*
* @param production The production map.
*/
private void updateGoodsTypeLists(Map<GoodsType, Map<WorkLocation, Integer>> production) {
foodGoodsTypes.clear();
libertyGoodsTypes.clear();
immigrationGoodsTypes.clear();
militaryGoodsTypes.clear();
rawBuildingGoodsTypes.clear();
buildingGoodsTypes.clear();
rawLuxuryGoodsTypes.clear();
luxuryGoodsTypes.clear();
otherRawGoodsTypes.clear();
for (GoodsType g : new ArrayList<GoodsType>(production.keySet())) {
if (g.isFoodType()) {
foodGoodsTypes.add(g);
} else if (g.isLibertyType()) {
libertyGoodsTypes.add(g);
} else if (g.isImmigrationType()) {
immigrationGoodsTypes.add(g);
} else if (g.isMilitaryGoods()) {
militaryGoodsTypes.add(g);
} else if (g.isRawBuildingMaterial()) {
rawBuildingGoodsTypes.add(g);
} else if (g.isBuildingMaterial()
&& g.getRawMaterial().isRawBuildingMaterial()) {
buildingGoodsTypes.add(g);
} else if (g.isNewWorldGoodsType()) {
rawLuxuryGoodsTypes.add(g);
} else if (g.isRefined()
&& g.getRawMaterial().isNewWorldGoodsType()) {
luxuryGoodsTypes.add(g);
} else if (g.isFarmed()) {
otherRawGoodsTypes.add(g);
} else { // Not interested in this goods type.
logger.warning("Ignoring goods type " + g
+ " at " + colony.getName());
production.remove(g);
}
}
}
/**
* Chooses the two best raw materials, updating the production
* map and lists.
*
* @param production The production map.
*/
private void updateRawMaterials(Map<GoodsType, Map<WorkLocation, Integer>> production) {
Player player = colony.getOwner();
Market market = player.getMarket();
NationType nationType = player.getNationType();
GoodsType primaryRawMaterial = null;
GoodsType secondaryRawMaterial = null;
int primaryValue = -1;
int secondaryValue = -1;
produce.clear();
List<GoodsType> rawMaterials
= new ArrayList<GoodsType>(rawLuxuryGoodsTypes);
rawMaterials.addAll(otherRawGoodsTypes);
for (GoodsType g : rawMaterials) {
int value = 0;
for (Entry<WorkLocation, Integer> e
: production.get(g).entrySet()) {
value += e.getValue().intValue();
}
if (value <= LOW_PRODUCTION_THRESHOLD) {
production.remove(g);
continue;
}
if (market != null) {
// If the market is available, weight by sale price of
// the material, or if it is the raw material for a
// refined goods type, the average of the raw and
// refined goods prices.
if (g.getProducedMaterial() == null) {
value *= market.getSalePrice(g, 1);
} else if (production.containsKey(g.getProducedMaterial())) {
value *= (market.getSalePrice(g, 1)
+ market.getSalePrice(g.getProducedMaterial(), 1)) / 2;
}
}
if (!nationType.getModifierSet(g.getId()).isEmpty()) {
value = (value * 12) / 10; // Bonus for national advantages
}
if (value > secondaryValue && secondaryRawMaterial != null) {
production.remove(secondaryRawMaterial);
production.remove(secondaryRawMaterial.getProducedMaterial());
if (rawLuxuryGoodsTypes.contains(secondaryRawMaterial)) {
rawLuxuryGoodsTypes.remove(secondaryRawMaterial);
luxuryGoodsTypes.remove(secondaryRawMaterial.getProducedMaterial());
} else if (rawMaterials.contains(otherRawGoodsTypes)) {
rawMaterials.remove(otherRawGoodsTypes);
}
}
if (value > primaryValue) {
secondaryRawMaterial = primaryRawMaterial;
secondaryValue = primaryValue;
primaryRawMaterial = g;
primaryValue = value;
} else if (value > secondaryValue) {
secondaryRawMaterial = g;
secondaryValue = value;
}
}
if (primaryRawMaterial != null) {
produce.add(primaryRawMaterial);
if (primaryRawMaterial.getProducedMaterial() != null) {
produce.add(primaryRawMaterial.getProducedMaterial());
}
if (secondaryRawMaterial != null) {
produce.add(secondaryRawMaterial);
if (secondaryRawMaterial.getProducedMaterial() != null) {
produce.add(secondaryRawMaterial.getProducedMaterial());
}
}
}
}
// Relative weights of the various building categories.
// TODO: split out/parameterize into a `building strategy'
//
// BuildableTypes that improve breeding.
private static final double BREEDING_WEIGHT = 0.1;
// BuildableTypes that improve building production.
private static final double BUILDING_WEIGHT = 0.9;
// BuildableTypes that produce defensive units.
private static final double DEFENCE_WEIGHT = 0.1;
// BuildableTypes that provide export ability.
private static final double EXPORT_WEIGHT = 0.6;
// BuildableTypes that allow water to be used.
private static final double FISH_WEIGHT = 0.25;
// BuildableTypes that improve the colony fortifications.
private static final double FORTIFY_WEIGHT = 0.3;
// BuildableTypes that improve immigration production.
private static final double IMMIGRATION_WEIGHT = 0.05;
// BuildableTypes that improve liberty production.
private static final double LIBERTY_WEIGHT = 0.75;
// BuildableTypes that improve military goods production.
private static final double MILITARY_WEIGHT = 0.4;
// BuildableTypes that improve luxury goods production.
private static final double PRODUCTION_WEIGHT = 0.25;
// BuildableTypes that improve colony storage.
private static final double REPAIR_WEIGHT = 0.1;
// BuildableTypes that improve colony storage.
private static final double STORAGE_WEIGHT = 0.85;
// BuildableTypes that improve education.
private static final double TEACH_WEIGHT = 0.2;
// BuildableTypes that improve transport.
private static final double TRANSPORT_WEIGHT = 0.15;
/**
* Finds a build plan for this type.
*
* @param type The <code>BuildableType</code> to search for.
* @return A <code>BuildPlan</code> with this type, or null if not found.
*/
private BuildPlan findBuildPlan(BuildableType type) {
for (BuildPlan bp : buildPlans) {
if (bp.type == type) return bp;
}
return null;
}
/**
* Adds or improves the priority of a buildable in a list.
*
* @param type The <code>BuildableType</code> to use.
* @param weight The relative weight of this class of buildable with
* respect to other buildable classes.
* @param support The support for this buildable within its class.
* @return True if this type was prioritized.
*/
private boolean prioritize(BuildableType type,
double weight, double support) {
BuildPlan bp = findBuildPlan(type);
if (bp == null) {
buildPlans.add(new BuildPlan(type, weight, support));
return true;
}
if (bp.weight * bp.support < weight * support) {
bp.weight = weight;
bp.support = support;
return true;
}
return false;
}
/**
* Given a buildable that improves production of a goods type,
* prioritize it.
*
* @param type The <code>BuildableType</code> to consider.
* @param goodsType The <code>GoodsType</code> improved by the buildable.
* @return True if this type was prioritized.
*/
private boolean prioritizeProduction(BuildableType type,
GoodsType goodsType) {
Player player = colony.getOwner();
NationType nationType = player.getNationType();
String advantage = getAIMain().getAIPlayer(player).getAIAdvantage();
boolean ret = false;
double factor = 1.0;
if (!nationType.getModifierSet(goodsType.getId()).isEmpty()) {
// Handles building, agriculture, furTrapping advantages
factor *= 1.2;
}
if (goodsType.isMilitaryGoods()) {
if ("conquest".equals(advantage)) factor = 1.2;
ret = prioritize(type, MILITARY_WEIGHT * factor,
1.0/*FIXME: amount present wrt amount to equip*/);
} else if (goodsType.isBuildingMaterial()) {
ret = prioritize(type, BUILDING_WEIGHT * factor,
1.0/*FIXME: need for this type*/);
} else if (goodsType.isLibertyType()) {
ret = prioritize(type, LIBERTY_WEIGHT,
(colony.getSoL() >= 100) ? 0.01 : 1.0);
} else if (goodsType.isImmigrationType()) {
if ("immigration".equals(advantage)) factor = 1.2;
ret = prioritize(type, IMMIGRATION_WEIGHT * factor,
1.0/*FIXME: Brewster?*/);
} else if (produce.contains(goodsType)) {
if ("trade".equals(advantage)) factor = 1.2;
double f = 0.1 * colony.getProductionOf(goodsType.getRawMaterial());
ret = prioritize(type, PRODUCTION_WEIGHT,
f/*FIXME: improvement?*/);
}
return ret;
}
/**
* Updates the build plans for this colony.
*/
private void updateBuildableTypes() {
String advantage = getAIMain().getAIPlayer(colony.getOwner())
.getAIAdvantage();
buildPlans.clear();
int maxLevel;
switch (profileType) {
case OUTPOST:
case SMALL: maxLevel = 1; break;
case MEDIUM: maxLevel = 2; break;
case LARGE: maxLevel = 3; break;
case CAPITAL: maxLevel = 4; break;
default:
throw new IllegalStateException("Bogus profile type: "
+ profileType);
}
Player player = colony.getOwner();
for (BuildingType type : spec().getBuildingTypeList()) {
boolean expectFail = false;
if (!colony.canBuild(type)) continue;
// Exempt defence and export from the level check.
if (!type.getModifierSet(Modifier.DEFENCE).isEmpty()) {
double factor = 1.0;
if ("conquest".equals(advantage)) factor = 1.1;
prioritize(type, FORTIFY_WEIGHT * factor,
1.0/*FIXME: 0 if FF underway*/);
}
if (type.hasAbility(Ability.EXPORT)) {
double factor = 1.0;
if ("trade".equals(advantage)) factor = 1.1;
prioritize(type, EXPORT_WEIGHT * factor,
1.0/*FIXME: weigh production v transport*/);
}
// Skip later stage buildings for smaller settlements.
if (type.getLevel() > maxLevel) continue;
// Scale docks by the improvement available to the food supply.
if (type.hasAbility(Ability.PRODUCE_IN_WATER)
&& !colony.hasAbility(Ability.PRODUCE_IN_WATER)
&& colony.getTile().isCoast()) {
int landFood = 0, seaFood = 0;
for (Tile t : colony.getTile().getSurroundingTiles(1)) {
if (t.getOwningSettlement() == colony
|| player.canClaimForSettlement(t)) {
for (AbstractGoods ag : t.getSortedPotential()) {
if (ag.getType().isFoodType()) {
if (t.isLand()) {
landFood += ag.getAmount();
} else {
seaFood += ag.getAmount();
}
}
}
}
}
if (seaFood > 0) {
prioritize(type, FISH_WEIGHT,
seaFood / (double)(seaFood + landFood));
}
}
if (type.hasAbility(Ability.BUILD)) {
double factor = 1.0;
if ("building".equals(advantage)) factor = 1.1;
double support = 1.0;
for (Ability a : type.getFeatureContainer().getAbilitySet(Ability.BUILD)) {
List<Scope> scopes = a.getScopes();
if (scopes != null && !scopes.isEmpty()) support = 0.1;
}
prioritize(type, BUILDING_WEIGHT * factor,
support/*FIXME: need for the thing now buildable*/);
}
if (type.hasAbility(Ability.CAN_TEACH)) {
prioritize(type, TEACH_WEIGHT,
1.0/*FIXME: #students, #specialists here, #wanted*/);
}
if (type.hasAbility(Ability.REPAIR_UNITS)) {
double factor = 1.0;
if ("naval".equals(advantage)) factor = 1.1;
prioritize(type, REPAIR_WEIGHT * factor,
1.0/*FIXME: #units-to-repair, has-Europe etc*/);
}
GoodsType output = type.getProducedGoodsType();
if (output != null) {
if (!prioritizeProduction(type, output)) {
// Allow failure if this building can not build.
expectFail = true;
}
} else {
for (GoodsType g : spec().getGoodsTypeList()) {
if (!type.getModifierSet(g.getId()).isEmpty()) {
if (!prioritizeProduction(type, g)) {
expectFail = true;
}
}
}
// Hacks. No good way to make this really generic.
if (!type.getModifierSet("model.modifier.warehouseStorage")
.isEmpty()) {
double factor = 1.0;
if ("trade".equals(advantage)) factor = 1.1;
prioritize(type, STORAGE_WEIGHT * factor,
1.0/*FIXME: amount of goods*/);
}
if (!type.getModifierSet("model.modifier.breedingDivisor")
.isEmpty()) {
prioritize(type, BREEDING_WEIGHT,
1.0/*FIXME: horses present?*/);
}
}
if (findBuildPlan(type) == null && !expectFail) {
logger.warning("No building priority found for: " + type);
}
}
double wagonNeed = 0.0;
if (!colony.isConnected()) { // Inland colonies need transportation
int wagons = 0;
for (Unit u : player.getUnits()) {
if (u.hasAbility(Ability.CARRY_GOODS)
&& !u.isNaval()) wagons++;
}
int inland = 0;
for (Colony c : player.getColonies()) {
if (!c.isConnected()) inland++;
}
if (inland > wagons) {
wagonNeed = (double)(inland - wagons) / inland;
}
}
for (UnitType unitType : spec().getUnitTypeList()) {
if (!colony.canBuild(unitType)) continue;
if (unitType.hasAbility(Ability.NAVAL_UNIT)) {
; // TODO: decide to build a ship
} else if (unitType.getDefence() > UnitType.DEFAULT_DEFENCE) {
if (AIColony.isBadlyDefended(colony)) {
prioritize(unitType, DEFENCE_WEIGHT,
1.0/*FIXME: how badly defended?*/);
}
} else if (unitType.hasAbility(Ability.CARRY_GOODS)) {
if (wagonNeed > 0.0) {
double factor = 1.0;
if ("trade".equals(advantage)) factor = 1.1;
prioritize(unitType, TRANSPORT_WEIGHT * factor,
wagonNeed/*FIXME: type.getSpace()*/);
}
}
}
// Weight by lower required goods.
for (BuildPlan bp : buildPlans) {
double difficulty = 0.0f;
for (AbstractGoods ag : bp.type.getGoodsRequired()) {
GoodsType g = ag.getType();
int need = ag.getAmount() - colony.getGoodsCount(g);
if (need > 0) {
// Penalize building with type that can not be
// made locally.
double f = (produce.contains(g.getRawMaterial())) ? 1.0
: 5.0;
difficulty += need * f;
}
}
bp.difficulty = Math.max(1.0f, Math.sqrt(difficulty));
}
Collections.sort(buildPlans, buildPlanComparator);
}
/**
* Makes a plan for each type of possible production, that is
* those work locations that can use a unit or can auto-produce.
* Note that this will almost certainly include clashes over work
* locations. That gets sorted out elsewhere as ColonyPlans do
* not examine the units present.
*
* With the complete list of work plans, finish creating the list
* of goods to produce.
*
* Then filter out the auto-production plans as they are not
* going to be helpful for unit allocation.
*
* Finally sort by desirability.
*/
private void updatePlans(Map<GoodsType, Map<WorkLocation, Integer>> production) {
workPlans.clear();
for (GoodsType g : production.keySet()) {
// Do not make plans to produce into a full warehouse.
if (g.isStorable()
&& colony.getGoodsCount(g) >= colony.getWarehouseCapacity()
&& !g.limitIgnored()) continue;
for (WorkLocation wl : production.get(g).keySet()) {
if (wl.canBeWorked() || wl.canAutoProduce()) {
workPlans.add(new WorkLocationPlan(getAIMain(), wl, g));
}
}
}
// Now we have lots of plans, determine what goods to produce.
updateProductionList(production);
// Filter out plans that can not use a unit.
List<WorkLocationPlan> oldPlans
= new ArrayList<WorkLocationPlan>(workPlans);
workPlans.clear();
for (WorkLocationPlan wlp : oldPlans) {
if (wlp.getWorkLocation().canBeWorked()) workPlans.add(wlp);
}
// Sort the work plans by earliest presence in the produce
// list, and then by amount. If the type of goods produced is
// not on the produce list, then make sure such plans sort to
// the end, except for food plans.
Collections.sort(workPlans, new Comparator<WorkLocationPlan>() {
public int compare(WorkLocationPlan w1, WorkLocationPlan w2) {
GoodsType g1 = w1.getGoodsType();
GoodsType g2 = w2.getGoodsType();
int i1 = produce.indexOf(g1);
int i2 = produce.indexOf(g2);
if (i1 < 0 && !g1.isFoodType()) i1 = 99999;
if (i2 < 0 && !g2.isFoodType()) i2 = 99999;
int cmp = i1 - i2;
if (cmp == 0) {
cmp = getWorkLocationProduction(w2.getWorkLocation(), g2)
- getWorkLocationProduction(w1.getWorkLocation(), g1);
}
return cmp;
}
});
}
/**
* Add the other goods types to the production list. When this is
* called the new world goods production is already present on the
* produce list. Ignores food which is treated separately.
*/
private void updateProductionList(final Map<GoodsType, Map<WorkLocation, Integer>> production) {
final Comparator<GoodsType> productionComparator
= new Comparator<GoodsType>() {
public int compare(GoodsType g1, GoodsType g2) {
int p1 = 0;
for (Integer i : production.get(g1).values()) {
p1 += i.intValue();
}
int p2 = 0;
for (Integer i : production.get(g2).values()) {
p2 += i.intValue();
}
return p2 - p1;
}
};
List<GoodsType> toAdd = new ArrayList<GoodsType>();
// If we need liberty put it before the new world production.
if (colony.getSoL() < 100) {
for (GoodsType g : libertyGoodsTypes) {
if (production.containsKey(g)) toAdd.add(g);
}
Collections.sort(toAdd, productionComparator);
produce.addAll(0, toAdd);
toAdd.clear();
}
// Always add raw/building materials first.
Collections.sort(rawBuildingGoodsTypes, productionComparator);
for (GoodsType g : buildingGoodsTypes) {
if (production.containsKey(g)) {
GoodsType raw = g.getRawMaterial();
if (colony.getGoodsCount(raw) >= GoodsContainer.CARGO_SIZE/2
|| production.containsKey(raw)) {
toAdd.add(g);
}
}
}
Collections.sort(toAdd, new Comparator<GoodsType>() {
public int compare(GoodsType g1, GoodsType g2) {
int i1 = rawBuildingGoodsTypes.indexOf(g1.getRawMaterial());
int i2 = rawBuildingGoodsTypes.indexOf(g2.getRawMaterial());
return i1 - i2;
}
});
for (int i = toAdd.size()-1; i >= 0; i--) {
GoodsType make = toAdd.get(i);
GoodsType raw = make.getRawMaterial();
if (production.containsKey(raw)) {
if (colony.getGoodsCount(raw) >= GoodsContainer.CARGO_SIZE/2) {
produce.add(raw); // Add at the end, enough in stock
produce.add(0, make);
} else {
produce.add(0, make);
produce.add(0, raw);
}
} else {
produce.add(0, make);
}
}
toAdd.clear();
// Military goods after lucrative production.
for (GoodsType g : militaryGoodsTypes) {
if (production.containsKey(g)) toAdd.add(g);
}
Collections.sort(toAdd, productionComparator);
produce.addAll(toAdd);
toAdd.clear();
// Immigration last.
if (colony.getOwner().getEurope() != null) {
for (GoodsType g : immigrationGoodsTypes) {
if (production.containsKey(g)) toAdd.add(g);
}
Collections.sort(toAdd, productionComparator);
produce.addAll(toAdd);
toAdd.clear();
}
}
/**
* Equips a unit for a role.
*
* @param unit The <code>Unit</code> to equip if possible.
* @param role The <code>Role</code> for the unit to take.
* @param colony The <code>Colony</code> that provides the equipment.
* @return True if the unit was equipped.
*/
private boolean equipUnit(Unit unit, Role role, Colony colony) {
if (role == Unit.Role.SOLDIER) role = Unit.Role.DRAGOON; // Special case
List<EquipmentType> equipment = role.getRoleEquipment(spec());
if (equipment.isEmpty() || !unit.isPerson()) return false;
boolean result = false;
for (EquipmentType et : equipment) {
if (colony.canProvideEquipment(et)
&& unit.canBeEquippedWith(et)) {
unit.setLocation(colony.getTile());
unit.changeEquipment(et, 1);
colony.addEquipmentGoods(et, -1);
result = true;
}
}
return result;
}
/**
* Tries to swap an expert unit for another doing its job.
*
* @param expert The expert <code>Unit</code>.
* @param others A list of other <code>Unit</code>s to test against.
* @return The unit that was replaced by the expert, or null if none.
*/
private Unit trySwapExpert(Unit expert, List<Unit> others) {
GoodsType work = expert.getType().getExpertProduction();
GoodsType oldWork = expert.getWorkType();
for (int i = 0; i < others.size(); i++) {
Unit other = others.get(i);
if (!other.isPerson()) continue;
if (other.getWorkType() == work
&& other.getType().getExpertProduction() != work) {
Location l1 = expert.getLocation();
Location l2 = other.getLocation();
other.setLocation(colony.getTile());
expert.setLocation(l2);
expert.setWorkType(work);
other.setLocation(l1);
if (oldWork != null) other.setWorkType(oldWork);
TypeCountMap<EquipmentType> equipment = expert.getEquipment();
for (EquipmentType e : new ArrayList<EquipmentType>(equipment.keySet())) {
int n = equipment.getCount(e);
expert.changeEquipment(e, -n);
other.changeEquipment(e, n);
}
return other;
}
}
return null;
}
/**
* Finds a plan on a list that produces a given goods type.
*
* @param goodsType The <code>GoodsType</code> to produce.
* @param plans The list of <code>WorkLocationPlan</code>s to check.
* @return The first plan found that produces the goods type, or null
* if none found.
*/
private WorkLocationPlan findPlan(GoodsType goodsType,
List<WorkLocationPlan> plans) {
for (WorkLocationPlan wlp : plans) {
if (wlp.getGoodsType() == goodsType) return wlp;
}
return null;
}
/**
* Gets the best worker to execute a work location plan.
* - The most productive one wins (which will automatically pick a
* relevant expert).
* - If they are all relevant experts, pick any.
* - Pick the unit that can upgrade to the required expert with the most
* relevant experience or least irrelevant expertise.
* - Pick a unit that can not upgrade at all.
* - Pick an otherwise upgradeable unit with the most relevant experience
* or least irrelevant experience.
* - Pick the least skillful unit.
*
* Public for the benefit of the test suite.
*
* @param wl The <code>WorkLocation</code> to work at.
* @param goodsType The <code>GoodsType</code> to make.
* @param workers A list of potential <code>Unit</code>s to try.
* @return The best worker for the job.
*/
static public Unit getBestWorker(WorkLocation wl, GoodsType goodsType,
List<Unit> workers) {
if (workers == null || workers.isEmpty()) return null;
Colony colony = wl.getColony();
// Do not mutate the workers list!
List<Unit> todo = new ArrayList<Unit>(workers);
List<Unit> best = new ArrayList<Unit>();
Unit special = null;
GoodsType outputType = (goodsType.isStoredAs())
? goodsType.getStoredAs() : goodsType;
int bestValue;
best.clear();
bestValue = colony.getAdjustedNetProductionOf(outputType);
for (Unit u : todo) {
if (!wl.canAdd(u)) continue;
Location oldLoc = u.getLocation();
GoodsType oldWork = u.getWorkType();
u.setLocation(wl);
u.setWorkType(goodsType);
int value = colony.getAdjustedNetProductionOf(outputType);
if (value > bestValue) {
bestValue = value;
best.clear();
best.add(u);
if (u.getType().getExpertProduction() == goodsType) special = u;
} else if (value == bestValue && !best.isEmpty()) {
best.add(u);
}
u.setLocation(oldLoc);
u.setWorkType(oldWork);
}
switch (best.size()) {
case 0: return null; // Not good. No unit improves production.
case 1: return best.get(0);
default:todo.clear(); todo.addAll(best); break;
}
// Several winners including an expert implies they are all experts.
if (special != null) return special;
// Partition units into those that can upgrade-by-experience
// to the relevant expert (which we favour), those that can
// upgrade-by-experience in some way but not to the expert
// (which we avoid), and the rest. Within the groups, favour
// those with the most relevant experience and the least irrelevant
// experience.
Specification spec = colony.getSpecification();
UnitType expert = spec.getExpertForProducing(goodsType);
best.clear();
bestValue = Integer.MIN_VALUE;
for (Unit u : todo) {
boolean relevant = u.getWorkType() == goodsType;
int score = (relevant) ? u.getExperience() : -u.getExperience();
if (expert != null
&& u.getType().canBeUpgraded(expert, ChangeType.EXPERIENCE)) {
score += 10000;
} else if (expert != null
&& u.getType().canBeUpgraded(null, ChangeType.EXPERIENCE)) {
score -= 10000;
}
if (score > bestValue) {
best.clear();
best.add(u);
bestValue = score;
} else if (score == bestValue) {
best.add(u);
}
}
switch (best.size()) {
case 0: break;
case 1: return best.get(0);
default:todo.clear(); todo.addAll(best); break;
}
// Use the unit with the least skill, in the hope that
// remaining experts will be called upon in due course.
int worstSkill = Integer.MAX_VALUE;
special = null;
for (Unit u : todo) {
if (u.getType().getSkill() < worstSkill) {
special = u;
worstSkill = u.getType().getSkill();
}
}
return special;
}
/**
* Tries to apply a colony plan given a list of workers.
*
* @param colonyPlan The <code>ColonyPlan</code> to apply.
* @param workers A list of <code>Unit</code>s to assign.
* @return A scratch colony with the workers in place.
*/
public Colony assignWorkers(List<Unit> workers) {
final GoodsType foodType = spec().getPrimaryFoodType();
final int maxUnitFood = colony.getOwner().getMaximumFoodConsumption();
final Turn turn = aiMain.getGame().getTurn();
// Collect the work location plans. Note that the plans are
// pre-sorted in order of desirability.
final List<GoodsType> produce = getPreferredProduction();
List<WorkLocationPlan> foodPlans = getFoodPlans();
List<WorkLocationPlan> workPlans = getWorkPlans();
// Make a scratch colony to work on.
Colony scratch = colony.getScratchColony();
Tile tile = scratch.getTile();
String report = "Worker assignment at " + colony.getName()
+ " of " + workers.size() + " workers "
+ " in " + turn + "/" + turn.getNumber() + "\n";
// Move all workers to the tile, removing storable equipment.
for (Unit u : workers) {
TypeCountMap<EquipmentType> equipment = u.getEquipment();
u.setLocation(tile);
for (EquipmentType e
: new ArrayList<EquipmentType>(equipment.keySet())) {
int n = equipment.getCount(e);
u.changeEquipment(e, -n);
scratch.addEquipmentGoods(e, n);
}
}
// Move outdoor experts outside if possible.
// Prefer scouts in early game if there are very few.
final Role outdoorRoles[] = new Role[] { Role.PIONEER,
Role.SOLDIER,
Role.SCOUT };
if (turn.getAge() <= 1) {
int nScouts = 0;
for (Unit u : colony.getOwner().getUnits()) {
if (u.getRole() == Role.SCOUT) nScouts++;
}
if (nScouts < 3) {
outdoorRoles[1] = Role.SCOUT;
outdoorRoles[2] = Role.SOLDIER;
}
}
for (int j = 0; j < outdoorRoles.length; j++) {
String ability = "model.ability.expert"
+ outdoorRoles[j].toString().substring(0, 1)
+ outdoorRoles[j].toString().substring(1).toLowerCase();
for (Unit u : new ArrayList<Unit>(workers)) {
if (workers.size() <= 1) break;
if (u.hasAbility(ability)
&& equipUnit(u, outdoorRoles[j], scratch)) {
workers.remove(u);
report += u.getId() + "("
+ u.getType().toString().substring(11)
+ ") -> " + outdoorRoles[j] + "\n";
}
}
}
// Consider the defence situation.
// TODO: scan for neighbouring hostiles
// Favour low-skill/experience units for defenders, order experts
// in reverse order of their production on the produce-list.
Comparator<Unit> soldierComparator = new Comparator<Unit>() {
public int compare(Unit u1, Unit u2) {
int cmp = u1.getSkillLevel() - u2.getSkillLevel();
if (cmp != 0) return cmp;
GoodsType g1 = u1.getType().getExpertProduction();
GoodsType g2 = u2.getType().getExpertProduction();
if (g1 != null && g2 != null) {
return produce.indexOf(g2) - produce.indexOf(g1);
}
return u1.getExperience() - u2.getExperience();
}
};
Collections.sort(workers, soldierComparator);
for (Unit u : new ArrayList<Unit>(workers)) {
if (workers.size() <= 1) break;
if (!AIColony.isBadlyDefended(scratch)) break;
if (equipUnit(u, Role.SOLDIER, scratch)) {
workers.remove(u);
report += u.getId() + "("
+ u.getType().toString().substring(11) + ") -> SOLDIER\n";
}
}
// Greedy assignment of other workers to plans.
List<AbstractGoods> buildGoods = new ArrayList<AbstractGoods>();
BuildableType build = colony.getCurrentlyBuilding();
if (build != null) buildGoods.addAll(build.getGoodsRequired());
List<WorkLocationPlan> wlps;
WorkLocationPlan wlp;
boolean done = false;
while (!workers.isEmpty() && !done) {
// Decide what to produce: set the work location plan to
// try (wlp), and the list the plan came from so it can
// be recycled if successful (wlps).
wlps = null;
wlp = null;
if (scratch.getAdjustedNetProductionOf(foodType) > 0) {
// Try to produce something.
wlps = workPlans;
while (!produce.isEmpty()) {
if ((wlp = findPlan(produce.get(0), workPlans)) != null) {
break; // Found a plan to try.
}
produce.remove(0); // Can not produce this goods type
}
}
// See if a plan can be satisfied.
Unit best;
WorkLocation wl;
GoodsType goodsType;
for (;;) {
if (wlp == null) { // Time to use a food plan.
if (foodPlans.isEmpty()) {
report += "Food plans exhausted\n";
done = true;
break;
}
wlps = foodPlans;
wlp = wlps.get(0);
}
String err = null;
goodsType = wlp.getGoodsType();
wl = wlp.getWorkLocation();
wl = scratch.getCorrespondingWorkLocation(wl);
best = null;
report += String.format("%-2d: %15s@%-25s => ",
scratch.getUnitCount(),
goodsType.toString().substring(12),
((wl instanceof Building)
? ((Building)wl).getType().toString().substring(15)
: (wl instanceof ColonyTile)
? (((ColonyTile)wl).toString().substring(11,20)
+ ((ColonyTile)wl).getWorkTile().getType().toString().substring(11))
: wl.toString()));
if (!wl.canBeWorked()) {
err = "can not be worked";
} else if (wl.isFull()) {
err = "full";
} else if ((best = getBestWorker(wl, goodsType,
workers)) == null) {
err = "no worker found";
}
if (err != null) {
wlps.remove(wlp); // The plan can not be worked, dump it.
report += err + "\n";
break;
}
// Found a suitable worker, place it.
best.setLocation(wl);
// Did the placement break the production bonus?
if (scratch.getProductionBonus() < 0) {
best.setLocation(tile);
done = true;
report += "broke production bonus\n";
break;
}
// Is the colony going to starve because of this placement?
if (scratch.getAdjustedNetProductionOf(foodType) < 0) {
int net = scratch.getAdjustedNetProductionOf(foodType);
int count = scratch.getGoodsCount(foodType);
if (count / -net < PRODUCTION_TURNOVER_TURNS) {
// Too close for comfort. Back out the
// placement and try a food plan, unless this
// was already a food plan.
best.setLocation(tile);
wlp = null;
if (goodsType.isFoodType()) {
report += "starvation (" + count
+ "/" + net + ")\n";
done = true;
break;
}
report += "would starve (" + count + "/" + net + ")\n";
continue;
}
// Otherwise tolerate the food stock running down.
// Rely on the warehouse-exhaustion code to fire
// another rearrangement before units starve.
}
// Check if placing the worker will soon exhaust the
// raw material. Do not reduce raw materials below
// what is needed for a building--- e.g. prevent
// musket production from hogging the tools.
GoodsType raw = goodsType.getRawMaterial();
int rawNeeded = 0;
for (AbstractGoods ag : buildGoods) {
if (raw == ag.getType()) rawNeeded += ag.getAmount();
}
if (raw == null
|| scratch.getAdjustedNetProductionOf(raw) >= 0
|| (((scratch.getGoodsCount(raw) - rawNeeded)
/ -scratch.getAdjustedNetProductionOf(raw))
>= PRODUCTION_TURNOVER_TURNS)) {
// No raw material problems, the placement
// succeeded. Set the work type, move the
// successful goods type to the end of the produce
// list for later reuse, remove the worker from
// the workers pool, but leave the successful plan
// on its list.
best.setWorkType(goodsType);
workers.remove(best);
report += best.getId() + "("
+ best.getType().toString().substring(11) + ")\n";
if (!goodsType.isFoodType() && produce.remove(goodsType)) {
produce.add(goodsType);
}
break;
}
// Yes, we need more of the raw material. Pull the
// unit out again and see if we can make more.
best.setLocation(tile);
WorkLocationPlan rawWlp = findPlan(raw, workPlans);
if (rawWlp != null) {
// OK, we have an alternate plan. Put the raw
// material at the start of the produce list and
// loop trying to satisfy the alternate plan.
if (produce.remove(raw)) produce.add(0, raw);
wlp = rawWlp;
report += "retry with " + raw.toString().substring(12)
+ "\n";
continue;
}
// No raw material available, so we have to give up on
// both the plan and the type of production.
// Hopefully the raw production is positive again and
// we will succeed next time.
wlps.remove(wlp);
produce.remove(goodsType);
report += "needs more " + raw.toString().substring(12) + "\n";
break;
}
}
// Put the rest of the workers on the tile.
for (Unit u : workers) {
if (u.getLocation() != tile) u.setLocation(tile);
}
// Check for failure to assign any workers. This happens when
// food is low, and perhaps partly eaten by horses, and no
// unit can *improve* production by being added. Find a place
// to produce food that at least avoids starvation and add one
// worker, thereby avoiding colony collapse.
if (scratch.getUnitCount() == 0) {
plans: for (WorkLocationPlan w : getFoodPlans()) {
GoodsType goodsType = w.getGoodsType();
WorkLocation wl = w.getWorkLocation();
for (Unit u : new ArrayList<Unit>(workers)) {
GoodsType oldWork = u.getWorkType();
u.setLocation(wl);
u.setWorkType(goodsType);
if (scratch.getAdjustedNetProductionOf(foodType) >= 0) {
report += "Subsist with " + u + "\n";
workers.remove(u);
break plans;
}
u.setLocation(tile);
u.setWorkType(oldWork);
}
}
}
// The greedy algorithm works reasonably well, but will
// misplace experts when they are more productive at the
// immediately required task than a lesser unit, not knowing
// that a requirement for their speciality will subsequently
// follow. Do a cleanup pass to sort these out.
List<Unit> experts = new ArrayList<Unit>();
List<Unit> nonExperts = new ArrayList<Unit>();
for (Unit u : scratch.getUnitList()) {
if (u.getType().getExpertProduction() != null) {
if (u.getType().getExpertProduction() != u.getWorkType()) {
experts.add(u);
}
} else {
nonExperts.add(u);
}
}
int expert = 0;
while (expert < experts.size()) {
Unit u1 = experts.get(expert);
Unit other;
if ((other = trySwapExpert(u1, experts)) != null) {
report += "Swapped " + u1.getId() + "("
+ u1.getType().toString().substring(11)
+ ") for " + other + "\n";
experts.remove(u1);
} else if ((other = trySwapExpert(u1, nonExperts)) != null) {
report += "Swapped " + u1.getId() + "("
+ u1.getType().toString().substring(11)
+ ") for " + other + "\n";
experts.remove(u1);
} else {
expert++;
}
}
for (Unit u : tile.getUnitList()) {
GoodsType work = u.getType().getExpertProduction();
if (work != null) {
Unit other = trySwapExpert(u, scratch.getUnitList());
if (other != null) {
report += "Swapped " + u.getId() + "("
+ u.getType().toString().substring(11)
+ ") for " + other + "\n";
}
}
}
// Rearm what remains as far as possible.
workers.clear();
for (Unit u : tile.getUnitList()) {
if (u.getEquipment().isEmpty()) workers.add(u);
}
Collections.sort(workers, soldierComparator);
for (Unit u : workers) {
if (equipUnit(u, Role.SOLDIER, scratch)) {
report += u.getId() + "("
+ u.getType().toString().substring(11) + ") -> SOLDIER\n";
}
}
// Log
logger.finest(report.substring(0, report.length()-1));
return scratch;
}
public String getBuildableReport() {
String ret = "Buildables:\n";
for (BuildPlan b : buildPlans) ret += b.toString() + "\n";
return ret;
}
/**
* {@inherit-doc}
*/
public String toString() {
final Tile tile = colony.getTile();
final StringBuilder sb = new StringBuilder();
sb.append("ColonyPlan: " + colony.getName()
+ " " + colony.getTile().getPosition()
+ "\nProfile: " + profileType.toString()
+ "\nPreferred production:\n");
for (GoodsType goodsType : getPreferredProduction()) {
sb.append(goodsType.toString().substring(12) + "\n");
}
sb.append(getBuildableReport());
sb.append("Food Plans:\n");
for (WorkLocationPlan wlp : getFoodPlans()) {
WorkLocation wl = wlp.getWorkLocation();
String wlStr = (wl instanceof Building)
? ((Building)wl).getType().toString().substring(15)
: (wl instanceof ColonyTile)
? tile.getDirection(((ColonyTile)wl).getWorkTile()).toString()
: wl.getId();
sb.append(wlStr
+ ": " + getWorkLocationProduction(wl, wlp.getGoodsType())
+ " " + wlp.getGoodsType().toString().substring(12)
+ "\n");
}
sb.append("Work Plans:\n");
for (WorkLocationPlan wlp : getWorkPlans()) {
WorkLocation wl = wlp.getWorkLocation();
String wlStr = (wl instanceof Building)
? ((Building)wl).getType().toString().substring(15)
: (wl instanceof ColonyTile)
? tile.getDirection(((ColonyTile)wl).getWorkTile()).toString()
: wl.getId();
sb.append(wlStr
+ ": " + getWorkLocationProduction(wl, wlp.getGoodsType())
+ " " + wlp.getGoodsType().toString().substring(12)
+ "\n");
}
return sb.toString();
}
// Serialization
/**
* Creates an XML-representation of this object.
*
* @param document The <code>Document</code> in which the
* XML-representation should be created.
* @return The XML-representation.
*/
public Element toXMLElement(Document document) {
Element element = document.createElement(getXMLElementTagName());
element.setAttribute(FreeColObject.ID_ATTRIBUTE, colony.getId());
return element;
}
/**
* Updates this object from an XML-representation of a
* <code>ColonyPlan</code>.
*
* @param element The XML-representation.
*/
public void readFromXMLElement(Element element) {
String colonyId = element.getAttribute(FreeColObject.ID_ATTRIBUTE);
colony = (Colony) getAIMain().getFreeColGameObject(colonyId);
// TODO: serialize profile?
profileType = ProfileType
.getProfileTypeFromSize(colony.getUnitCount());
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "colonyPlan"
*/
public static String getXMLElementTagName() {
return "colonyPlan";
}
}