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 resources and improvements =
Show tile yields = Show tile yields =
Show unit movement arrows = Show unit movement arrows =
Show suggested city locations for units that can found cities =
Show pixel units = Show pixel units =
Show pixel improvements = Show pixel improvements =
Unit icon opacity = 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.battle.MapUnitCombatant
import com.unciv.logic.city.City import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitAction 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.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsReligion import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsReligion
import kotlin.math.max
import kotlin.math.min
object SpecificUnitAutomation { object SpecificUnitAutomation {
@ -129,95 +124,36 @@ object SpecificUnitAutomation {
.firstOrNull()?.action?.invoke() .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) { 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. 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()) 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 // 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 } val otherSettlers = unit.civ.units.getCivUnits().filter { it.currentMovement > 0 && it.baseUnit == unit.baseUnit }
if(foundCityAction?.action != null && if(foundCityAction?.action != null &&
otherSettlers.none { 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() foundCityAction.action.invoke()
return return
} }
} }
val citiesByRanking = possibleCityLocations if (unit.getTile().militaryUnit == null // Don't move until you're accompanied by a military unit
.map { Pair(it, rankTileAsCityCenter(it, nearbyTileRankings, luxuryResourcesInCivArea)) } && !unit.civ.isCityState() // ..unless you're a city state that was unable to settle its city on turn 1
.sortedByDescending { it.second }.toList() && 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! // 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 // Also this lead to some routing problems, see https://github.com/yairm210/Unciv/issues/3653
val bestCityLocation: Tile? = citiesByRanking.firstOrNull { val bestCityLocation: Tile? =
val pathSize = unit.movement.getShortestPath(it.first).size CityLocationTileRanker.getBestTilesToFoundCity(unit).firstOrNull {
return@firstOrNull pathSize in 1..3 val pathSize = unit.movement.getShortestPath(it.first).size
}?.first return@firstOrNull pathSize in 1..3
}?.first
if (bestCityLocation == null) { // We got a badass over here, all tiles within 5 are taken? if (bestCityLocation == null) { // We got a badass over here, all tiles within 5 are taken?
// Try to move towards the frontier // Try to move towards the frontier

View File

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

View File

@ -99,6 +99,7 @@ open class TileGroup(
layerMisc.removeHexOutline() layerMisc.removeHexOutline()
layerOverlay.hideHighlight() layerOverlay.hideHighlight()
layerOverlay.hideCrosshair() layerOverlay.hideCrosshair()
layerOverlay.hideGoodCityLocationIndicator()
// Show all layers by default // Show all layers by default
setAllLayersVisible(true) 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 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 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 fog = ImageGetter.getImage(strings().crosshatchHexagon ).setHexagonSize()
private val unexplored = ImageGetter.getImage(strings().unexploredTile ).setHexagonSize() private val unexplored = ImageGetter.getImage(strings().unexploredTile ).setHexagonSize()
@ -21,6 +22,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
highlight.isVisible = false highlight.isVisible = false
crosshair.isVisible = false crosshair.isVisible = false
goodCityLocationIndicator.isVisible = false
fog.isVisible = false fog.isVisible = false
fog.color = Color.WHITE.cpy().apply { a = 0.2f } fog.color = Color.WHITE.cpy().apply { a = 0.2f }
@ -29,6 +31,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
addActor(highlight) addActor(highlight)
addActor(fog) addActor(fog)
addActor(crosshair) addActor(crosshair)
addActor(goodCityLocationIndicator)
} }
fun showCrosshair(alpha: Float = 1f) { fun showCrosshair(alpha: Float = 1f) {
@ -58,10 +61,21 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
determineVisibility() determineVisibility()
} }
fun showGoodCityLocationIndicator() {
goodCityLocationIndicator.isVisible = true
determineVisibility()
}
fun hideGoodCityLocationIndicator() {
goodCityLocationIndicator.isVisible = false
determineVisibility()
}
fun reset() { fun reset() {
fog.isVisible = true fog.isVisible = true
highlight.isVisible = false highlight.isVisible = false
crosshair.isVisible = false crosshair.isVisible = false
goodCityLocationIndicator.isVisible = false
determineVisibility() determineVisibility()
} }
@ -79,7 +93,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup,
} }
override fun determineVisibility() { 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() 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 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 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 worked tiles", settings.showWorkedTiles, true) { settings.showWorkedTiles = it }
optionsPopup.addCheckbox(this, "Show resources and improvements", settings.showResourcesAndImprovements, true) { settings.showResourcesAndImprovements = 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.UncivGame
import com.unciv.logic.automation.unit.AttackableTile import com.unciv.logic.automation.unit.AttackableTile
import com.unciv.logic.automation.unit.BattleHelper 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.automation.unit.UnitAutomation
import com.unciv.logic.battle.Battle import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.MapUnitCombatant 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) { private fun updateBombardableTilesForSelectedCity(city: City) {