diff --git a/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt index c75df82b47..100700b63f 100644 --- a/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt @@ -164,11 +164,9 @@ object AirUnitAutomation { return } - if (unit.baseUnit.isAirUnit()) { - val pathsToCities = unit.movement.getAerialPathsToCities() - if (pathsToCities.isEmpty()) return // can't actually move anywhere else - tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit) - } else UnitAutomation.tryHeadTowardsEnemyCity(unit) + val pathsToCities = unit.movement.getAerialPathsToCities() + if (pathsToCities.isEmpty()) return // can't actually move anywhere else + tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit) } private fun tryRelocateToCitiesWithEnemyNearBy(unit: MapUnit): Boolean { diff --git a/core/src/com/unciv/logic/automation/unit/CivilianUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/CivilianUnitAutomation.kt new file mode 100644 index 0000000000..2628fe75ae --- /dev/null +++ b/core/src/com/unciv/logic/automation/unit/CivilianUnitAutomation.kt @@ -0,0 +1,181 @@ +package com.unciv.logic.automation.unit + +import com.unciv.Constants +import com.unciv.logic.civilization.Civilization +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 + +object CivilianUnitAutomation { + + fun automateCivilianUnit(unit: MapUnit) { + if (tryRunAwayIfNeccessary(unit)) return + + if (unit.currentTile.isCityCenter() && unit.currentTile.getCity()!!.isCapital() + && !unit.hasUnique(UniqueType.AddInCapital) + && unit.civ.units.getCivUnits().any { unit.hasUnique(UniqueType.AddInCapital) }){ + // First off get out of the way, then decide if you actually want to do something else + val tilesCanMoveTo = unit.movement.getDistanceToTiles() + .filter { unit.movement.canMoveTo(it.key) } + if (tilesCanMoveTo.isNotEmpty()) + unit.movement.moveToTile(tilesCanMoveTo.minByOrNull { it.value.totalDistance }!!.key) + } + + val tilesWhereWeWillBeCaptured = unit.currentTile.getTilesInDistance(5) + .mapNotNull { it.militaryUnit } + .filter { it.civ.isAtWarWith(unit.civ) } + .flatMap { it.movement.getReachableTilesInCurrentTurn() } + .filter { it.militaryUnit?.civ != unit.civ } + .toSet() + + if (unit.hasUnique(UniqueType.FoundCity)) + return SpecificUnitAutomation.automateSettlerActions(unit, tilesWhereWeWillBeCaptured) + + if (unit.cache.hasUniqueToBuildImprovements) + return unit.civ.getWorkerAutomation().automateWorkerAction(unit, tilesWhereWeWillBeCaptured) + + if (unit.cache.hasUniqueToCreateWaterImprovements){ + if (!unit.civ.getWorkerAutomation().automateWorkBoats(unit)) + UnitAutomation.tryExplore(unit) + return + } + + if (unit.hasUnique(UniqueType.MayFoundReligion) + && unit.civ.religionManager.religionState < ReligionState.Religion + && unit.civ.religionManager.mayFoundReligionAtAll() + ) + return ReligiousUnitAutomation.foundReligion(unit) + + if (unit.hasUnique(UniqueType.MayEnhanceReligion) + && unit.civ.religionManager.religionState < ReligionState.EnhancedReligion + && unit.civ.religionManager.mayEnhanceReligionAtAll(unit) + ) + return ReligiousUnitAutomation.enhanceReligion(unit) + + // We try to add any unit in the capital we can, though that might not always be desirable + // For now its a simple option to allow AI to win a science victory again + if (unit.hasUnique(UniqueType.AddInCapital)) + return SpecificUnitAutomation.automateAddInCapital(unit) + + //todo this now supports "Great General"-like mod units not combining 'aura' and citadel + // abilities, but not additional capabilities if automation finds no use for those two + if (unit.cache.hasStrengthBonusInRadiusUnique + && SpecificUnitAutomation.automateGreatGeneral(unit)) + return + if (unit.cache.hasCitadelPlacementUnique && SpecificUnitAutomation.automateCitadelPlacer(unit)) + return + if (unit.cache.hasCitadelPlacementUnique || unit.cache.hasStrengthBonusInRadiusUnique) + return SpecificUnitAutomation.automateGreatGeneralFallback(unit) + + if (unit.civ.religionManager.maySpreadReligionAtAll(unit)) + return ReligiousUnitAutomation.automateMissionary(unit) + + if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoLimitedAction(Constants.removeHeresy)) + return ReligiousUnitAutomation.automateInquisitor(unit) + + val isLateGame = isLateGame(unit.civ) + // Great scientist -> Hurry research if late game + if (isLateGame) { + val hurriedResearch = UnitActions.invokeUnitAction(unit, UnitActionType.HurryResearch) + if (hurriedResearch) return + } + + // 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.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) + UnitActions.invokeUnitAction(unit, UnitActionType.StartGoldenAge) + } + + // 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 + } + + /** Returns whether the civilian spends its turn hiding and not moving */ + private fun tryRunAwayIfNeccessary(unit: MapUnit): Boolean { + // This is a little 'Bugblatter Beast of Traal': Run if we can attack an enemy + // Cheaper than determining which enemies could attack us next turn + val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys + .filter { UnitAutomation.containsEnemyMilitaryUnit(unit, it) } + + if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary() + && unit.getTile().militaryUnit == null && !unit.getTile().isCityCenter()) { + runAway(unit) + return true + } + + return false + } + + private fun runAway(unit: MapUnit) { + val reachableTiles = unit.movement.getDistanceToTiles() + val enterableCity = reachableTiles.keys + .firstOrNull { it.isCityCenter() && unit.movement.canMoveTo(it) } + if (enterableCity != null) { + unit.movement.moveToTile(enterableCity) + return + } + val defensiveUnit = reachableTiles.keys + .firstOrNull { + it.militaryUnit != null && it.militaryUnit!!.civ == unit.civ && it.civilianUnit == null + } + if (defensiveUnit != null) { + unit.movement.moveToTile(defensiveUnit) + return + } + val tileFurthestFromEnemy = reachableTiles.keys + .filter { unit.movement.canMoveTo(it) && unit.getDamageFromTerrain(it) < unit.health } + .maxByOrNull { countDistanceToClosestEnemy(unit, it) } + ?: return // can't move anywhere! + unit.movement.moveToTile(tileFurthestFromEnemy) + } + + + private fun countDistanceToClosestEnemy(unit: MapUnit, tile: Tile): Int { + for (i in 1..3) + if (tile.getTilesAtDistance(i).any { UnitAutomation.containsEnemyMilitaryUnit(unit, it) }) + return i + return 4 + } +} diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index e3cdf5b13f..9bc60e916a 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -11,15 +11,11 @@ import com.unciv.logic.battle.ICombatant import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.battle.TargetHelper 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 import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade @@ -155,7 +151,7 @@ object UnitAutomation { if (unit.isCivilian()) { - automateCivilianUnit(unit) + CivilianUnitAutomation.automateCivilianUnit(unit) return } @@ -174,7 +170,7 @@ object UnitAutomation { if ((unit.hasUnique(UniqueType.BuildImprovements) || unit.hasUnique(UniqueType.FoundCity) || unit.hasUnique(UniqueType.ReligiousUnit) || unit.hasUnique(UniqueType.CreateWaterImprovements)) && !unit.civ.isAtWar()){ - automateCivilianUnit(unit) + CivilianUnitAutomation.automateCivilianUnit(unit) return } @@ -187,7 +183,7 @@ object UnitAutomation { if (unit.baseUnit.isNuclearWeapon()) return AirUnitAutomation.automateNukes(unit) - if (unit.hasUnique(UniqueType.SelfDestructs)) + if (unit.baseUnit.isAirUnit() && unit.hasUnique(UniqueType.SelfDestructs)) return AirUnitAutomation.automateMissile(unit) if (tryGoToRuinAndEncampment(unit) && unit.currentMovement == 0f) return @@ -235,127 +231,6 @@ object UnitAutomation { wander(unit, stayInTerritory = true) } - private fun automateCivilianUnit(unit: MapUnit) { - if (tryRunAwayIfNeccessary(unit)) return - - if (unit.currentTile.isCityCenter() && unit.currentTile.getCity()!!.isCapital() - && !unit.hasUnique(UniqueType.AddInCapital) - && unit.civ.units.getCivUnits().any { unit.hasUnique(UniqueType.AddInCapital) }){ - // First off get out of the way, then decide if you actually want to do something else - val tilesCanMoveTo = unit.movement.getDistanceToTiles() - .filter { unit.movement.canMoveTo(it.key) } - if (tilesCanMoveTo.isNotEmpty()) - unit.movement.moveToTile(tilesCanMoveTo.minByOrNull { it.value.totalDistance }!!.key) - } - - val tilesWhereWeWillBeCaptured = unit.currentTile.getTilesInDistance(5) - .mapNotNull { it.militaryUnit } - .filter { it.civ.isAtWarWith(unit.civ) } - .flatMap { it.movement.getReachableTilesInCurrentTurn() } - .filter { it.militaryUnit?.civ != unit.civ } - .toSet() - - if (unit.hasUnique(UniqueType.FoundCity)) - return SpecificUnitAutomation.automateSettlerActions(unit, tilesWhereWeWillBeCaptured) - - if (unit.cache.hasUniqueToBuildImprovements) - return unit.civ.getWorkerAutomation().automateWorkerAction(unit, tilesWhereWeWillBeCaptured) - - if (unit.cache.hasUniqueToCreateWaterImprovements){ - if (!unit.civ.getWorkerAutomation().automateWorkBoats(unit)) - tryExplore(unit) - return - } - - if (unit.hasUnique(UniqueType.MayFoundReligion) - && unit.civ.religionManager.religionState < ReligionState.Religion - && unit.civ.religionManager.mayFoundReligionAtAll() - ) - return ReligiousUnitAutomation.foundReligion(unit) - - if (unit.hasUnique(UniqueType.MayEnhanceReligion) - && unit.civ.religionManager.religionState < ReligionState.EnhancedReligion - && unit.civ.religionManager.mayEnhanceReligionAtAll(unit) - ) - return ReligiousUnitAutomation.enhanceReligion(unit) - - // We try to add any unit in the capital we can, though that might not always be desirable - // For now its a simple option to allow AI to win a science victory again - if (unit.hasUnique(UniqueType.AddInCapital)) - return SpecificUnitAutomation.automateAddInCapital(unit) - - //todo this now supports "Great General"-like mod units not combining 'aura' and citadel - // abilities, but not additional capabilities if automation finds no use for those two - if (unit.cache.hasStrengthBonusInRadiusUnique - && SpecificUnitAutomation.automateGreatGeneral(unit)) - return - if (unit.cache.hasCitadelPlacementUnique && SpecificUnitAutomation.automateCitadelPlacer(unit)) - return - if (unit.cache.hasCitadelPlacementUnique || unit.cache.hasStrengthBonusInRadiusUnique) - return SpecificUnitAutomation.automateGreatGeneralFallback(unit) - - if (unit.civ.religionManager.maySpreadReligionAtAll(unit)) - return ReligiousUnitAutomation.automateMissionary(unit) - - if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoLimitedAction(Constants.removeHeresy)) - return ReligiousUnitAutomation.automateInquisitor(unit) - - val isLateGame = isLateGame(unit.civ) - // Great scientist -> Hurry research if late game - if (isLateGame) { - val hurriedResearch = UnitActions.invokeUnitAction(unit, UnitActionType.HurryResearch) - if (hurriedResearch) return - } - - // 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.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) - UnitActions.invokeUnitAction(unit, UnitActionType.StartGoldenAge) - } - - // 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 - } /** @return true only if the unit has 0 movement left */ private fun tryAttacking(unit: MapUnit): Boolean { @@ -389,21 +264,7 @@ object UnitAutomation { val currentUnitTile = unit.getTile() - val nearbyRangedEnemyUnits = unit.currentTile.getTilesInDistance(3) - .flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } } - - val tilesInRangeOfAttack = nearbyRangedEnemyUnits - .flatMap { it.getTile().getTilesInDistance(it.getRange()) } - - val tilesWithinBombardmentRange = unit.currentTile.getTilesInDistance(3) - .filter { it.isCityCenter() && it.getCity()!!.civ.isAtWarWith(unit.civ) } - .flatMap { it.getTilesInDistance(it.getCity()!!.range) } - - val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(3) - .filter { unit.getDamageFromTerrain(it) > 0 } - - val dangerousTiles = (tilesInRangeOfAttack + tilesWithinBombardmentRange + tilesWithTerrainDamage).toHashSet() - + val dangerousTiles = getDangerousTiles(unit) val viableTilesForHealing = unitDistanceToTiles.keys .filter { it !in dangerousTiles && unit.movement.canMoveTo(it) } @@ -449,6 +310,23 @@ object UnitAutomation { return true } + private fun getDangerousTiles(unit: MapUnit): HashSet { + val nearbyRangedEnemyUnits = unit.currentTile.getTilesInDistance(3) + .flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } } + + val tilesInRangeOfAttack = nearbyRangedEnemyUnits + .flatMap { it.getTile().getTilesInDistance(it.getRange()) } + + val tilesWithinBombardmentRange = unit.currentTile.getTilesInDistance(3) + .filter { it.isCityCenter() && it.getCity()!!.civ.isAtWarWith(unit.civ) } + .flatMap { it.getTilesInDistance(it.getCity()!!.range) } + + val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(3) + .filter { unit.getDamageFromTerrain(it) > 0 } + + return (tilesInRangeOfAttack + tilesWithinBombardmentRange + tilesWithTerrainDamage).toHashSet() + } + fun tryPillageImprovement(unit: MapUnit): Boolean { if (unit.isCivilian()) return false val unitDistanceToTiles = unit.movement.getDistanceToTiles() @@ -720,7 +598,6 @@ object UnitAutomation { ) } return false - } private fun tryGarrisoningRangedLandUnit(unit: MapUnit): Boolean { @@ -731,14 +608,6 @@ object UnitAutomation { && unit.movement.canMoveTo(centerTile) } - fun isCityThatNeedsDefendingInWartime(city: City): Boolean { - if (city.health < city.getMaxHealth()) return true // this city is under attack! - for (enemyCivCity in unit.civ.diplomacy.values - .filter { it.diplomaticStatus == DiplomaticStatus.War } - .map { it.otherCiv() }.flatMap { it.cities }) - if (city.getCenterTile().aerialDistanceTo(enemyCivCity.getCenterTile()) <= 5) return true // this is an edge city that needs defending - return false - } val citiesToTry = if (!unit.civ.isAtWar()) { if (unit.getTile().isCityCenter()) return true // It's always good to have a unit in the city center, so if you haven't found anyone around to attack, forget it. @@ -760,6 +629,15 @@ object UnitAutomation { return true } + private fun isCityThatNeedsDefendingInWartime(city: City): Boolean { + if (city.health < city.getMaxHealth()) return true // this city is under attack! + for (enemyCivCity in city.civ.diplomacy.values + .filter { it.diplomaticStatus == DiplomaticStatus.War } + .map { it.otherCiv() }.flatMap { it.cities }) + if (city.getCenterTile().aerialDistanceTo(enemyCivCity.getCenterTile()) <= 5) return true // this is an edge city that needs defending + return false + } + private fun tryStationingMeleeNavalUnit(unit: MapUnit): Boolean { fun isMeleeNaval(mapUnit: MapUnit) = mapUnit.baseUnit.isMelee() && mapUnit.type.isWaterUnit() @@ -799,54 +677,8 @@ object UnitAutomation { unit.action = null } - /** Returns whether the civilian spends its turn hiding and not moving */ - private fun tryRunAwayIfNeccessary(unit: MapUnit): Boolean { - // This is a little 'Bugblatter Beast of Traal': Run if we can attack an enemy - // Cheaper than determining which enemies could attack us next turn - val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys - .filter { containsEnemyMilitaryUnit(unit, it) } - if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary() - && unit.getTile().militaryUnit == null && !unit.getTile().isCityCenter()) { - runAway(unit) - return true - } - - return false - } - - private fun runAway(unit: MapUnit) { - val reachableTiles = unit.movement.getDistanceToTiles() - val enterableCity = reachableTiles.keys - .firstOrNull { it.isCityCenter() && unit.movement.canMoveTo(it) } - if (enterableCity != null) { - unit.movement.moveToTile(enterableCity) - return - } - val defensiveUnit = reachableTiles.keys - .firstOrNull { - it.militaryUnit != null && it.militaryUnit!!.civ == unit.civ && it.civilianUnit == null - } - if (defensiveUnit != null) { - unit.movement.moveToTile(defensiveUnit) - return - } - val tileFurthestFromEnemy = reachableTiles.keys - .filter { unit.movement.canMoveTo(it) && unit.getDamageFromTerrain(it) < unit.health } - .maxByOrNull { countDistanceToClosestEnemy(unit, it) } - ?: return // can't move anywhere! - unit.movement.moveToTile(tileFurthestFromEnemy) - } - - - private fun countDistanceToClosestEnemy(unit: MapUnit, tile: Tile): Int { - for (i in 1..3) - if (tile.getTilesAtDistance(i).any { containsEnemyMilitaryUnit(unit, it) }) - return i - return 4 - } - - private fun containsEnemyMilitaryUnit(unit: MapUnit, tile: Tile) = + internal fun containsEnemyMilitaryUnit(unit: MapUnit, tile: Tile) = tile.militaryUnit != null && tile.militaryUnit!!.civ.isAtWarWith(unit.civ)