mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-23 19:43:13 -04:00
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:
parent
24bbfa49c6
commit
cc45cefb99
@ -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() {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user