/*******************************************************************************
* Copyright (c) 2016 - 2017
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*******************************************************************************/
package jsettlers.ai.economy;
import jsettlers.ai.highlevel.AiStatistics;
import jsettlers.common.buildings.EBuildingType;
import jsettlers.common.material.EMaterialType;
import jsettlers.common.movable.EMovableType;
import jsettlers.logic.player.Player;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import static jsettlers.common.buildings.EBuildingType.*;
import static jsettlers.common.buildings.EBuildingType.IRONMELT;
import static jsettlers.common.buildings.EBuildingType.WEAPONSMITH;
/**
* This economy minister is as optimized as possible to create fast and many level 3 soldiers with high combat strength. It builds a longterm economy
* with rush defence if needed. It starts with 8 lumberjacks first, then it builds mana. Then food, weapons, more lumberjacks and gold economy is
* build in parallel until the full amount of possible buildings of the map is reached. If the map is smaller than 8 lumberjacks, it builds weapon
* smiths before mana. The minister is down sizable by a weapon smiths factor and a building industry factor.
*
* @author codingberlin
*/
public class BuildingListEconomyMinister implements EconomyMinister {
private static final Collection<EBuildingType> RUSH_DEFENCE_BUILDINGS =
EnumSet.of(LUMBERJACK, SAWMILL, STONECUTTER, IRONMELT, WEAPONSMITH, BARRACK, SMALL_LIVINGHOUSE, COALMINE, IRONMINE, MEDIUM_LIVINGHOUSE);
private static final Collection<EBuildingType> BUILDING_INDUSTRY =
EnumSet.of(LUMBERJACK, FORESTER, SAWMILL, STONECUTTER);
private int[] mapBuildingCounts;
private final List<EBuildingType> buildingsToBuild;
private int numberOfMidGameStoneCutters = 0;
private AiStatistics aiStatistics;
private float buildingIndustryFactor;
private boolean limitByWeakestEnemy;
private byte playerId;
private float weaponSmithFactor;
private boolean isHighGoodsGame;
private boolean isMiddleGoodsGame;
/**
* @param weaponSmithFactor
* influences the power of the AI. Use 1 for full power. Use < 1 for weaker AIs. The factor is used to determine the maximum amount of
* weapon smiths build on the map and shifts the point of time when the weapon smiths are build.
* @param buildingIndustryFactor
* influences the power of the AI. Use 1 for full power. Use < 1 for weaker AIs. The factor is used to determine the maximum amount of
* building industry buildings. If the AI e.g. builds less lumberjacks it is slower.
* @param limitByWeakestEnemy
* when set limits the AI in all amounts of buildings by the average building count of all alive enemies.
*/
public BuildingListEconomyMinister(
AiStatistics aiStatistics, Player player, float weaponSmithFactor, float buildingIndustryFactor, boolean limitByWeakestEnemy) {
this.aiStatistics = aiStatistics;
this.buildingIndustryFactor = buildingIndustryFactor;
this.limitByWeakestEnemy = limitByWeakestEnemy;
this.playerId = player.playerId;
this.weaponSmithFactor = weaponSmithFactor;
this.buildingsToBuild = new ArrayList<>();
this.isHighGoodsGame = isHighGoodsGame();
this.isMiddleGoodsGame = isMiddleGoodsGame();
}
@Override
public void update() {
buildingsToBuild.clear();
this.mapBuildingCounts = aiStatistics.getAiMapInformation().getBuildingCounts(playerId);
addMinimalBuildingMaterialBuildings();
if (isVerySmallMap()) {
addSmallWeaponProduction();
addFoodAndBuildingMaterialAndWeaponAndGoldIndustry();
addManaBuildings();
} else {
addManaBuildings();
addFoodAndBuildingMaterialAndWeaponAndGoldIndustry();
addSecondToolSmith();
}
}
private void addSecondToolSmith() {
if (mapBuildingCounts[WEAPONSMITH.ordinal] * weaponSmithFactor >= 16) {
buildingsToBuild.add(80, TOOLSMITH);
}
}
@Override
public boolean isEndGame() {
double remaningGrass = aiStatistics.getAiMapInformation().getRemainingGrassTiles(aiStatistics, playerId)
- aiStatistics.getTreesForPlayer(playerId).size()
- aiStatistics.getStonesForPlayer(playerId).size();
double availableGrass = aiStatistics.getAiMapInformation().getGrassTilesOf(playerId);
return remaningGrass / availableGrass <= 0.6F;
}
private void addFoodAndBuildingMaterialAndWeaponAndGoldIndustry() {
List<EBuildingType> weaponsBuildings = determineWeaponAndGoldBuildings();
List<EBuildingType> foodBuildings = determineFoodBuildings();
List<EBuildingType> buildingMaterialBuildings = determineBuildingMaterialBuildings();
float allBuildingsCount = foodBuildings.size() + buildingMaterialBuildings.size() + weaponsBuildings.size();
float weaponsBuildingsRatio = ((float) weaponsBuildings.size()) / allBuildingsCount;
float foodBuildingsRatio = ((float) foodBuildings.size()) / (allBuildingsCount * weaponSmithFactor);
float buildingMaterialBuildingRatio = ((float) buildingMaterialBuildings.size()) / (allBuildingsCount * weaponSmithFactor);
int maxSize = Math.max(foodBuildings.size(), Math.max(buildingMaterialBuildings.size(), weaponsBuildings.size()));
for (int i = 0; i < maxSize; i++) {
if (weaponsBuildingsRatio > foodBuildingsRatio && weaponsBuildingsRatio > buildingMaterialBuildingRatio) {
mergeAndAddNextItems(weaponsBuildings, foodBuildings, foodBuildingsRatio, buildingMaterialBuildings, buildingMaterialBuildingRatio);
} else if (buildingMaterialBuildingRatio > foodBuildingsRatio && buildingMaterialBuildingRatio > weaponsBuildingsRatio) {
mergeAndAddNextItems(buildingMaterialBuildings, weaponsBuildings, weaponsBuildingsRatio, foodBuildings, foodBuildingsRatio);
} else {
mergeAndAddNextItems(foodBuildings, weaponsBuildings, weaponsBuildingsRatio, buildingMaterialBuildings, buildingMaterialBuildingRatio);
}
}
numberOfMidGameStoneCutters = (currentCountOf(STONECUTTER) / 2);
}
private List<EBuildingType> determineBuildingMaterialBuildings() {
List<EBuildingType> buildingMaterialBuildings = new ArrayList<>();
for (int i = 0; i < Math.ceil(mapBuildingCounts[LUMBERJACK.ordinal] * buildingIndustryFactor) - 8; i++) {
buildingMaterialBuildings.add(LUMBERJACK);
if (i % 3 == 1)
buildingMaterialBuildings.add(FORESTER);
if (i % 2 == 1)
buildingMaterialBuildings.add(SAWMILL);
if (i % 2 == 1)
buildingMaterialBuildings.add(STONECUTTER);
}
return buildingMaterialBuildings;
}
private List<EBuildingType> determineFoodBuildings() {
List<EBuildingType> foodBuildings = new ArrayList<>();
for (int i = 0; i < mapBuildingCounts[FISHER.ordinal]; i++) {
foodBuildings.add(FISHER);
}
for (int i = 0; i < mapBuildingCounts[FARM.ordinal]; i++) {
foodBuildings.add(FARM);
if (i % 2 == 0)
foodBuildings.add(WATERWORKS);
if (i % 3 == 0)
foodBuildings.add(MILL);
if (i % 3 == 0)
foodBuildings.add(BAKER);
if (i % 3 == 0)
foodBuildings.add(BAKER);
if (i % 3 == 1)
foodBuildings.add(BAKER);
if (i % 6 == 1 || i % 6 == 2 || i % 6 == 5)
foodBuildings.add(PIG_FARM);
if (i % 6 == 1)
foodBuildings.add(SLAUGHTERHOUSE);
}
return foodBuildings;
}
private List<EBuildingType> determineWeaponAndGoldBuildings() {
List<EBuildingType> weaponsBuildings = new ArrayList<>();
for (int i = 0; i < (mapBuildingCounts[WEAPONSMITH.ordinal] * weaponSmithFactor); i++) {
weaponsBuildings.add(COALMINE);
if (i % 2 == 0)
weaponsBuildings.add(IRONMINE);
weaponsBuildings.add(IRONMELT);
if (i == 0 && currentCountOf(TOOLSMITH) < 1)
weaponsBuildings.add(TOOLSMITH);
weaponsBuildings.add(WEAPONSMITH);
if (i % 3 == 0)
weaponsBuildings.add(BARRACK);
if (i == 2)
weaponsBuildings.add(TOWER);
if (i == 6)
weaponsBuildings.add(BIG_TOWER);
if (i == 16)
weaponsBuildings.add(CASTLE);
if (i == 3)
addGoldBuildings(weaponsBuildings);
}
if (mapBuildingCounts[WEAPONSMITH.ordinal] < 4)
addGoldBuildings(weaponsBuildings);
return weaponsBuildings;
}
private void addSmallWeaponProduction() {
buildingsToBuild.add(FISHER);
buildingsToBuild.add(COALMINE);
buildingsToBuild.add(IRONMINE);
buildingsToBuild.add(IRONMELT);
buildingsToBuild.add(WEAPONSMITH);
buildingsToBuild.add(BARRACK);
}
protected int currentCountOf(EBuildingType targetBuildingType) {
int result = 0;
for (EBuildingType buildingType : buildingsToBuild) {
if (buildingType == targetBuildingType) {
result++;
}
}
return result;
}
protected void addIfPossible(EBuildingType buildingType) {
if (buildingType.isMilitaryBuilding()) {
buildingsToBuild.add(buildingType);
} else {
float factor = 1F;
if (BUILDING_INDUSTRY.contains(buildingType)) {
factor = buildingIndustryFactor;
}
double currentCount = currentCountOf(buildingType);
if (currentCount < Math.ceil(mapBuildingCounts[buildingType.ordinal] * factor)
&& currentCount < maximumAllowedCount(buildingType)) {
buildingsToBuild.add(buildingType);
}
}
}
private int maximumAllowedCount(EBuildingType buildingType) {
if (!limitByWeakestEnemy) {
return Integer.MAX_VALUE;
}
List<Byte> enemies = aiStatistics.getAliveEnemiesOf(playerId);
float sumOfBuildings = 0;
for (byte playerId : enemies) {
sumOfBuildings += aiStatistics.getTotalNumberOfBuildingTypeForPlayer(buildingType, playerId);
}
return Math.max(1, (int) (sumOfBuildings / enemies.size()));
}
private boolean isVerySmallMap() {
return mapBuildingCounts[LUMBERJACK.ordinal] < 8;
}
private void addManaBuildings() {
for (int i = 0; i < mapBuildingCounts[WINEGROWER.ordinal]; i++) {
addIfPossible(WINEGROWER);
}
for (int i = 0; i < mapBuildingCounts[WINEGROWER.ordinal]; i++) {
addIfPossible(TEMPLE);
}
if (mapBuildingCounts[BIG_TEMPLE.ordinal] > 0) {
addIfPossible(BIG_TEMPLE);
}
}
private void mergeAndAddNextItems(
List<EBuildingType> dominantBuildingList,
List<EBuildingType> slaveBuildingListA, float targetSlaveRatioA,
List<EBuildingType> slaveBuildingListB, float targetSlaveRatioB) {
addFirstItemToBuildingList(dominantBuildingList);
float allBuildingsCount = dominantBuildingList.size() + slaveBuildingListA.size() + slaveBuildingListB.size();
if (allBuildingsCount == 0)
return;
float currentSlaveRatioA = ((float) slaveBuildingListA.size()) / allBuildingsCount;
float currentSlaveRatioB = ((float) slaveBuildingListB.size()) / allBuildingsCount;
if (currentSlaveRatioA > targetSlaveRatioA) {
addFirstItemToBuildingList(slaveBuildingListA);
}
if (currentSlaveRatioB > targetSlaveRatioB) {
addFirstItemToBuildingList(slaveBuildingListB);
}
}
private void addFirstItemToBuildingList(List<EBuildingType> buildingList) {
if (buildingList.size() > 0) {
addIfPossible(buildingList.remove(0));
}
}
private void addMinimalBuildingMaterialBuildings() {
buildingsToBuild.add(TOWER); // Start Tower
if (isHighGoodsGame) {
addIfPossible(LUMBERJACK);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
buildingsToBuild.add(MEDIUM_LIVINGHOUSE);
addIfPossible(STONECUTTER);
addIfPossible(LUMBERJACK);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
addIfPossible(FORESTER);
addIfPossible(LUMBERJACK);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
addIfPossible(STONECUTTER);
addIfPossible(STONECUTTER);
addIfPossible(STONECUTTER);
addIfPossible(STONECUTTER);
} else if (isMiddleGoodsGame) {
addIfPossible(LUMBERJACK);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
buildingsToBuild.add(MEDIUM_LIVINGHOUSE);
addIfPossible(STONECUTTER);
buildingsToBuild.add(MEDIUM_LIVINGHOUSE);
addIfPossible(LUMBERJACK);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
addIfPossible(STONECUTTER);
addIfPossible(IRONMELT);
addIfPossible(TOOLSMITH);
addIfPossible(FORESTER);
addIfPossible(LUMBERJACK);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
addIfPossible(STONECUTTER);
addIfPossible(STONECUTTER);
addIfPossible(STONECUTTER);
} else {
addIfPossible(LUMBERJACK);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
buildingsToBuild.add(SMALL_LIVINGHOUSE);
addIfPossible(STONECUTTER);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
buildingsToBuild.add(MEDIUM_LIVINGHOUSE);
buildingsToBuild.add(MEDIUM_LIVINGHOUSE);
addIfPossible(IRONMELT);
addIfPossible(TOOLSMITH);
addIfPossible(COALMINE);
addIfPossible(IRONMINE);
addIfPossible(FISHER);
addIfPossible(FARM);
addIfPossible(WATERWORKS);
addIfPossible(MILL);
addIfPossible(BAKER);
addIfPossible(COALMINE);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
addIfPossible(STONECUTTER);
addIfPossible(LUMBERJACK);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
addIfPossible(LUMBERJACK);
addIfPossible(SAWMILL);
addIfPossible(LUMBERJACK);
addIfPossible(FORESTER);
buildingsToBuild.add(MEDIUM_LIVINGHOUSE);
addIfPossible(STONECUTTER);
addIfPossible(STONECUTTER);
addIfPossible(STONECUTTER);
}
}
private boolean isHighGoodsGame() {
return aiStatistics.getNumberOfMaterialTypeForPlayer(EMaterialType.AXE, playerId) >= 8 &&
aiStatistics.getNumberOfMaterialTypeForPlayer(EMaterialType.SAW, playerId) >= 3 &&
aiStatistics.getNumberOfMaterialTypeForPlayer(EMaterialType.PICK, playerId) >= 5;
}
private boolean isMiddleGoodsGame() {
return aiStatistics.getNumberOfMaterialTypeForPlayer(EMaterialType.AXE, playerId) >= 6 &&
aiStatistics.getNumberOfMaterialTypeForPlayer(EMaterialType.SAW, playerId) >= 2 &&
aiStatistics.getNumberOfMaterialTypeForPlayer(EMaterialType.PICK, playerId) >= 4;
}
private void addGoldBuildings(List<EBuildingType> weaponsBuildings) {
if (mapBuildingCounts[GOLDMELT.ordinal] > 0) {
weaponsBuildings.add(GOLDMINE);
for (int ii = 0; ii < mapBuildingCounts[GOLDMELT.ordinal]; ii++) {
weaponsBuildings.add(GOLDMELT);
weaponsBuildings.add(STOCK);
}
}
}
@Override
public int getNumberOfParallelConstructionSites() {
if (aiStatistics.getNumberOfMaterialTypeForPlayer(EMaterialType.PLANK, playerId) > 1
&& aiStatistics.getNumberOfMaterialTypeForPlayer(EMaterialType.STONE, playerId) > 1) {
// If plank and stone is still offered, we can build the next building.
// If the next building will consume all remaining offers we won't return 100 in the next tick
return 100;
}
return Math.max((int) Math.ceil((float) aiStatistics.getNumberOfBuildingTypeForPlayer(LUMBERJACK, playerId) / 2F), 2);
}
@Override
public List<EBuildingType> getBuildingsToBuild() {
if (isDanger()) {
return prefixBuildingsToBuildWithRushDefence();
} else {
return buildingsToBuild;
}
}
private boolean isDanger() {
if (aiStatistics.getTotalNumberOfBuildingTypeForPlayer(BARRACK, playerId) < 1) {
int numberOfSwordsmen = aiStatistics.getMovablePositionsByTypeForPlayer(EMovableType.SWORDSMAN_L1, playerId).size()
+ aiStatistics.getMovablePositionsByTypeForPlayer(EMovableType.SWORDSMAN_L2, playerId).size()
+ aiStatistics.getMovablePositionsByTypeForPlayer(EMovableType.SWORDSMAN_L3, playerId).size();
if (aiStatistics.getTotalNumberOfBuildingTypeForPlayer(TOWER, playerId) >= numberOfSwordsmen) {
return true;
}
for (byte enemy : aiStatistics.getEnemiesOf(playerId)) {
if (aiStatistics.getNumberOfBuildingTypeForPlayer(WEAPONSMITH, enemy) > 0) {
return true;
}
}
}
return false;
}
private List<EBuildingType> prefixBuildingsToBuildWithRushDefence() {
List<EBuildingType> allBuildingsToBuild = new ArrayList<EBuildingType>(RUSH_DEFENCE_BUILDINGS.size() + buildingsToBuild.size());
allBuildingsToBuild.addAll(RUSH_DEFENCE_BUILDINGS);
allBuildingsToBuild.addAll(buildingsToBuild);
return allBuildingsToBuild;
}
@Override
public int getMidGameNumberOfStoneCutters() {
return numberOfMidGameStoneCutters;
}
@Override
public boolean automaticLivingHousesEnabled() {
return aiStatistics.getNumberOfBuildingTypeForPlayer(LUMBERJACK, playerId) >= 8
|| aiStatistics.getNumberOfBuildingTypeForPlayer(LUMBERJACK, playerId) >= mapBuildingCounts[LUMBERJACK.ordinal]
|| aiStatistics.getNumberOfBuildingTypeForPlayer(WEAPONSMITH, playerId) >= 1;
}
@Override
public String toString() {
return this.getClass().getName();
}
}