From 3f6fe234b36c0e0108451b2fd9732e9a9dbd20ea Mon Sep 17 00:00:00 2001 From: Interdice <67503847+Interdice@users.noreply.github.com> Date: Sun, 12 Dec 2021 21:27:09 +1100 Subject: [PATCH] AI for Inquisitor and Missionary (#5590) * First pull request to add missionaries and inquisitors * First pull request to add missionaries and inquisitors * First pull request to add missionaries and inquisitors * First pull request to add missionaries and inquisitors * Ai choice * some fixes * some fixes * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * adding a system for the ai to buy with faith * finally done * finally done * some fixes * some fixes * some fixes * some fixes * some fixes * some fixes * some fixes --- .../automation/ConstructionAutomation.kt | 64 ++++++++++++- .../automation/SpecificUnitAutomation.kt | 89 +++++++++++++++++++ .../unciv/logic/automation/UnitAutomation.kt | 8 ++ core/src/com/unciv/logic/city/CityReligion.kt | 8 ++ .../unciv/ui/worldscreen/unit/UnitActions.kt | 5 +- 5 files changed, 170 insertions(+), 4 deletions(-) diff --git a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt index 93e384bfbf..1796d35849 100644 --- a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt @@ -2,6 +2,7 @@ package com.unciv.logic.automation import com.unciv.UncivGame import com.unciv.logic.city.CityConstructions +import com.unciv.logic.city.INonPerpetualConstruction import com.unciv.logic.city.PerpetualConstruction import com.unciv.logic.civilization.CityAction import com.unciv.logic.civilization.NotificationIcon @@ -10,6 +11,7 @@ import com.unciv.logic.map.BFS import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.VictoryType import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import kotlin.math.max import kotlin.math.min @@ -39,12 +41,15 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val relativeCostEffectiveness = ArrayList() + private val faithConstruction = arrayListOf() + data class ConstructionChoice(val choice:String, var choiceModifier:Float,val remainingWork:Int) fun addChoice(choices:ArrayList, choice:String, choiceModifier: Float){ choices.add(ConstructionChoice(choice,choiceModifier,cityConstructions.getRemainingWork(choice))) } + fun chooseNextConstruction() { if (!UncivGame.Current.settings.autoAssignCityProduction && civInfo.playerType == PlayerType.Human && !cityInfo.isPuppet @@ -62,6 +67,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ addCultureBuildingChoice() addSpaceshipPartChoice() addOtherBuildingChoice() + addReligousUnit() if (!cityInfo.isPuppet) { addWondersChoice() @@ -94,14 +100,24 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ NotificationIcon.Construction ) cityConstructions.currentConstructionFromQueue = chosenConstruction + + if (civInfo.isPlayerCivilization()) return // don't want the ai to control what a player uses faith for + + val chosenItem = faithConstruction.asSequence() + .filterNotNull() + .filter { it.getStatBuyCost(cityInfo, stat = Stat.Faith)!! <= civInfo.religionManager.storedFaith } + .firstOrNull() ?: return + + + cityConstructions.purchaseConstruction(chosenItem.name, -1, false, stat=Stat.Faith) + } private fun addMilitaryUnitChoice() { if (!isAtWar && !cityIsOverAverageProduction) return // don't make any military units here. Infrastructure first! if ((!isAtWar && civInfo.statsForNextTurn.gold > 0 && militaryUnits < max(5, cities * 2)) || (isAtWar && civInfo.gold > -50)) { - val militaryUnit = Automation.chooseMilitaryUnit(cityInfo) - if (militaryUnit == null) return + val militaryUnit = Automation.chooseMilitaryUnit(cityInfo) ?: return val unitsToCitiesRatio = cities.toFloat() / (militaryUnits + 1) // most buildings and civ units contribute the the civ's growth, military units are anti-growth var modifier = sqrt(unitsToCitiesRatio) / 2 @@ -322,4 +338,48 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ } } + private fun addReligousUnit(){ + + var modifier = 0f + + val missionary = cityInfo.getRuleset().units.values.asSequence() + .firstOrNull { it -> it.canBePurchasedWithStat(cityInfo, Stat.Faith) + && it.getMatchingUniques("Can [] [] times").any { it.params[0] == "Spread Religion"} } + + + val inquisitor = cityInfo.getRuleset().units.values.asSequence() + .firstOrNull { it.canBePurchasedWithStat(cityInfo, Stat.Faith) + && it.hasUnique("Prevents spreading of religion to the city it is next to") } + + + + // these 4 if conditions are used to determine if an AI should buy units to spread religion, or spend faith to buy things like new military units or new buildings. + // currently this AI can only buy inquisitors and missionaries with faith + // this system will have to be reengineered to support buying other stuff with faith + if (preferredVictoryType == VictoryType.Domination) return + if (civInfo.religionManager.religion?.name == null) return + if (preferredVictoryType == VictoryType.Cultural) modifier += 1 + if (isAtWar) modifier -= 0.5f + if (cityInfo.religion.getMajorityReligion()?.name != civInfo.religionManager.religion?.name) + return // you don't want to build units of opposing religions. + + + val citiesNotFollowingOurReligion = civInfo.cities.asSequence() + .filterNot { it.religion.getMajorityReligion()?.name == civInfo.religionManager.religion!!.name } + + val buildInqusitor = citiesNotFollowingOurReligion + .filter { it.religion.getMajorityReligion()?.name == civInfo.religionManager.religion?.name } + .toList().size.toFloat() / 10 + modifier + + val possibleSpreadReligionTargets = civInfo.gameInfo.getCities() + .filter { it.getCenterTile().aerialDistanceTo(cityInfo.getCenterTile()) < 30 } + + val buildMissionary = possibleSpreadReligionTargets.toList().size.toFloat() / 15 + modifier + + if (buildMissionary > buildInqusitor && missionary != null) faithConstruction.add(missionary) + else if(inquisitor != null) faithConstruction.add(inquisitor) + + + } + } \ No newline at end of file diff --git a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt index 83ea486b95..a4cd2d9834 100644 --- a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt @@ -1,12 +1,19 @@ package com.unciv.logic.automation +import com.badlogic.gdx.math.Vector2 +import com.unciv.Constants import com.unciv.logic.battle.Battle import com.unciv.logic.battle.MapUnitCombatant +import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.ReligionManager import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.TileMap +import com.unciv.models.UnitAction +import com.unciv.models.UnitActionType import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.stats.Stat @@ -280,6 +287,81 @@ object SpecificUnitAutomation { } } + fun automateMissionary(unit: MapUnit){ + if (unit.religion != unit.civInfo.religionManager.religion?.name) + return unit.destroy() + + val cities = unit.civInfo.gameInfo.getCities().asSequence() + .filter { it.religion.getMajorityReligion()?.name != unit.getReligionDisplayName() } + .filterNot { it.civInfo.isAtWarWith(unit.civInfo) } + .minByOrNull { it.getCenterTile().aerialDistanceTo(unit.currentTile) } ?: return + + + val destination = cities.getTiles().asSequence() + .filterNot { unit.getTile().owningCity == it.owningCity } // to prevent the ai from moving around randomly + .filter { unit.movement.canMoveTo(it) } + .minByOrNull { it.aerialDistanceTo(unit.currentTile) } + + + if (destination != null) { + unit.movement.headTowards(destination) + } + + if (unit.currentTile.owningCity?.religion?.getMajorityReligion()?.name != unit.religion) + doReligiousAction(unit, unit.getTile()) + } + + fun automateInquisitor(unit: MapUnit){ + val cityToConvert = unit.civInfo.cities.asSequence() + .filterNot { it.religion.getMajorityReligion()?.name == null } + .filterNot { it.religion.getMajorityReligion()?.name == unit.religion } + .minByOrNull { it.getCenterTile().aerialDistanceTo(unit.currentTile) } + + val cityToProtect = unit.civInfo.cities.asSequence() + .filter { it.religion.getMajorityReligion()?.name == unit.religion } + .filter { isInquisitorInTheCity(it, unit)} + .maxByOrNull { it.population.population } // cities with most populations will be prioritized by the AI + + var destination: TileInfo? = null + + if (cityToProtect != null){ + destination = cityToProtect.getCenterTile().neighbors.asSequence() + .filterNot { unit.getTile().owningCity == it.owningCity } // to prevent the ai from moving around randomly + .filter { unit.movement.canMoveTo(it) } + .minByOrNull { it.aerialDistanceTo(unit.currentTile) } + } + if (destination == null){ + if (cityToConvert == null) return + destination = cityToConvert.getCenterTile().neighbors.asSequence() + .filterNot { unit.getTile().owningCity == it.owningCity } // to prevent the ai from moving around randomly + .filter { unit.movement.canMoveTo(it) } + .minByOrNull { it.aerialDistanceTo(unit.currentTile) } + } + + if (destination != null) + unit.movement.headTowards(destination) + + + if (cityToConvert != null && unit.currentTile.getCity() == destination!!.getCity()){ + doReligiousAction(unit, destination) + } + + + } + + private fun isInquisitorInTheCity(city: CityInfo, unit: MapUnit): Boolean { + if (!city.religion.isProtectedByInquisitor()) + return false + + for (tile in city.getCenterTile().neighbors) + if (unit.currentTile == tile) + return true + return false + } + + + + fun automateFighter(unit: MapUnit) { val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange()) val enemyAirUnitsInRange = tilesInRange @@ -420,4 +502,11 @@ object SpecificUnitAutomation { UnitActions.getEnhanceReligionAction(unit)() } + + private fun doReligiousAction(unit: MapUnit, destination: TileInfo){ + val actionList: java.util.ArrayList = ArrayList() + UnitActions.addActionsWithLimitedUses(unit, actionList, destination) + if (actionList.firstOrNull()?.action == null) return + actionList.first().action!!.invoke() + } } diff --git a/core/src/com/unciv/logic/automation/UnitAutomation.kt b/core/src/com/unciv/logic/automation/UnitAutomation.kt index 3821673b90..b57c1c8fb4 100644 --- a/core/src/com/unciv/logic/automation/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/UnitAutomation.kt @@ -159,6 +159,14 @@ object UnitAutomation { if (unit.hasUnique("Can construct []")) return SpecificUnitAutomation.automateImprovementPlacer(unit) // includes great people plus moddable units + if (unit.getMatchingUniques("Can [] [] times").any{ it.params[0] == "Spread Religion" }) + return SpecificUnitAutomation.automateMissionary(unit) + + if (unit.hasUnique("Prevents spreading of religion to the city it is next to")) + return SpecificUnitAutomation.automateInquisitor(unit) + + + // ToDo: automation of great people skills (may speed up construction, provides a science boost, etc.) return // The AI doesn't know how to handle unknown civilian units diff --git a/core/src/com/unciv/logic/city/CityReligion.kt b/core/src/com/unciv/logic/city/CityReligion.kt index fdfc340f89..76bc78477d 100644 --- a/core/src/com/unciv/logic/city/CityReligion.kt +++ b/core/src/com/unciv/logic/city/CityReligion.kt @@ -284,6 +284,14 @@ class CityInfoReligionManager { } return addedPressure } + + fun isProtectedByInquisitor(): Boolean { + for (tile in cityInfo.getCenterTile().neighbors) + if (tile.civilianUnit?.hasUnique("Prevents spreading of religion to the city it is next to") == true) + return true + if (cityInfo.getCenterTile().civilianUnit?.name == "Inquisitor") return true + return false + } private fun pressureAmountToAdjacentCities(pressuredCity: CityInfo): Int { var pressure = pressureFromAdjacentCities.toFloat() diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index dd57018889..480e0d6f1b 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -528,7 +528,8 @@ object UnitActions { } } - private fun addActionsWithLimitedUses(unit: MapUnit, actionList: ArrayList, tile: TileInfo) { + fun addActionsWithLimitedUses(unit: MapUnit, actionList: ArrayList, tile: TileInfo) { + val actionsToAdd = unit.religiousActionsUnitCanDo() if (actionsToAdd.none()) return if (unit.religion == null || unit.civInfo.gameInfo.religions[unit.religion]!!.isPantheon()) return @@ -551,7 +552,7 @@ object UnitActions { } } - private fun addSpreadReligionActions(unit: MapUnit, actionList: ArrayList, city: CityInfo) { + fun addSpreadReligionActions(unit: MapUnit, actionList: ArrayList, city: CityInfo) { if (!unit.civInfo.gameInfo.isReligionEnabled()) return val blockedByInquisitor = city.getCenterTile()