mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-06 16:28:40 +07:00
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:
@ -11,6 +11,8 @@ import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
|
|||||||
import com.unciv.logic.map.mapunit.MapUnit
|
import com.unciv.logic.map.mapunit.MapUnit
|
||||||
import com.unciv.logic.map.tile.Tile
|
import com.unciv.logic.map.tile.Tile
|
||||||
import com.unciv.models.UnitAction
|
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.ruleset.unique.UniqueType
|
||||||
import com.unciv.models.stats.Stat
|
import com.unciv.models.stats.Stat
|
||||||
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
|
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
|
||||||
@ -183,13 +185,15 @@ object SpecificUnitAutomation {
|
|||||||
foundCityAction.action.invoke()
|
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) +
|
val improvementBuildingUniques = unit.getMatchingUniques(UniqueType.ConstructImprovementConsumingUnit) +
|
||||||
unit.getMatchingUniques(UniqueType.ConstructImprovementInstantly)
|
unit.getMatchingUniques(UniqueType.ConstructImprovementInstantly)
|
||||||
|
|
||||||
val improvementName = improvementBuildingUniques.first().params[0]
|
val improvementName = improvementBuildingUniques.first().params[0]
|
||||||
val improvement = unit.civ.gameInfo.ruleset.tileImprovements[improvementName]
|
val improvement = unit.civ.gameInfo.ruleset.tileImprovements[improvementName]
|
||||||
?: return
|
?: return false
|
||||||
val relatedStat = improvement.maxByOrNull { it.value }?.key ?: Stat.Culture
|
val relatedStat = improvement.maxByOrNull { it.value }?.key ?: Stat.Culture
|
||||||
|
|
||||||
val citiesByStatBoost = unit.civ.cities.sortedByDescending {
|
val citiesByStatBoost = unit.civ.cities.sortedByDescending {
|
||||||
@ -209,9 +213,18 @@ object SpecificUnitAutomation {
|
|||||||
|
|
||||||
if (pathToCity.isEmpty()) continue
|
if (pathToCity.isEmpty()) continue
|
||||||
if (pathToCity.size > 2 && unit.getTile().getCity() != city) {
|
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())
|
unit.movement.headTowards(city.getCenterTile())
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we got here, we're pretty close, start looking!
|
// if we got here, we're pretty close, start looking!
|
||||||
@ -224,14 +237,108 @@ object SpecificUnitAutomation {
|
|||||||
.firstOrNull { unit.movement.canReach(it) }
|
.firstOrNull { unit.movement.canReach(it) }
|
||||||
?: continue // to another city
|
?: continue // to another city
|
||||||
|
|
||||||
|
val unitTileBeforeMovement = unit.currentTile
|
||||||
unit.movement.headTowards(chosenTile)
|
unit.movement.headTowards(chosenTile)
|
||||||
if (unit.currentTile == chosenTile)
|
if (unit.currentTile == chosenTile) {
|
||||||
if (unit.currentTile.isPillaged())
|
if (unit.currentTile.isPillaged())
|
||||||
UnitActions.getRepairAction(unit).invoke()
|
UnitActions.getRepairAction(unit).invoke()
|
||||||
else
|
else
|
||||||
UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke()
|
UnitActions.getImprovementConstructionActions(unit, unit.currentTile)
|
||||||
return
|
.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) {
|
fun automateAddInCapital(unit: MapUnit) {
|
||||||
|
@ -9,11 +9,13 @@ import com.unciv.logic.battle.CityCombatant
|
|||||||
import com.unciv.logic.battle.ICombatant
|
import com.unciv.logic.battle.ICombatant
|
||||||
import com.unciv.logic.battle.MapUnitCombatant
|
import com.unciv.logic.battle.MapUnitCombatant
|
||||||
import com.unciv.logic.city.City
|
import com.unciv.logic.city.City
|
||||||
|
import com.unciv.logic.civilization.Civilization
|
||||||
import com.unciv.logic.civilization.NotificationCategory
|
import com.unciv.logic.civilization.NotificationCategory
|
||||||
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
||||||
import com.unciv.logic.civilization.managers.ReligionState
|
import com.unciv.logic.civilization.managers.ReligionState
|
||||||
import com.unciv.logic.map.mapunit.MapUnit
|
import com.unciv.logic.map.mapunit.MapUnit
|
||||||
import com.unciv.logic.map.tile.Tile
|
import com.unciv.logic.map.tile.Tile
|
||||||
|
import com.unciv.models.UnitActionType
|
||||||
import com.unciv.models.ruleset.unique.UniqueType
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
|
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
|
||||||
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage
|
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage
|
||||||
@ -270,16 +272,72 @@ object UnitAutomation {
|
|||||||
if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoLimitedAction(Constants.removeHeresy))
|
if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoLimitedAction(Constants.removeHeresy))
|
||||||
return SpecificUnitAutomation.automateInquisitor(unit)
|
return SpecificUnitAutomation.automateInquisitor(unit)
|
||||||
|
|
||||||
if (unit.hasUnique(UniqueType.ConstructImprovementConsumingUnit)
|
val isLateGame = isLateGame(unit.civ)
|
||||||
|| unit.hasUnique(UniqueType.ConstructImprovementInstantly))
|
// Great scientist -> Hurry research if late game
|
||||||
// catch great prophet for civs who can't found/enhance/spread religion
|
if (UnitActions.getUnitActions(unit).any { it.type == UnitActionType.HurryResearch }
|
||||||
return SpecificUnitAutomation.automateImprovementPlacer(unit) // includes great people plus moddable units
|
&& 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
|
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 */
|
/** @return true only if the unit has 0 movement left */
|
||||||
private fun tryAttacking(unit: MapUnit): Boolean {
|
private fun tryAttacking(unit: MapUnit): Boolean {
|
||||||
for (attackNumber in unit.attacksThisTurn until unit.maxAttacksPerTurn()) {
|
for (attackNumber in unit.attacksThisTurn until unit.maxAttacksPerTurn()) {
|
||||||
|
@ -726,6 +726,8 @@ class UnitMovement(val unit: MapUnit) {
|
|||||||
if (!unitSpecificAllowOcean && unit.cache.cannotEnterOceanTiles) return false
|
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
|
if (!unit.cache.canEnterForeignTerrain && !tile.canCivPassThrough(unit.civ)) return false
|
||||||
|
|
||||||
// The first unit is:
|
// The first unit is:
|
||||||
|
Reference in New Issue
Block a user