Great people automation (#9125)

* Add automation for great scientist & merchant

* Automate great people (great merchant, great engineer & great scientist).

* Address comments

* Rename method for consistency

* Resolve comments
This commit is contained in:
WhoIsJohannes 2023-04-17 07:19:55 +02:00 committed by GitHub
parent b9a7925285
commit d25804ffb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 179 additions and 12 deletions

View File

@ -11,6 +11,8 @@ import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitAction
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
@ -183,13 +185,15 @@ object SpecificUnitAutomation {
foundCityAction.action.invoke()
}
fun automateImprovementPlacer(unit: MapUnit) {
/** @return whether there was any progress in placing the improvement. A return value of `false`
* can be interpreted as: the unit doesn't know where to place the improvement or is stuck. */
fun automateImprovementPlacer(unit: MapUnit) : Boolean {
val improvementBuildingUniques = unit.getMatchingUniques(UniqueType.ConstructImprovementConsumingUnit) +
unit.getMatchingUniques(UniqueType.ConstructImprovementInstantly)
val improvementName = improvementBuildingUniques.first().params[0]
val improvement = unit.civ.gameInfo.ruleset.tileImprovements[improvementName]
?: return
?: return false
val relatedStat = improvement.maxByOrNull { it.value }?.key ?: Stat.Culture
val citiesByStatBoost = unit.civ.cities.sortedByDescending {
@ -209,9 +213,18 @@ object SpecificUnitAutomation {
if (pathToCity.isEmpty()) continue
if (pathToCity.size > 2 && unit.getTile().getCity() != city) {
if (unit.getTile().militaryUnit == null) return // Don't move until you're accompanied by a military unit
// Radius 5 is quite arbitrary. Few units have such a high movement radius although
// streets might modify it. Also there might be invisible units, so this is just an
// approximation for relative safety and simplicity.
val enemyUnitsNearby = unit.getTile().getTilesInDistance(5).any { tileNearby ->
tileNearby.getUnits().any { unitOnTileNearby ->
unitOnTileNearby.isMilitary() && unitOnTileNearby.civ.isAtWarWith(unit.civ)
}
}
// Don't move until you're accompanied by a military unit if there are enemies nearby.
if (unit.getTile().militaryUnit == null && enemyUnitsNearby) return true
unit.movement.headTowards(city.getCenterTile())
return
return true
}
// if we got here, we're pretty close, start looking!
@ -224,14 +237,108 @@ object SpecificUnitAutomation {
.firstOrNull { unit.movement.canReach(it) }
?: continue // to another city
val unitTileBeforeMovement = unit.currentTile
unit.movement.headTowards(chosenTile)
if (unit.currentTile == chosenTile)
if (unit.currentTile == chosenTile) {
if (unit.currentTile.isPillaged())
UnitActions.getRepairAction(unit).invoke()
else
UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke()
return
UnitActions.getImprovementConstructionActions(unit, unit.currentTile)
.firstOrNull()?.action?.invoke()
return true
}
return unitTileBeforeMovement != unit.currentTile
}
// No city needs this improvement.
return false
}
/** @return whether there was any progress in conducting the trade mission. A return value of
* `false` can be interpreted as: the unit doesn't know where to go or there are no city
* states. */
fun conductTradeMission(unit: MapUnit): Boolean {
val closestCityStateTile =
unit.civ.gameInfo.civilizations
.filter {
!unit.civ.isAtWarWith(it) && it.isCityState() && it.cities.isNotEmpty()
}
.flatMap { it.cities[0].getTiles() }
.filter { unit.civ.hasExplored(it) }
.mapNotNull { tile ->
val path = unit.movement.getShortestPath(tile)
if (path.size <= 10) tile to path.size else null
}
.minByOrNull { it.second }?.first
?: return false
val conductTradeMissionAction = UnitActions.getUnitActions(unit)
.firstOrNull { it.type == UnitActionType.ConductTradeMission }
if (conductTradeMissionAction?.action != null) {
conductTradeMissionAction.action.invoke()
return true
}
val unitTileBeforeMovement = unit.currentTile
unit.movement.headTowards(closestCityStateTile)
return unitTileBeforeMovement != unit.currentTile
}
/**
* If there's a city nearby that can construct a wonder, walk there an get it built. Typically I
* like to build all wonders in the same city to have the boni accumulate (and it typically ends
* up being my capital), but that would need too much logic (e.g. how far away is the capital,
* is the wonder likely still available by the time I'm there, is this particular wonder even
* buildable in the capital, etc.)
*
* @return whether there was any progress in speeding up a wonder construction. A return value
* of `false` can be interpreted as: the unit doesn't know where to go or is stuck. */
fun speedupWonderConstruction(unit: MapUnit): Boolean {
val nearbyCityWithAvailableWonders = unit.civ.cities.filter { city ->
// Maybe it would be nice to make space in the city if there's already some
// other civilian unit in there for whatever reason, but again that seems a lot of
// additional complexity for questionable gain.
(unit.movement.canMoveTo(city.getCenterTile()) || unit.currentTile == city.getCenterTile())
// Don't speed up construction in small cities. There's a risk the great
// engineer can't get it done entirely and then it takes forever for the small
// city to finish the rest.
&& city.population.population >= 3
&& getWonderThatWouldBenefitFromBeingSpedUp(city) != null
}.mapNotNull { city ->
val path = unit.movement.getShortestPath(city.getCenterTile())
if (path.size <= 5) city to path.size else null
}.minByOrNull { it.second }?.first
if (nearbyCityWithAvailableWonders == null) {
return false
}
if (unit.currentTile == nearbyCityWithAvailableWonders.getCenterTile()) {
val wonderToHurry =
getWonderThatWouldBenefitFromBeingSpedUp(nearbyCityWithAvailableWonders)!!
nearbyCityWithAvailableWonders.cityConstructions.constructionQueue.add(
0,
wonderToHurry.name
)
UnitActions.getUnitActions(unit)
.first {
it.type == UnitActionType.HurryBuilding
|| it.type == UnitActionType.HurryWonder }
.action!!.invoke()
return true
}
// Walk towards the city.
val tileBeforeMoving = unit.getTile()
unit.movement.headTowards(nearbyCityWithAvailableWonders.getCenterTile())
return tileBeforeMoving != unit.currentTile
}
private fun getWonderThatWouldBenefitFromBeingSpedUp(city: City): Building? {
return city.cityConstructions.getBuildableBuildings().filter { building ->
building.isWonder && !building.hasUnique(UniqueType.CannotBeHurried)
&& city.cityConstructions.turnsToConstruction(building.name) >= 5
}.sortedBy { -city.cityConstructions.getRemainingWork(it.name) }.firstOrNull()
}
fun automateAddInCapital(unit: MapUnit) {

View File

@ -9,11 +9,13 @@ import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.managers.ReligionState
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage
@ -270,16 +272,72 @@ object UnitAutomation {
if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoLimitedAction(Constants.removeHeresy))
return SpecificUnitAutomation.automateInquisitor(unit)
if (unit.hasUnique(UniqueType.ConstructImprovementConsumingUnit)
|| unit.hasUnique(UniqueType.ConstructImprovementInstantly))
// catch great prophet for civs who can't found/enhance/spread religion
return SpecificUnitAutomation.automateImprovementPlacer(unit) // includes great people plus moddable units
val isLateGame = isLateGame(unit.civ)
// Great scientist -> Hurry research if late game
if (UnitActions.getUnitActions(unit).any { it.type == UnitActionType.HurryResearch }
&& isLateGame) {
UnitActions.getUnitActions(unit)
.first { it.type == UnitActionType.HurryResearch }.action!!.invoke()
return
}
// ToDo: automation of great people skills (may speed up construction, provides a science boost, etc.)
// Great merchant -> Conduct trade mission if late game and if not at war.
// TODO: This could be more complex to walk to the city state that is most beneficial to
// also have more influence.
if (unit.hasUnique(UniqueType.CanTradeWithCityStateForGoldAndInfluence)
// Don't wander around with the great merchant when at war. Barbs might also be a
// problem, but hopefully by the time we have a great merchant, they're under
// control.
&& !unit.civ.isAtWar()
&& isLateGame
) {
val tradeMissionCanBeConductedEventually =
SpecificUnitAutomation.conductTradeMission(unit)
if (tradeMissionCanBeConductedEventually)
return
}
// Great engineer -> Try to speed up wonder construction if late game
if (isLateGame &&
(unit.hasUnique(UniqueType.CanSpeedupConstruction)
|| unit.hasUnique(UniqueType.CanSpeedupWonderConstruction))) {
val wonderCanBeSpedUpEventually = SpecificUnitAutomation.speedupWonderConstruction(unit)
if (wonderCanBeSpedUpEventually)
return
}
// This has to come after the individual abilities for the great people that can also place
// instant improvements (e.g. great scientist).
if (unit.hasUnique(UniqueType.ConstructImprovementConsumingUnit)
|| unit.hasUnique(UniqueType.ConstructImprovementInstantly)
) {
// catch great prophet for civs who can't found/enhance/spread religion
// includes great people plus moddable units
val improvementCanBePlacedEventually =
SpecificUnitAutomation.automateImprovementPlacer(unit)
if (!improvementCanBePlacedEventually)
startGoldenAgeIfHasAbility(unit)
}
// TODO: The AI tends to have a lot of great generals. Maybe there should be a cutoff
// (depending on number of cities) and after that they should just be used to start golden
// ages?
return // The AI doesn't know how to handle unknown civilian units
}
private fun isLateGame(civ: Civilization): Boolean {
val researchCompletePercent =
(civ.tech.researchedTechnologies.size * 1.0f) / civ.gameInfo.ruleset.technologies.size
return researchCompletePercent >= 0.8f
}
private fun startGoldenAgeIfHasAbility(unit: MapUnit) {
UnitActions.getUnitActions(unit).filter { it.type == UnitActionType.StartGoldenAge }
.firstOrNull()?.action!!.invoke()
}
/** @return true only if the unit has 0 movement left */
private fun tryAttacking(unit: MapUnit): Boolean {
for (attackNumber in unit.attacksThisTurn until unit.maxAttacksPerTurn()) {

View File

@ -726,6 +726,8 @@ class UnitMovement(val unit: MapUnit) {
if (!unitSpecificAllowOcean && unit.cache.cannotEnterOceanTiles) return false
}
if (unit.hasUnique(UniqueType.CanTradeWithCityStateForGoldAndInfluence) && tile.getOwner()?.isCityState() == true)
return true
if (!unit.cache.canEnterForeignTerrain && !tile.canCivPassThrough(unit.civ)) return false
// The first unit is: