Show a highlight for the tile that seems most suitable to found a cit… (#9099)

* Show a highlight for the tile that seems most suitable to found a city (can be turned off in settings)

* Don't allow players to cheat through highlighting suggested map tiles for city founding.

* Don't pass whether cheating is allowed, but just decide based on whether civ is AI or not. That way it will also work correctly for automated settlers (by human players). Also show it in the first round, because why not. If the map generator puts us on a shitty starting tile, why not tell the player?

* Address comments

* Address comments
This commit is contained in:
WhoIsJohannes 2023-04-13 14:43:46 +02:00 committed by GitHub
parent b0f4e42e99
commit 193114078b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 190 additions and 80 deletions

View File

@ -729,6 +729,7 @@ Show worked tiles =
Show resources and improvements =
Show tile yields =
Show unit movement arrows =
Show suggested city locations for units that can found cities =
Show pixel units =
Show pixel improvements =
Unit icon opacity =

View File

@ -0,0 +1,145 @@
package com.unciv.logic.automation.unit
import com.unciv.logic.automation.Automation
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileResource
class CityLocationTileRanker {
companion object {
fun getBestTilesToFoundCity(unit: MapUnit): Sequence<Pair<Tile, Float>> {
val modConstants = unit.civ.gameInfo.ruleset.modOptions.constants
val tilesNearCities = sequence {
for (city in unit.civ.gameInfo.getCities()) {
val center = city.getCenterTile()
if (unit.civ.knows(city.civ) &&
// If the CITY OWNER knows that the UNIT OWNER agreed not to settle near them
city.civ.getDiplomacyManager(unit.civ)
.hasFlag(DiplomacyFlags.AgreedToNotSettleNearUs)
) {
yieldAll(
center.getTilesInDistance(6)
.filter { canUseTileForRanking(it, unit.civ) })
continue
}
yieldAll(
center.getTilesInDistance(modConstants.minimalCityDistance)
.filter { canUseTileForRanking(it, unit.civ) }
.filter { it.getContinent() == center.getContinent() }
)
yieldAll(
center.getTilesInDistance(modConstants.minimalCityDistanceOnDifferentContinents)
.filter { canUseTileForRanking(it, unit.civ) }
.filter { it.getContinent() != center.getContinent() }
)
}
}.toSet()
// This is to improve performance - instead of ranking each tile in the area up to 19 times, do it once.
val nearbyTileRankings = getNearbyTileRankings(unit.getTile(), unit.civ)
val distanceFromHome = if (unit.civ.cities.isEmpty()) 0
else unit.civ.cities.minOf { it.getCenterTile().aerialDistanceTo(unit.getTile()) }
val range = (8 - distanceFromHome).coerceIn(
1,
5
) // Restrict vision when far from home to avoid death marches
val possibleCityLocations = unit.getTile().getTilesInDistance(range)
.filter { canUseTileForRanking(it, unit.civ) }
.filter {
val tileOwner = it.getOwner()
it.isLand && !it.isImpassible() && (tileOwner == null || tileOwner == unit.civ) // don't allow settler to settle inside other civ's territory
&& (unit.currentTile == it || unit.movement.canMoveTo(it))
&& it !in tilesNearCities
}
val luxuryResourcesInCivArea = getLuxuryResourcesInCivArea(unit.civ)
return possibleCityLocations
.map {
Pair(
it,
rankTileAsCityCenterWithCachedValues(
it,
nearbyTileRankings,
luxuryResourcesInCivArea,
unit.civ
),
)
}
.sortedByDescending { it.second }
}
fun rankTileAsCityCenter(tile: Tile, civ: Civilization): Float {
val nearbyTileRankings = getNearbyTileRankings(tile, civ)
val luxuryResourcesInCivArea = getLuxuryResourcesInCivArea(civ)
return rankTileAsCityCenterWithCachedValues(
tile,
nearbyTileRankings,
luxuryResourcesInCivArea,
civ
)
}
private fun canUseTileForRanking(
tile: Tile,
civ: Civilization
) =
// The AI is allowed to cheat and act like it knows the whole map.
tile.isExplored(civ) || civ.isAI()
private fun getNearbyTileRankings(
tile: Tile,
civ: Civilization
): Map<Tile, Float> {
return tile.getTilesInDistance(7)
.filter { canUseTileForRanking(it, civ) }
.associateBy({ it }, { Automation.rankTile(it, civ) })
}
private fun getLuxuryResourcesInCivArea(civ: Civilization): Sequence<TileResource> {
return civ.cities.asSequence()
.flatMap { it.getTiles().asSequence() }.filter { it.resource != null }
.map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury }
.distinct()
}
private fun rankTileAsCityCenterWithCachedValues(
tile: Tile, nearbyTileRankings: Map<Tile, Float>,
luxuryResourcesInCivArea: Sequence<TileResource>,
civ: Civilization
): Float {
val bestTilesFromOuterLayer = tile.getTilesAtDistance(2)
.filter { canUseTileForRanking(it, civ) }
.sortedByDescending { nearbyTileRankings[it] }.take(2)
val top5Tiles =
(tile.neighbors.filter {
canUseTileForRanking(
it,
civ
)
} + bestTilesFromOuterLayer)
.sortedByDescending { nearbyTileRankings[it] }
.take(5)
var rank = top5Tiles.map { nearbyTileRankings.getValue(it) }.sum()
if (tile.isCoastalTile()) rank += 5
val luxuryResourcesInCityArea =
tile.getTilesAtDistance(2).filter { canUseTileForRanking(it, civ) }
.filter { it.resource != null }
.map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury }
.distinct()
val luxuryResourcesAlreadyInCivArea =
luxuryResourcesInCivArea.map { it.name }.toHashSet()
val luxuryResourcesNotYetInCiv = luxuryResourcesInCityArea
.count { it.name !in luxuryResourcesAlreadyInCivArea }
rank += luxuryResourcesNotYetInCiv * 10
return rank
}
}
}

View File

@ -7,19 +7,14 @@ import com.unciv.logic.battle.GreatGeneralImplementation
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitAction
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsReligion
import kotlin.math.max
import kotlin.math.min
object SpecificUnitAutomation {
@ -129,95 +124,36 @@ object SpecificUnitAutomation {
.firstOrNull()?.action?.invoke()
}
private fun rankTileAsCityCenter(tile: Tile, nearbyTileRankings: Map<Tile, Float>,
luxuryResourcesInCivArea: Sequence<TileResource>): Float {
val bestTilesFromOuterLayer = tile.getTilesAtDistance(2)
.sortedByDescending { nearbyTileRankings[it] }.take(2)
val top5Tiles = (tile.neighbors + bestTilesFromOuterLayer)
.sortedByDescending { nearbyTileRankings[it] }
.take(5)
var rank = top5Tiles.map { nearbyTileRankings.getValue(it) }.sum()
if (tile.isCoastalTile()) rank += 5
val luxuryResourcesInCityArea = tile.getTilesAtDistance(2).filter { it.resource != null }
.map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury }.distinct()
val luxuryResourcesAlreadyInCivArea = luxuryResourcesInCivArea.map { it.name }.toHashSet()
val luxuryResourcesNotYetInCiv = luxuryResourcesInCityArea
.count { it.name !in luxuryResourcesAlreadyInCivArea }
rank += luxuryResourcesNotYetInCiv * 10
return rank
}
fun automateSettlerActions(unit: MapUnit) {
val modConstants = unit.civ.gameInfo.ruleset.modOptions.constants
if (unit.getTile().militaryUnit == null // Don't move until you're accompanied by a military unit
&& !unit.civ.isCityState() // ..unless you're a city state that was unable to settle its city on turn 1
&& unit.getDamageFromTerrain() < unit.health) return // Also make sure we won't die waiting
val tilesNearCities = sequence {
for (city in unit.civ.gameInfo.getCities()) {
val center = city.getCenterTile()
if (unit.civ.knows(city.civ) &&
// If the CITY OWNER knows that the UNIT OWNER agreed not to settle near them
city.civ.getDiplomacyManager(unit.civ).hasFlag(DiplomacyFlags.AgreedToNotSettleNearUs)
) {
yieldAll(center.getTilesInDistance(6))
continue
}
yieldAll(center.getTilesInDistance(modConstants.minimalCityDistance)
.filter { it.getContinent() == center.getContinent() }
)
yieldAll(center.getTilesInDistance(modConstants.minimalCityDistanceOnDifferentContinents)
.filter { it.getContinent() != center.getContinent() }
)
}
}.toSet()
// This is to improve performance - instead of ranking each tile in the area up to 19 times, do it once.
val nearbyTileRankings = unit.getTile().getTilesInDistance(7)
.associateBy({ it }, { Automation.rankTile(it, unit.civ) })
val distanceFromHome = if (unit.civ.cities.isEmpty()) 0
else unit.civ.cities.minOf { it.getCenterTile().aerialDistanceTo(unit.getTile()) }
val range = max(1, min(5, 8 - distanceFromHome)) // Restrict vision when far from home to avoid death marches
val possibleCityLocations = unit.getTile().getTilesInDistance(range)
.filter {
val tileOwner = it.getOwner()
it.isLand && !it.isImpassible() && (tileOwner == null || tileOwner == unit.civ) // don't allow settler to settle inside other civ's territory
&& (unit.currentTile == it || unit.movement.canMoveTo(it))
&& it !in tilesNearCities
}.toList()
val luxuryResourcesInCivArea = unit.civ.cities.asSequence()
.flatMap { it.getTiles().asSequence() }.filter { it.resource != null }
.map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury }
.distinct()
if (unit.civ.gameInfo.turns == 0) { // Special case, we want AI to settle in place on turn 1.
val foundCityAction = UnitActions.getFoundCityAction(unit, unit.getTile())
// Depending on era and difficulty we might start with more than one settler. In that case settle the one with the best location
val otherSettlers = unit.civ.units.getCivUnits().filter { it.currentMovement > 0 && it.baseUnit == unit.baseUnit }
if(foundCityAction?.action != null &&
otherSettlers.none {
rankTileAsCityCenter(it.getTile(), nearbyTileRankings, emptySequence()) > rankTileAsCityCenter(unit.getTile(), nearbyTileRankings, emptySequence())
} ) {
CityLocationTileRanker.rankTileAsCityCenter(
it.getTile(), unit.civ
) > CityLocationTileRanker.rankTileAsCityCenter(
unit.getTile(), unit.civ
)
}
) {
foundCityAction.action.invoke()
return
}
}
val citiesByRanking = possibleCityLocations
.map { Pair(it, rankTileAsCityCenter(it, nearbyTileRankings, luxuryResourcesInCivArea)) }
.sortedByDescending { it.second }.toList()
if (unit.getTile().militaryUnit == null // Don't move until you're accompanied by a military unit
&& !unit.civ.isCityState() // ..unless you're a city state that was unable to settle its city on turn 1
&& unit.getDamageFromTerrain() < unit.health) return // Also make sure we won't die waiting
// It's possible that we'll see a tile "over the sea" that's better than the tiles close by, but that's not a reason to abandon the close tiles!
// Also this lead to some routing problems, see https://github.com/yairm210/Unciv/issues/3653
val bestCityLocation: Tile? = citiesByRanking.firstOrNull {
val pathSize = unit.movement.getShortestPath(it.first).size
return@firstOrNull pathSize in 1..3
}?.first
val bestCityLocation: Tile? =
CityLocationTileRanker.getBestTilesToFoundCity(unit).firstOrNull {
val pathSize = unit.movement.getShortestPath(it.first).size
return@firstOrNull pathSize in 1..3
}?.first
if (bestCityLocation == null) { // We got a badass over here, all tiles within 5 are taken?
// Try to move towards the frontier

View File

@ -44,6 +44,7 @@ class GameSettings {
var showResourcesAndImprovements: Boolean = true
var showTileYields: Boolean = false
var showUnitMovements: Boolean = false
var showSettlersSuggestedCityLocations: Boolean = true
var checkForDueUnits: Boolean = true
var autoUnitCycle: Boolean = true

View File

@ -99,6 +99,7 @@ open class TileGroup(
layerMisc.removeHexOutline()
layerOverlay.hideHighlight()
layerOverlay.hideCrosshair()
layerOverlay.hideGoodCityLocationIndicator()
// Show all layers by default
setAllLayersVisible(true)

View File

@ -14,6 +14,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
private val highlight = ImageGetter.getImage(strings().highlight).setHexagonSize() // for blue and red circles/emphasis on the tile
private val crosshair = ImageGetter.getImage(strings().crosshair).setHexagonSize() // for when a unit is targeted
private val goodCityLocationIndicator = ImageGetter.getImage("OtherIcons/Cities").setHexagonSize(0.25f)
private val fog = ImageGetter.getImage(strings().crosshatchHexagon ).setHexagonSize()
private val unexplored = ImageGetter.getImage(strings().unexploredTile ).setHexagonSize()
@ -21,6 +22,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
highlight.isVisible = false
crosshair.isVisible = false
goodCityLocationIndicator.isVisible = false
fog.isVisible = false
fog.color = Color.WHITE.cpy().apply { a = 0.2f }
@ -29,6 +31,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
addActor(highlight)
addActor(fog)
addActor(crosshair)
addActor(goodCityLocationIndicator)
}
fun showCrosshair(alpha: Float = 1f) {
@ -58,10 +61,21 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
determineVisibility()
}
fun showGoodCityLocationIndicator() {
goodCityLocationIndicator.isVisible = true
determineVisibility()
}
fun hideGoodCityLocationIndicator() {
goodCityLocationIndicator.isVisible = false
determineVisibility()
}
fun reset() {
fog.isVisible = true
highlight.isVisible = false
crosshair.isVisible = false
goodCityLocationIndicator.isVisible = false
determineVisibility()
}
@ -79,7 +93,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
}
override fun determineVisibility() {
isVisible = fog.isVisible || highlight.isVisible || crosshair.isVisible
isVisible = fog.isVisible || highlight.isVisible || crosshair.isVisible || goodCityLocationIndicator.isVisible
}
}

View File

@ -76,6 +76,7 @@ fun displayTab(
add("Visual Hints".toLabel(fontSize = 24)).colspan(2).row()
optionsPopup.addCheckbox(this, "Show unit movement arrows", settings.showUnitMovements, true) { settings.showUnitMovements = it }
optionsPopup.addCheckbox(this, "Show suggested city locations for units that can found cities", settings.showSettlersSuggestedCityLocations, true) { settings.showSettlersSuggestedCityLocations = it }
optionsPopup.addCheckbox(this, "Show tile yields", settings.showTileYields, true) { settings.showTileYields = it } // JN
optionsPopup.addCheckbox(this, "Show worked tiles", settings.showWorkedTiles, true) { settings.showWorkedTiles = it }
optionsPopup.addCheckbox(this, "Show resources and improvements", settings.showResourcesAndImprovements, true) { settings.showResourcesAndImprovements = it }

View File

@ -19,6 +19,7 @@ import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.automation.unit.AttackableTile
import com.unciv.logic.automation.unit.BattleHelper
import com.unciv.logic.automation.unit.CityLocationTileRanker
import com.unciv.logic.automation.unit.UnitAutomation
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.MapUnitCombatant
@ -697,6 +698,16 @@ class WorldMapHolder(
)
}
}
// Highlight best tiles for city founding
if (unit.hasUnique(UniqueType.FoundCity)
&& UncivGame.Current.settings.showSettlersSuggestedCityLocations
) {
CityLocationTileRanker.getBestTilesToFoundCity(unit).map { it.first }
.filter { it.isExplored(unit.civ) }.take(3).forEach {
tileGroups[it]!!.layerOverlay.showGoodCityLocationIndicator()
}
}
}
private fun updateBombardableTilesForSelectedCity(city: City) {