Creates calculator classes replacing the functionality previously in ColonyTile and Building.

* Production output can now reliably be tested without having built a Colony. This will be used by the new AI Colony Management code.
* Fixes several differences/bugs that occured because of code duplication.
This commit is contained in:
Stian Grenborgen 2022-09-18 18:31:36 +02:00
parent f59cc15566
commit 3bdda582bd
11 changed files with 861 additions and 247 deletions

View File

@ -19,19 +19,26 @@
package net.sf.freecol.common.model;
import static net.sf.freecol.common.util.CollectionUtils.any;
import static net.sf.freecol.common.util.CollectionUtils.concat;
import static net.sf.freecol.common.util.CollectionUtils.sum;
import static net.sf.freecol.common.util.CollectionUtils.toList;
import static net.sf.freecol.common.util.CollectionUtils.transform;
import static net.sf.freecol.common.util.StringUtils.lastPart;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import net.sf.freecol.common.option.GameOptions;
import static net.sf.freecol.common.util.CollectionUtils.*;
import static net.sf.freecol.common.util.StringUtils.*;
import net.sf.freecol.common.model.production.BuildingProductionCalculator;
import net.sf.freecol.common.model.production.WorkerAssignment;
/**
@ -135,33 +142,6 @@ public class Building extends WorkLocation
return eject;
}
/**
* Gets the production modifiers for the given type of goods and
* unit type.
*
* We use UnitType.getModifiers but modify this according to the
* competence factor of this building type. Note that we do not modify
* *multiplicative* modifiers, as this would capture the master blacksmith
* doubling.
*
* @param id The String identifier
* @param turn The turn number of type {@link Turn}
* @param unitType The optional {@code UnitType} to produce them.
* @return A stream of the applicable modifiers.
*/
public Stream<Modifier> getCompetenceModifiers(String id,
UnitType unitType, Turn turn) {
final float competence = getCompetenceFactor();
return (competence == 1.0f) // Floating comparison OK!
? unitType.getModifiers(id, getType(), turn)
: map(unitType.getModifiers(id, getType(), turn),
m -> {
return (m.getType() == Modifier.ModifierType.ADDITIVE)
? Modifier.makeModifier(m).setValue(m.getValue() * competence)
: m;
});
}
/**
* Does this building have a higher level?
@ -222,18 +202,6 @@ public class Building extends WorkLocation
return canBeWorked() && getType().canAdd(unitType);
}
/**
* Convenience function to extract a goods amount from a list of
* available goods.
*
* @param type The {@code GoodsType} to extract the amount for.
* @param available The list of available goods to query.
* @return The goods amount, or zero if none found.
*/
private int getAvailable(GoodsType type, List<AbstractGoods> available) {
return AbstractGoods.getCount(type, available);
}
/**
* Gets the production information for this building taking account
* of the available input and output goods.
@ -244,136 +212,14 @@ public class Building extends WorkLocation
* @return The production information.
* @see ProductionCache#update
*/
public ProductionInfo getAdjustedProductionInfo(List<AbstractGoods> inputs,
List<AbstractGoods> outputs) {
ProductionInfo result = new ProductionInfo();
if (!hasOutputs()) return result;
final Specification spec = getSpecification();
public ProductionInfo getAdjustedProductionInfo(List<AbstractGoods> inputs, List<AbstractGoods> outputs) {
final BuildingProductionCalculator pc = new BuildingProductionCalculator(getOwner(), getColony().getFeatureContainer(), getColony().getProductionBonus());
final List<WorkerAssignment> workerAssignments = getUnits()
.map(u -> new WorkerAssignment(u.getType(), getProductionType()))
.collect(Collectors.toList());
final Turn turn = getGame().getTurn();
final boolean avoidOverflow
= hasAbility(Ability.AVOID_EXCESS_PRODUCTION);
final int capacity = getColony().getWarehouseCapacity();
// Calculate two production ratios, the minimum (and actual)
// possible multiplier between the nominal input and output
// goods and the amount actually consumed and produced, and
// the maximum possible ratio that would apply but for
// circumstances such as limited input availability.
double maximumRatio = 0.0, minimumRatio = Double.MAX_VALUE;
// First, calculate the nominal production ratios.
if (canAutoProduce()) {
// Autoproducers are special
for (AbstractGoods output : transform(getOutputs(),
AbstractGoods::isPositive)) {
final GoodsType goodsType = output.getType();
int available = getColony().getGoodsCount(goodsType);
if (available >= capacity) {
minimumRatio = maximumRatio = 0.0;
} else {
int divisor = (int)getType().apply(0f, turn,
Modifier.BREEDING_DIVISOR);
int factor = (int)getType().apply(0f, turn,
Modifier.BREEDING_FACTOR);
int production = (available < goodsType.getBreedingNumber()
|| divisor <= 0) ? 0
// Deliberate use of integer division
: ((available - 1) / divisor + 1) * factor;
double newRatio = (double)production / output.getAmount();
minimumRatio = Math.min(minimumRatio, newRatio);
maximumRatio = Math.max(maximumRatio, newRatio);
}
}
} else {
for (AbstractGoods output : iterable(getOutputs())) {
final GoodsType goodsType = output.getType();
float production = sum(getUnits(),
u -> getUnitProduction(u, goodsType));
// Unattended production always applies for buildings!
production += getBaseProduction(null, goodsType, null);
production = applyModifiers(production, turn,
getProductionModifiers(goodsType, null));
production = (int)Math.floor(production);
// Beware! If we ever unify this code with ColonyTile,
// ColonyTiles have outputs with zero amount.
double newRatio = production / output.getAmount();
minimumRatio = Math.min(minimumRatio, newRatio);
maximumRatio = Math.max(maximumRatio, newRatio);
}
}
// Then reduce the minimum ratio if some input is in short supply.
for (AbstractGoods input : iterable(getInputs())) {
long required = (long)Math.floor(input.getAmount() * minimumRatio);
long available = getAvailable(input.getType(), inputs);
// Do not allow auto-production to go negative.
if (canAutoProduce()) available = Math.max(0, available);
// Experts in factory level buildings may produce a
// certain amount of goods even when no input is available.
// Factories have the EXPERTS_USE_CONNECTIONS ability.
long minimumGoodsInput;
if (available < required
&& hasAbility(Ability.EXPERTS_USE_CONNECTIONS)
&& spec.getBoolean(GameOptions.EXPERTS_HAVE_CONNECTIONS)
&& ((minimumGoodsInput = getType().getExpertConnectionProduction()
* count(getUnits(),
matchKey(getExpertUnitType(), Unit::getType)))
> available)) {
available = minimumGoodsInput;
}
// Scale production by limitations on availability.
if (available < required) {
minimumRatio *= (double)available / required;
//maximumRatio = Math.max(maximumRatio, minimumRatio);
}
}
// Check whether there is space enough to store the goods
// produced in order to avoid excess production.
if (avoidOverflow) {
for (AbstractGoods output : iterable(getOutputs())) {
double production = output.getAmount() * minimumRatio;
if (production <= 0) continue;
double headroom = (double)capacity
- getAvailable(output.getType(), outputs);
// Clamp production at warehouse capacity
if (production > headroom) {
minimumRatio = Math.min(minimumRatio,
headroom / output.getAmount());
}
production = output.getAmount() * maximumRatio;
if (production > headroom) {
maximumRatio = Math.min(maximumRatio,
headroom / output.getAmount());
}
}
}
for (AbstractGoods input : iterable(getInputs())) {
GoodsType type = input.getType();
// maximize consumption
int consumption = (int)Math.floor(input.getAmount()
* minimumRatio + EPSILON);
int maximumConsumption = (int)Math.floor(input.getAmount()
* maximumRatio);
result.addConsumption(new AbstractGoods(type, consumption));
if (consumption < maximumConsumption) {
result.addMaximumConsumption(new AbstractGoods(type, maximumConsumption));
}
}
for (AbstractGoods output : iterable(getOutputs())) {
GoodsType type = output.getType();
// minimize production, but add a magic little something
// to counter rounding errors
int production = (int)Math.floor(output.getAmount() * minimumRatio
+ EPSILON);
int maximumProduction = (int)Math.floor(output.getAmount()
* maximumRatio);
result.addProduction(new AbstractGoods(type, production));
if (production < maximumProduction) {
result.addMaximumProduction(new AbstractGoods(type, maximumProduction));
}
}
return result;
final int warehouseCapacity = getColony().getWarehouseCapacity();
return pc.getAdjustedProductionInfo(buildingType, turn, workerAssignments, inputs, outputs, warehouseCapacity);
}
/**
@ -574,7 +420,7 @@ public class Building extends WorkLocation
// With a unit, unit specific bonuses apply
? concat(this.getModifiers(id, unitType, turn),
colony.getProductionModifiers(goodsType, unitType, this),
getCompetenceModifiers(id, unitType, turn),
getType().getCompetenceModifiers(id, unitType, turn),
owner.getModifiers(id, unitType, turn))
// With no unit, only the building-specific bonuses
: concat(colony.getModifiers(id, type, turn),

View File

@ -19,8 +19,18 @@
package net.sf.freecol.common.model;
import static net.sf.freecol.common.model.Constants.INFINITY;
import static net.sf.freecol.common.model.Constants.UNDEFINED;
import static net.sf.freecol.common.util.CollectionUtils.concat;
import static net.sf.freecol.common.util.CollectionUtils.first;
import static net.sf.freecol.common.util.CollectionUtils.iterable;
import static net.sf.freecol.common.util.CollectionUtils.map;
import static net.sf.freecol.common.util.CollectionUtils.sum;
import static net.sf.freecol.common.util.CollectionUtils.transform;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import javax.swing.JList;
import javax.swing.ListModel;
@ -29,9 +39,7 @@ import javax.xml.stream.XMLStreamException;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import net.sf.freecol.common.model.Colony.NoBuildReason;
import static net.sf.freecol.common.model.Constants.*;
import net.sf.freecol.common.model.UnitLocation.NoAddReason;
import static net.sf.freecol.common.util.CollectionUtils.*;
/**
@ -353,7 +361,7 @@ public final class BuildingType extends BuildableType
amount = (int)apply(amount, null, goodsType.getId(), unitType);
return (amount < 0) ? 0 : amount;
}
/**
* {@inheritDoc}
*/
@ -447,6 +455,32 @@ public final class BuildingType extends BuildableType
return buildQueueLastPos;
}
/**
* Gets the production modifiers for the given type of goods and
* unit type.
*
* We use UnitType.getModifiers but modify this according to the
* competence factor of this building type. Note that we do not modify
* *multiplicative* modifiers, as this would capture the master blacksmith
* doubling.
*
* @param id The String identifier
* @param turn The turn number of type {@link Turn}
* @param unitType The optional {@code UnitType} to produce them.
* @return A stream of the applicable modifiers.
*/
public Stream<Modifier> getCompetenceModifiers(String id,
UnitType unitType, Turn turn) {
final float competence = getCompetenceFactor();
return (competence == 1.0f) // Floating comparison OK!
? unitType.getModifiers(id, getType(), turn)
: map(unitType.getModifiers(id, getType(), turn),
m -> {
return (m.getType() == Modifier.ModifierType.ADDITIVE)
? Modifier.makeModifier(m).setValue(m.getValue() * competence)
: m;
});
}
// Override FreeColObject

View File

@ -29,6 +29,8 @@ import javax.xml.stream.XMLStreamException;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import net.sf.freecol.common.model.Player.NoClaimReason;
import net.sf.freecol.common.model.production.TileProductionCalculator;
import net.sf.freecol.common.model.production.WorkerAssignment;
import net.sf.freecol.common.option.GameOptions;
import static net.sf.freecol.common.util.CollectionUtils.*;
@ -157,32 +159,17 @@ public class ColonyTile extends WorkLocation {
* @see ProductionCache#update
*/
public ProductionInfo getBasicProductionInfo() {
final Colony colony = getColony();
ProductionInfo pi = new ProductionInfo();
if (isColonyCenterTile()) {
forEach(getOutputs(), output -> {
boolean onlyNaturalImprovements = getSpecification()
.getBoolean(GameOptions.ONLY_NATURAL_IMPROVEMENTS)
&& !output.getType().isFoodType();
int potential = output.getAmount();
if (workTile.getTileItemContainer() != null) {
potential = workTile.getTileItemContainer()
.getTotalBonusPotential(output.getType(), null,
potential, onlyNaturalImprovements);
}
potential += Math.max(0, colony.getProductionBonus());
AbstractGoods production
= new AbstractGoods(output.getType(), potential);
pi.addProduction(production);
});
} else {
forEach(map(getOutputs(), AbstractGoods::getType),
gt -> {
int n = sum(getUnits(), u -> getUnitProduction(u, gt));
if (n > 0) pi.addProduction(new AbstractGoods(gt, n));
});
}
return pi;
final TileProductionCalculator tpc = new TileProductionCalculator(getOwner(),
colony.getProductionBonus());
final UnitType workerUnitType = getUnits().findFirst()
.map(Unit::getType)
.orElse(null);
return tpc.getBasicProductionInfo(workTile,
getGame().getTurn(),
new WorkerAssignment(workerUnitType, getProductionType()),
isColonyCenterTile());
}
/**
@ -456,30 +443,8 @@ public class ColonyTile extends WorkLocation {
@Override
public Stream<Modifier> getProductionModifiers(GoodsType goodsType,
UnitType unitType) {
if (!canProduce(goodsType, unitType)) return Stream.<Modifier>empty();
final Tile workTile = getWorkTile();
final TileType type = workTile.getType();
final String id = goodsType.getId();
final Colony colony = getColony();
final Player owner = colony.getOwner();
final Turn turn = getGame().getTurn();
return (unitType != null)
// Unit modifiers apply
? concat(workTile.getProductionModifiers(goodsType, unitType),
colony.getProductionModifiers(goodsType, unitType, this),
unitType.getModifiers(id, type, turn),
((owner == null) ? null
: owner.getModifiers(id, unitType, turn)))
// Unattended only possible in center, colony modifiers apply
: (isColonyCenterTile())
? concat(workTile.getProductionModifiers(goodsType, null),
colony.getProductionModifiers(goodsType, null, this),
colony.getModifiers(id, null, turn),
((owner == null) ? null
: owner.getModifiers(id, type, turn)))
// Otherwise impossible
: Stream.<Modifier>empty();
return new TileProductionCalculator(getOwner(), getColony().getProductionBonus())
.getProductionModifiers(getGame().getTurn(), workTile, goodsType, unitType);
}
/**

View File

@ -19,6 +19,25 @@
package net.sf.freecol.common.model;
import static net.sf.freecol.common.model.Constants.INFINITY;
import static net.sf.freecol.common.util.CollectionUtils.all;
import static net.sf.freecol.common.util.CollectionUtils.any;
import static net.sf.freecol.common.util.CollectionUtils.cacheInt;
import static net.sf.freecol.common.util.CollectionUtils.cachingDoubleComparator;
import static net.sf.freecol.common.util.CollectionUtils.concat;
import static net.sf.freecol.common.util.CollectionUtils.count;
import static net.sf.freecol.common.util.CollectionUtils.find;
import static net.sf.freecol.common.util.CollectionUtils.flatten;
import static net.sf.freecol.common.util.CollectionUtils.forEachMapEntry;
import static net.sf.freecol.common.util.CollectionUtils.isNotNull;
import static net.sf.freecol.common.util.CollectionUtils.matchKey;
import static net.sf.freecol.common.util.CollectionUtils.max;
import static net.sf.freecol.common.util.CollectionUtils.maximize;
import static net.sf.freecol.common.util.CollectionUtils.none;
import static net.sf.freecol.common.util.CollectionUtils.sum;
import static net.sf.freecol.common.util.CollectionUtils.transform;
import static net.sf.freecol.common.util.RandomUtils.randomShuffle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -39,11 +58,11 @@ import javax.xml.stream.XMLStreamException;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import static net.sf.freecol.common.model.Constants.*;
import static net.sf.freecol.common.util.CollectionUtils.*;
import net.sf.freecol.common.model.Constants.IntegrityType;
import net.sf.freecol.common.model.production.TileProductionCalculator;
import net.sf.freecol.common.model.production.WorkerAssignment;
import net.sf.freecol.common.util.LogBuilder;
import net.sf.freecol.common.util.RandomChoice;
import static net.sf.freecol.common.util.RandomUtils.*;
/**
@ -1712,12 +1731,23 @@ public final class Tile extends UnitLocation implements Named, Ownable {
*/
public int getPotentialProduction(GoodsType goodsType,
UnitType unitType) {
if (!canProduce(goodsType, unitType)) return 0;
int amount = getBaseProduction(null, goodsType, unitType);
amount = (int)applyModifiers(amount, getGame().getTurn(),
getProductionModifiers(goodsType, unitType));
return (amount < 0) ? 0 : amount;
final TileProductionCalculator tpc = new TileProductionCalculator(null, 0);
final ProductionType productionType = ProductionType.getBestProductionType(goodsType,
getType().getAvailableProductionTypes(unitType == null));
final boolean colonyCenterTile = (unitType == null);
final ProductionInfo pi = tpc.getBasicProductionInfo(this,
getGame().getTurn(),
new WorkerAssignment(unitType, productionType),
colonyCenterTile);
return pi.getProduction().stream()
.filter(a -> a.getType().equals(goodsType))
.map(AbstractGoods::getAmount)
.findFirst()
.orElse(0);
}
/**
@ -1871,11 +1901,24 @@ public final class Tile extends UnitLocation implements Named, Ownable {
*/
public AbstractGoods getBestFoodProduction() {
final Comparator<AbstractGoods> goodsComp
= Comparator.comparingInt(ag ->
getPotentialProduction(ag.getType(), null));
return maximize(flatten(getType().getAvailableProductionTypes(true),
ProductionType::getOutputs),
AbstractGoods::isFoodType, goodsComp);
= Comparator.comparingInt(ag ->
getPotentialProduction(ag.getType(), null));
return maximize(flatten(getType().getAvailableProductionTypes(true),
ProductionType::getOutputs),
AbstractGoods::isFoodType, goodsComp);
}
/**
* Get the best food type to produce here with an appropriate expert.,
*
* @return The {@code AbstractGoods} to produce.
*/
public AbstractGoods getMaximumPotentialFoodProductionWithExpert() {
return getSpecification().getFoodGoodsTypeList().stream()
.map(goodsType -> new AbstractGoods(goodsType,
getMaximumPotential(goodsType, getSpecification().getExpertForProducing(goodsType))))
.max(Comparator.comparingInt(AbstractGoods::getAmount))
.orElse(null);
}

View File

@ -383,21 +383,62 @@ public final class TileImprovementType extends FreeColSpecObjectType {
* @return The increase in production
*/
public int getImprovementValue(Tile tile, GoodsType goodsType) {
final UnitType colonistType
= getSpecification().getDefaultUnitType();
final UnitType colonistType = getSpecification().getDefaultUnitType();
return getImprovementValue(tile, goodsType, colonistType);
}
/**
* Gets the increase in production of the given GoodsType
* this tile improvement type would yield at a specified tile.
*
* @param tile The {@code Tile} to be considered.
* @param goodsType An optional preferred {@code GoodsType}.
* @param unitType The unit type working on the tile.
* @return The increase in production
*/
public int getImprovementValue(Tile tile, GoodsType goodsType, UnitType unitType) {
int value = 0;
if (goodsType.isFarmed()) {
final int oldProduction = tile.getType()
.getPotentialProduction(goodsType, colonistType);
.getPotentialProduction(goodsType, unitType);
TileType tt = getChange(tile.getType());
if (tt == null) { // simple bonus
int production = tile.getPotentialProduction(goodsType, colonistType);
int production = tile.getPotentialProduction(goodsType, unitType);
if (production > 0) {
float chg = apply(production, null, goodsType.getId());
value = (int)(chg - production);
}
} else { // tile type change
int chg = tt.getPotentialProduction(goodsType, colonistType)
int chg = tt.getPotentialProduction(goodsType, unitType)
- oldProduction;
value = chg;
}
}
return value;
}
/**
* Gets the increase in production of the given GoodsType
* this tile improvement type would yield at a specified tile.
*
* @param tile The {@code Tile} to be considered.
* @param goodsType An optional preferred {@code GoodsType}.
* @return The increase in production
*/
public int getImprovementValue(TileType tileType, GoodsType goodsType, UnitType unitType) {
int value = 0;
if (goodsType.isFarmed()) {
final int oldProduction = tileType
.getPotentialProduction(goodsType, unitType);
TileType tt = getChange(tileType);
if (tt == null) { // simple bonus
int production = tileType.getPotentialProduction(goodsType, unitType);
if (production > 0) {
float chg = apply(production, null, goodsType.getId());
value = (int)(chg - production);
}
} else { // tile type change
int chg = tt.getPotentialProduction(goodsType, unitType)
- oldProduction;
value = chg;
}

View File

@ -426,7 +426,15 @@ public final class TileType extends FreeColSpecObjectType
UnitType unitType) {
if (goodsType == null) return 0;
int amount = getBaseProduction(null, goodsType, unitType);
amount = (int)apply(amount, null, goodsType.getId(), unitType);
if (unitType != null) {
amount = (int) unitType.apply(amount, null, goodsType.getId(), unitType);
} else {
/*
* XXX: The feature container is always null for TileType. What was
* the desired behaviour here?
*/
amount = (int)apply(amount, null, goodsType.getId(), unitType);
}
return (amount < 0) ? 0 : amount;
}

View File

@ -0,0 +1,377 @@
/**
* 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.production;
import static net.sf.freecol.common.util.CollectionUtils.concat;
import static net.sf.freecol.common.util.CollectionUtils.count;
import static net.sf.freecol.common.util.CollectionUtils.find;
import static net.sf.freecol.common.util.CollectionUtils.isNotNull;
import static net.sf.freecol.common.util.CollectionUtils.map;
import static net.sf.freecol.common.util.CollectionUtils.matchKey;
import static net.sf.freecol.common.util.CollectionUtils.sum;
import static net.sf.freecol.common.util.CollectionUtils.transform;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import net.sf.freecol.common.model.Ability;
import net.sf.freecol.common.model.AbstractGoods;
import net.sf.freecol.common.model.BuildingType;
import net.sf.freecol.common.model.FeatureContainer;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.ProductionCache;
import net.sf.freecol.common.model.ProductionInfo;
import net.sf.freecol.common.model.ProductionType;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.Turn;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.option.GameOptions;
/**
* Calculates the production for a building of a given type.
*/
public class BuildingProductionCalculator {
private final static double EPSILON = 0.0001;
private Player owner;
private FeatureContainer colonyFeatureContainer;
private int colonyProductionBonus;
/**
* Creates a calculator for the given owner and colony data.
*
* @param owner The {@code Player} owning the building.
* @param colonyFeatureContainer The {@code FeatureContainer} for the colony where the
* building is located. This is used for applying bonus to the production.
* @param colonyProductionBonus The production bonus for the colony where the building
* is located.
*/
public BuildingProductionCalculator(Player owner, FeatureContainer colonyFeatureContainer, int colonyProductionBonus) {
this.owner = owner;
this.colonyFeatureContainer = colonyFeatureContainer;
this.colonyProductionBonus = colonyProductionBonus;
}
/**
* Gets the production information for a building taking account
* of the available input and output goods.
*
* @param buildingType The type of building.
* @param turn The current game turn.
* @param workerAssignments A list of workers assigned to the building.
* @param inputs The input goods available.
* @param outputs The output goods already available in the colony,
* necessary in order to avoid excess production.
* @param warehouseCapacity The storage capacity of the settlement
* producing the goods.
* @return The production information.
* @see ProductionCache#update
*/
public ProductionInfo getAdjustedProductionInfo(
BuildingType buildingType,
Turn turn,
List<WorkerAssignment> workerAssignments,
List<AbstractGoods> inputs,
List<AbstractGoods> outputs,
int warehouseCapacity) {
ProductionInfo result = new ProductionInfo();
final List<AbstractGoods> buildingOutputs = getOutputs(buildingType, workerAssignments);
if (buildingOutputs.isEmpty()) return result;
final List<AbstractGoods> buildingInputs = getInputs(buildingType, workerAssignments);
final Specification spec = buildingType.getSpecification();
final boolean avoidOverflow = buildingType.hasAbility(Ability.AVOID_EXCESS_PRODUCTION);
// Calculate two production ratios, the minimum (and actual)
// possible multiplier between the nominal input and output
// goods and the amount actually consumed and produced, and
// the maximum possible ratio that would apply but for
// circumstances such as limited input availability.
double maximumRatio = 0.0, minimumRatio = Double.MAX_VALUE;
// First, calculate the nominal production ratios.
if (buildingType.hasAbility(Ability.AUTO_PRODUCTION)) {
// Autoproducers are special
for (AbstractGoods output : transform(buildingOutputs.stream(),
AbstractGoods::isPositive)) {
final GoodsType goodsType = output.getType();
//int available = colony.getGoodsCount(goodsType);
int available = outputs.stream().filter(gt -> gt.getType().equals(goodsType)).findAny().map(AbstractGoods::getAmount).orElse(0);
if (available >= warehouseCapacity) {
minimumRatio = maximumRatio = 0.0;
} else {
int divisor = (int) buildingType.apply(0f, turn, Modifier.BREEDING_DIVISOR);
int factor = (int) buildingType.apply(0f, turn, Modifier.BREEDING_FACTOR);
int production = (available < goodsType.getBreedingNumber()
|| divisor <= 0) ? 0
// Deliberate use of integer division
: ((available - 1) / divisor + 1) * factor;
double newRatio = (double)production / output.getAmount();
minimumRatio = Math.min(minimumRatio, newRatio);
maximumRatio = Math.max(maximumRatio, newRatio);
}
}
} else {
for (AbstractGoods output : buildingOutputs) {
final GoodsType goodsType = output.getType();
float production = determineProduction(buildingType, workerAssignments, turn, goodsType);
// Beware! If we ever unify this code with ColonyTile,
// ColonyTiles have outputs with zero amount.
double newRatio = production / output.getAmount();
minimumRatio = Math.min(minimumRatio, newRatio);
maximumRatio = Math.max(maximumRatio, newRatio);
}
}
// Then reduce the minimum ratio if some input is in short supply.
for (AbstractGoods input : buildingInputs) {
long required = (long)Math.floor(input.getAmount() * minimumRatio);
long available = getAvailable(input.getType(), inputs);
// Do not allow auto-production to go negative.
if (buildingType.hasAbility(Ability.AUTO_PRODUCTION)) available = Math.max(0, available);
// Experts in factory level buildings may produce a
// certain amount of goods even when no input is available.
// Factories have the EXPERTS_USE_CONNECTIONS ability.
long minimumGoodsInput;
if (available < required
&& buildingType.hasAbility(Ability.EXPERTS_USE_CONNECTIONS)
&& spec.getBoolean(GameOptions.EXPERTS_HAVE_CONNECTIONS)
&& ((minimumGoodsInput = buildingType.getExpertConnectionProduction()
* count(workerAssignments.stream().map(WorkerAssignment::getUnitType),
matchKey(getExpertUnitType(buildingType))))
> available)) {
available = minimumGoodsInput;
}
// Scale production by limitations on availability.
if (available < required) {
minimumRatio *= (double)available / required;
//maximumRatio = Math.max(maximumRatio, minimumRatio);
}
}
// Check whether there is space enough to store the goods
// produced in order to avoid excess production.
if (avoidOverflow) {
for (AbstractGoods output : buildingOutputs) {
double production = output.getAmount() * minimumRatio;
if (production <= 0) continue;
double headroom = (double)warehouseCapacity
- getAvailable(output.getType(), outputs);
// Clamp production at warehouse capacity
if (production > headroom) {
minimumRatio = Math.min(minimumRatio,
headroom / output.getAmount());
}
production = output.getAmount() * maximumRatio;
if (production > headroom) {
maximumRatio = Math.min(maximumRatio,
headroom / output.getAmount());
}
}
}
for (AbstractGoods input : buildingInputs) {
GoodsType type = input.getType();
// maximize consumption
int consumption = (int)Math.floor(input.getAmount()
* minimumRatio + EPSILON);
int maximumConsumption = (int)Math.floor(input.getAmount()
* maximumRatio);
result.addConsumption(new AbstractGoods(type, consumption));
if (consumption < maximumConsumption) {
result.addMaximumConsumption(new AbstractGoods(type, maximumConsumption));
}
}
for (AbstractGoods output : buildingOutputs) {
GoodsType type = output.getType();
// minimize production, but add a magic little something
// to counter rounding errors
int production = (int)Math.floor(output.getAmount() * minimumRatio
+ EPSILON);
int maximumProduction = (int)Math.floor(output.getAmount()
* maximumRatio);
result.addProduction(new AbstractGoods(type, production));
if (production < maximumProduction) {
result.addMaximumProduction(new AbstractGoods(type, maximumProduction));
}
}
return result;
}
/**
* Gets the unit type that is the expert for this work location
* using its first output for which an expert type can be found.
*
* @return The expert {@code UnitType} or null if none found.
*/
public UnitType getExpertUnitType(BuildingType buildingType) {
final Specification spec = buildingType.getSpecification();
ProductionType pt = getBestProductionType(buildingType);
return (pt == null) ? null
: find(map(pt.getOutputs(),
ag -> spec.getExpertForProducing(ag.getType())),
isNotNull());
}
private ProductionType getBestProductionType(BuildingType buildingType) {
return ProductionType.getBestProductionType(null, buildingType.getAvailableProductionTypes(false));
}
private List<AbstractGoods> getOutputs(BuildingType buildingType, List<WorkerAssignment> workerAssignments) {
final List<AbstractGoods> unattendedOutputs = buildingType.getAvailableProductionTypes(true).stream()
.map(pt -> pt.getOutputList())
.flatMap(Collection::stream)
.collect(Collectors.toList());
final List<AbstractGoods> workerOutputs = workerAssignments.stream()
//.map(wa -> wa.getProductionType().getOutputList())
.map(WorkerAssignment::getProductionType)
/*
* XXX: This code is needed when a production type have yet to be
* chosen for the worker. But why are we calling this method
* in that case?
*/
.filter(Objects::nonNull)
.map(pt -> pt.getOutputList())
.flatMap(Collection::stream)
.collect(Collectors.toList());
final List<AbstractGoods> allOutputs = new ArrayList<>(unattendedOutputs);
allOutputs.addAll(workerOutputs);
final Map<GoodsType, Integer> amounts = new HashMap<>();
for (AbstractGoods ag : allOutputs) {
if (amounts.get(ag.getType()) == null) {
amounts.put(ag.getType(), 0);
}
amounts.put(ag.getType(), amounts.get(ag.getType()) + ag.getAmount());
}
return amounts.entrySet().stream().map(e -> new AbstractGoods(e.getKey(), e.getValue())).collect(Collectors.toList());
}
private List<AbstractGoods> getInputs(BuildingType buildingType, List<WorkerAssignment> workerAssignments) {
final List<AbstractGoods> unattendedInputs = buildingType.getAvailableProductionTypes(true).stream()
.map(pt -> pt.getInputList())
.flatMap(Collection::stream)
.collect(Collectors.toList());
final List<AbstractGoods> workerInputs = workerAssignments.stream()
//.map(wa -> wa.getProductionType().getInputList())
.map(WorkerAssignment::getProductionType)
/*
* XXX: This code is needed when a production type have yet to be
* chosen for the worker. But why are we calling this method
* in that case?
*/
.filter(Objects::nonNull)
.map(pt -> pt.getInputList())
.flatMap(Collection::stream)
.collect(Collectors.toList());
final List<AbstractGoods> allInputs = new ArrayList<>(unattendedInputs);
allInputs.addAll(workerInputs);
final Map<GoodsType, Integer> amounts = new HashMap<>();
for (AbstractGoods ag : allInputs) {
if (amounts.get(ag.getType()) == null) {
amounts.put(ag.getType(), 0);
}
amounts.put(ag.getType(), amounts.get(ag.getType()) + ag.getAmount());
}
return amounts.entrySet().stream().map(e -> new AbstractGoods(e.getKey(), e.getValue())).collect(Collectors.toList());
}
private int determineProduction(BuildingType buildingType, List<WorkerAssignment> workerAssignments, final Turn turn, final GoodsType goodsType) {
float production = sum(workerAssignments,
wa -> getUnitProduction(turn, buildingType, wa, goodsType));
// Unattended production always applies for buildings!
production += getBaseProduction(buildingType, null, goodsType, null);
production = FeatureContainer.applyModifiers(production, turn,
getProductionModifiers(turn, buildingType, goodsType, null));
return (int) Math.floor(production);
}
/**
* Convenience function to extract a goods amount from a list of
* available goods.
*
* @param type The {@code GoodsType} to extract the amount for.
* @param available The list of available goods to query.
* @return The goods amount, or zero if none found.
*/
private int getAvailable(GoodsType type, List<AbstractGoods> available) {
return AbstractGoods.getCount(type, available);
}
/**
* Gets the productivity of a unit working in this work location,
* considering *only* the contribution of the unit, exclusive of
* that of the work location.
*
* @param turn The current game turn.
* @param buildingType The type of building.
* @param workerAssignment The worker assigned to the building.
* @param goodsType The {@code GoodsType} to check the production of.
* @return The maximum return from this unit.
*/
private int getUnitProduction(Turn turn, BuildingType buildingType, WorkerAssignment workerAssignment, GoodsType goodsType) {
if (workerAssignment == null || workerAssignment.getProductionType().getOutputs().noneMatch(g -> goodsType.equals(g.getType()))) {
return 0;
}
return Math.max(0,
(int) FeatureContainer.applyModifiers(getBaseProduction(buildingType, workerAssignment.getProductionType(), goodsType, workerAssignment.getUnitType()),
turn,
getProductionModifiers(turn, buildingType, goodsType, workerAssignment.getUnitType())));
}
private int getBaseProduction(BuildingType buildingType, ProductionType productionType, GoodsType goodsType, UnitType unitType) {
return (buildingType == null) ? 0 : buildingType.getBaseProduction(productionType, goodsType, unitType);
}
private Stream<Modifier> getProductionModifiers(Turn turn, BuildingType buildingType, GoodsType goodsType,
UnitType unitType) {
final String id = (goodsType == null) ? null : goodsType.getId();
return (unitType != null)
// With a unit, unit specific bonuses apply
? concat(buildingType.getModifiers(id, unitType, turn),
ProductionUtils.getRebelProductionModifiers(colonyProductionBonus, goodsType, buildingType),
buildingType.getCompetenceModifiers(id, unitType, turn),
owner.getModifiers(id, unitType, turn))
// With no unit, only the building-specific bonuses
: concat(colonyFeatureContainer.getModifiers(id, buildingType, turn), // XXX: Can we simplify this?
owner.getModifiers(id, buildingType, turn));
}
}

View File

@ -0,0 +1,36 @@
package net.sf.freecol.common.model.production;
import java.util.stream.Stream;
import net.sf.freecol.common.model.BuildingType;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.Specification;
public final class ProductionUtils {
private ProductionUtils() {}
/**
* Gets the current production {@code Modifier}, which is
* generated from the current production bonus.
*
* @param colonyProductionBonus The production bonus in the colony.
* @param goodsType The {@code GoodsType} to produce.
* @param buildingType A {@code BuildingType} for getting a rebel factor. Use {@code null}
* for a tile.
* @return A stream of suitable {@code Modifier}s.
*/
public static Stream<Modifier> getRebelProductionModifiers(int colonyProductionBonus,
GoodsType goodsType, BuildingType buildingType) {
final float rebelFactor = (buildingType != null) ? buildingType.getRebelFactor() : 1.0F;
if (colonyProductionBonus == 0) return Stream.<Modifier>empty();
int bonus = (int)Math.floor(colonyProductionBonus * rebelFactor);
Modifier mod = new Modifier(goodsType.getId(), bonus,
Modifier.ModifierType.ADDITIVE,
Specification.SOL_MODIFIER_SOURCE);
mod.setModifierIndex(Modifier.COLONY_PRODUCTION_INDEX);
return Stream.of(mod);
}
}

View File

@ -0,0 +1,208 @@
/**
* 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.production;
import static net.sf.freecol.common.util.CollectionUtils.concat;
import static net.sf.freecol.common.util.CollectionUtils.forEach;
import static net.sf.freecol.common.util.CollectionUtils.map;
import java.util.stream.Stream;
import net.sf.freecol.common.model.AbstractGoods;
import net.sf.freecol.common.model.FeatureContainer;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.ProductionCache;
import net.sf.freecol.common.model.ProductionInfo;
import net.sf.freecol.common.model.ProductionType;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Turn;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.option.GameOptions;
/**
* Calculates the production for a tile.
*/
public class TileProductionCalculator {
private Player owner;
private int colonyProductionBonus;
/**
* Creates a calculator for the given owner and colony data.
*
* @param owner The {@code Player} owning the building.
* @param colonyProductionBonus The production bonus for the colony where the building
* is located.
*/
public TileProductionCalculator(Player owner, int colonyProductionBonus) {
this.owner = owner;
this.colonyProductionBonus = colonyProductionBonus;
}
/**
* Gets the basic production information for the colony tile,
* ignoring any colony limits (which for now, should be
* irrelevant).
*
* In the original game, the following special rules apply to
* colony center tiles:
* - All tile improvements contribute to the production of food
* - Only natural tile improvements, such as rivers, contribute
* to the production of other types of goods.
* - Artificial tile improvements, such as plowing, are ignored.
*
* @param tile The {@code Tile} where the production is happening.
* @param turn The current game turn.
* @param workerAssignment If any, the worker assign to this tile.
* @param colonyCenterTile If true, then the tile will autoproduce.
* @return The raw production of this colony tile.
* @see ProductionCache#update
*/
public ProductionInfo getBasicProductionInfo(Tile tile,
Turn turn,
WorkerAssignment workerAssignment,
boolean colonyCenterTile) {
ProductionInfo pi = new ProductionInfo();
if (workerAssignment.getProductionType() == null) {
/*
* XXX: It's silly that the production is calculated
* before the productionType is set.
*/
return pi;
}
if (colonyCenterTile) {
forEach(workerAssignment.getProductionType().getOutputs(), output -> {
boolean onlyNaturalImprovements = tile.getSpecification()
.getBoolean(GameOptions.ONLY_NATURAL_IMPROVEMENTS)
&& !output.getType().isFoodType();
int potential = output.getAmount();
if (tile.getTileItemContainer() != null) {
potential = tile.getTileItemContainer()
.getTotalBonusPotential(output.getType(), null,
potential, onlyNaturalImprovements);
}
potential += Math.max(0, colonyProductionBonus);
AbstractGoods production
= new AbstractGoods(output.getType(), potential);
pi.addProduction(production);
});
} else {
forEach(map(workerAssignment.getProductionType().getOutputs(), AbstractGoods::getType),
gt -> {
int n = getUnitProduction(turn, tile, workerAssignment, gt);
if (n > 0) pi.addProduction(new AbstractGoods(gt, n));
});
}
return pi;
}
/**
* Gets the productivity of a unit working in this work location,
* considering *only* the contribution of the unit, exclusive of
* that of the work location.
*
* Used below, only public for the test suite.
*
* @param turn The current game turn.
* @param tile The tile where the production is happening.
* @param workerAssignment If any, the worker assigned to the {@code Tile}.
* @param goodsType The {@code GoodsType} to check the production of.
* @return The maximum return from this unit.
*/
public int getUnitProduction(Turn turn, Tile tile, WorkerAssignment workerAssignment, GoodsType goodsType) {
if (workerAssignment == null
|| workerAssignment.getProductionType().getOutputs().noneMatch(g -> goodsType.equals(g.getType()))
|| workerAssignment.getUnitType() == null) {
return 0;
}
return Math.max(0, (int) FeatureContainer.applyModifiers(
getBaseProduction(tile, workerAssignment.getProductionType(), goodsType, workerAssignment.getUnitType()),
turn,
getProductionModifiers(turn, tile, goodsType, workerAssignment.getUnitType())));
}
/**
* Get the base production exclusive of any bonuses.
*
* @param tile The tile where the production is happening.
* @param productionType An optional {@code ProductionType} to use,
* if null the best available one is used.
* @param goodsType The {@code GoodsType} to produce.
* @param unitType An optional {@code UnitType} to use.
* @return The base production due to tile type and resources.
*/
private int getBaseProduction(Tile tile, ProductionType productionType,
GoodsType goodsType, UnitType unitType) {
if (tile == null || goodsType == null || !goodsType.isFarmed()) {
return 0;
}
final int amount = tile.getBaseProduction(productionType, goodsType, unitType);
return (amount < 0) ? 0 : amount;
}
/**
* Gets the production modifiers for the given type of goods and
* unit type.
*
* @param goodsType The {@code GoodsType} to produce.
* @param unitType The optional {@code UnitType} to produce them.
* @return A stream of the applicable modifiers.
*/
public Stream<Modifier> getProductionModifiers(Turn turn, Tile tile, GoodsType goodsType, UnitType unitType) {
if (unitType == null || !tile.canProduce(goodsType, unitType)) {
return Stream.<Modifier>empty();
}
return concat(tile.getProductionModifiers(goodsType, unitType),
ProductionUtils.getRebelProductionModifiers(colonyProductionBonus, goodsType, null),
unitType.getModifiers(goodsType.getId(), tile.getType(), turn),
((owner == null) ? null
: owner.getModifiers(goodsType.getId(), unitType, turn)));
}
/**
* Gets the production modifiers for the given type of goods on
* the colony center tile.
*
* @param goodsType The {@code GoodsType} to produce.
* @param unitType The optional {@code UnitType} to produce them.
* @return A stream of the applicable modifiers.
*/
public Stream<Modifier> getCenterTileProductionModifiers(Turn turn, Tile tile, GoodsType goodsType) {
if (!tile.canProduce(goodsType, null)) {
return Stream.<Modifier>empty();
}
return concat(tile.getProductionModifiers(goodsType, null),
ProductionUtils.getRebelProductionModifiers(colonyProductionBonus, goodsType, null),
// This does not seem to influence center tile production, but was present in the old code.
//colony.getModifiers(id, null, turn),
((owner == null) ? null
: owner.getModifiers(goodsType.getId(), tile.getType(), turn)));
}
}

View File

@ -0,0 +1,43 @@
/**
* 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.production;
import net.sf.freecol.common.model.ProductionType;
import net.sf.freecol.common.model.UnitType;
public class WorkerAssignment {
private final UnitType unitType;
private final ProductionType productionType;
public WorkerAssignment(UnitType unitType, ProductionType productionType) {
this.unitType = unitType;
this.productionType = productionType;
}
public UnitType getUnitType() {
return unitType;
}
public ProductionType getProductionType() {
return productionType;
}
}

View File

@ -19,10 +19,13 @@
package net.sf.freecol.common.model;
import static net.sf.freecol.common.util.CollectionUtils.any;
import static net.sf.freecol.common.util.CollectionUtils.count;
import static net.sf.freecol.common.util.CollectionUtils.matchKeyEquals;
import java.util.List;
import java.util.stream.Stream;
import static net.sf.freecol.common.util.CollectionUtils.*;
import net.sf.freecol.util.test.FreeColTestCase;
import net.sf.freecol.util.test.FreeColTestUtils;
@ -323,14 +326,24 @@ public class TileTest extends FreeColTestCase {
tile2.getPotentialProduction(grain, null));
assertEquals("Plains/grain max", 6,
tile2.getMaximumPotential(grain, null));
assertEquals("Plains/grain/colonist", 5,
tile2.getPotentialProduction(grain, colonistType));
assertEquals("Plains/grain/colonist max", 6,
tile2.getMaximumPotential(grain, colonistType));
assertEquals("Plains/grain/expertFarmer", 8,
tile2.getPotentialProduction(grain, expertFarmerType));
tile2.addResource(new Resource(game, tile2, grainResource));
assertEquals("Plains+Resource/grain", 7,
tile2.getPotentialProduction(grain, null));
assertEquals("Plains+Resource/grain max", 8,
tile2.getMaximumPotential(grain, null));
assertEquals("Plains+Resource/grain/expertFarmer", 9,
assertEquals("Plains+Resource/grain/colonist", 7,
tile2.getPotentialProduction(grain, colonistType));
assertEquals("Plains+Resource/grain/colonist max", 8,
tile2.getMaximumPotential(grain, colonistType));
assertEquals("Plains+Resource/grain/expertFarmer", 12,
tile2.getPotentialProduction(grain, expertFarmerType));
assertEquals("Plains+Resource/grain/expertFarmer max", 10,
assertEquals("Plains+Resource/grain/expertFarmer max", 13,
tile2.getMaximumPotential(grain, expertFarmerType));
Tile tile3 = new Tile(game, plainsForest, 1, 1);