Workers now build forts (#10944)

* Workers now build forts

* Workers prioritise other tiles over removing forts

* Units don't pillage forts

* Increased how close a city has to be to be viable for fort building

* Decreased fort value, especially for allied city-states

* Units no longer prioritise pillaging forts instead of not pillaging them at all
This commit is contained in:
Oskar Niesen 2024-01-28 03:07:46 -06:00 committed by GitHub
parent f93a3f462b
commit f8d97968ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 42 deletions

View File

@ -388,10 +388,10 @@ object UnitAutomation {
.filter { it.value.totalDistance < unit.currentMovement }.keys .filter { it.value.totalDistance < unit.currentMovement }.keys
.filter { unit.movement.canMoveTo(it) && UnitActionsPillage.canPillage(unit, it) .filter { unit.movement.canMoveTo(it) && UnitActionsPillage.canPillage(unit, it)
&& (it.canPillageTileImprovement() && (it.canPillageTileImprovement()
|| (it.canPillageRoad() && it.getRoadOwner() != null && unit.civ.isAtWarWith(it.getRoadOwner()!!)))} || (it.canPillageRoad() && it.getRoadOwner() != null && unit.civ.isAtWarWith(it.getRoadOwner()!!))) }
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxByOrNull { it.getDefensiveBonus() }!! val tileToPillage = tilesThatCanWalkToAndThenPillage.maxByOrNull { it.getDefensiveBonus(false) }!!
if (unit.getTile() != tileToPillage) if (unit.getTile() != tileToPillage)
unit.movement.moveToTile(tileToPillage) unit.movement.moveToTile(tileToPillage)

View File

@ -11,6 +11,7 @@ import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
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.diplomacy.RelationshipLevel
import com.unciv.logic.map.BFS import com.unciv.logic.map.BFS
import com.unciv.logic.map.HexMath import com.unciv.logic.map.HexMath
import com.unciv.logic.map.MapPathing import com.unciv.logic.map.MapPathing
@ -29,6 +30,7 @@ import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.utils.debug import com.unciv.utils.debug
import kotlin.math.abs
private object WorkerAutomationConst { private object WorkerAutomationConst {
/** BFS max size is determined by the aerial distance of two cities to connect, padded with this */ /** BFS max size is determined by the aerial distance of two cities to connect, padded with this */
@ -350,9 +352,7 @@ class WorkerAutomation(
&& tileHasWorkToDo(reachedTile, unit) && tileHasWorkToDo(reachedTile, unit)
) { ) {
debug("WorkerAutomation: ${unit.label()} -> start improving $reachedTile") debug("WorkerAutomation: ${unit.label()} -> start improving $reachedTile")
return reachedTile.startWorkingOnImprovement( return reachedTile.startWorkingOnImprovement(tileRankings[reachedTile]!!.bestImprovement!!, civInfo, unit)
chooseImprovement(unit, reachedTile)!!, civInfo, unit
)
} }
} }
return return
@ -726,6 +726,9 @@ class WorkerAutomation(
} }
if (isImprovementProbablyAFort(improvement)) { if (isImprovementProbablyAFort(improvement)) {
value += evaluateFortSurroundings(tile, improvement.hasUnique(UniqueType.TakesOverAdjacentTiles)) value += evaluateFortSurroundings(tile, improvement.hasUnique(UniqueType.TakesOverAdjacentTiles))
} else if (tile.getTileImprovement() != null && isImprovementProbablyAFort(tile.getTileImprovement()!!)) {
// Replace/build improvements on other tiles before this one
value /= 2
} }
return value return value
} }
@ -756,7 +759,8 @@ class WorkerAutomation(
//todo Should this not also check impassable and the fort improvement's terrainsCanBeBuiltOn/uniques? //todo Should this not also check impassable and the fort improvement's terrainsCanBeBuiltOn/uniques?
if (tile.isCityCenter() // don't build fort in the city if (tile.isCityCenter() // don't build fort in the city
|| !tile.isLand // don't build fort in the water || !tile.isLand // don't build fort in the water
|| tile.hasViewableResource(civInfo) // don't build on resource tiles || (tile.hasViewableResource(civInfo)
&& tile.tileResource.resourceType != ResourceType.Bonus) // don't build on resource tiles
|| tile.containsGreatImprovement() // don't build on great improvements (including citadel) || tile.containsGreatImprovement() // don't build on great improvements (including citadel)
) return false ) return false
@ -771,35 +775,38 @@ class WorkerAutomation(
* @return Yes the location is good for a Fort here * @return Yes the location is good for a Fort here
*/ */
private fun evaluateFortSurroundings(tile: Tile, isCitadel: Boolean): Float { private fun evaluateFortSurroundings(tile: Tile, isCitadel: Boolean): Float {
//todo for placement is disabled for now, it should be re-enabled in the future.
return -100f
//todo Is the Citadel code dead anyway? If not - why does the nearestTiles check not respect the param?
// build on our land only // build on our land only
if (tile.owningCity?.civ != civInfo && if (tile.owningCity?.civ != civInfo &&
// except citadel which can be built near-by // except citadel which can be built near-by
(!isCitadel || tile.neighbors.all { it.getOwner() != civInfo }) || (!isCitadel || tile.neighbors.all { it.getOwner() != civInfo }) ||
!isAcceptableTileForFort(tile)) return 0f !isAcceptableTileForFort(tile)) return 0f
val enemyCivs = civInfo.getKnownCivs() val enemyCivs = civInfo.getKnownCivs()
.filter { it != civInfo && it.cities.isNotEmpty() && civInfo.isAtWarWith(it) } .filter { it != civInfo && it.cities.isNotEmpty() && (civInfo.isAtWarWith(it) || civInfo.getDiplomacyManager(it).isRelationshipLevelLE(RelationshipLevel.Enemy)) }
// no potential enemies // no potential enemies
if (enemyCivs.none()) return 0f if (enemyCivs.none()) return 0f
var valueOfFort = 3f var valueOfFort = 2f
if (civInfo.isCityState() && civInfo.getAllyCiv() != null) valueOfFort -= 1f // Allied city states probably don't need to build forts
if (tile.hasViewableResource(civInfo)) valueOfFort -= 1
// if this place is not perfect, let's see if there is a better one // if this place is not perfect, let's see if there is a better one
val nearestTiles = tile.getTilesInDistance(2).filter { it.owningCity?.civ == civInfo }.toList() val nearestTiles = tile.getTilesInDistance(1).filter { it.owningCity?.civ == civInfo }.toList()
for (closeTile in nearestTiles) { for (closeTile in nearestTiles) {
// don't build forts too close to the cities // don't build forts too close to the cities
if (closeTile.isCityCenter()) valueOfFort -= 1f if (closeTile.isCityCenter()) {
valueOfFort -= .5f
continue
}
// don't build forts too close to other forts // don't build forts too close to other forts
if (closeTile.improvement != null if (closeTile.improvement != null
&& isImprovementProbablyAFort(closeTile.getTileImprovement()!!) && isImprovementProbablyAFort(closeTile.getTileImprovement()!!)
|| (closeTile.improvementInProgress != null && isImprovementProbablyAFort(closeTile.improvementInProgress!!))) || (closeTile.improvementInProgress != null && isImprovementProbablyAFort(closeTile.improvementInProgress!!)))
valueOfFort -= .5f valueOfFort -= 1f
// there is another better tile for the fort // there is probably another better tile for the fort
if (!tile.isHill() && closeTile.isHill() && if (!tile.isHill() && closeTile.isHill() &&
isAcceptableTileForFort(closeTile)) valueOfFort -= .5f isAcceptableTileForFort(closeTile)) valueOfFort -= .2f
// We want to build forts more in choke points // We want to build forts more in choke points
if (tile.isImpassible()) valueOfFort += .2f if (tile.isImpassible()) valueOfFort += .2f
} }
@ -807,38 +814,43 @@ class WorkerAutomation(
val threatMapping: (Civilization) -> Int = { val threatMapping: (Civilization) -> Int = {
// the war is already a good nudge to build forts // the war is already a good nudge to build forts
(if (civInfo.isAtWarWith(it)) 5 else 0) + (if (civInfo.isAtWarWith(it)) 5 else 0) +
// let's check also the force of the enemy // let's check also the force of the enemy
when (Automation.threatAssessment(civInfo, it)) { when (Automation.threatAssessment(civInfo, it)) {
ThreatLevel.VeryLow -> 1 // do not build forts ThreatLevel.VeryLow -> 1 // do not build forts
ThreatLevel.Low -> 6 // too close, let's build until it is late ThreatLevel.Low -> 6 // too close, let's build until it is late
ThreatLevel.Medium -> 10 ThreatLevel.Medium -> 10
ThreatLevel.High -> 15 // they are strong, let's built until they reach us ThreatLevel.High -> 15 // they are strong, let's built until they reach us
ThreatLevel.VeryHigh -> 20 ThreatLevel.VeryHigh -> 20
} }
} }
val enemyCivsIsCloseEnough = enemyCivs.filter { NextTurnAutomation.getMinDistanceBetweenCities( val enemyCivsIsCloseEnough = enemyCivs.filter { NextTurnAutomation.getMinDistanceBetweenCities(
civInfo, it) <= threatMapping(it) } civInfo, it) <= threatMapping(it) }
// no threat, let's not build fort // No threat, let's not build fort
if (enemyCivsIsCloseEnough.none()) return 0f if (enemyCivsIsCloseEnough.none()) return 0f
// make list of enemy cities as sources of threat // Make a list of enemy cities as sources of threat
val enemyCities = mutableListOf<Tile>() val enemyCities = mutableListOf<Tile>()
enemyCivsIsCloseEnough.forEach { enemyCities.addAll(it.cities.map { city -> city.getCenterTile() }) } enemyCivsIsCloseEnough.forEach { enemyCities.addAll(it.cities.map { city -> city.getCenterTile() }) }
// find closest enemy city // Find closest enemy city
val closestEnemyCity = enemyCities.minByOrNull { it.aerialDistanceTo(tile) }!! val closestEnemyCity = enemyCities.minByOrNull { it.aerialDistanceTo(tile) }!!
val distanceToEnemy = tile.aerialDistanceTo(closestEnemyCity) val distanceToEnemyCity = tile.aerialDistanceTo(closestEnemyCity)
// Find our closest city to defend from this enemy city
val closestCity = civInfo.cities.minByOrNull { it.getCenterTile().aerialDistanceTo(tile) }!!.getCenterTile()
val distanceToCity = tile.aerialDistanceTo(closestCity)
val distanceBetweenCities = closestEnemyCity.aerialDistanceTo(closestCity)
// Find the distance between the target enemy city to our closest city
val distanceOfEnemyCityToClosestCityOfUs = civInfo.cities.map { it.getCenterTile().aerialDistanceTo(closestEnemyCity) }.minBy { it }
// find closest our city to defend from this enemy city // We don't want to defend city closest to this this tile if it is behind other cities
val closestOurCity = civInfo.cities.minByOrNull { it.getCenterTile().aerialDistanceTo(tile) }!!.getCenterTile() if (distanceBetweenCities >= distanceOfEnemyCityToClosestCityOfUs + 2) return 0f
val distanceToOurCity = tile.aerialDistanceTo(closestOurCity)
val distanceBetweenCities = closestEnemyCity.aerialDistanceTo(closestOurCity) // This location is not between the city and the enemy
// This location is not between a city and the enemy if (distanceToEnemyCity >= distanceBetweenCities
if (distanceToEnemy >= distanceBetweenCities) return 0f // Don't build in enemy city range
|| distanceToEnemyCity <= 2) return 0f
valueOfFort += 2 - Math.abs(distanceBetweenCities - 1 - distanceToEnemy) valueOfFort += 2 - abs(distanceBetweenCities - 1 - distanceToEnemyCity)
// let's build fort on the front line, not behind the city
// +2 is a acceptable deviation from the straight line between cities // +2 is a acceptable deviation from the straight line between cities
return valueOfFort.coerceAtLeast(0f) return valueOfFort.coerceAtLeast(0f)
} }

View File

@ -561,7 +561,7 @@ open class Tile : IsPartOfGameInfoSerialization {
fun getTilesInDistanceRange(range: IntRange): Sequence<Tile> = tileMap.getTilesInDistanceRange(position, range) fun getTilesInDistanceRange(range: IntRange): Sequence<Tile> = tileMap.getTilesInDistanceRange(position, range)
fun getTilesAtDistance(distance: Int): Sequence<Tile> =tileMap.getTilesAtDistance(position, distance) fun getTilesAtDistance(distance: Int): Sequence<Tile> =tileMap.getTilesAtDistance(position, distance)
fun getDefensiveBonus(): Float { fun getDefensiveBonus(includeImprovementBonus: Boolean = true): Float {
var bonus = baseTerrainObject.defenceBonus var bonus = baseTerrainObject.defenceBonus
if (terrainFeatureObjects.isNotEmpty()) { if (terrainFeatureObjects.isNotEmpty()) {
val otherTerrainBonus = terrainFeatureObjects.maxOf { it.defenceBonus } val otherTerrainBonus = terrainFeatureObjects.maxOf { it.defenceBonus }
@ -569,7 +569,7 @@ open class Tile : IsPartOfGameInfoSerialization {
} }
if (naturalWonder != null) bonus += getNaturalWonder().defenceBonus if (naturalWonder != null) bonus += getNaturalWonder().defenceBonus
val tileImprovement = getUnpillagedTileImprovement() val tileImprovement = getUnpillagedTileImprovement()
if (tileImprovement != null) { if (tileImprovement != null && includeImprovementBonus) {
for (unique in tileImprovement.getMatchingUniques(UniqueType.DefensiveBonus, StateForConditionals(tile = this))) for (unique in tileImprovement.getMatchingUniques(UniqueType.DefensiveBonus, StateForConditionals(tile = this)))
bonus += unique.params[0].toFloat() / 100 bonus += unique.params[0].toFloat() / 100
} }