diff --git a/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt index cd24357017..927024c8d6 100644 --- a/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt @@ -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) { diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index f3ae04d7dd..c2ba6b868b 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -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()) { diff --git a/core/src/com/unciv/logic/map/mapunit/UnitMovement.kt b/core/src/com/unciv/logic/map/mapunit/UnitMovement.kt index d86f91252d..cbf03a552b 100644 --- a/core/src/com/unciv/logic/map/mapunit/UnitMovement.kt +++ b/core/src/com/unciv/logic/map/mapunit/UnitMovement.kt @@ -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: