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
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.logic.automation.Automation
import com.unciv.logic.automation.civilization.NextTurnAutomation
import com.unciv.logic.automation.unit.WorkerAutomation
import com.unciv.logic.city.CityConstructions
import com.unciv.logic.civilization.CityAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
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.IConstruction
import com.unciv.models.ruleset.INonPerpetualConstruction
@ -166,36 +169,50 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
}
private fun addWorkBoatChoice() {
// Does the ruleset even have "Workboats"?
val buildableWorkboatUnits = units
.filter {
it.hasUnique(UniqueType.CreateWaterImprovements)
&& Automation.allowAutomatedConstruction(civInfo, city, it)
}.filterBuildable()
val alreadyHasWorkBoat = buildableWorkboatUnits.any()
&& !city.getTiles().any {
it.civilianUnit?.hasUnique(UniqueType.CreateWaterImprovements) == true
}
if (!alreadyHasWorkBoat) return
.toSet()
if (buildableWorkboatUnits.isEmpty()) 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()) {
(it.isWater || it.isCityCenter()) && (it.getOwner() == null || it.isFriendlyTerritory(civInfo))
// Define what makes a tile worth sending a Workboat to
// 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()
.any { tile ->
tile.hasViewableResource(civInfo) && tile.improvement == null && tile.getOwner() == civInfo
&& tile.tileResource.getImprovements().any {
tile.improvementFunctions.canBuildImprovement(tile.ruleset.tileImprovements[it]!!, civInfo)
}
// Search for a tile justifiying producing a Workboat
// todo should workboatAutomationSearchMaxTiles depend on game state?
fun findTileWorthImproving(): Boolean {
val searchMaxTiles = civInfo.gameInfo.ruleset.modOptions.constants.workboatAutomationSearchMaxTiles
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(
relativeCostEffectiveness, buildableWorkboatUnits.minByOrNull { it.cost }!!.name,
0.6f
)
if (!findTileWorthImproving()) return
addChoice(relativeCostEffectiveness, buildableWorkboatUnits.minBy { it.cost }.name, 0.6f)
}
private fun addWorkerChoice() {

View File

@ -574,15 +574,9 @@ class WorkerAutomation(
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
*
* 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)
*/
@ -597,13 +591,38 @@ class WorkerAutomation(
.firstOrNull { unit.movement.canReach(it) && isNotBonusResourceOrWorkable(it, unit.civ) }
?: return false
// could be either fishing boats or oil well
val isImprovable = closestReachableResource.tileResource.getImprovements().any()
if (!isImprovable) return false
unit.movement.headTowards(closestReachableResource)
if (unit.currentTile != closestReachableResource) return true // moving counts as progress
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!
* Also note that REACHABLE tiles are not necessarily tiles that the unit CAN ENTER */
fun canReach(destination: Tile): Boolean {
if (unit.cache.cannotMove) return destination == unit.getTile()
if (unit.baseUnit.movesLikeAirUnits() || unit.isPreparingParadrop())
return canReachInCurrentTurn(destination)
return getShortestPath(destination).any()
* Also note that REACHABLE tiles are not necessarily tiles that the unit CAN ENTER
* @see canReachInCurrentTurn
*/
fun canReach(destination: Tile) = canReachCommon(destination) {
getShortestPath(it).any()
}
fun canReachInCurrentTurn(destination: Tile): Boolean {
if (unit.cache.cannotMove) return destination == unit.getTile()
if (unit.baseUnit.movesLikeAirUnits())
return unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits()
if (unit.isPreparingParadrop())
return unit.currentTile.aerialDistanceTo(destination) <= unit.cache.paradropRange && canParadropOn(destination)
return getDistanceToTiles().containsKey(destination)
/** Cached and thus not as performance-heavy as [canReach] */
fun canReachInCurrentTurn(destination: Tile) = canReachCommon(destination) {
getDistanceToTiles().containsKey(it)
}
private inline fun canReachCommon(destination: Tile, specificFunction: (Tile) -> Boolean) = when {
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,
passThroughCache: HashMap<Tile, Boolean> = HashMap(),
movementCostCache: HashMap<Pair<Tile, Tile>, Float> = HashMap(),
includeOtherEscortUnit: Boolean = true)
: PathsToTilesWithinTurn {
includeOtherEscortUnit: Boolean = true
): PathsToTilesWithinTurn {
val cacheResults = pathfindingCache.getDistanceToTiles(considerZoneOfControl)
if (cacheResults != null) {
return cacheResults

View File

@ -78,10 +78,12 @@ class ModConstants {
var religionLimitBase = 1
var religionLimitMultiplier = 0.5f
//Factors in formula for pantheon cost
// Factors in formula for pantheon cost
var pantheonBase = 10
var pantheonGrowth = 5
var workboatAutomationSearchMaxTiles = 20
fun merge(other: ModConstants) {
for (field in this::class.java.declaredFields) {
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] |
| pantheonBase | Int | 10 | [^L] |
| pantheonGrowth | Int | 5 | [^L] |
| workboatAutomationSearchMaxTiles | Int | 20 | [^M] |
Legend:
@ -231,6 +232,7 @@ Legend:
- [^J]: A [UnitUpgradeCost](#unitupgradecost) sub-structure.
- [^K]: Maximum foundable Religions = religionLimitBase + floor(MajorCivCount * religionLimitMultiplier)
- [^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