Work boat construction automation tweaks (#11395)

* Minor lint and optimize addWorkBoatChoice

* Moddable findTileWorthImproving search distance

* Don't count bonus resources outside any city work range as worth improving

* Look for existing work boat in a fixed radius instead of city-owned tiles, depending on work boat speed

* Some UnitMovement readability

* Work boat construction and automation code synergies
This commit is contained in:
SomeTroglodyte 2024-04-07 10:27:12 +02:00 committed by GitHub
parent 24bbfa49c6
commit cc45cefb99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 92 additions and 46 deletions

View File

@ -3,12 +3,15 @@ package com.unciv.logic.automation.city
import com.unciv.GUI import com.unciv.GUI
import com.unciv.logic.automation.Automation import com.unciv.logic.automation.Automation
import com.unciv.logic.automation.civilization.NextTurnAutomation import com.unciv.logic.automation.civilization.NextTurnAutomation
import com.unciv.logic.automation.unit.WorkerAutomation
import com.unciv.logic.city.CityConstructions import com.unciv.logic.city.CityConstructions
import com.unciv.logic.civilization.CityAction import com.unciv.logic.civilization.CityAction
import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.BFS import com.unciv.logic.map.BFS
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.IConstruction import com.unciv.models.ruleset.IConstruction
import com.unciv.models.ruleset.INonPerpetualConstruction import com.unciv.models.ruleset.INonPerpetualConstruction
@ -166,36 +169,50 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
} }
private fun addWorkBoatChoice() { private fun addWorkBoatChoice() {
// Does the ruleset even have "Workboats"?
val buildableWorkboatUnits = units val buildableWorkboatUnits = units
.filter { .filter {
it.hasUnique(UniqueType.CreateWaterImprovements) it.hasUnique(UniqueType.CreateWaterImprovements)
&& Automation.allowAutomatedConstruction(civInfo, city, it) && Automation.allowAutomatedConstruction(civInfo, city, it)
}.filterBuildable() }.filterBuildable()
val alreadyHasWorkBoat = buildableWorkboatUnits.any() .toSet()
&& !city.getTiles().any { if (buildableWorkboatUnits.isEmpty()) return
it.civilianUnit?.hasUnique(UniqueType.CreateWaterImprovements) == true
}
if (!alreadyHasWorkBoat) return
// Is there already a Workboat nearby?
// todo Still ignores whether that boat can reach the not-yet-found tile to improve
val twoTurnsMovement = buildableWorkboatUnits.maxOf { (it as BaseUnit).movement } * 2
fun MapUnit.isOurWorkBoat() = cache.hasUniqueToCreateWaterImprovements && this.civ == this@ConstructionAutomation.civInfo
val alreadyHasWorkBoat = city.getCenterTile().getTilesInDistanceRange(1..twoTurnsMovement)
.any { it.civilianUnit?.isOurWorkBoat() == true }
if (alreadyHasWorkBoat) return
val bfs = BFS(city.getCenterTile()) { // Define what makes a tile worth sending a Workboat to
(it.isWater || it.isCityCenter()) && (it.getOwner() == null || it.isFriendlyTerritory(civInfo)) // todo Prepare for mods that allow improving water tiles without a resource?
fun Tile.isWorthImproving(): Boolean {
if (getOwner() != civInfo) return false
if (!WorkerAutomation.hasWorkableSeaResource(this, civInfo)) return false
return WorkerAutomation.isNotBonusResourceOrWorkable(this, civInfo)
} }
repeat(20) { bfs.nextStep() }
if (!bfs.getReachedTiles() // Search for a tile justifiying producing a Workboat
.any { tile -> // todo should workboatAutomationSearchMaxTiles depend on game state?
tile.hasViewableResource(civInfo) && tile.improvement == null && tile.getOwner() == civInfo fun findTileWorthImproving(): Boolean {
&& tile.tileResource.getImprovements().any { val searchMaxTiles = civInfo.gameInfo.ruleset.modOptions.constants.workboatAutomationSearchMaxTiles
tile.improvementFunctions.canBuildImprovement(tile.ruleset.tileImprovements[it]!!, civInfo) val bfs = BFS(city.getCenterTile()) {
} (it.isWater || it.isCityCenter())
&& (it.getOwner() == null || it.isFriendlyTerritory(civInfo))
&& it.isExplored(civInfo) // Sending WB's through unexplored terrain would be cheating
} }
) return do {
val tile = bfs.nextStep() ?: break
if (tile.isWorthImproving()) return true
} while (bfs.size() < searchMaxTiles)
return false
}
addChoice( if (!findTileWorthImproving()) return
relativeCostEffectiveness, buildableWorkboatUnits.minByOrNull { it.cost }!!.name,
0.6f addChoice(relativeCostEffectiveness, buildableWorkboatUnits.minBy { it.cost }.name, 0.6f)
)
} }
private fun addWorkerChoice() { private fun addWorkerChoice() {

View File

@ -574,15 +574,9 @@ class WorkerAutomation(
fun isImprovementProbablyAFort(improvement: TileImprovement): Boolean = improvement.hasUnique(UniqueType.DefensiveBonus) fun isImprovementProbablyAFort(improvement: TileImprovement): Boolean = improvement.hasUnique(UniqueType.DefensiveBonus)
private fun hasWorkableSeaResource(tile: Tile, civInfo: Civilization): Boolean =
tile.isWater && tile.improvement == null && tile.hasViewableResource(civInfo)
private fun isNotBonusResourceOrWorkable(tile: Tile, civInfo: Civilization): Boolean =
tile.tileResource.resourceType != ResourceType.Bonus || civInfo.cities.any { it.tilesInRange.contains(tile) }
/** Try improving a Water Resource /** Try improving a Water Resource
* *
* No logic to avoid capture by enemies yet! * todo: No logic to avoid capture by enemies yet!
* *
* @return Whether any progress was made (improved a tile or at least moved towards an opportunity) * @return Whether any progress was made (improved a tile or at least moved towards an opportunity)
*/ */
@ -597,13 +591,38 @@ class WorkerAutomation(
.firstOrNull { unit.movement.canReach(it) && isNotBonusResourceOrWorkable(it, unit.civ) } .firstOrNull { unit.movement.canReach(it) && isNotBonusResourceOrWorkable(it, unit.civ) }
?: return false ?: return false
// could be either fishing boats or oil well
val isImprovable = closestReachableResource.tileResource.getImprovements().any()
if (!isImprovable) return false
unit.movement.headTowards(closestReachableResource) unit.movement.headTowards(closestReachableResource)
if (unit.currentTile != closestReachableResource) return true // moving counts as progress if (unit.currentTile != closestReachableResource) return true // moving counts as progress
return UnitActions.invokeUnitAction(unit, UnitActionType.CreateImprovement) return UnitActions.invokeUnitAction(unit, UnitActionType.CreateImprovement)
} }
companion object {
// Static methods so they can be reused in ConstructionAutomation
/** Checks whether [tile] is water and has a resource [civInfo] can improve
*
* Does check whether a matching improvement can currently be built (e.g. Oil before Refrigeration).
* Can return `true` if there is an improvement that does not match the resource (for future modding abilities).
* Does not check tile ownership - caller [automateWorkBoats] already did, other callers need to ensure this explicitly.
*/
fun hasWorkableSeaResource(tile: Tile, civInfo: Civilization) = when {
!tile.isWater -> false
tile.resource == null -> false
tile.improvement != null && tile.tileResource.isImprovedBy(tile.improvement!!) -> false
!tile.hasViewableResource(civInfo) -> false
else -> tile.tileResource.getImprovements().any {
val improvement = civInfo.gameInfo.ruleset.tileImprovements[it]!!
tile.improvementFunctions.canBuildImprovement(improvement, civInfo)
}
}
/** Test whether improving the resource on [tile] benefits [civInfo] (yields or strategic or luxury)
*
* Only tests resource type and city range, not any improvement requirements.
* @throws NullPointerException on tiles without a resource
*/
fun isNotBonusResourceOrWorkable(tile: Tile, civInfo: Civilization): Boolean =
tile.tileResource.resourceType != ResourceType.Bonus // Improve Oil even if no City reaps the yields
|| civInfo.cities.any { it.tilesInRange.contains(tile) } // Improve Fish only if any of our Cities reaps the yields
}
} }

View File

@ -248,21 +248,27 @@ class UnitMovement(val unit: MapUnit) {
} }
/** This is performance-heavy - use as last resort, only after checking everything else! /** This is performance-heavy - use as last resort, only after checking everything else!
* Also note that REACHABLE tiles are not necessarily tiles that the unit CAN ENTER */ * Also note that REACHABLE tiles are not necessarily tiles that the unit CAN ENTER
fun canReach(destination: Tile): Boolean { * @see canReachInCurrentTurn
if (unit.cache.cannotMove) return destination == unit.getTile() */
if (unit.baseUnit.movesLikeAirUnits() || unit.isPreparingParadrop()) fun canReach(destination: Tile) = canReachCommon(destination) {
return canReachInCurrentTurn(destination) getShortestPath(it).any()
return getShortestPath(destination).any()
} }
fun canReachInCurrentTurn(destination: Tile): Boolean { /** Cached and thus not as performance-heavy as [canReach] */
if (unit.cache.cannotMove) return destination == unit.getTile() fun canReachInCurrentTurn(destination: Tile) = canReachCommon(destination) {
if (unit.baseUnit.movesLikeAirUnits()) getDistanceToTiles().containsKey(it)
return unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits() }
if (unit.isPreparingParadrop())
return unit.currentTile.aerialDistanceTo(destination) <= unit.cache.paradropRange && canParadropOn(destination) private inline fun canReachCommon(destination: Tile, specificFunction: (Tile) -> Boolean) = when {
return getDistanceToTiles().containsKey(destination) unit.cache.cannotMove ->
destination == unit.getTile()
unit.baseUnit.movesLikeAirUnits() ->
unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits()
unit.isPreparingParadrop() ->
unit.currentTile.aerialDistanceTo(destination) <= unit.cache.paradropRange && canParadropOn(destination)
else ->
specificFunction(destination) // Note: Could pass destination as implicit closure from outer fun to lambda, but explicit is clearer
} }
/** /**
@ -689,8 +695,8 @@ class UnitMovement(val unit: MapUnit) {
considerZoneOfControl: Boolean = true, considerZoneOfControl: Boolean = true,
passThroughCache: HashMap<Tile, Boolean> = HashMap(), passThroughCache: HashMap<Tile, Boolean> = HashMap(),
movementCostCache: HashMap<Pair<Tile, Tile>, Float> = HashMap(), movementCostCache: HashMap<Pair<Tile, Tile>, Float> = HashMap(),
includeOtherEscortUnit: Boolean = true) includeOtherEscortUnit: Boolean = true
: PathsToTilesWithinTurn { ): PathsToTilesWithinTurn {
val cacheResults = pathfindingCache.getDistanceToTiles(considerZoneOfControl) val cacheResults = pathfindingCache.getDistanceToTiles(considerZoneOfControl)
if (cacheResults != null) { if (cacheResults != null) {
return cacheResults return cacheResults

View File

@ -78,10 +78,12 @@ class ModConstants {
var religionLimitBase = 1 var religionLimitBase = 1
var religionLimitMultiplier = 0.5f var religionLimitMultiplier = 0.5f
//Factors in formula for pantheon cost // Factors in formula for pantheon cost
var pantheonBase = 10 var pantheonBase = 10
var pantheonGrowth = 5 var pantheonGrowth = 5
var workboatAutomationSearchMaxTiles = 20
fun merge(other: ModConstants) { fun merge(other: ModConstants) {
for (field in this::class.java.declaredFields) { for (field in this::class.java.declaredFields) {
val value = field.get(other) val value = field.get(other)

View File

@ -202,6 +202,7 @@ and city distance in another. In case of conflicts, there is no guarantee which
| religionLimitMultiplier | Float | 0.5 | [^K] | | religionLimitMultiplier | Float | 0.5 | [^K] |
| pantheonBase | Int | 10 | [^L] | | pantheonBase | Int | 10 | [^L] |
| pantheonGrowth | Int | 5 | [^L] | | pantheonGrowth | Int | 5 | [^L] |
| workboatAutomationSearchMaxTiles | Int | 20 | [^M] |
Legend: Legend:
@ -231,6 +232,7 @@ Legend:
- [^J]: A [UnitUpgradeCost](#unitupgradecost) sub-structure. - [^J]: A [UnitUpgradeCost](#unitupgradecost) sub-structure.
- [^K]: Maximum foundable Religions = religionLimitBase + floor(MajorCivCount * religionLimitMultiplier) - [^K]: Maximum foundable Religions = religionLimitBase + floor(MajorCivCount * religionLimitMultiplier)
- [^L]: Cost of pantheon = pantheonBase + CivsWithReligion * pantheonGrowth - [^L]: Cost of pantheon = pantheonBase + CivsWithReligion * pantheonGrowth
- [^M]: When the AI decidees whether to build a work boat, how many tiles to search from the city center for an improvable tile
#### UnitUpgradeCost #### UnitUpgradeCost