chore: 'use gold' automation from NextTurnAutomation

Cleaned up Tile.kt a bit by moving single-use functions to where they are used
This commit is contained in:
Yair Morgenstern
2023-10-08 17:15:53 +03:00
parent dd67a7d3df
commit 6dc7f0ec4c
6 changed files with 223 additions and 202 deletions

View File

@ -32,10 +32,8 @@ import com.unciv.models.Counter
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.BeliefType
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.models.ruleset.MilestoneType
import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.PerpetualConstruction
import com.unciv.models.ruleset.Policy
import com.unciv.models.ruleset.PolicyBranch
import com.unciv.models.ruleset.Victory
@ -47,8 +45,6 @@ import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr
import com.unciv.ui.screens.victoryscreen.RankingType
import java.util.SortedMap
import java.util.TreeMap
import kotlin.math.min
import kotlin.random.Random
@ -84,7 +80,7 @@ object NextTurnAutomation {
chooseTechToResearch(civInfo)
automateCityBombardment(civInfo)
useGold(civInfo)
UseGoldAutomation.useGold(civInfo)
if (!civInfo.isCityState()) {
protectCityStates(civInfo)
bullyCityStates(civInfo)
@ -291,149 +287,6 @@ object NextTurnAutomation {
civInfo.popupAlerts.clear() // AIs don't care about popups.
}
private fun tryGainInfluence(civInfo: Civilization, cityState: Civilization) {
if (civInfo.gold < 250) return // save up
if (cityState.getDiplomacyManager(civInfo).getInfluence() < 20) {
cityState.cityStateFunctions.receiveGoldGift(civInfo, 250)
return
}
if (civInfo.gold < 500) return // it's not worth it to invest now, wait until you have enough for 2
cityState.cityStateFunctions.receiveGoldGift(civInfo, 500)
return
}
private fun useGoldForCityStates(civ: Civilization) {
// RARE EDGE CASE: If you ally with a city-state, you may reveal more map that includes ANOTHER civ!
// So if we don't lock this list, we may later discover that there are more known civs, concurrent modification exception!
val knownCityStates = civ.getKnownCivs().filter { it.isCityState() }.toList()
// canBeMarriedBy checks actual cost, but it can't be below 500*speedmodifier, and the later check is expensive
if (civ.gold >= 330 && civ.getHappiness() > 0 && civ.hasUnique(UniqueType.CityStateCanBeBoughtForGold)) {
for (cityState in knownCityStates.toList() ) { // Materialize sequence as diplomaticMarriage may kill a CS
if (cityState.cityStateFunctions.canBeMarriedBy(civ))
cityState.cityStateFunctions.diplomaticMarriage(civ)
if (civ.getHappiness() <= 0) break // Stop marrying if happiness is getting too low
}
}
if (civ.gold < 250) return // skip checks if tryGainInfluence will bail anyway
if (civ.wantsToFocusOn(Victory.Focus.Culture)) {
for (cityState in knownCityStates.filter { it.cityStateFunctions.canProvideStat(Stat.Culture) }) {
val diploManager = cityState.getDiplomacyManager(civ)
if (diploManager.getInfluence() < 40) { // we want to gain influence with them
tryGainInfluence(civ, cityState)
}
}
}
if (civ.gold < 250 || knownCityStates.none()) return
val cityState = knownCityStates
.filter { it.getAllyCiv() != civ.civName }
.associateWith { valueCityStateAlliance(civ, it) }
.maxByOrNull { it.value }?.takeIf { it.value > 0 }?.key
if (cityState != null) {
tryGainInfluence(civ, cityState)
}
}
/** allow AI to spend money to purchase city-state friendship, buildings & unit */
private fun useGold(civ: Civilization) {
if (civ.isMajorCiv())
useGoldForCityStates(civ)
for (city in civ.cities.sortedByDescending { it.population.population }) {
val construction = city.cityConstructions.getCurrentConstruction()
if (construction is PerpetualConstruction) continue
if ((construction as INonPerpetualConstruction).canBePurchasedWithStat(city, Stat.Gold)
&& city.civ.gold / 3 >= construction.getStatBuyCost(city, Stat.Gold)!!) {
city.cityConstructions.purchaseConstruction(construction, 0, true)
}
}
maybeBuyCityTiles(civ)
}
private fun maybeBuyCityTiles(civInfo: Civilization) {
if (civInfo.gold <= 0)
return
// Don't buy tiles in the very early game. It is unlikely that we already have the required
// tech, the necessary worker and that there is a reasonable threat from another player to
// grab the tile. We could also check all that, but it would require a lot of cycles each
// turn and this is probably a good approximation.
if (civInfo.gameInfo.turns < (civInfo.gameInfo.speed.scienceCostModifier * 20).toInt())
return
val highlyDesirableTiles: SortedMap<Tile, MutableSet<City>> = TreeMap(
compareByDescending<Tile?> { it?.naturalWonder != null }
.thenByDescending { it?.resource != null && it.tileResource.resourceType == ResourceType.Luxury }
.thenByDescending { it?.resource != null && it.tileResource.resourceType == ResourceType.Strategic }
// This is necessary, so that the map keeps Tiles with the same resource as two
// separate entries.
.thenBy { it.hashCode() }
)
for (city in civInfo.cities.filter { !it.isPuppet && !it.isBeingRazed }) {
val highlyDesirableTilesInCity = city.tilesInRange.filter {
val hasNaturalWonder = it.naturalWonder != null
val hasLuxuryCivDoesntOwn =
it.hasViewableResource(civInfo)
&& it.tileResource.resourceType == ResourceType.Luxury
&& !civInfo.hasResource(it.resource!!)
val hasResourceCivHasNoneOrLittle =
it.hasViewableResource(civInfo)
&& it.tileResource.resourceType == ResourceType.Strategic
&& civInfo.getResourceAmount(it.resource!!) <= 3
it.isVisible(civInfo) && it.getOwner() == null
&& it.neighbors.any { neighbor -> neighbor.getCity() == city }
(hasNaturalWonder || hasLuxuryCivDoesntOwn || hasResourceCivHasNoneOrLittle)
}
for (highlyDesirableTileInCity in highlyDesirableTilesInCity) {
highlyDesirableTiles.getOrPut(highlyDesirableTileInCity) { mutableSetOf() }
.add(city)
}
}
// Always try to buy highly desirable tiles if it can be afforded.
for (highlyDesirableTile in highlyDesirableTiles) {
val cityWithLeastCostToBuy = highlyDesirableTile.value.minBy {
it.getCenterTile().aerialDistanceTo(highlyDesirableTile.key)
}
val bfs = BFS(cityWithLeastCostToBuy.getCenterTile())
{
it.getOwner() == null || it.owningCity == cityWithLeastCostToBuy
}
bfs.stepUntilDestination(highlyDesirableTile.key)
val tilesThatNeedBuying =
bfs.getPathTo(highlyDesirableTile.key).filter { it.getOwner() == null }
.toList().reversed() // getPathTo is from destination to source
// We're trying to acquire everything and revert if it fails, because of the difficult
// way how tile acquisition cost is calculated. Everytime you buy a tile, the next one
// gets more expensive and by how much depends on other things such as game speed. To
// not introduce hidden dependencies on that and duplicate that logic here to calculate
// the price of the whole path, this is probably simpler.
var ranOutOfMoney = false
var goldSpent = 0
for (tileThatNeedsBuying in tilesThatNeedBuying) {
val goldCostOfTile =
cityWithLeastCostToBuy.expansion.getGoldCostOfTile(tileThatNeedsBuying)
if (civInfo.gold >= goldCostOfTile) {
cityWithLeastCostToBuy.expansion.buyTile(tileThatNeedsBuying)
goldSpent += goldCostOfTile
} else {
ranOutOfMoney = true
break
}
}
if (ranOutOfMoney) {
for (tileThatNeedsBuying in tilesThatNeedBuying) {
cityWithLeastCostToBuy.expansion.relinquishOwnership(tileThatNeedsBuying)
}
civInfo.addGold(goldSpent)
}
}
}
internal fun valueCityStateAlliance(civInfo: Civilization, cityState: Civilization): Int {
var value = 0
@ -532,7 +385,7 @@ object NextTurnAutomation {
if (civInfo.tech.techsToResearch.isEmpty()) {
val costs = getGroupedResearchableTechs()
if (costs.isEmpty()) return
val cheapestTechs = costs[0]
//Do not consider advanced techs if only one tech left in cheapest group
val techToResearch: Technology =

View File

@ -0,0 +1,177 @@
package com.unciv.logic.automation.civilization
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.BFS
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.models.ruleset.PerpetualConstruction
import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import java.util.SortedMap
import java.util.TreeMap
object UseGoldAutomation {
/** allow AI to spend money to purchase city-state friendship, buildings & unit */
fun useGold(civ: Civilization) {
if (civ.isMajorCiv())
useGoldForCityStates(civ)
for (city in civ.cities.sortedByDescending { it.population.population }) {
val construction = city.cityConstructions.getCurrentConstruction()
if (construction is PerpetualConstruction) continue
if ((construction as INonPerpetualConstruction).canBePurchasedWithStat(city, Stat.Gold)
&& city.civ.gold / 3 >= construction.getStatBuyCost(city, Stat.Gold)!!) {
city.cityConstructions.purchaseConstruction(construction, 0, true)
}
}
maybeBuyCityTiles(civ)
}
private fun useGoldForCityStates(civ: Civilization) {
// RARE EDGE CASE: If you ally with a city-state, you may reveal more map that includes ANOTHER civ!
// So if we don't lock this list, we may later discover that there are more known civs, concurrent modification exception!
val knownCityStates = civ.getKnownCivs().filter { it.isCityState() }.toList()
// canBeMarriedBy checks actual cost, but it can't be below 500*speedmodifier, and the later check is expensive
if (civ.gold >= 330 && civ.getHappiness() > 0 && civ.hasUnique(UniqueType.CityStateCanBeBoughtForGold)) {
for (cityState in knownCityStates.toList() ) { // Materialize sequence as diplomaticMarriage may kill a CS
if (cityState.cityStateFunctions.canBeMarriedBy(civ))
cityState.cityStateFunctions.diplomaticMarriage(civ)
if (civ.getHappiness() <= 0) break // Stop marrying if happiness is getting too low
}
}
if (civ.gold < 250) return // skip checks if tryGainInfluence will bail anyway
if (civ.wantsToFocusOn(Victory.Focus.Culture)) {
for (cityState in knownCityStates.filter { it.cityStateFunctions.canProvideStat(Stat.Culture) }) {
val diploManager = cityState.getDiplomacyManager(civ)
if (diploManager.getInfluence() < 40) { // we want to gain influence with them
tryGainInfluence(civ, cityState)
}
}
}
if (civ.gold < 250 || knownCityStates.none()) return
val cityState = knownCityStates
.filter { it.getAllyCiv() != civ.civName }
.associateWith { NextTurnAutomation.valueCityStateAlliance(civ, it) }
.maxByOrNull { it.value }?.takeIf { it.value > 0 }?.key
if (cityState != null) {
tryGainInfluence(civ, cityState)
}
}
private fun maybeBuyCityTiles(civInfo: Civilization) {
if (civInfo.gold <= 0)
return
// Don't buy tiles in the very early game. It is unlikely that we already have the required
// tech, the necessary worker and that there is a reasonable threat from another player to
// grab the tile. We could also check all that, but it would require a lot of cycles each
// turn and this is probably a good approximation.
if (civInfo.gameInfo.turns < (civInfo.gameInfo.speed.scienceCostModifier * 20).toInt())
return
val highlyDesirableTiles: SortedMap<Tile, MutableSet<City>> = getHighlyDesirableTilesToCityMap(civInfo)
// Always try to buy highly desirable tiles if it can be afforded.
for (highlyDesirableTile in highlyDesirableTiles) {
val cityWithLeastCostToBuy = highlyDesirableTile.value.minBy {
it.getCenterTile().aerialDistanceTo(highlyDesirableTile.key)
}
val bfs = BFS(cityWithLeastCostToBuy.getCenterTile())
{
it.getOwner() == null || it.owningCity == cityWithLeastCostToBuy
}
bfs.stepUntilDestination(highlyDesirableTile.key)
val tilesThatNeedBuying =
bfs.getPathTo(highlyDesirableTile.key).filter { it.getOwner() == null }
.toList().reversed() // getPathTo is from destination to source
// We're trying to acquire everything and revert if it fails, because of the difficult
// way how tile acquisition cost is calculated. Everytime you buy a tile, the next one
// gets more expensive and by how much depends on other things such as game speed. To
// not introduce hidden dependencies on that and duplicate that logic here to calculate
// the price of the whole path, this is probably simpler.
var ranOutOfMoney = false
var goldSpent = 0
for (tileThatNeedsBuying in tilesThatNeedBuying) {
val goldCostOfTile =
cityWithLeastCostToBuy.expansion.getGoldCostOfTile(tileThatNeedsBuying)
if (civInfo.gold >= goldCostOfTile) {
cityWithLeastCostToBuy.expansion.buyTile(tileThatNeedsBuying)
goldSpent += goldCostOfTile
} else {
ranOutOfMoney = true
break
}
}
if (ranOutOfMoney) {
for (tileThatNeedsBuying in tilesThatNeedBuying) {
cityWithLeastCostToBuy.expansion.relinquishOwnership(tileThatNeedsBuying)
}
civInfo.addGold(goldSpent)
}
}
}
private fun getHighlyDesirableTilesToCityMap(civInfo: Civilization): SortedMap<Tile, MutableSet<City>> {
val highlyDesirableTiles: SortedMap<Tile, MutableSet<City>> = TreeMap(
compareByDescending<Tile?> { it?.naturalWonder != null }
.thenByDescending { it?.resource != null && it.tileResource.resourceType == ResourceType.Luxury }
.thenByDescending { it?.resource != null && it.tileResource.resourceType == ResourceType.Strategic }
// This is necessary, so that the map keeps Tiles with the same resource as two
// separate entries.
.thenBy { it.hashCode() }
)
for (city in civInfo.cities.filter { !it.isPuppet && !it.isBeingRazed }) {
val highlyDesirableTilesInCity = city.tilesInRange.filter {
isHighlyDesirableTile(it, civInfo, city)
}
for (highlyDesirableTileInCity in highlyDesirableTilesInCity) {
highlyDesirableTiles.getOrPut(highlyDesirableTileInCity) { mutableSetOf() }
.add(city)
}
}
return highlyDesirableTiles
}
private fun isHighlyDesirableTile(it: Tile, civInfo: Civilization, city: City): Boolean {
if (!it.isVisible(civInfo)) return false
if (it.getOwner() != null) return false
if (it.neighbors.none { neighbor -> neighbor.getCity() == city }) return false
fun hasNaturalWonder() = it.naturalWonder != null
fun hasLuxuryCivDoesntOwn() =
it.hasViewableResource(civInfo)
&& it.tileResource.resourceType == ResourceType.Luxury
&& !civInfo.hasResource(it.resource!!)
fun hasResourceCivHasNoneOrLittle() =
it.hasViewableResource(civInfo)
&& it.tileResource.resourceType == ResourceType.Strategic
&& civInfo.getResourceAmount(it.resource!!) <= 3
return (hasNaturalWonder() || hasLuxuryCivDoesntOwn() || hasResourceCivHasNoneOrLittle())
}
private fun tryGainInfluence(civInfo: Civilization, cityState: Civilization) {
if (civInfo.gold < 250) return // save up
if (cityState.getDiplomacyManager(civInfo).getInfluence() < 20) {
cityState.cityStateFunctions.receiveGoldGift(civInfo, 250)
return
}
if (civInfo.gold < 500) return // it's not worth it to invest now, wait until you have enough for 2
cityState.cityStateFunctions.receiveGoldGift(civInfo, 500)
return
}
}

View File

@ -890,6 +890,22 @@ class MapRegions (val ruleset: Ruleset){
}
// For dividing the map into Regions to determine start locations
fun Tile.getTileFertility(checkCoasts: Boolean): Int {
var fertility = 0
for (terrain in allTerrains) {
if (terrain.hasUnique(UniqueType.OverrideFertility))
return terrain.getMatchingUniques(UniqueType.OverrideFertility).first().params[0].toInt()
else
fertility += terrain.getMatchingUniques(UniqueType.AddFertility)
.sumOf { it.params[0].toInt() }
}
if (isAdjacentToRiver()) fertility += 1
if (isAdjacentTo(Constants.freshWater)) fertility += 1 // meaning total +2 for river
if (checkCoasts && isCoastalTile()) fertility += 2
return fertility
}
fun getRegionPriority(terrain: Terrain?): Int? {
if (terrain == null) // ie "hybrid"
return 99999 // a big number

View File

@ -620,6 +620,16 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun removeFromTile() = currentTile.removeUnit(this)
/** Return null if military on tile, or no civilian */
private fun Tile.getUnguardedCivilian(attacker: MapUnit): MapUnit? {
return when {
militaryUnit != null && militaryUnit != attacker -> null
civilianUnit != null -> civilianUnit!!
else -> null
}
}
fun moveThroughTile(tile: Tile) {
// addPromotion requires currentTile to be valid because it accesses ruleset through it.
// getAncientRuinBonus, if it places a new unit, does too

View File

@ -205,13 +205,6 @@ open class Tile : IsPartOfGameInfoSerialization {
return null
}
/** Return null if military on tile, or no civilian */
fun getUnguardedCivilian(attacker: MapUnit): MapUnit? {
if (militaryUnit != null && militaryUnit != attacker) return null
if (civilianUnit != null) return civilianUnit!!
return null
}
fun getCity(): City? = owningCity
@Transient
@ -353,16 +346,6 @@ open class Tile : IsPartOfGameInfoSerialization {
// We have to .toList() so that the values are stored together once for caching,
// and the toSequence so that aggregations (like neighbors.flatMap{it.units} don't take up their own space
/** Returns the left shared neighbor of `this` and [neighbor] (relative to the view direction `this`->[neighbor]), or null if there is no such tile. */
fun getLeftSharedNeighbor(neighbor: Tile): Tile? {
return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) - 2) % 12)
}
/** Returns the right shared neighbor of `this` and [neighbor] (relative to the view direction `this`->[neighbor]), or null if there is no such tile. */
fun getRightSharedNeighbor(neighbor: Tile): Tile? {
return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) + 2) % 12)
}
fun getRow() = HexMath.getRow(position)
fun getColumn() = HexMath.getColumn(position)
@ -444,16 +427,12 @@ open class Tile : IsPartOfGameInfoSerialization {
else -> false // Neutral - unblocks tile;
}
}
if (isLand) // Only water tiles are blocked if empty
return false
// For water tiles need also to check neighbors:
// enemy military naval units blockade all adjacent water tiles.
for (neighbor in neighbors) {
if (!neighbor.isWater)
continue
for (neighbor in neighbors.filter { it.isWater }) {
val neighborUnit = neighbor.militaryUnit ?: continue
// Embarked units do not blockade adjacent tiles
@ -473,22 +452,6 @@ open class Tile : IsPartOfGameInfoSerialization {
return workingCity != null && workingCity.lockedTiles.contains(position)
}
// For dividing the map into Regions to determine start locations
fun getTileFertility(checkCoasts: Boolean): Int {
var fertility = 0
for (terrain in allTerrains) {
if (terrain.hasUnique(UniqueType.OverrideFertility))
return terrain.getMatchingUniques(UniqueType.OverrideFertility).first().params[0].toInt()
else
fertility += terrain.getMatchingUniques(UniqueType.AddFertility)
.sumOf { it.params[0].toInt() }
}
if (isAdjacentToRiver()) fertility += 1
if (isAdjacentTo(Constants.freshWater)) fertility += 1 // meaning total +2 for river
if (checkCoasts && isCoastalTile()) fertility += 2
return fertility
}
fun providesResources(civInfo: Civilization): Boolean {
if (!hasViewableResource(civInfo)) return false
if (isCityCenter()) return true
@ -576,17 +539,10 @@ open class Tile : IsPartOfGameInfoSerialization {
resource != null && (tileResource.revealedBy == null || civInfo.tech.isResearched(
tileResource.revealedBy!!))
fun getViewableTilesList(distance: Int): List<Tile> =
tileMap.getViewableTiles(position, distance)
fun getTilesInDistance(distance: Int): Sequence<Tile> =
tileMap.getTilesInDistance(position, distance)
fun getTilesInDistanceRange(range: IntRange): Sequence<Tile> =
tileMap.getTilesInDistanceRange(position, range)
fun getTilesAtDistance(distance: Int): Sequence<Tile> =
tileMap.getTilesAtDistance(position, distance)
fun getViewableTilesList(distance: Int): List<Tile> = tileMap.getViewableTiles(position, distance)
fun getTilesInDistance(distance: Int): Sequence<Tile> = tileMap.getTilesInDistance(position, distance)
fun getTilesInDistanceRange(range: IntRange): Sequence<Tile> = tileMap.getTilesInDistanceRange(position, range)
fun getTilesAtDistance(distance: Int): Sequence<Tile> =tileMap.getTilesAtDistance(position, distance)
fun getDefensiveBonus(): Float {
var bonus = baseTerrainObject.defenceBonus
@ -937,9 +893,7 @@ open class Tile : IsPartOfGameInfoSerialization {
owningCity!!.civ.cache.updateCivResources()
}
fun isPillaged(): Boolean {
return improvementIsPillaged || roadIsPillaged
}
fun isPillaged(): Boolean = improvementIsPillaged || roadIsPillaged
fun setRepaired() {
improvementInProgress = null

View File

@ -5,8 +5,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.LocalUniqueCache
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.components.tilegroups.TileGroup
import com.unciv.ui.images.ImageGetter
import kotlin.math.PI
import kotlin.math.atan
@ -33,6 +33,17 @@ class TileLayerBorders(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
}
}
/** Returns the left shared neighbor of `this` and [neighbor] (relative to the view direction `this`->[neighbor]), or null if there is no such tile. */
private fun Tile.getLeftSharedNeighbor(neighbor: Tile): Tile? {
return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) - 2) % 12)
}
/** Returns the right shared neighbor of `this` and [neighbor] (relative to the view direction `this`->[neighbor]), or null if there is no such tile. */
private fun Tile.getRightSharedNeighbor(neighbor: Tile): Tile? {
return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) + 2) % 12)
}
private fun updateBorders() {
// This is longer than it could be, because of performance -