freecol/src/net/sf/freecol/common/model/Colony.java

3269 lines
111 KiB
Java

/**
* Copyright (C) 2002-2022 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.common.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException;
import net.sf.freecol.FreeCol;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import static net.sf.freecol.common.model.Constants.*;
import net.sf.freecol.common.option.GameOptions;
import static net.sf.freecol.common.util.CollectionUtils.*;
import net.sf.freecol.common.util.LogBuilder;
import net.sf.freecol.common.util.RandomChoice;
/**
* Represents a colony. A colony contains {@link Building}s and
* {@link ColonyTile}s. The latter represents the tiles around the
* {@code Colony} where working is possible.
*/
public class Colony extends Settlement implements TradeLocation {
private static final Logger logger = Logger.getLogger(Colony.class.getName());
/** Class index for colonies. */
private static final int COLONY_CLASS_INDEX = 20;
public static final String TAG = "colony";
public static final String REARRANGE_COLONY = "rearrangeColony";
public static final int LIBERTY_PER_REBEL = 200;
public static final int CHANGE_UPPER_BOUND = 10;
/** The number of turns of advanced warning of starvation. */
public static final int FAMINE_TURNS = 3;
/** Number of colonies that a player will trade down to. */
public static final int TRADE_MARGIN = 5;
public enum ColonyChangeEvent {
POPULATION_CHANGE,
PRODUCTION_CHANGE,
BONUS_CHANGE,
WAREHOUSE_CHANGE,
BUILD_QUEUE_CHANGE,
UNIT_TYPE_CHANGE
}
/** Reasons for not building a buildable. */
public enum NoBuildReason {
NONE,
NOT_BUILDING,
NOT_BUILDABLE,
POPULATION_TOO_SMALL,
MISSING_BUILD_ABILITY,
MISSING_ABILITY,
WRONG_UPGRADE,
COASTAL,
LIMIT_EXCEEDED
}
/** Government bonuses/penalties */
public int GOVERNMENT_VERY_GOOD = 2,
GOVERNMENT_GOOD = 1,
GOVERNMENT_ORDINARY = 0,
GOVERNMENT_BAD = -1,
GOVERNMENT_VERY_BAD = -2;
/** A map of Buildings, indexed by the id of their basic type. */
protected final java.util.Map<String, Building> buildingMap = new HashMap<>();
/** A list of the ColonyTiles. */
protected final List<ColonyTile> colonyTiles = new ArrayList<>();
/** A map of ExportData, indexed by the ids of GoodsTypes. */
protected final java.util.Map<String, ExportData> exportData = new HashMap<>();
/**
* The number of liberty points. Liberty points are an
* abstract game concept. They are generated by but are not
* identical to bells, and subject to further modification.
*/
protected int liberty;
/** The SoL membership this turn (percentage). */
protected int sonsOfLiberty;
/** The SoL membership last turn. */
protected int oldSonsOfLiberty;
/** The number of tories this turn. */
protected int tories;
/** The number of tories last turn. */
protected int oldTories;
/** The current production bonus. */
protected int productionBonus;
/**
* The number of immigration points. Immigration points are an
* abstract game concept. They are generated by but are not
* identical to crosses.
*/
protected int immigration;
/** The turn in which this colony was established. */
protected Turn established = new Turn(0);
/** A list of items to be built. */
protected final BuildQueue<BuildableType> buildQueue
= new BuildQueue<>(this,
BuildQueue.CompletionAction.REMOVE_EXCEPT_LAST,
Consumer.COLONY_PRIORITY);
/** The colonists that may be born. */
protected final BuildQueue<UnitType> populationQueue
= new BuildQueue<>(this,
BuildQueue.CompletionAction.SHUFFLE,
Consumer.POPULATION_PRIORITY);
// Will only be used on enemy colonies:
protected int displayUnitCount = -1;
// Do not serialize below.
/** Contains information about production and consumption. */
private final ProductionCache productionCache = new ProductionCache(this);
/** The occupation tracing status. Do not serialize. */
private boolean traceOccupation = false;
/**
* Constructor for ServerColony.
*
* @param game The enclosing {@code Game}.
* @param owner The {@code Player} owning this {@code Colony}.
* @param name The name of the new {@code Colony}.
* @param tile The containing {@code Tile}.
*/
protected Colony(Game game, Player owner, String name, Tile tile) {
super(game, owner, name, tile);
}
/**
* Create a new {@code Colony} with the given
* identifier. The object should later be initialized by calling
* either {@link #readFromXML(FreeColXMLReader)}.
*
* @param game The enclosing {@code Game}.
* @param id The object identifier.
*/
public Colony(Game game, String id) {
super(game, id);
}
// Primitive accessors.
/**
* Get a list of every {@link Building} in this {@code Colony}.
*
* @return A list of {@code Building}s.
*/
public List<Building> getBuildings() {
synchronized (this.buildingMap) {
return new ArrayList<>(this.buildingMap.values());
}
}
/**
* Get building of the specified general type (note: *not*
* necessarily the exact building type supplied, but the building
* present in the colony that is a descendant of the ultimate
* ancestor of the specified type).
*
* @param type The type of the building to get.
* @return The {@code Building} found.
*/
public Building getBuilding(BuildingType type) {
synchronized (this.buildingMap) {
return this.buildingMap.get(type.getFirstLevel().getId());
}
}
/**
* Reset the building map.
*
* @param buildings The list of buildings to use.
*/
protected void setBuildingMap(List<Building> buildings) {
clearBuildingMap();
for (Building b : buildings) addBuilding(b);
}
/**
* Clear the building map.
*/
protected void clearBuildingMap() {
synchronized (this.buildingMap) { this.buildingMap.clear(); }
}
/**
* Gets a {@code List} of every {@link ColonyTile} in this
* {@code Colony}.
*
* @return A list of {@code ColonyTile}s.
* @see ColonyTile
*/
public List<ColonyTile> getColonyTiles() {
synchronized (this.colonyTiles) {
return new ArrayList<>(this.colonyTiles);
}
}
/**
* Set the colony tile list.
*
* @param colonyTiles The new list of {@code ColonyTile}s.
*/
protected void setColonyTiles(List<ColonyTile> colonyTiles) {
synchronized (this.colonyTiles) {
this.colonyTiles.clear();
this.colonyTiles.addAll(colonyTiles);
}
}
/**
* Clear the colony tiles.
*/
private void clearColonyTiles() {
synchronized (this.colonyTiles) { this.colonyTiles.clear(); }
}
/**
* Get the {@code ColonyTile} matching the given {@code Tile}.
*
* @param tile The {@code Tile} to check.
* @return The corresponding {@code ColonyTile}, or null if not found.
*/
public ColonyTile getColonyTile(Tile tile) {
return find(getColonyTiles(), matchKey(tile, ColonyTile::getWorkTile));
}
/**
* Get the export data.
*
* @return The list of {@code ExportData}.
*/
protected Collection<ExportData> getExportData() {
return this.exportData.values();
}
/**
* Set the export data.
*
* @param exportData The new list of {@code ExportData}.
*/
protected void setExportData(Collection<ExportData> exportData) {
this.exportData.clear();
for (ExportData ed : exportData) setExportData(ed);
}
/**
* Get the export date for a goods type.
*
* @param goodsType The {@code GoodsType} to check.
* @return The required {@code ExportData}.
*/
public ExportData getExportData(final GoodsType goodsType) {
ExportData result = this.exportData.get(goodsType.getId());
if (result == null) {
result = new ExportData(goodsType, getWarehouseCapacity());
setExportData(result);
}
return result;
}
/**
* Set some export data.
*
* @param newExportData A new {@code ExportData} value.
*/
public final void setExportData(final ExportData newExportData) {
this.exportData.put(newExportData.getId(), newExportData);
}
/**
* Get the sons-of-liberty percentage.
*
* @return The sol%.
*/
public int getSonsOfLiberty() {
return this.sonsOfLiberty;
}
protected int getOldSonsOfLiberty() {
return this.oldSonsOfLiberty;
}
protected int getToryCount() {
return this.tories;
}
protected int getOldToryCount() {
return this.oldTories;
}
/**
* Gets the production bonus of the colony.
*
* @return The current production bonus of the colony.
*/
public int getProductionBonus() {
return this.productionBonus;
}
/**
* Sets the production bonus of the colony.
*
* Only public for the convenience of the test suite.
*
* @param productionBonus The new production bonus of the colony.
*/
public void setProductionBonus(int productionBonus) {
this.productionBonus = productionBonus;
}
/**
* Modify the immigration points by amount given.
*
* @param amount An amount of immigration.
*/
public void modifyImmigration(int amount) {
this.immigration += amount;
}
/**
* Get the turn this colony was established.
*
* @return The establishment {@code Turn}.
*/
public Turn getEstablished() {
return this.established;
}
/**
* Set the turn of establishment.
*
* @param newEstablished The new {@code Turn} of establishment.
*/
public void setEstablished(final Turn newEstablished) {
this.established = newEstablished;
}
/**
* Get the build queue contents.
*
* @return A list of {@code Buildable}s.
*/
public List<BuildableType> getBuildQueue() {
return this.buildQueue.getValues();
}
/**
* Set the build queue.
*
* @param buildQueue A list of new values for the build queue.
*/
public void setBuildQueue(final List<BuildableType> buildQueue) {
this.buildQueue.setValues(buildQueue);
}
/**
* Get the population queue contents.
*
* @return A list of {@code Buildable}s.
*/
public List<UnitType> getPopulationQueue() {
return this.populationQueue.getValues();
}
/**
* Set the population queue.
*
* @param populationQueue A list of new values for the population queue.
*/
public void setPopulationQueue(final List<UnitType> populationQueue) {
this.populationQueue.setValues(populationQueue);
}
/**
* Get the display unit count.
*
* @return The explicit unit count for display purposes.
*/
public int getDisplayUnitCount() {
return this.displayUnitCount;
}
/**
* Sets the apparent number of units at this colony.
* Used in client enemy colonies
*
* @param count The new apparent number of {@code Unit}s at
* this colony.
*/
public void setDisplayUnitCount(int count) {
this.displayUnitCount = count;
}
// Occupation routines
/**
* Gets the occupation tracing status.
*
* @return The occupation tracing status.
*/
public boolean getOccupationTrace() {
return this.traceOccupation;
}
/**
* Sets the occupation tracing status.
*
* @param trace The new occupation tracing status.
* @return The original occupation tracing status.
*/
public boolean setOccupationTrace(boolean trace) {
boolean ret = this.traceOccupation;
this.traceOccupation = trace;
return ret;
}
private void accumulateChoices(Collection<GoodsType> workTypes,
Collection<GoodsType> tried,
List<Collection<GoodsType>> result) {
workTypes.removeAll(tried);
if (!workTypes.isEmpty()) {
result.add(workTypes);
tried.addAll(workTypes);
}
}
private void accumulateChoice(GoodsType workType,
Collection<GoodsType> tried,
List<Collection<GoodsType>> result) {
if (workType == null) return;
accumulateChoices(workType.getEquivalentTypes(), tried, result);
}
/**
* Get a list of collections of goods types, in order of priority
* to try to produce in this colony by a given unit.
*
* @param unit The {@code Unit} to check.
* @param userMode If a user requested this, favour the current
* work type, if not favour goods that the unit requires.
* @return The list of collections of {@code GoodsType}s.
*/
public List<Collection<GoodsType>> getWorkTypeChoices(Unit unit,
boolean userMode) {
final Specification spec = getSpecification();
List<Collection<GoodsType>> result = new ArrayList<>();
Set<GoodsType> tried = new HashSet<>();
// Find the food and non-food goods types required by this unit
// and which are underproduced at present.
Set<GoodsType> food = new HashSet<>();
Set<GoodsType> nonFood = new HashSet<>();
for (AbstractGoods ag : transform(unit.getType().getConsumedGoods(),
g -> productionCache.getNetProductionOf(g.getType())
< g.getAmount())) {
if (ag.isFoodType()) {
food.addAll(ag.getType().getEquivalentTypes());
} else {
nonFood.addAll(ag.getType().getEquivalentTypes());
}
}
if (userMode) { // Favour current and expert types in user mode
accumulateChoice(unit.getWorkType(), tried, result);
accumulateChoice(unit.getType().getExpertProduction(), tried, result);
accumulateChoice(unit.getExperienceType(), tried, result);
accumulateChoices(food, tried, result);
accumulateChoices(nonFood, tried, result);
} else { // Otherwise favour the required goods types
accumulateChoices(food, tried, result);
accumulateChoices(nonFood, tried, result);
accumulateChoice(unit.getWorkType(), tried, result);
accumulateChoice(unit.getType().getExpertProduction(), tried, result);
accumulateChoice(unit.getExperienceType(), tried, result);
}
accumulateChoices(spec.getFoodGoodsTypeList(), tried, result);
accumulateChoices(spec.getNewWorldLuxuryGoodsTypeList(), tried, result);
accumulateChoices(spec.getGoodsTypeList(), tried, result);
return result;
}
/**
* Gets the best occupation for a given unit to produce one of
* a given set of goods types.
*
* @param unit The {@code Unit} to find an
* {@code Occupation} for.
* @param workTypes A collection of {@code GoodsType} to
* consider producing.
* @param lb A {@code LogBuilder} to log to.
* @return An {@code Occupation} for the given unit, or null
* if none found.
*/
private Occupation getOccupationFor(Unit unit,
Collection<GoodsType> workTypes,
LogBuilder lb) {
if (workTypes.isEmpty()) return null;
Occupation best = new Occupation(null, null, null);
int bestAmount = 0;
for (WorkLocation wl : getCurrentWorkLocationsList()) {
bestAmount = best.improve(unit, wl, bestAmount, workTypes, lb);
}
if (best.workLocation != null) {
lb.add("\n => ", best, " = ", bestAmount);
}
return (best.workLocation == null) ? null : best;
}
/**
* Gets the best occupation for a given unit.
*
* @param unit The {@code Unit} to find an
* {@code Occupation} for.
* @param userMode If a user requested this, favour the current
* work type, if not favour goods that the unit requires.
* @param lb A {@code LogBuilder} to log to.
* @return An {@code Occupation} for the given unit, or
* null if none found.
*/
private Occupation getOccupationFor(Unit unit, boolean userMode,
LogBuilder lb) {
for (Collection<GoodsType> types : getWorkTypeChoices(unit, userMode)) {
lb.add("\n ");
FreeColObject.logFreeColObjects(types, lb);
Occupation occupation = getOccupationFor(unit, types, lb);
if (occupation != null) return occupation;
}
lb.add("\n => FAILED");
return null;
}
/**
* Gets the best occupation for a given unit to produce one of
* a given set of goods types.
*
* @param unit The {@code Unit} to find an
* {@code Occupation} for.
* @param workTypes A collection of {@code GoodsType} to
* consider producing.
* @return An {@code Occupation} for the given unit, or null
* if none found.
*/
private Occupation getOccupationFor(Unit unit,
Collection<GoodsType> workTypes) {
LogBuilder lb = new LogBuilder((getOccupationTrace()) ? 64 : 0);
lb.add(getName(), ".getOccupationFor(", unit, ", ");
FreeColObject.logFreeColObjects(workTypes, lb);
lb.add(")");
Occupation occupation = getOccupationFor(unit, workTypes, lb);
lb.log(logger, Level.WARNING);
return occupation;
}
/**
* Gets the best occupation for a given unit.
*
* @param unit The {@code Unit} to find an
* {@code Occupation} for.
* @param userMode If a user requested this, favour the current
* work type, if not favour goods that the unit requires.
* @return An {@code Occupation} for the given unit, or
* null if none found.
*/
private Occupation getOccupationFor(Unit unit, boolean userMode) {
LogBuilder lb = new LogBuilder((getOccupationTrace()) ? 64 : 0);
lb.add(getName(), ".getOccupationFor(", unit, ")");
Occupation occupation = getOccupationFor(unit, userMode, lb);
lb.log(logger, Level.WARNING);
return occupation;
}
// WorkLocations, Buildings, ColonyTiles
/**
* Gets a list of every work location in this colony.
*
* @return The list of work locations.
*/
public List<WorkLocation> getAllWorkLocationsList() {
List<WorkLocation> ret = new ArrayList<>();
synchronized (this.colonyTiles) {
ret.addAll(this.colonyTiles);
}
synchronized (this.buildingMap) {
ret.addAll(this.buildingMap.values());
}
return ret;
}
/**
* Gets a stream of every work location in this colony.
*
* @return The stream of work locations.
*/
public Stream<WorkLocation> getAllWorkLocations() {
Stream<WorkLocation> ret = Stream.<WorkLocation>empty();
synchronized (this.colonyTiles) {
ret = concat(ret, map(this.colonyTiles,
Function.<WorkLocation>identity()));
}
synchronized (this.buildingMap) {
ret = concat(ret, map(this.buildingMap.values(),
Function.<WorkLocation>identity()));
}
return ret;
}
/**
* Gets a list of all freely available work locations
* in this colony.
*
* @return The list of available {@code WorkLocation}s.
*/
public List<WorkLocation> getAvailableWorkLocationsList() {
return transform(getAllWorkLocations(), WorkLocation::isAvailable);
}
/**
* Get a stream of all freely available work locations in this
* colony.
*
* @return The stream of available {@code WorkLocation}s.
*/
public Stream<WorkLocation> getAvailableWorkLocations() {
return getAvailableWorkLocationsList().stream();
}
/**
* Gets a list of all current work locations in this colony.
*
* @return The list of current {@code WorkLocation}s.
*/
public List<WorkLocation> getCurrentWorkLocationsList() {
return transform(getAllWorkLocations(), WorkLocation::isCurrent);
}
/**
* Get a stream of all current work locations in this colony.
*
* @return The stream of current {@code WorkLocation}s.
*/
public Stream<WorkLocation> getCurrentWorkLocations() {
return getCurrentWorkLocationsList().stream();
}
/**
* Add a Building to this Colony.
*
* Lower level routine, do not use directly in-game (use buildBuilding).
* Used for serialization and public for the test suite.
*
* -til: Could change the tile appearance if the building is
* stockade-type
*
* @param building The {@code Building} to build.
* @return True if the building was added.
*/
public boolean addBuilding(final Building building) {
if (building == null || building.getType() == null) return false;
final BuildingType buildingType = building.getType().getFirstLevel();
if (buildingType == null || buildingType.getId() == null) return false;
synchronized (buildingMap) {
buildingMap.put(buildingType.getId(), building);
}
addFeatures(building.getType());
return true;
}
/**
* Remove a building from this Colony.
*
* -til: Could change the tile appearance if the building is
* stockade-type
*
* @param building The {@code Building} to remove.
* @return True if the building was removed.
*/
protected boolean removeBuilding(final Building building) {
final BuildingType buildingType = building.getType().getFirstLevel();
synchronized (buildingMap) {
if (buildingMap.remove(buildingType.getId()) == null) return false;
}
removeFeatures(building.getType());
return true;
}
/**
* Add a colony tile.
*
* @param ct The {@code ColonyTile} to add.
*/
private void addColonyTile(ColonyTile ct) {
if (ct == null) return;
synchronized (colonyTiles) {
colonyTiles.add(ct);
}
}
/**
* Gets a work location with a given ability.
*
* @param ability An ability key.
* @return A {@code WorkLocation} with the required
* {@code Ability}, or null if not found.
*/
public WorkLocation getWorkLocationWithAbility(String ability) {
return find(getCurrentWorkLocations(), wl -> wl.hasAbility(ability));
}
/**
* Gets a work location of a specific class with a given ability.
*
* @param <T> The actual return type.
* @param ability An ability key.
* @param returnClass The expected subclass.
* @return A {@code WorkLocation} with the required
* {@code Ability}, or null if not found.
*/
public <T extends WorkLocation> T getWorkLocationWithAbility(String ability,
Class<T> returnClass) {
WorkLocation wl = getWorkLocationWithAbility(ability);
try {
if (wl != null) return returnClass.cast(wl);
} catch (ClassCastException cce) {};
return null;
}
/**
* Gets a work location with a given modifier.
*
* @param modifier A modifier key.
* @return A {@code WorkLocation} with the required
* {@code Modifier}, or null if not found.
*/
public WorkLocation getWorkLocationWithModifier(String modifier) {
return find(getCurrentWorkLocations(), wl -> wl.hasModifier(modifier));
}
/**
* Gets a work location of a specific class with a given modifier.
*
* @param <T> The actual return type.
* @param modifier A modifier key.
* @param returnClass The expected subclass.
* @return A {@code WorkLocation} with the required
* {@code Modifier}, or null if not found.
*/
public <T extends WorkLocation> T getWorkLocationWithModifier(String modifier,
Class<T> returnClass) {
WorkLocation wl = getWorkLocationWithModifier(modifier);
if (wl != null) try { return returnClass.cast(wl); } catch (ClassCastException cce) {}
return null;
}
/**
* Collect the work locations for consuming a given type of goods.
*
* @param goodsType The {@code GoodsType} to consume.
* @return A list of {@code WorkLocation}s which consume
* the given type of goods.
*/
public List<WorkLocation> getWorkLocationsForConsuming(GoodsType goodsType) {
return transform(getCurrentWorkLocations(),
wl -> any(wl.getInputs(), AbstractGoods.matches(goodsType)));
}
/**
* Collect the work locations for producing a given type of goods.
*
* @param goodsType The {@code GoodsType} to produce.
* @return A list of {@code WorkLocation}s which produce
* the given type of goods.
*/
public List<WorkLocation> getWorkLocationsForProducing(GoodsType goodsType) {
return transform(getCurrentWorkLocations(),
wl -> any(wl.getOutputs(), AbstractGoods.matches(goodsType)));
}
/**
* Find a work location for producing a given type of goods.
* Beware that this may not be the optimal location for the
* production, for which {@link #getWorkLocationFor} is better.
*
* @param goodsType The {@code GoodsType} to produce.
* @return A {@code WorkLocation}s which produces
* the given type of goods, or null if not found.
*/
public WorkLocation getWorkLocationForProducing(GoodsType goodsType) {
return first(getWorkLocationsForProducing(goodsType));
}
/**
* Gets the work location best suited for the given unit to
* produce a type of goods.
*
* @param unit The {@code Unit} to get the building for.
* @param goodsType The {@code GoodsType} to produce.
* @return The best {@code WorkLocation} found.
*/
public WorkLocation getWorkLocationFor(Unit unit, GoodsType goodsType) {
if (goodsType == null) return getWorkLocationFor(unit);
Occupation occupation
= getOccupationFor(unit, goodsType.getEquivalentTypes());
return (occupation == null) ? null : occupation.workLocation;
}
/**
* Gets the work location best suited for the given unit.
*
* @param unit The {@code Unit} to check for.
* @return The best {@code WorkLocation} found.
*/
public WorkLocation getWorkLocationFor(Unit unit) {
Occupation occupation = getOccupationFor(unit, false);
return (occupation == null) ? null : occupation.workLocation;
}
/**
* Is a tile actually in use by this colony?
*
* @param tile The {@code Tile} to test.
* @return True if this tile is actively in use by this colony.
*/
public boolean isTileInUse(Tile tile) {
ColonyTile colonyTile = getColonyTile(tile);
return colonyTile != null && !colonyTile.isEmpty();
}
/**
* Get the warehouse-type building in this colony.
*
* @return The warehouse {@code Building}.
*/
public Building getWarehouse() {
return getWorkLocationWithModifier(Modifier.WAREHOUSE_STORAGE,
Building.class);
}
/**
* Does this colony have a stockade?
*
* @return True if the colony has a stockade.
*/
public boolean hasStockade() {
return getStockade() != null;
}
/**
* Gets the stockade building in this colony.
*
* @return The stockade {@code Building}.
*/
public Building getStockade() {
return getWorkLocationWithModifier(Modifier.DEFENCE, Building.class);
}
/**
* Gets the stockade key, as should be visible to the owner
* or a player that can see this colony.
*
* @return The stockade key, or null if no stockade-building is present.
*/
public String getStockadeKey() {
Building stockade = getStockade();
return (stockade == null) ? null : stockade.getType().getSuffix();
}
/**
* Get a weighted list of natural disasters than can strike this
* colony. This list comprises all natural disasters that can
* strike the colony's tiles.
*
* @return A stream of {@code Disaster}s.
*/
public Stream<RandomChoice<Disaster>> getDisasterChoices() {
return flatten(getColonyTiles(),
ct -> ct.getWorkTile().getDisasterChoices());
}
// What are we building? What can we build?
/**
* Is a building type able to be automatically built at no cost.
* True when the player has a modifier that collapses the cost to zero.
*
* @param buildingType a {@code BuildingType} value
* @return True if the building is available at zero cost.
*/
public boolean isAutomaticBuild(BuildingType buildingType) {
float value = owner.apply(100f, getGame().getTurn(),
Modifier.BUILDING_PRICE_BONUS, buildingType);
return value == 0f && canBuild(buildingType);
}
/**
* Gets a list of every unit type this colony may build.
*
* @return A list of buildable {@code UnitType}s.
*/
public List<UnitType> getBuildableUnits() {
return transform(getSpecification().getUnitTypeList(),
ut -> ut.needsGoodsToBuild() && canBuild(ut));
}
/**
* Returns how many turns it would take to build the given
* {@code BuildableType}.
*
* @param buildable The {@code BuildableType} to build.
* @return The number of turns to build the buildable, negative if
* some goods are not being built, UNDEFINED if none is.
*/
public int getTurnsToComplete(BuildableType buildable) {
return getTurnsToComplete(buildable, null);
}
/**
* Returns how many turns it would take to build the given
* {@code BuildableType}.
*
* @param buildable The {@code BuildableType} to build.
* @param needed The {@code AbstractGoods} needed to continue
* the build.
* @return The number of turns to build the buildable (which may
* be zero, UNDEFINED if no useful work is being done, negative
* if some requirement is or will block completion (value is
* the negation of (turns-to-blockage + 1), and if the needed
* argument is supplied it is set to the goods deficit).
*/
public int getTurnsToComplete(BuildableType buildable,
AbstractGoods needed) {
final List<AbstractGoods> required = buildable.getRequiredGoodsList();
int turns = 0, satisfied = 0, failing = 0, underway = 0;
ProductionInfo info = productionCache.getProductionInfo(buildQueue);
for (AbstractGoods ag : required) {
final GoodsType type = ag.getType();
final int amountNeeded = ag.getAmount();
final int amountAvailable = getGoodsCount(type);
if (amountAvailable >= amountNeeded) {
satisfied++;
continue;
}
int production = productionCache.getNetProductionOf(type);
if (info != null) {
AbstractGoods consumption = find(info.getConsumption(),
AbstractGoods.matches(type));
if (consumption != null) {
// add the amount the build queue itself will consume
production += consumption.getAmount();
}
}
if (production <= 0) {
failing++;
if (needed != null) {
needed.setType(type);
needed.setAmount(amountNeeded - amountAvailable);
}
continue;
}
underway++;
int amountRemaining = amountNeeded - amountAvailable;
int eta = amountRemaining / production;
if (amountRemaining % production != 0) eta++;
turns = Math.max(turns, eta);
}
return (satisfied + underway == required.size()) ? turns // Will finish
: (failing == required.size()) ? UNDEFINED // Not even trying
: -(turns + 1); // Blocked by something
}
/**
* Returns {@code true} if this Colony can breed the given
* type of Goods. Only animals (such as horses) are expected to be
* breedable.
*
* @param goodsType a {@code GoodsType} value
* @return a {@code boolean} value
*/
public boolean canBreed(GoodsType goodsType) {
int breedingNumber = goodsType.getBreedingNumber();
return (breedingNumber < INFINITY
&& breedingNumber <= getGoodsCount(goodsType));
}
/**
* Gets the type of building currently being built.
*
* @return The type of building currently being built.
*/
public BuildableType getCurrentlyBuilding() {
return buildQueue.getCurrentlyBuilding();
}
/**
* Sets the current type of buildable to be built and if it is a building
* insist that there is only one in the queue.
*
* @param buildable The {@code BuildableType} to build.
*/
public void setCurrentlyBuilding(BuildableType buildable) {
buildQueue.setCurrentlyBuilding(buildable);
}
public boolean canBuild() {
return canBuild(getCurrentlyBuilding());
}
/**
* Returns true if this Colony can build the given BuildableType.
*
* @param buildableType a {@code BuildableType} value
* @return a {@code boolean} value
*/
public boolean canBuild(BuildableType buildableType) {
return getNoBuildReason(buildableType, null) == NoBuildReason.NONE;
}
/**
* Return the reason why the give {@code BuildableType} can
* not be built.
*
* @param buildableType A {@code BuildableType} to build.
* @param assumeBuilt An optional list of other buildable types
* which can be assumed to be built, for the benefit of build
* queue checks.
* @return A {@code NoBuildReason} value decribing the failure,
* including {@code NoBuildReason.NONE} on success.
*/
public NoBuildReason getNoBuildReason(BuildableType buildableType,
List<BuildableType> assumeBuilt) {
if (buildableType == null) {
return NoBuildReason.NOT_BUILDING;
} else if (!buildableType.needsGoodsToBuild()) {
return NoBuildReason.NOT_BUILDABLE;
} else if (buildableType.getRequiredPopulation() > getUnitCount()) {
return NoBuildReason.POPULATION_TOO_SMALL;
} else if (buildableType.hasAbility(Ability.COASTAL_ONLY)
&& !getTile().isCoastland()) {
return NoBuildReason.COASTAL;
} else {
if (any(buildableType.getRequiredAbilities().entrySet(),
e -> e.getValue() != hasAbility(e.getKey()))) {
return NoBuildReason.MISSING_ABILITY;
}
if (!all(buildableType.getLimits(), l -> l.evaluate(this))) {
return NoBuildReason.LIMIT_EXCEEDED;
}
}
if (assumeBuilt == null) {
assumeBuilt = Collections.<BuildableType>emptyList();
}
return buildableType.canBeBuiltInColony(this.getColony(), assumeBuilt);
}
/**
* Returns the price for the remaining hammers and tools for the
* {@link Building} that is currently being built.
*
* @return The price.
* @see net.sf.freecol.client.control.InGameController#payForBuilding
*/
public int getPriceForBuilding() {
return getPriceForBuilding(getCurrentlyBuilding());
}
/**
* Gets the price for the remaining resources to build a given buildable.
*
* @param type The {@code BuildableType} to build.
* @return The price.
* @see net.sf.freecol.client.control.InGameController#payForBuilding
*/
public int getPriceForBuilding(BuildableType type) {
return priceGoodsForBuilding(getRequiredGoods(type));
}
/**
* Gets a price for a map of resources to build a given buildable.
*
* @param required A list of required {@code AbstractGoods}.
* @return The price.
* @see net.sf.freecol.client.control.InGameController#payForBuilding
*/
public int priceGoodsForBuilding(List<AbstractGoods> required) {
final Market market = getOwner().getMarket();
// FIXME: magic number!
return sum(required,
ag -> (ag.getType().isStorable())
? (market.getBidPrice(ag.getType(), ag.getAmount()) * 110)/100
: ag.getType().getPrice() * ag.getAmount());
}
/**
* Gets a map of the types of goods and amount thereof required to
* finish a buildable in this colony.
*
* @param type The {@code BuildableType} to build.
* @return The map to completion.
*/
public List<AbstractGoods> getRequiredGoods(BuildableType type) {
return transform(type.getRequiredGoods(),
ag -> ag.getAmount() > getGoodsCount(ag.getType()),
ag -> new AbstractGoods(ag.getType(),
ag.getAmount() - getGoodsCount(ag.getType())));
}
/**
* Gets all 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 AI planning and the test suite.
*
* @param buildable The {@code BuildableType} to consider.
* @return A list of required abstract goods.
*/
public List<AbstractGoods> getFullRequiredGoods(BuildableType buildable) {
if (buildable == null) return Collections.<AbstractGoods>emptyList();
List<AbstractGoods> required = new ArrayList<>();
for (AbstractGoods ag : buildable.getRequiredGoodsList()) {
int amount = ag.getAmount();
GoodsType type = ag.getType();
while (type != null) {
if (amount <= this.getGoodsCount(type)) break; // Shortcut
required.add(0, new AbstractGoods(type,
amount - this.getGoodsCount(type)));
type = type.getInputType();
}
}
return required;
}
/**
* Check if the owner can buy the remaining hammers and tools for
* the {@link Building} that is currently being built.
*
* @return True if the user can afford to pay.
* @exception IllegalStateException If the owner of this
* {@code Colony} has an insufficient amount of gold.
* @see #getPriceForBuilding
*/
public boolean canPayToFinishBuilding() {
return canPayToFinishBuilding(getCurrentlyBuilding());
}
/**
* Check if the owner can buy the remaining hammers and tools for
* the {@link Building} given.
*
* @param buildableType a {@code BuildableType} value
* @return True if the user can afford to pay.
* @exception IllegalStateException If the owner of this
* {@code Colony} has an insufficient amount of gold.
* @see #getPriceForBuilding
*/
public boolean canPayToFinishBuilding(BuildableType buildableType) {
return buildableType != null
&& getOwner().checkGold(getPriceForBuilding(buildableType));
}
// Liberty and the consequences
/**
* Adds to the liberty points by increasing the liberty goods present.
* Used only by DebugMenu.
*
* @param amount The number of liberty to add.
*/
public void addLiberty(int amount) {
List<GoodsType> libertyTypeList = getSpecification()
.getLibertyGoodsTypeList();
final int uc = getUnitCount();
if (amount > 0 && !libertyTypeList.isEmpty()) {
addGoods(libertyTypeList.get(0), amount);
}
updateSoL();
updateProductionBonus();
}
/**
* Modify the liberty points by amount given.
*
* @param amount An amount of liberty.
*/
public void modifyLiberty(int amount) {
// Produced liberty always applies to the player (for FFs etc)
getOwner().modifyLiberty(amount);
liberty += amount;
// Liberty can not meaningfully go negative.
liberty = Math.max(0, liberty);
updateSoL();
updateProductionBonus();
// If the bell accumulation cap option is set, and the colony
// has reached 100%, liberty can not rise higher.
boolean capped = getSpecification()
.getBoolean(GameOptions.BELL_ACCUMULATION_CAPPED);
if (capped && sonsOfLiberty >= 100) {
liberty = LIBERTY_PER_REBEL * getUnitCount();
}
}
/**
* Calculates the current SoL membership of the colony based on
* the liberty value and colonists.
*/
public void updateSoL() {
int uc = getUnitCount();
this.oldSonsOfLiberty = this.sonsOfLiberty;
this.oldTories = this.tories;
this.sonsOfLiberty = calculateSoLPercentage(uc, getLiberty());
this.tories = calculateToryCount(uc, this.sonsOfLiberty);
}
/**
* Calculate the SoL membership percentage of this colony based on
* a given number of colonists and liberty.
*
* @param uc The proposed number of units in the colony.
* @param liberty The proposed amount of liberty.
* @return The percentage of SoLs, negative if not calculable.
*/
private int calculateSoLPercentage(int uc, int liberty) {
if (uc <= 0) return -1;
float membership = (liberty * 100.0f) / (LIBERTY_PER_REBEL * uc);
membership = applyModifiers(membership, getGame().getTurn(),
getOwner().getModifiers(Modifier.SOL));
if (membership < 0.0f) {
membership = 0.0f;
} else if (membership > 100.0f) {
membership = 100.0f;
}
return (int)membership;
}
/**
* Calculate the number of rebels in a colony given a unit count
* and sons-of-liberty percentage.
*
* @param uc The number of units in the colony.
* @param solPercent The percentage of sons-of-liberty.
* @return The number of rebels.
*/
public static int calculateRebelCount(int uc, int solPercent) {
return (int)Math.floor(0.01 * solPercent * uc);
}
/**
* Calculate the number of tories in a colony given a
* sons-of-liberty percentage and unit count.
*
* @param uc The number of units in the colony.
* @param solPercent The percentage of sons-of-liberty.
* @return The number of tories.
*/
public static int calculateToryCount(int uc, int solPercent) {
return uc - calculateRebelCount(uc, solPercent);
}
/**
* Calculate the colony production bonus for a given
* sons-of-liberty percentage.
*
* @param solPercent The sons-of-liberty percentage to use.
* @return The production bonus.
*/
private int calculateProductionBonus(int solPercent) {
final Specification spec = getSpecification();
final int veryBadGovernment
= spec.getInteger(GameOptions.VERY_BAD_GOVERNMENT_LIMIT);
final int badGovernment
= spec.getInteger(GameOptions.BAD_GOVERNMENT_LIMIT);
final int veryGoodGovernment
= spec.getInteger(GameOptions.VERY_GOOD_GOVERNMENT_LIMIT);
final int goodGovernment
= spec.getInteger(GameOptions.GOOD_GOVERNMENT_LIMIT);
if (solPercent >= veryGoodGovernment) return GOVERNMENT_VERY_GOOD;
if (solPercent >= goodGovernment) return GOVERNMENT_GOOD;
int tc = calculateToryCount(getUnitCount(), solPercent);
return (tc > veryBadGovernment) ? GOVERNMENT_VERY_BAD
: (tc > badGovernment) ? GOVERNMENT_BAD
: GOVERNMENT_ORDINARY;
}
/**
* Update the colony's production bonus.
*
* @return True if the bonus changed.
*/
protected boolean updateProductionBonus() {
int newBonus = calculateProductionBonus(sonsOfLiberty);
if (productionBonus != newBonus) {
invalidateCache();
setProductionBonus(newBonus);
return true;
}
return false;
}
/**
* Gets the number of units that would be good to add/remove from this
* colony. That is the number of extra units that can be added without
* damaging the production bonus, or the number of units to remove to
* improve it.
*
* @return The number of units to add to the colony, or if negative
* the negation of the number of units to remove.
*/
public int getPreferredSizeChange() {
return productionBonus < 0 ? -getUnitsToRemove() : getUnitsToAdd();
}
public int getUnitsToAdd() {
int pop = getUnitCount();
for (int i = 1; i <= CHANGE_UPPER_BOUND; i++) {
if (governmentChange(pop + i) == -1) {
return i - 1;
}
}
return CHANGE_UPPER_BOUND;
}
public int getUnitsToRemove() {
int pop = getUnitCount();
for (int i = 1; i < pop; i++) {
if (governmentChange(pop - i) == 1) {
return i;
}
}
return 0;
}
/**
* The RebelToolTip shows many things, but the number of turns to
* the next bonus point needs to account for the bonuses in
* calculateSoLPercentage, which is an awkward calculation to do
* in reverse. Given the tooltip already calculates the
* libertyProduction, we use that and count forward to the turns
* it takes to reach the next bonus change, the good government
* mark, and the very good government mark.
*
* @param libertyProduction The projected colony liberty production.
* @return A list of number of turns to next,good,very good bonus marks.
*/
public List<Integer> rebelHelper(int libertyProduction) {
List<Integer> ret = new ArrayList<>();
ret.add(-1);
ret.add(-1);
ret.add(-1);
if (libertyProduction <= 0) return ret;
final int uc = getUnitCount();
int liberty = getLiberty();
int soLPercent = calculateSoLPercentage(uc, liberty);
int bonus0 = calculateProductionBonus(soLPercent);
int n = 0, bonus = bonus0;
for (;;) {
if (bonus != bonus0 && ret.get(0) < 0) {
ret.set(0, n);
}
if (bonus == GOVERNMENT_GOOD && ret.get(1) < 0) {
ret.set(1, n);
}
if (bonus == GOVERNMENT_VERY_GOOD && ret.get(2) < 0) {
ret.set(2, n);
break;
}
liberty += libertyProduction;
soLPercent = calculateSoLPercentage(uc, liberty);
bonus = calculateProductionBonus(soLPercent);
n++;
}
return ret;
}
// Unit manipulation and population
/**
* Special routine to handle non-specific add of unit to colony.
*
* @param unit The {@code Unit} to add.
* @return True if the add succeeds.
*/
public boolean joinColony(Unit unit) {
boolean ret;
Occupation occupation = getOccupationFor(unit, false);
if (occupation == null) {
if (!traceOccupation) {
LogBuilder lb = new LogBuilder(64);
getOccupationFor(unit, false, lb);
lb.log(logger, Level.WARNING);
}
ret = false;
} else {
ret = occupation.install(unit);
}
if (!ret) {
unit.setLocation(getTile()); // Fall back to safe value
logger.warning("Failed to join " + getName() + ": " + unit);
}
return ret;
}
/**
* Can this colony reduce its population voluntarily?
*
* This is generally the case, but can be prevented by buildings
* such as the stockade in classic mode.
*
* @return True if the population can be reduced.
*/
public boolean canReducePopulation() {
return getUnitCount() > apply(0f, getGame().getTurn(),
Modifier.MINIMUM_COLONY_SIZE);
}
/**
* Gets the message to display if the colony can not reduce its
* population.
*
* @return A {@code StringTemplate} describing why a colony
* can not reduce its population, or null if it can.
*/
public StringTemplate getReducePopulationMessage() {
if (canReducePopulation()) return null;
Modifier min = first(getModifiers(Modifier.MINIMUM_COLONY_SIZE));
if (min == null) return null;
FreeColObject source = min.getSource();
if (source instanceof BuildingType) {
// If the modifier source is a building type, use the
// building in the colony, which may be of a different
// level to the modifier source.
// This prevents the stockade modifier from matching a
// colony-fort, and thus the message attributing the
// failure to reduce population to a non-existing
// stockade, BR#3522055.
source = getBuilding((BuildingType)source).getType();
}
return StringTemplate.template("model.colony.minimumColonySize")
.addName("%object%", source);
}
/**
* Gets the message to display if a colony can not build something.
*
* @param buildable The {@code BuildableType} that can not be built.
* @return A {@code ModelMessage} describing the build failure.
*/
public ModelMessage getUnbuildableMessage(BuildableType buildable) {
return new ModelMessage(ModelMessage.MessageType.WARNING,
"model.colony.unbuildable", this, buildable)
.addName("%colony%", getName())
.addNamed("%object%", buildable);
}
/**
* Returns 1, 0, or -1 to indicate that government would improve,
* remain the same, or deteriorate if the colony had the given
* population.
*
* @param unitCount The proposed population for the colony.
* @return 1, 0 or -1.
*/
public int governmentChange(int unitCount) {
final Specification spec = getSpecification();
final int veryBadGovernment
= spec.getInteger(GameOptions.VERY_BAD_GOVERNMENT_LIMIT);
final int badGovernment
= spec.getInteger(GameOptions.BAD_GOVERNMENT_LIMIT);
final int veryGoodGovernment
= spec.getInteger(GameOptions.VERY_GOOD_GOVERNMENT_LIMIT);
final int goodGovernment
= spec.getInteger(GameOptions.GOOD_GOVERNMENT_LIMIT);
int newSoLPercent = calculateSoLPercentage(unitCount, getLiberty());
int newToryCount = calculateToryCount(unitCount, newSoLPercent);
int oldSoLPercent = getSonsOfLiberty();
int oldToryCount = getToryCount();
int result = 0;
if (newSoLPercent >= veryGoodGovernment) { // There are no tories left.
if (oldSoLPercent < veryGoodGovernment) {
result = 1;
}
} else if (newSoLPercent >= goodGovernment) {
if (oldSoLPercent >= veryGoodGovernment) {
result = -1;
} else if (oldSoLPercent < goodGovernment) {
result = 1;
}
} else {
if (oldSoLPercent >= goodGovernment) {
result = -1;
} else { // Now that no bonus is applied, penalties may.
if (newToryCount > veryBadGovernment) {
if (oldToryCount <= veryBadGovernment) {
result = -1;
}
} else if (newToryCount > badGovernment) {
if (oldToryCount <= badGovernment) {
result = -1;
} else if (oldToryCount > veryBadGovernment) {
result = 1;
}
} else {
if (oldToryCount > badGovernment) {
result = 1;
}
}
}
}
return result;
}
/**
* Has the government bonus changed? If so, return a suitable message.
*
* @return A {@code ModelMessage} describing the change, or null if none.
*/
public ModelMessage checkForGovMgtChangeMessage() {
final Specification spec = getSpecification();
final int veryBadGovernment
= spec.getInteger(GameOptions.VERY_BAD_GOVERNMENT_LIMIT);
final int badGovernment
= spec.getInteger(GameOptions.BAD_GOVERNMENT_LIMIT);
final int veryGoodGovernment
= spec.getInteger(GameOptions.VERY_GOOD_GOVERNMENT_LIMIT);
final int goodGovernment
= spec.getInteger(GameOptions.GOOD_GOVERNMENT_LIMIT);
String msgId = null;
int number = 0;
ModelMessage.MessageType msgType = ModelMessage.MessageType.GOVERNMENT_EFFICIENCY;
if (this.sonsOfLiberty >= veryGoodGovernment) {
// there are no tories left
if (this.oldSonsOfLiberty < veryGoodGovernment) {
msgId = "model.colony.veryGoodGovernment";
msgType = ModelMessage.MessageType.SONS_OF_LIBERTY;
number = veryGoodGovernment;
}
} else if (this.sonsOfLiberty >= goodGovernment) {
if (this.oldSonsOfLiberty == veryGoodGovernment) {
msgId = "model.colony.lostVeryGoodGovernment";
msgType = ModelMessage.MessageType.SONS_OF_LIBERTY;
number = veryGoodGovernment;
} else if (this.oldSonsOfLiberty < goodGovernment) {
msgId = "model.colony.goodGovernment";
msgType = ModelMessage.MessageType.SONS_OF_LIBERTY;
number = goodGovernment;
}
} else {
if (this.oldSonsOfLiberty >= goodGovernment) {
msgId = "model.colony.lostGoodGovernment";
msgType = ModelMessage.MessageType.SONS_OF_LIBERTY;
number = goodGovernment;
}
// Now that no bonus is applied, penalties may.
if (this.tories > veryBadGovernment) {
if (this.oldTories <= veryBadGovernment) {
// government has become very bad
msgId = "model.colony.veryBadGovernment";
}
} else if (this.tories > badGovernment) {
if (this.oldTories <= badGovernment) {
// government has become bad
msgId = "model.colony.badGovernment";
} else if (this.oldTories > veryBadGovernment) {
// government has improved, but is still bad
msgId = "model.colony.governmentImproved1";
}
} else if (this.oldTories > badGovernment) {
// government was bad, but has improved
msgId = "model.colony.governmentImproved2";
}
}
GoodsType bells = getSpecification().getGoodsType("model.goods.bells");
return (msgId == null) ? null
: new ModelMessage(msgType, msgId, this, bells)
.addName("%colony%", getName())
.addAmount("%number%", number);
}
/**
* Signal to the colony that its population is changing.
* Called from Unit.setLocation when a unit moves into or out of this
* colony, but *not* if it is moving within the colony.
*/
public void updatePopulation() {
updateSoL();
updateProductionBonus();
if (getOwner().isAI()) {
firePropertyChange(Colony.REARRANGE_COLONY, true, false);
}
}
/**
* Signal to the colony that a unit is moving in or out or
* changing its internal work location to one with a different
* teaching ability. This requires either checking for a new
* teacher or student, or clearing any existing education
* relationships.
*
* @param unit The {@code Unit} that is changing its education state.
* @param enable If true, check for new education opportunities, otherwise
* clear existing ones.
*/
public void updateEducation(Unit unit, boolean enable) {
WorkLocation wl = unit.getWorkLocation();
if (wl == null) {
throw new RuntimeException("updateEducation(" + unit
+ ") unit not at work location.");
} else if (wl.getColony() != this) {
throw new RuntimeException("updateEducation(" + unit
+ ") unit not at work location in this colony.");
}
if (enable) {
if (wl.canTeach()) {
Unit student = unit.getStudent();
if (student == null
&& (student = findStudent(unit)) != null) {
unit.setStudent(student);
student.setTeacher(unit);
unit.setTurnsOfTraining(0);// Teacher starts teaching
unit.changeWorkType(null);
}
} else {
Unit teacher = unit.getTeacher();
if (teacher == null
&& (teacher = findTeacher(unit)) != null) {
unit.setTeacher(teacher);
teacher.setStudent(unit);
}
}
} else {
if (wl.canTeach()) {
Unit student = unit.getStudent();
if (student != null) {
student.setTeacher(null);
unit.setStudent(null);
unit.setTurnsOfTraining(0);// Teacher stops teaching
}
} else {
Unit teacher = unit.getTeacher();
if (teacher != null) {
teacher.setStudent(null);
unit.setTeacher(null);
}
}
}
}
/**
* Does this colony have undead units?
*
* @return True if this colony has undead units.
*/
public boolean isUndead() {
Unit u = getFirstUnit();
return u != null && u.isUndead();
}
/**
* Gets the apparent number of units at this colony.
* Used in client enemy colonies
*
* @return The apparent number of {@code Unit}s at this colony.
*/
public int getApparentUnitCount() {
return (this.displayUnitCount > 0) ? this.displayUnitCount
: getUnitCount();
}
// Defence, offense and trade response
/**
* Gets the best defender type available to this colony.
*
* @return The best available defender type.
*/
public UnitType getBestDefenderType() {
final Predicate<UnitType> defenderPred = ut ->
ut.getDefence() > 0
&& !ut.isNaval()
&& ut.isAvailableTo(getOwner());
return maximize(getSpecification().getUnitTypeList(), defenderPred,
UnitType.defenceComparator);
}
/**
* Gets the total defence power.
*
* @return The total defence power.
*/
public double getTotalDefencePower() {
final CombatModel cm = getGame().getCombatModel();
return sumDouble(getTile().getUnits(), Unit::isDefensiveUnit,
u -> cm.getDefencePower(null, u));
}
/**
* Determines whether this colony is sufficiently unprotected and
* contains something worth pillaging. To be called by CombatModels
* when the attacker has defeated an unarmed colony defender.
*
* @param attacker The {@code Unit} that has defeated the defender.
* @return True if the attacker can pillage this colony.
*/
public boolean canBePillaged(Unit attacker) {
return !hasStockade()
&& attacker.hasAbility(Ability.PILLAGE_UNPROTECTED_COLONY)
&& !(getBurnableBuildings().isEmpty()
&& getTile().getNavalUnits().isEmpty()
&& (getLootableGoodsList().isEmpty()
|| !attacker.getType().canCarryGoods()
|| !attacker.hasSpaceLeft())
&& !canBePlundered());
}
/**
* Checks if this colony can be plundered. That is, can it yield
* non-zero gold.
*
* @return True if at least one piece of gold can be plundered from this
* colony.
*/
public boolean canBePlundered() {
return owner.checkGold(1);
}
/**
* Gets the buildings in this colony that could be burned by a raid.
*
* @return A list of burnable buildings.
*/
public List<Building> getBurnableBuildings() {
return transform(getBuildings(), Building::canBeDamaged);
}
/**
* Gets a list of all stored goods in this colony, suitable for
* being looted.
*
* @return A list of lootable goods in this colony.
*/
public List<Goods> getLootableGoodsList() {
return transform(getGoodsList(), AbstractGoods::isStorable);
}
/**
* Decide if the number of enemy combat units on all tiles that
* belong to the colony exceeds the number of friendly combat
* units. At the moment, only the colony owner's own units are
* considered friendly, but that could be extended to include the
* units of allied players.
*
* FIXME: if a colony is under siege, it should not be possible to
* put units outside the colony, unless those units are armed.
*
* @return Whether the colony is under siege.
*/
public boolean isUnderSiege() {
int friendlyUnits = 0;
int enemyUnits = 0;
for (Unit u : iterable(flatten(getColonyTiles(),
ct -> ct.getWorkTile().getUnits()))) {
if (u.getOwner() == getOwner()) {
if (u.isDefensiveUnit()) friendlyUnits++;
} else if (getOwner().atWarWith(u.getOwner())) {
if (u.isOffensiveUnit()) enemyUnits++;
}
}
return enemyUnits > friendlyUnits;
}
/**
* Evaluate this colony for a given player.
*
* @param player The {@code Player} to evaluate for.
* @return A value for the player.
*/
public int evaluateFor(Player player) {
if (player.isAI()
&& player.owns(this)
&& player.getSettlementCount() < Colony.TRADE_MARGIN) {
return Integer.MIN_VALUE;
}
int result, v;
if (player.owns(this)) {
result = 0;
for (WorkLocation wl : getAvailableWorkLocationsList()) {
v = wl.evaluateFor(player);
if (v == Integer.MIN_VALUE) return Integer.MIN_VALUE;
result += v;
}
for (Unit u : getTile().getUnitList()) {
v = u.evaluateFor(player);
if (v == Integer.MIN_VALUE) return Integer.MIN_VALUE;
result += v;
}
for (Goods g : getCompactGoodsList()) {
v = g.evaluateFor(player);
if (v == Integer.MIN_VALUE) return Integer.MIN_VALUE;
result += v;
}
} else { // Much guesswork
result = getApparentUnitCount() * 1000
+ 500 // Some useful goods?
+ 200 * count(getTile().getSurroundingTiles(0, 1),
matchKey(this, Tile::getOwningSettlement));
Building stockade = getStockade();
if (stockade != null) result *= stockade.getLevel();
}
return result;
}
// Education
/**
* Returns true if this colony has a schoolhouse and the unit type is a
* skilled unit type with a skill level not exceeding the level of the
* schoolhouse. @see Building#canAdd
*
* @param unit The unit to add as a teacher.
* @return {@code true} if this unit type could be added.
*/
public boolean canTrain(Unit unit) {
return canTrain(unit.getType());
}
/**
* Returns true if this colony has a schoolhouse and the unit type is a
* skilled unit type with a skill level not exceeding the level of the
* schoolhouse. The number of units already in the schoolhouse and
* the availability of pupils are not taken into account. @see
* Building#canAdd
*
* @param unitType The unit type to add as a teacher.
* @return {@code true} if this unit type could be added.
*/
public boolean canTrain(UnitType unitType) {
return hasAbility(Ability.TEACH)
&& any(getBuildings(),
b -> b.canTeach() && b.canAddType(unitType));
}
/**
* Gets a list of all teachers currently present in the school
* building.
*
* @return A stream of teacher {@code Unit}s.
*/
public Stream<Unit> getTeachers() {
return flatten(getBuildings(), Building::canTeach, Building::getUnits);
}
/**
* Find a teacher for the specified student.
* Do not search if ALLOW_STUDENT_SELECTION is true--- it is the
* player's job then.
*
* @param student The student {@code Unit} that needs a teacher.
* @return A potential teacher, or null of none found.
*/
public Unit findTeacher(Unit student) {
return (getSpecification().getBoolean(GameOptions.ALLOW_STUDENT_SELECTION))
? null // No automatic assignment
: find(getTeachers(), u ->
u.getStudent() == null && student.canBeStudent(u));
}
/**
* Find a student for the specified teacher.
* Do not search if ALLOW_STUDENT_SELECTION is true--- its the
* player's job then.
*
* @param teacher The teacher {@code Unit} that needs a student.
* @return A potential student, or null of none found.
*/
public Unit findStudent(final Unit teacher) {
if (getSpecification().getBoolean(GameOptions.ALLOW_STUDENT_SELECTION))
return null; // No automatic assignment
final GoodsType expertProduction
= teacher.getType().getExpertProduction();
final Predicate<Unit> teacherPred = u ->
u.getTeacher() == null && u.canBeStudent(teacher);
// Always pick the student with the least skill first.
// Break ties by favouring the one working in the teacher's trade,
// otherwise first applicant wins.
final Comparator<Unit> skillComparator
= Comparator.comparingInt(Unit::getSkillLevel);
final Comparator<Unit> tradeComparator
= Comparator.comparingInt(u ->
(u.getWorkType() == expertProduction) ? 0 : 1);
final Comparator<Unit> fullComparator
= skillComparator.thenComparing(tradeComparator);
return minimize(getUnits(), teacherPred, fullComparator);
}
// Production and consumption
/**
* Does this colony produce a goods type?
*
* This is more reliable than checking net or total production,
* either of which might be cancelling to zero.
*
* @param goodsType The {@code GoodsType} to check.
* @return True if goods type is produced.
*/
public boolean isProducing(GoodsType goodsType) {
return productionCache.isProducing(goodsType);
}
/**
* Does this colony consume a goods type?
*
* This is more reliable than checking net or total consumption,
* either of which might be cancelling to zero.
*
* @param goodsType The {@code GoodsType} to check.
* @return True if goods type is consumed.
*/
public boolean isConsuming(GoodsType goodsType) {
return productionCache.isConsuming(goodsType);
}
/**
* Get a list of all {@link Consumer}s in the colony sorted by
* priority. Consumers include all object that consume goods,
* e.g. Units, Buildings and BuildQueues.
*
* @return a list of consumers
*/
public List<Consumer> getConsumers() {
List<Consumer> result = new ArrayList<>();
result.addAll(getUnitList());
result.addAll(getBuildings());
result.add(buildQueue);
result.add(populationQueue);
result.sort(Consumer.COMPARATOR);
return result;
}
/**
* Returns the number of goods of a given type used by the settlement
* each turn.
*
* @param goodsType {@code GoodsType} values
* @return an {@code int} value
*/
@Override
public int getConsumptionOf(GoodsType goodsType) {
final Specification spec = getSpecification();
int result = super.getConsumptionOf(goodsType);
if (spec.getGoodsType("model.goods.bells").equals(goodsType)) {
result -= spec.getInteger(GameOptions.UNITS_THAT_USE_NO_BELLS);
}
return Math.max(0, result);
}
/**
* Gets the combined production of all food types.
*
* @return an {@code int} value
*/
public int getFoodProduction() {
return sum(getSpecification().getFoodGoodsTypeList(),
ft -> getTotalProductionOf(ft));
}
/**
* Get the number of turns before starvation occurs at this colony
* with current production levels.
*
* @return The number of turns before starvation occurs, or negative
* if it will not.
*/
public int getStarvationTurns() {
final GoodsType foodType = getSpecification().getPrimaryFoodType();
final int food = getGoodsCount(foodType);
final int newFood = getAdjustedNetProductionOf(foodType);
return (newFood >= 0) ? -1 : food / -newFood;
}
/**
* Get the number of turns before a new colonist will be born in
* this colony with current production levels.
*
* @return A number of turns, or negative if no colonist will be born.
*/
public int getNewColonistTurns() {
final GoodsType foodType = getSpecification().getPrimaryFoodType();
final int food = getGoodsCount(foodType);
final int newFood = getAdjustedNetProductionOf(foodType);
return (food + newFood >= Settlement.FOOD_PER_COLONIST) ? 1
: (newFood <= 0) ? -1
: (Settlement.FOOD_PER_COLONIST - food) / newFood + 1;
}
/**
* Get the current production {@code Modifier}, which is
* generated from the current production bonus.
*
* @param goodsType The {@code GoodsType} to produce.
* @param unitType An optional {@code UnitType} to do the work.
* @param wl The {@link WorkLocation}
* @return A stream of suitable {@code Modifier}s.
*/
public Stream<Modifier> getProductionModifiers(GoodsType goodsType,
UnitType unitType, WorkLocation wl) {
if (productionBonus == 0) return Stream.<Modifier>empty();
int bonus = (int)Math.floor(productionBonus * wl.getRebelFactor());
Modifier mod = new Modifier(goodsType.getId(), bonus,
Modifier.ModifierType.ADDITIVE,
Specification.SOL_MODIFIER_SOURCE);
mod.setModifierIndex(Modifier.COLONY_PRODUCTION_INDEX);
return Stream.of(mod);
}
/**
* Get the net production of the given goods type.
*
* (Also part of interface TradeLocation)
*
* @param goodsType a {@code GoodsType} value
* @return an {@code int} value
*/
public int getNetProductionOf(GoodsType goodsType) {
return productionCache.getNetProductionOf(goodsType);
}
/**
* Is a work location productive?
*
* @param workLocation The {@code WorkLocation} to check.
* @return True if something is being produced at the
* {@code WorkLocation}.
*/
public boolean isProductive(WorkLocation workLocation) {
ProductionInfo info = productionCache.getProductionInfo(workLocation);
return info != null && info.getProduction() != null
&& !info.getProduction().isEmpty()
&& info.getProduction().get(0).getAmount() > 0;
}
/**
* Returns the net production of the given GoodsType adjusted by
* the possible consumption of BuildQueues.
*
* @param goodsType a {@code GoodsType} value
* @return an {@code int} value
*/
public int getAdjustedNetProductionOf(final GoodsType goodsType) {
final ToIntFunction<BuildQueue> consumes = q -> {
ProductionInfo pi = productionCache.getProductionInfo(q);
return (pi == null) ? 0
: AbstractGoods.getCount(goodsType, pi.getConsumption());
};
return productionCache.getNetProductionOf(goodsType)
+ sum(Stream.of(buildQueue, populationQueue), consumes);
}
/**
* Gets a copy of the current production map.
* Useful in the server at the point net production is applied to a colony.
*
* @return A copy of the current production map.
*/
protected TypeCountMap<GoodsType> getProductionMap() {
return productionCache.getProductionMap();
}
/**
* Returns the ProductionInfo for the given Object.
*
* @param object an {@code Object} value
* @return a {@code ProductionInfo} value
*/
public ProductionInfo getProductionInfo(Object object) {
return productionCache.getProductionInfo(object);
}
/**
* Update all the production types.
*
* Called at initialization, to default to something rational when
* nothing was specified. This can not be done until all the tiles are
* present.
*/
public void updateProductionTypes() {
for (WorkLocation wl : getAvailableWorkLocationsList()) {
wl.updateProductionType();
}
}
/**
* Can this colony produce certain goods?
*
* @param goodsType The {@code GoodsType} to check production of.
* @return True if the goods can be produced.
*/
public boolean canProduce(GoodsType goodsType) {
return (getNetProductionOf(goodsType) > 0)
? true // Obviously:-)
// Breeding requires the breedable number to be present
: (goodsType.isBreedable())
? getGoodsCount(goodsType) >= goodsType.getBreedingNumber()
// Is there a work location that can produce the goods, with
// positive generic production potential and all inputs satisfied?
: any(getWorkLocationsForProducing(goodsType),
wl -> wl.getGenericPotential(goodsType) > 0
&& all(wl.getInputs(), ag -> canProduce(ag.getType())));
}
// Planning support
/** Container class for tile exploration or improvement suggestions. */
public static class TileImprovementSuggestion {
/**
* Comparator to order suggestions by descending improvement
* amount.
*/
public static final Comparator<TileImprovementSuggestion> descendingAmountComparator
= Comparator.comparingInt(TileImprovementSuggestion::getAmount)
.reversed()
.thenComparing(tis -> (FreeColObject)tis.tile);
/** The tile to explore or improve. */
public Tile tile;
/** The tile improvement to make, or if null to explore an LCR. */
public TileImprovementType tileImprovementType;
/** The expected improvement. INFINITY for LCRs. */
public int amount;
public TileImprovementSuggestion(Tile tile, TileImprovementType t,
int amount) {
this.tile = tile;
this.tileImprovementType = t;
this.amount = amount;
}
public boolean isExploration() {
return this.tileImprovementType == null;
}
public int getAmount() {
return this.amount;
}
};
/**
* Collect suggestions for tiles that need exploration or
* improvement (which may depend on current use within the colony).
*
* @return A list of {@code TileImprovementSuggestion}s.
*/
public List<TileImprovementSuggestion> getTileImprovementSuggestions() {
final Specification spec = getSpecification();
// Encourage exploration of neighbouring rumours.
List<TileImprovementSuggestion> result
= transform(getTile().getSurroundingTiles(1, 1),
Tile::hasLostCityRumour,
t -> new TileImprovementSuggestion(t, null, INFINITY));
// Consider improvements for all available colony tiles.
for (final ColonyTile ct : transform(getColonyTiles(),
WorkLocation::isAvailable)) {
final ToIntFunction<TileImprovementType> improve = cacheInt(ti ->
ct.improvedBy(ti));
result.addAll(transform(spec.getTileImprovementTypeList(),
ti -> !ti.isNatural() && improve.applyAsInt(ti) > 0,
ti -> new TileImprovementSuggestion(ct.getWorkTile(),
ti, improve.applyAsInt(ti))));
}
result.sort(TileImprovementSuggestion.descendingAmountComparator);
return result;
}
/**
* Finds another unit in this colony that would be better at doing the
* job of the specified unit.
*
* @param expert The {@code Unit} to consider.
* @return A better expert, or null if none available.
*/
public Unit getBetterExpert(Unit expert) {
GoodsType production = expert.getWorkType();
UnitType expertType = expert.getType();
GoodsType expertise = expertType.getExpertProduction();
Unit bestExpert = null;
int bestImprovement = 0;
if (production == null || expertise == null
|| production == expertise) return null;
// We have an expert not doing the job of their expertise.
// Check if there is a non-expert doing the job instead.
for (Unit nonExpert : transform(getUnits(), u ->
u.getWorkType() == expertise && u.getType() != expertType)) {
// We have found a unit of a different type doing the
// job of this expert's expertise now check if the
// production would be better if the units swapped
// positions.
int expertProductionNow = 0;
int nonExpertProductionNow = 0;
int expertProductionPotential = 0;
int nonExpertProductionPotential = 0;
// Get the current and potential productions for the
// work location of the expert.
WorkLocation ewl = expert.getWorkLocation();
if (ewl != null) {
expertProductionNow = ewl.getPotentialProduction(expertise,
expert.getType());
nonExpertProductionPotential
= ewl.getPotentialProduction(expertise,
nonExpert.getType());
}
// Get the current and potential productions for the
// work location of the non-expert.
WorkLocation nwl = nonExpert.getWorkLocation();
if (nwl != null) {
nonExpertProductionNow = nwl.getPotentialProduction(expertise,
nonExpert.getType());
expertProductionPotential
= nwl.getPotentialProduction(expertise, expertType);
}
// Find the unit that achieves the best improvement.
int improvement = expertProductionPotential
+ nonExpertProductionPotential
- expertProductionNow
- nonExpertProductionNow;
if (improvement > bestImprovement) {
bestImprovement = improvement;
bestExpert = nonExpert;
}
}
return bestExpert;
}
/**
* Determine if there is a problem with the production of a given
* goods type.
*
* @param goodsType The {@code GoodsType} to check.
* @return A collection of warning messages.
*/
public Collection<StringTemplate> getProductionWarnings(GoodsType goodsType) {
final int amount = getGoodsCount(goodsType);
final int production = getNetProductionOf(goodsType);
List<StringTemplate> result = new ArrayList<>();
if (goodsType.isStorable()) {
if (goodsType.limitIgnored()) {
if (goodsType.isFoodType()) { // Check for famine/starvation
int starve = getStarvationTurns();
if (starve == 0) {
result.add(StringTemplate
.template("model.colony.starving")
.addName("%colony%", getName()));
} else if (starve <= Colony.FAMINE_TURNS) {
result.add(StringTemplate
.template("model.colony.famineFeared")
.addName("%colony%", getName())
.addAmount("%number%", starve));
}
}
} else { // Check for overflow
int waste;
if (!getExportData(goodsType).getExported()
&& (waste = amount + production - getWarehouseCapacity()) > 0) {
result.add(StringTemplate
.template("model.building.warehouseSoonFull")
.addNamed("%goods%", goodsType)
.addName("%colony%", getName())
.addAmount("%amount%", waste));
}
}
}
// Add a message for goods required for the current building if any.
BuildableType currentlyBuilding = getCurrentlyBuilding();
if (currentlyBuilding != null) {
final Function<AbstractGoods, StringTemplate> bMapper = ag ->
StringTemplate.template("model.colony.buildableNeedsGoods")
.addName("%colony%", getName())
.addNamed("%buildable%", currentlyBuilding)
.addAmount("%amount%", ag.getAmount() - amount)
.addNamed("%goodsType%", goodsType);
result.addAll(transform(currentlyBuilding.getRequiredGoods(),
ag -> ag.getType() == goodsType
&& amount < ag.getAmount(),
bMapper));
}
// Add insufficient production messages for each production location
// that has a deficit in producing the goods type.
final Function<WorkLocation, ProductionInfo> piMapper = wl ->
getProductionInfo(wl);
final Predicate<WorkLocation> prodPred = isNotNull(piMapper);
final Function<WorkLocation, StringTemplate> pMapper = wl ->
getInsufficientProductionMessage(getProductionInfo(wl),
wl.getProductionDeficit(goodsType));
result.addAll(transform(getWorkLocationsForProducing(goodsType),
prodPred, pMapper, toListNoNulls()));
// Add insufficient production messages for each consumption
// location for the goods type where there is a consequent
// deficit in production of a dependent goods.
final Function<WorkLocation, List<StringTemplate>> cMapper = wl -> {
final ProductionInfo info = getProductionInfo(wl);
final Function<AbstractGoods, StringTemplate> gMapper = ag ->
getInsufficientProductionMessage(info,
wl.getProductionDeficit(ag.getType()));
return transform(wl.getOutputs(), AbstractGoods::isStorable,
gMapper, toListNoNulls());
};
result.addAll(transform(getWorkLocationsForConsuming(goodsType),
prodPred, cMapper, toAppendedList()));
return result;
}
/**
* Get a message about insufficient production for a building
*
* @param info The {@code ProductionInfo} for the work location.
* @param deficit The {@code AbstractGoods} in deficit.
* @return A suitable {@code StringTemplate} or null if none required.
*/
private StringTemplate getInsufficientProductionMessage(ProductionInfo info,
AbstractGoods deficit) {
if (info == null || deficit == null) return null;
List<AbstractGoods> input = info.getConsumptionDeficit();
if (input.isEmpty()) return null;
StringTemplate label = StringTemplate.label(", ");
for (AbstractGoods ag : input) label.addStringTemplate(ag.getLabel());
return StringTemplate.template("model.colony.insufficientProduction")
.addName("%colony%", getName())
.addNamed("%outputType%", deficit.getType())
.addAmount("%outputAmount%", deficit.getAmount())
.addStringTemplate("%consumptionDeficit%", label);
}
/**
* Check if a goods type is still useful to this colony.
*
* In general, all goods are useful. However post-independence there is
* no need for more liberty once Sol% reaches 100, nor immigration.
* Note the latter may change when we implement sailing to other European
* ports.
*
* @param goodsType The {@code GoodsType} to check.
* @return True if these goods are still useful here.
*/
public boolean goodsUseful(GoodsType goodsType) {
if (getOwner().getPlayerType() == Player.PlayerType.INDEPENDENT) {
if ((goodsType.isLibertyType() && getSonsOfLiberty() >= 100)
|| goodsType.isImmigrationType()) return false;
}
return true;
}
/**
* Special goods need modifiers applied when changed, and immigration
* accumulates to the owner.
*
* @param goodsType The {@code GoodsType} to modify.
* @param amount The amount of modification.
*/
private void modifySpecialGoods(GoodsType goodsType, int amount) {
final Turn turn = getGame().getTurn();
List<Modifier> mods;
mods = toList(goodsType.getModifiers(Modifier.LIBERTY));
if (!mods.isEmpty()) {
modifyLiberty((int)applyModifiers(amount, turn, mods));
}
mods = toList(goodsType.getModifiers(Modifier.IMMIGRATION));
if (!mods.isEmpty()) {
int migration = (int)applyModifiers(amount, turn, mods);
modifyImmigration(migration);
getOwner().modifyImmigration(migration);
}
}
/**
* Creates a temporary copy of this colony for planning purposes.
*
* A simple colony.copy() can not work because all the colony
* tiles will be left referring to uncopied work tiles which the
* colony-copy does not own, which prevents them being used as
* valid work locations. We have to copy the colony tile (which
* includes the colony), and fix up all the colony tile work tiles
* to point to copies of the original tile, and fix the ownership
* of those tiles.
*
* @return A scratch version of this colony.
*/
public Colony copyColony() {
final Game game = getGame();
Tile tile = getTile();
Tile tileCopy = tile.copy(game);
Colony colony = tileCopy.getColony();
for (ColonyTile ct : colony.getColonyTiles()) {
Tile wt;
if (ct.isColonyCenterTile()) {
wt = tileCopy;
} else {
wt = ct.getWorkTile();
wt = wt.copy(game);
if (wt.getOwningSettlement() == this) {
wt.setOwningSettlement(colony);
}
}
ct.setWorkTile(wt);
}
return colony;
}
/**
* Finds the corresponding FreeColObject from another copy of this colony.
*
* @param <T> The actual return type.
* @param fco The {@code FreeColObject} in the other colony.
* @return The corresponding {@code FreeColObject} in this
* colony, or null if not found.
*/
@SuppressWarnings("unchecked")
public <T extends FreeColObject> T getCorresponding(T fco) {
final String id = fco.getId();
return (fco instanceof WorkLocation)
? (T)find(getAllWorkLocations(),
matchKeyEquals(id, WorkLocation::getId))
: (fco instanceof Tile)
? (T)((getTile().getId().equals(id)) ? getTile()
: find(map(getColonyTiles(), ColonyTile::getWorkTile),
matchKeyEquals(id, Tile::getId)))
: (fco instanceof Unit)
? (T)find(getAllUnitsList(),
matchKeyEquals(id, Unit::getId))
: null;
}
// Override FreeColObject
/**
* {@inheritDoc}
*/
@Override
public Stream<Ability> getAbilities(String id, FreeColSpecObjectType type,
Turn turn) {
if (turn == null) turn = getGame().getTurn();
return concat(super.getAbilities(id, type, turn),
((owner == null) ? Stream.<Ability>empty()
: owner.getAbilities(id, type, turn)));
}
// Override FreeColGameObject
/**
* {@inheritDoc}
*/
@Override
public Stream<FreeColGameObject> getDisposables() {
return concat(flatten(getAllWorkLocations(),
WorkLocation::getDisposables),
super.getDisposables());
}
// Interface Location (from Settlement via GoodsLocation via UnitLocation)
// UnitLocation.units is not used in Colony. getUnits/List is defined
// to return the union of the units in the work locations, which may
// or not be the best idea. Another choice would be to return all
// the units in the work locations, plus those present on the tile,
// which is provided by Settlement.getAllUnitsList.
// TODO: look at all the uses, see if this makes sense.
// Inherits
// FreeColObject.getId
// Settlement.getTile
// Settlement.getLocationLabel
// GoodsLocation.canAdd
// GoodsLocation.getGoodsContainer
// Settlement.getSettlement
/**
* {@inheritDoc}
*/
@Override
public StringTemplate getLocationLabelFor(Player player) {
// Everyone can always work out a colony name, but it can be invalid
final String name = getName();
return StringTemplate.name((name == null) ? "?" : name);
}
/**
* {@inheritDoc}
*/
@Override
public boolean add(Locatable locatable) {
if (locatable instanceof Unit) {
return joinColony((Unit)locatable);
}
return super.add(locatable);
}
/**
* {@inheritDoc}
*/
@Override
public boolean remove(Locatable locatable) {
if (locatable instanceof Unit) {
Location loc = locatable.getLocation();
if (loc instanceof WorkLocation) {
WorkLocation wl = (WorkLocation)loc;
if (wl.getColony() == this) {
return wl.remove(locatable);
}
}
return false;
}
return super.remove(locatable);
}
/**
* {@inheritDoc}
*/
@Override
public boolean contains(Locatable locatable) {
if (locatable instanceof Unit) {
return any(getAvailableWorkLocations(),
wl -> wl.contains(locatable));
}
return super.contains(locatable);
}
/**
* {@inheritDoc}
*/
@Override
public int getUnitCount() {
return sum(getCurrentWorkLocations(), UnitLocation::getUnitCount);
}
/**
* {@inheritDoc}
*/
@Override
public Stream<Unit> getUnits() {
return flatten(getCurrentWorkLocations(), WorkLocation::getUnits);
}
/**
* {@inheritDoc}
*/
@Override
public List<Unit> getUnitList() {
return toList(getUnits());
}
/**
* {@inheritDoc}
*/
@Override
public final Colony getColony() {
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Location up() {
return this;
}
/**
* {@inheritDoc}
*/
@Override
public String toShortString() {
return getName();
}
// Interface UnitLocation
// Inherits
// UnitLocation.getSpaceTaken [Irrelevant!]
// UnitLocation.moveToFront [Irrelevant!]
// UnitLocation.clearUnitList [Irrelevant!]
// Settlement.equipForRole
// Settlement.getNoAddReason
// Interface GoodsLocation
/**
* {@inheritDoc}
*/
public void invalidateCache() {
this.productionCache.invalidate();
}
/**
* {@inheritDoc}
*/
@Override
public int getGoodsCapacity() {
return (int)apply(0f, getGame().getTurn(), Modifier.WAREHOUSE_STORAGE);
}
/**
* {@inheritDoc}
*/
@Override
public boolean addGoods(GoodsType type, int amount) {
super.addGoods(type, amount);
productionCache.invalidate(type);
modifySpecialGoods(type, amount);
return true;
}
/**
* {@inheritDoc}
*/
@Override
public Goods removeGoods(GoodsType type, int amount) {
Goods removed = super.removeGoods(type, amount);
productionCache.invalidate(type);
if (removed != null) modifySpecialGoods(type, -removed.getAmount());
return removed;
}
// Settlement
/**
* {@inheritDoc}
*/
@Override
public int getImmigration() {
return immigration;
}
/**
* {@inheritDoc}
*/
@Override
public int getLiberty() {
return liberty;
}
/**
* {@inheritDoc}
*/
@Override
public Unit getDefendingUnit(Unit attacker) {
if (displayUnitCount > 0) {
// There are units, but we don't see them
return null;
}
// Note that this function will only return a unit working
// inside the colony. Typically, colonies are also defended
// by units outside the colony on the same tile. To consider
// units outside the colony as well, use
// @see Tile#getDefendingUnit instead.
final CombatModel cm = getGame().getCombatModel();
final Comparator<Unit> comp
= cachingDoubleComparator(u -> cm.getDefencePower(attacker, u));
return maximize(getUnits(), comp);
}
/**
* {@inheritDoc}
*/
@Override
public double getDefenceRatio() {
return getTotalDefencePower() / (1 + getUnitCount());
}
/**
* {@inheritDoc}
*/
@Override
public boolean isBadlyDefended() {
final double defencePower = getTotalDefencePower();
if (getTile().getUnits().filter(u -> u.isOffensiveUnit()).count() < 1) {
return true;
}
if (getTile().getUnits().filter(u -> u.isOffensiveUnit()).count() > 5) {
return false;
}
return defencePower < 0.95 * getUnitCount() - 2.5;
}
public boolean isVeryWellDefended() {
final double defencePower = getTotalDefencePower();
if (getTile().getUnits().filter(u -> u.isOffensiveUnit()).count() < 3) {
return false;
}
if (getTile().getUnits().filter(u -> u.isOffensiveUnit()).count() > 5) {
return true;
}
return defencePower / 2 > 0.95 * getUnitCount() - 2.5;
}
/**
* {@inheritDoc}
*/
@Override
public RandomRange getPlunderRange(Unit attacker) {
if (canBePlundered()) {
int upper = (owner.getGold() * (getUnitCount() + 1))
/ (owner.getColoniesPopulation() + 1);
if (upper > 0) return new RandomRange(100, 1, upper+1, 1);
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public int getUpkeep() {
return sum(getBuildings(), b -> b.getType().getUpkeep());
}
/**
* {@inheritDoc}
*/
@Override
public int getTotalProductionOf(GoodsType goodsType) {
return sum(getCurrentWorkLocations(),
wl -> wl.getTotalProductionOf(goodsType));
}
/**
* {@inheritDoc}
*/
@Override
public boolean canProvideGoods(List<AbstractGoods> requiredGoods) {
// Unlike priceGoods, this takes goods "reserved" for other
// purposes into account.
BuildableType buildable = getCurrentlyBuilding();
for (AbstractGoods goods : requiredGoods) {
int available = getGoodsCount(goods.getType());
int breedingNumber = goods.getType().getBreedingNumber();
if (breedingNumber != INFINITY) available -= breedingNumber;
if (buildable != null) {
available -= AbstractGoods.getCount(goods.getType(),
buildable.getRequiredGoodsList());
}
if (available < goods.getAmount()) return false;
}
return true;
}
/**
* {@inheritDoc}
*/
public boolean hasContacted(Player player) {
return player != null
&& (player.isEuropean()
|| getOwner().getStance(player) != Stance.UNCONTACTED);
}
/**
* {@inheritDoc}
*/
@Override
public StringTemplate getAlarmLevelLabel(Player player) {
Stance stance = getOwner().getStance(player);
return StringTemplate.template("model.colony." + stance.getKey())
.addStringTemplate("%nation%", getOwner().getNationLabel());
}
/**
* Determines the value of a potential attack on a {@code Colony}
*
* @param value The previously calculated input value from
* {@link net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission
* #scoreSettlementPath(AIUnit, PathNode, Settlement)}
* @param unit The Unit doing the attacking.
* @return The newly calculated value.
*/
@Override
public int calculateSettlementValue(int value, Unit unit) {
// Favour high population (more loot:-).
value += this.getUnitCount();
if (this.hasStockade()) { // Avoid fortifications.
value -= 200 * this.getStockade().getLevel();
}
return value;
}
// Interface TradeLocation
/**
* Calculate the present field.
*
* @param goodsType The {@link GoodsType} to check for got import/export.
* @param turns The number of turns before the goods is required.
* @return The amount of goods to export.
*/
private int returnPresent(GoodsType goodsType, int turns) {
return Math.max(0, getGoodsCount(goodsType)
+ turns * getNetProductionOf(goodsType));
}
/**
* {@inheritDoc}
*/
@Override
public int getAvailableGoodsCount(GoodsType goodsType) {
return getGoodsCount(goodsType);
}
/**
* {@inheritDoc}
*/
@Override
public int getExportAmount(GoodsType goodsType, int turns) {
final int present = returnPresent(goodsType, turns);
final ExportData ed = getExportData(goodsType);
return Math.max(0, present - ed.getExportLevel());
}
/**
* {@inheritDoc}
*/
@Override
public int getImportAmount(GoodsType goodsType, int turns) {
if (goodsType.limitIgnored()) return GoodsContainer.HUGE_CARGO_SIZE;
final int present = returnPresent(goodsType, turns);
final ExportData ed = getExportData(goodsType);
int capacity = ed.getEffectiveImportLevel(getWarehouseCapacity());
return Math.max(0, capacity - present);
}
/**
* {@inheritDoc}
*/
@Override
public String getLocationName(TradeLocation tradeLocation) {
Colony colony = (Colony) tradeLocation;
return colony.getName();
}
/**
* {@inheritDoc}
*/
@Override
public boolean canBeInput() {
return true;
}
//
// Miscellaneous low level
//
/**
* Add port ability to non-landlocked colonies.
*/
protected void addPortAbility() {
addAbility(new Ability(Ability.HAS_PORT));
}
/**
* Check the integrity of the build queues. Catches build fails
* due to broken requirements.
*
* @param fix Fix problems if possible.
* @param lb An optional {@code LogBuilder} to log to.
* @return The integrity found.
*/
public IntegrityType checkBuildQueueIntegrity(boolean fix, LogBuilder lb) {
IntegrityType result = IntegrityType.INTEGRITY_GOOD;
List<BuildableType> buildables = buildQueue.getValues();
List<BuildableType> assumeBuilt = new ArrayList<>();
for (int i = 0; i < buildables.size(); i++) {
BuildableType bt = buildables.get(i);
NoBuildReason reason = getNoBuildReason(bt, assumeBuilt);
if (reason == NoBuildReason.NONE) {
assumeBuilt.add(bt);
} else if (fix) {
if (lb != null) lb.add("\n Invalid build queue item removed: ", bt.getId());
buildQueue.remove(i);
result = result.fix();
} else {
if (lb != null) lb.add("\n Invalid build queue item: ", bt.getId());
result = result.fail();
}
}
List<UnitType> unitTypes = populationQueue.getValues();
assumeBuilt.clear();
for (int i = 0; i < unitTypes.size(); i++) {
UnitType ut = unitTypes.get(i);
NoBuildReason reason = getNoBuildReason(ut, assumeBuilt);
if (reason == NoBuildReason.NONE) {
assumeBuilt.add(ut);
} else if (fix) {
if (lb != null) lb.add("\n Invalid population queue item removed: ", ut.getId());
populationQueue.remove(i);
result = result.fix();
} else {
if (lb != null) lb.add("\n Invalid population queue item: ", ut.getId());
result = result.fail();
}
}
return result;
}
// Override FreeColGameObject
/**
* {@inheritDoc}
*/
@Override
public IntegrityType checkIntegrity(boolean fix, LogBuilder lb) {
IntegrityType result = super.checkIntegrity(fix, lb);
return result.combine(checkBuildQueueIntegrity(fix, lb));
}
// Override FreeColObject
/**
* {@inheritDoc}
*/
@Override
public int getClassIndex () {
return COLONY_CLASS_INDEX;
}
/**
* {@inheritDoc}
*/
@Override
public <T extends FreeColObject> boolean copyIn(T other) {
Colony o = copyInCast(other, Colony.class);
if (o == null || !super.copyIn(o)) return false;
final Game game = getGame();
this.setBuildingMap(game.update(o.getBuildings(), true));
this.setColonyTiles(game.update(o.getColonyTiles(), true));
this.setExportData(o.getExportData());
this.liberty = o.getLiberty();
this.sonsOfLiberty = o.getSonsOfLiberty();
this.oldSonsOfLiberty = o.getOldSonsOfLiberty();
this.tories = o.getToryCount();
this.oldTories = o.getOldToryCount();
this.productionBonus = o.getProductionBonus();
this.immigration = o.getImmigration();
this.established = o.getEstablished();
this.setBuildQueue(o.getBuildQueue());
this.setPopulationQueue(o.getPopulationQueue());
this.displayUnitCount = o.getDisplayUnitCount();
for (WorkLocation wl : getAllWorkLocationsList()) wl.setColony(this);
invalidateCache(); // Almost any change will break the cache
return true;
}
// Serialization
private static final String BUILD_QUEUE_TAG = "buildQueueItem";
private static final String ESTABLISHED_TAG = "established";
private static final String IMMIGRATION_TAG = "immigration";
private static final String LIBERTY_TAG = "liberty";
private static final String PRODUCTION_BONUS_TAG = "productionBonus";
private static final String NAME_TAG = "name";
private static final String OLD_SONS_OF_LIBERTY_TAG = "oldSonsOfLiberty";
private static final String OLD_TORIES_TAG = "oldTories";
private static final String POPULATION_QUEUE_TAG = "populationQueueItem";
private static final String SONS_OF_LIBERTY_TAG = "sonsOfLiberty";
private static final String TORIES_TAG = "tories";
private static final String UNIT_COUNT_TAG = "unitCount";
/**
* {@inheritDoc}
*/
@Override
protected void writeAttributes(FreeColXMLWriter xw) throws XMLStreamException {
super.writeAttributes(xw);
// Delegated from Settlement
xw.writeAttribute(NAME_TAG, getName());
xw.writeAttribute(ESTABLISHED_TAG, established.getNumber());
// SoL has to be visible for the popular support bonus to be
// visible to an attacking rebel player.
xw.writeAttribute(SONS_OF_LIBERTY_TAG, sonsOfLiberty);
if (xw.validFor(getOwner())) {
xw.writeAttribute(OLD_SONS_OF_LIBERTY_TAG, oldSonsOfLiberty);
xw.writeAttribute(TORIES_TAG, tories);
xw.writeAttribute(OLD_TORIES_TAG, oldTories);
xw.writeAttribute(LIBERTY_TAG, liberty);
xw.writeAttribute(IMMIGRATION_TAG, immigration);
xw.writeAttribute(PRODUCTION_BONUS_TAG, productionBonus);
} else {
int uc = getApparentUnitCount();
if (uc > 0) { // Valid if above zero
xw.writeAttribute(UNIT_COUNT_TAG, uc);
} else if (uc == 0) { // Zero is an error! Find that bug
FreeCol.trace(logger, "Unit count fail: " + uc
+ " id=" + getId() + " name=" + getName()
+ " unitCount=" + getUnitCount()
+ " displayUnitCount=" + this.displayUnitCount
+ " scope=" + xw.getWriteScope()
+ "/" + xw.getClientPlayer());
} // else do nothing, negative means no value set
}
}
/**
* {@inheritDoc}
*/
@Override
protected void writeChildren(FreeColXMLWriter xw) throws XMLStreamException {
super.writeChildren(xw);
if (xw.validFor(getOwner())) {
for (Entry<String, ExportData> e : mapEntriesByKey(exportData)) {
e.getValue().toXML(xw);
}
for (WorkLocation wl : sort(getAllWorkLocations())) {
wl.toXML(xw);
}
for (BuildableType item : buildQueue.getValues()) { // In order!
xw.writeStartElement(BUILD_QUEUE_TAG);
xw.writeAttribute(ID_ATTRIBUTE_TAG, item);
xw.writeEndElement();
}
for (BuildableType item : populationQueue.getValues()) { // In order
xw.writeStartElement(POPULATION_QUEUE_TAG);
xw.writeAttribute(ID_ATTRIBUTE_TAG, item);
xw.writeEndElement();
}
} else {
// Special case. Serialize stockade-class buildings to
// otherwise unprivileged clients as the stockade level is
// visible to anyone who can see the colony. This should
// have no other information leaks because stockade
// buildings have no production or units inside.
Building stockade = getStockade();
if (stockade != null) stockade.toXML(xw);
}
}
/**
* {@inheritDoc}
*/
@Override
public void readAttributes(FreeColXMLReader xr) throws XMLStreamException {
super.readAttributes(xr);
established = new Turn(xr.getAttribute(ESTABLISHED_TAG, 0));
sonsOfLiberty = xr.getAttribute(SONS_OF_LIBERTY_TAG, 0);
oldSonsOfLiberty = xr.getAttribute(OLD_SONS_OF_LIBERTY_TAG, 0);
tories = xr.getAttribute(TORIES_TAG, 0);
oldTories = xr.getAttribute(OLD_TORIES_TAG, 0);
liberty = xr.getAttribute(LIBERTY_TAG, 0);
immigration = xr.getAttribute(IMMIGRATION_TAG, 0);
productionBonus = xr.getAttribute(PRODUCTION_BONUS_TAG, 0);
displayUnitCount = xr.getAttribute(UNIT_COUNT_TAG, -1);
}
/**
* {@inheritDoc}
*/
@Override
public void readChildren(FreeColXMLReader xr) throws XMLStreamException {
// Clear containers.
clearBuildingMap();
clearColonyTiles();
exportData.clear();
buildQueue.clear();
populationQueue.clear();
super.readChildren(xr);
invalidateCache();
}
/**
* {@inheritDoc}
*/
@Override
public void readChild(FreeColXMLReader xr) throws XMLStreamException {
final Specification spec = getSpecification();
final Game game = getGame();
final String tag = xr.getLocalName();
if (BUILD_QUEUE_TAG.equals(tag)) {
BuildableType bt = xr.getType(spec, ID_ATTRIBUTE_TAG,
BuildableType.class, (BuildableType)null);
if (bt != null) buildQueue.add(bt);
xr.closeTag(BUILD_QUEUE_TAG);
} else if (POPULATION_QUEUE_TAG.equals(xr.getLocalName())) {
UnitType ut = xr.getType(spec, ID_ATTRIBUTE_TAG,
UnitType.class, (UnitType)null);
if (ut != null) populationQueue.add(ut);
xr.closeTag(POPULATION_QUEUE_TAG);
} else if (Building.TAG.equals(tag)) {
addBuilding(xr.readFreeColObject(game, Building.class));
} else if (ColonyTile.TAG.equals(tag)) {
addColonyTile(xr.readFreeColObject(game, ColonyTile.class));
} else if (ExportData.TAG.equals(tag)) {
ExportData data = new ExportData(xr);
setExportData(data);
} else {
super.readChild(xr);
}
}
/**
* {@inheritDoc}
*/
public String getXMLTagName() { return TAG; }
// Override Object
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return getName();
}
}