mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-27 13:55:54 -04:00
chore: Separated start normalization into a separate object
This commit is contained in:
parent
f52e7d37f4
commit
e1a33f2116
@ -18,7 +18,6 @@ import com.unciv.models.ruleset.tile.TileResource
|
||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||
import com.unciv.models.ruleset.unique.Unique
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.stats.Stat
|
||||
import com.unciv.models.translations.equalsPlaceholderText
|
||||
import com.unciv.models.translations.getPlaceholderParameters
|
||||
import com.unciv.ui.components.extensions.randomWeighted
|
||||
@ -240,30 +239,7 @@ class MapRegions (val ruleset: Ruleset){
|
||||
fun assignRegions(tileMap: TileMap, civilizations: List<Civilization>, gameParameters: GameParameters) {
|
||||
if (civilizations.isEmpty()) return
|
||||
|
||||
// first assign region types
|
||||
val regionTypes = ruleset.terrains.values.filter { getRegionPriority(it) != null }
|
||||
.sortedBy { getRegionPriority(it) }
|
||||
|
||||
for (region in regions) {
|
||||
region.countTerrains()
|
||||
|
||||
for (type in regionTypes) {
|
||||
// Test exclusion criteria first
|
||||
if (type.getMatchingUniques(UniqueType.RegionRequireFirstLessThanSecond).any {
|
||||
region.getTerrainAmount(it.params[0]) >= region.getTerrainAmount(it.params[1]) } ) {
|
||||
continue
|
||||
}
|
||||
// Test inclusion criteria
|
||||
if (type.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).any {
|
||||
region.getTerrainAmount(it.params[1]) >= (it.params[0].toInt() * region.tiles.size) / 100 }
|
||||
|| type.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).any {
|
||||
region.getTerrainAmount(it.params[1]) + region.getTerrainAmount(it.params[2]) >= (it.params[0].toInt() * region.tiles.size) / 100 }
|
||||
) {
|
||||
region.type = type.name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
assignRegionTypes()
|
||||
|
||||
// Generate tile data for all tiles
|
||||
for (tile in tileMap.values) {
|
||||
@ -273,13 +249,9 @@ class MapRegions (val ruleset: Ruleset){
|
||||
|
||||
// Sort regions by fertility so the worse regions get to pick first
|
||||
val sortedRegions = regions.sortedBy { it.totalFertility }
|
||||
// Find a start for each region
|
||||
for (region in sortedRegions) {
|
||||
findStart(region)
|
||||
}
|
||||
// Normalize starts
|
||||
for (region in sortedRegions) findStart(region)
|
||||
for (region in regions) {
|
||||
normalizeStart(tileMap[region.startPosition!!], tileMap, isMinorCiv = false)
|
||||
StartNormalizer.normalizeStart(tileMap[region.startPosition!!], tileMap, tileData, ruleset, isMinorCiv = false)
|
||||
}
|
||||
|
||||
val civBiases = civilizations.associateWith { ruleset.nations[it.civName]!!.startBias }
|
||||
@ -415,6 +387,36 @@ class MapRegions (val ruleset: Ruleset){
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets region.type */
|
||||
private fun assignRegionTypes() {
|
||||
val regionTypes = ruleset.terrains.values.filter { getRegionPriority(it) != null }
|
||||
.sortedBy { getRegionPriority(it) }
|
||||
|
||||
for (region in regions) {
|
||||
region.countTerrains()
|
||||
|
||||
for (type in regionTypes) {
|
||||
// Test exclusion criteria first
|
||||
if (type.getMatchingUniques(UniqueType.RegionRequireFirstLessThanSecond).any {
|
||||
region.getTerrainAmount(it.params[0]) >= region.getTerrainAmount(it.params[1])
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
// Test inclusion criteria
|
||||
if (type.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).any {
|
||||
region.getTerrainAmount(it.params[1]) >= (it.params[0].toInt() * region.tiles.size) / 100
|
||||
}
|
||||
|| type.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).any {
|
||||
region.getTerrainAmount(it.params[1]) + region.getTerrainAmount(it.params[2]) >= (it.params[0].toInt() * region.tiles.size) / 100
|
||||
}
|
||||
) {
|
||||
region.type = type.name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logAssignRegion(success: Boolean, startBiasType: BiasTypes, civ: Civilization, region: Region? = null) {
|
||||
if (Log.backend.isRelease()) return
|
||||
|
||||
@ -561,260 +563,6 @@ class MapRegions (val ruleset: Ruleset){
|
||||
setRegionStart(region, panicPosition)
|
||||
}
|
||||
|
||||
/** Attempts to improve the start on [startTile] as needed to make it decent.
|
||||
* Relies on startPosition having been set previously.
|
||||
* Assumes unchanged baseline values ie citizens eat 2 food each, similar production costs
|
||||
* If [isMinorCiv] is true, different weightings will be used. */
|
||||
private fun normalizeStart(startTile: Tile, tileMap: TileMap, isMinorCiv: Boolean) {
|
||||
// Remove ice-like features adjacent to start
|
||||
for (tile in startTile.neighbors) {
|
||||
val lastTerrain = tile.terrainFeatureObjects.lastOrNull { it.impassable }
|
||||
if (lastTerrain != null) {
|
||||
tile.removeTerrainFeature(lastTerrain.name)
|
||||
}
|
||||
}
|
||||
|
||||
// evaluate production potential
|
||||
val innerProduction = startTile.neighbors.sumOf { getPotentialYield(it, Stat.Production).toInt() }
|
||||
val outerProduction = startTile.getTilesAtDistance(2).sumOf { getPotentialYield(it, Stat.Production).toInt() }
|
||||
// for very early production we ideally want tiles that also give food
|
||||
val earlyProduction = startTile.getTilesInDistanceRange(1..2).sumOf {
|
||||
if (getPotentialYield(it, Stat.Food, unimproved = true) > 0f) getPotentialYield(it, Stat.Production, unimproved = true).toInt()
|
||||
else 0 }
|
||||
|
||||
// If terrible, try adding a hill to a dry flat tile
|
||||
if (innerProduction == 0 || (innerProduction < 2 && outerProduction < 8) || (isMinorCiv && innerProduction < 4)) {
|
||||
val hillSpot = startTile.neighbors
|
||||
.filter { it.isLand && it.terrainFeatures.isEmpty() && !it.isAdjacentTo(Constants.freshWater) && !it.isImpassible() }
|
||||
.toList().randomOrNull()
|
||||
val hillEquivalent = ruleset.terrains.values
|
||||
.firstOrNull { it.type == TerrainType.TerrainFeature && it.production >= 2 && !it.hasUnique(UniqueType.RareFeature) }?.name
|
||||
if (hillSpot != null && hillEquivalent != null) {
|
||||
hillSpot.addTerrainFeature(hillEquivalent)
|
||||
}
|
||||
}
|
||||
|
||||
// Place Strategic Balance Resources
|
||||
if (tileMap.mapParameters.mapResources == MapResources.strategicBalance) {
|
||||
val candidateTiles = startTile.getTilesInDistanceRange(1..2).shuffled() + startTile.getTilesAtDistance(3).shuffled()
|
||||
for (resource in ruleset.tileResources.values.filter { it.hasUnique(UniqueType.StrategicBalanceResource) }) {
|
||||
if (MapRegionResources.tryAddingResourceToTiles(tileData, resource, 1, candidateTiles, majorDeposit = true) == 0) {
|
||||
// Fallback mode - force placement, even on an otherwise inappropriate terrain. Do still respect water and impassible tiles!
|
||||
val resourceTiles =
|
||||
if (isWaterOnlyResource(resource)) candidateTiles.filter { it.isWater && !it.isImpassible() }.toList()
|
||||
else candidateTiles.filter { it.isLand && !it.isImpassible() }.toList()
|
||||
MapRegionResources.placeResourcesInTiles(tileData, 999, resourceTiles, listOf(resource), majorDeposit = true, forcePlacement = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If bad early production, add a small strategic resource to SECOND ring (not for minors)
|
||||
if (!isMinorCiv && innerProduction < 3 && earlyProduction < 6) {
|
||||
val lastEraNumber = ruleset.eras.values.maxOf { it.eraNumber }
|
||||
val earlyEras = ruleset.eras.filterValues { it.eraNumber <= lastEraNumber / 3 }
|
||||
val validResources = ruleset.tileResources.values.filter {
|
||||
it.resourceType == ResourceType.Strategic &&
|
||||
(it.revealedBy == null ||
|
||||
ruleset.technologies[it.revealedBy]!!.era() in earlyEras)
|
||||
}.shuffled()
|
||||
val candidateTiles = startTile.getTilesAtDistance(2).shuffled()
|
||||
for (resource in validResources) {
|
||||
if (MapRegionResources.tryAddingResourceToTiles(tileData, resource, 1, candidateTiles, majorDeposit = false) > 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val foodBonusesNeeded = calculateFoodBonusesNeeded(startTile, isMinorCiv, tileMap)
|
||||
placeFoodBonuses(isMinorCiv, startTile, foodBonusesNeeded)
|
||||
|
||||
// Minor civs are done, go on with grassiness checks for major civs
|
||||
if (isMinorCiv) return
|
||||
|
||||
addProductionBonuses(startTile)
|
||||
}
|
||||
|
||||
/** Check for very food-heavy starts that might still need some stone to help with production */
|
||||
private fun addProductionBonuses(startTile: Tile) {
|
||||
val grassTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
|
||||
it.isLand &&
|
||||
getPotentialYield(it, Stat.Food, unimproved = true) >= 2f && // Food neutral natively
|
||||
getPotentialYield(it, Stat.Production) == 0f // Production can't even be improved
|
||||
}.toMutableList()
|
||||
val plainsTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
|
||||
it.isLand &&
|
||||
getPotentialYield(it, Stat.Food) >= 2f && // Something that can be improved to food neutral
|
||||
getPotentialYield(it, Stat.Production, unimproved = true) >= 1f // Some production natively
|
||||
}.toList()
|
||||
var productionBonusesNeeded = when {
|
||||
grassTypePlots.size >= 9 && plainsTypePlots.isEmpty() -> 2
|
||||
grassTypePlots.size >= 6 && plainsTypePlots.size <= 4 -> 1
|
||||
else -> 0
|
||||
}
|
||||
val productionBonuses =
|
||||
ruleset.tileResources.values.filter { it.resourceType == ResourceType.Bonus && it.production > 0 }
|
||||
|
||||
if (productionBonuses.isNotEmpty()) {
|
||||
while (productionBonusesNeeded > 0 && grassTypePlots.isNotEmpty()) {
|
||||
val plot = grassTypePlots.random()
|
||||
grassTypePlots.remove(plot)
|
||||
|
||||
if (plot.resource != null) continue
|
||||
|
||||
val bonusToPlace =
|
||||
productionBonuses.filter { plot.lastTerrain.name in it.terrainsCanBeFoundOn }
|
||||
.randomOrNull()
|
||||
if (bonusToPlace != null) {
|
||||
plot.resource = bonusToPlace.name
|
||||
productionBonusesNeeded--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateFoodBonusesNeeded(
|
||||
startTile: Tile,
|
||||
minorCiv: Boolean,
|
||||
tileMap: TileMap
|
||||
): Int {
|
||||
// evaluate food situation
|
||||
// Food²/4 because excess food is really good and lets us work other tiles or run specialists!
|
||||
// 2F is worth 1, 3F is worth 2, 4F is worth 4, 5F is worth 6 and so on
|
||||
val innerFood =
|
||||
startTile.neighbors.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
|
||||
val outerFood = startTile.getTilesAtDistance(2)
|
||||
.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
|
||||
val totalFood = innerFood + outerFood
|
||||
// we want at least some two-food tiles to keep growing
|
||||
val innerNativeTwoFood =
|
||||
startTile.neighbors.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
|
||||
val outerNativeTwoFood = startTile.getTilesAtDistance(2)
|
||||
.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
|
||||
val totalNativeTwoFood = innerNativeTwoFood + outerNativeTwoFood
|
||||
|
||||
// Determine number of needed bonuses. Different weightings for minor and major civs.
|
||||
var bonusesNeeded = if (minorCiv) {
|
||||
when { // From 2 to 0
|
||||
totalFood < 12 || innerFood < 4 -> 2
|
||||
totalFood < 16 || innerFood < 9 -> 1
|
||||
else -> 0
|
||||
}
|
||||
} else {
|
||||
when { // From 5 to 0
|
||||
innerFood == 0 && totalFood < 4 -> 5
|
||||
totalFood < 6 -> 4
|
||||
totalFood < 8 ||
|
||||
(totalFood < 12 && innerFood < 5) -> 3
|
||||
|
||||
(totalFood < 17 && innerFood < 9) ||
|
||||
totalNativeTwoFood < 2 -> 2
|
||||
|
||||
(totalFood < 24 && innerFood < 11) ||
|
||||
totalNativeTwoFood == 2 ||
|
||||
innerNativeTwoFood == 0 ||
|
||||
totalFood < 20 -> 1
|
||||
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
if (tileMap.mapParameters.mapResources == MapResources.legendaryStart)
|
||||
bonusesNeeded += 2
|
||||
|
||||
// Attempt to place one grassland at a plains-only spot (nor for minors)
|
||||
if (!minorCiv && bonusesNeeded < 3 && totalNativeTwoFood == 0) {
|
||||
val twoFoodTerrain =
|
||||
ruleset.terrains.values.firstOrNull { it.type == TerrainType.Land && it.food >= 2 }?.name
|
||||
val candidateInnerSpots = startTile.neighbors
|
||||
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
|
||||
val candidateOuterSpots = startTile.getTilesAtDistance(2)
|
||||
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
|
||||
val spot =
|
||||
candidateInnerSpots.shuffled().firstOrNull() ?: candidateOuterSpots.shuffled()
|
||||
.firstOrNull()
|
||||
if (twoFoodTerrain != null && spot != null) {
|
||||
spot.baseTerrain = twoFoodTerrain
|
||||
} else
|
||||
bonusesNeeded = 3 // Irredeemable plains situation
|
||||
}
|
||||
return bonusesNeeded
|
||||
}
|
||||
|
||||
private fun placeFoodBonuses(
|
||||
minorCiv: Boolean,
|
||||
startTile: Tile,
|
||||
foodBonusesNeeded: Int
|
||||
) {
|
||||
var bonusesStillNeeded = foodBonusesNeeded
|
||||
val oasisEquivalent = ruleset.terrains.values.firstOrNull {
|
||||
it.type == TerrainType.TerrainFeature &&
|
||||
it.hasUnique(UniqueType.RareFeature) &&
|
||||
it.food >= 2 &&
|
||||
it.food + it.production + it.gold >= 3 &&
|
||||
it.occursOn.any { base -> ruleset.terrains[base]!!.type == TerrainType.Land }
|
||||
}
|
||||
var canPlaceOasis =
|
||||
oasisEquivalent != null // One oasis per start is enough. Don't bother finding a place if there is no good oasis equivalent
|
||||
var placedInFirst = 0 // Attempt to put first 2 in inner ring and next 3 in second ring
|
||||
var placedInSecond = 0
|
||||
val rangeForBonuses = if (minorCiv) 2 else 3
|
||||
|
||||
// Start with list of candidate plots sorted in ring order 1,2,3
|
||||
val candidatePlots = startTile.getTilesInDistanceRange(1..rangeForBonuses)
|
||||
.filter { it.resource == null && oasisEquivalent !in it.terrainFeatureObjects }
|
||||
.shuffled().sortedBy { it.aerialDistanceTo(startTile) }.toMutableList()
|
||||
|
||||
// Place food bonuses (and oases) as able
|
||||
while (bonusesStillNeeded > 0 && candidatePlots.isNotEmpty()) {
|
||||
val plot = candidatePlots.first()
|
||||
candidatePlots.remove(plot) // remove the plot as it has now been tried, whether successfully or not
|
||||
if (plot.getBaseTerrain().hasUnique(
|
||||
UniqueType.BlocksResources,
|
||||
StateForConditionals(attackedTile = plot)
|
||||
)
|
||||
)
|
||||
continue // Don't put bonuses on snow hills
|
||||
|
||||
val validBonuses = ruleset.tileResources.values.filter {
|
||||
it.resourceType == ResourceType.Bonus &&
|
||||
it.food >= 1 &&
|
||||
plot.lastTerrain.name in it.terrainsCanBeFoundOn
|
||||
}
|
||||
val goodPlotForOasis =
|
||||
canPlaceOasis && plot.lastTerrain.name in oasisEquivalent!!.occursOn
|
||||
|
||||
if (validBonuses.isNotEmpty() || goodPlotForOasis) {
|
||||
if (goodPlotForOasis) {
|
||||
plot.addTerrainFeature(oasisEquivalent!!.name)
|
||||
canPlaceOasis = false
|
||||
} else {
|
||||
plot.setTileResource(validBonuses.random())
|
||||
}
|
||||
|
||||
if (plot.aerialDistanceTo(startTile) == 1) {
|
||||
placedInFirst++
|
||||
if (placedInFirst == 2) // Resort the list in ring order 2,3,1
|
||||
candidatePlots.sortBy { abs(it.aerialDistanceTo(startTile) * 10 - 22) }
|
||||
} else if (plot.aerialDistanceTo(startTile) == 2) {
|
||||
placedInSecond++
|
||||
if (placedInSecond == 3) // Resort the list in ring order 3,1,2
|
||||
candidatePlots.sortByDescending { abs(it.aerialDistanceTo(startTile) * 10 - 17) }
|
||||
}
|
||||
bonusesStillNeeded--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPotentialYield(tile: Tile, stat: Stat, unimproved: Boolean = false): Float {
|
||||
val baseYield = tile.stats.getTileStats(null)[stat]
|
||||
if (unimproved) return baseYield
|
||||
|
||||
val bestImprovementYield = tile.tileMap.ruleset!!.tileImprovements.values
|
||||
.filter { !it.hasUnique(UniqueType.GreatImprovement) &&
|
||||
it.uniqueTo == null &&
|
||||
tile.lastTerrain.name in it.terrainsCanBeBuiltOn }
|
||||
.maxOfOrNull { it[stat] }
|
||||
return baseYield + (bestImprovementYield ?: 0f)
|
||||
}
|
||||
|
||||
/** @returns the region most similar to a region of [type] */
|
||||
private fun getFallbackRegion(type: String, candidates: List<Region>): Region {
|
||||
return candidates.maxByOrNull { it.terrainCounts[type] ?: 0 }!!
|
||||
@ -1183,7 +931,7 @@ class MapRegions (val ruleset: Ruleset){
|
||||
tileData.placeImpact(ImpactType.Strategic,tile, 0)
|
||||
tileData.placeImpact(ImpactType.Bonus, tile, 3)
|
||||
|
||||
normalizeStart(tile, tileMap, isMinorCiv = true)
|
||||
StartNormalizer.normalizeStart(tile, tileMap, tileData, ruleset, isMinorCiv = true)
|
||||
}
|
||||
|
||||
/** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */
|
||||
@ -1247,7 +995,7 @@ class MapRegions (val ruleset: Ruleset){
|
||||
for (region in regions) {
|
||||
val resource = ruleset.tileResources[region.luxury] ?: continue
|
||||
fun Tile.isShoreOfContinent(continent: Int) = isWater && neighbors.any { it.getContinent() == continent }
|
||||
val candidates = if (isWaterOnlyResource(resource))
|
||||
val candidates = if (isWaterOnlyResource(resource, ruleset))
|
||||
tileMap.getTilesInRectangle(region.rect).filter { it.isShoreOfContinent(region.continentID) }
|
||||
else region.tiles.asSequence()
|
||||
MapRegionResources.tryAddingResourceToTiles(tileData, resource, regionTargetNumber, candidates.shuffled(), 0.4f, true, 4, 2)
|
||||
@ -1460,7 +1208,7 @@ class MapRegions (val ruleset: Ruleset){
|
||||
for (resource in strategicResources) {
|
||||
val extraNeeded = min(2, regions.size - totalPlaced[resource]!!)
|
||||
if (extraNeeded > 0) {
|
||||
if (isWaterOnlyResource(resource))
|
||||
if (isWaterOnlyResource(resource, ruleset))
|
||||
MapRegionResources.tryAddingResourceToTiles(tileData, resource, extraNeeded, tileMap.values.asSequence().filter { it.isWater }.shuffled(), respectImpacts = true)
|
||||
else
|
||||
MapRegionResources.tryAddingResourceToTiles(tileData, resource, extraNeeded, landList.asSequence(), respectImpacts = true)
|
||||
@ -1472,9 +1220,9 @@ class MapRegions (val ruleset: Ruleset){
|
||||
|
||||
// Sixth place bonus resources (and other resources that might have been assigned frequency-based generation).
|
||||
// Water-based bonuses go last and have extra impact, because coasts are very common and we don't want too much clustering
|
||||
val sortedResourceList = ruleset.tileResources.values.sortedBy { isWaterOnlyResource(it) }
|
||||
val sortedResourceList = ruleset.tileResources.values.sortedBy { isWaterOnlyResource(it, ruleset) }
|
||||
for (resource in sortedResourceList) {
|
||||
val extraImpact = if (isWaterOnlyResource(resource)) 1 else 0
|
||||
val extraImpact = if (isWaterOnlyResource(resource, ruleset)) 1 else 0
|
||||
for (rule in resource.uniqueObjects.filter { it.type == UniqueType.ResourceFrequency }) {
|
||||
// Figure out which list applies, if any
|
||||
val simpleRule = anonymizeUnique(rule)
|
||||
@ -1518,9 +1266,6 @@ class MapRegions (val ruleset: Ruleset){
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWaterOnlyResource(resource: TileResource) = resource.terrainsCanBeFoundOn
|
||||
.all { terrainName -> ruleset.terrains[terrainName]!!.type == TerrainType.Water }
|
||||
|
||||
enum class ImpactType {
|
||||
Strategic,
|
||||
Luxury,
|
||||
@ -1535,6 +1280,9 @@ class MapRegions (val ruleset: Ruleset){
|
||||
internal fun anonymizeUnique(unique: Unique) = Unique(
|
||||
"RULE" + unique.conditionals.sortedBy { it.text }.joinToString(prefix = " ", separator = " ") { "<" + it.text + ">" })
|
||||
|
||||
internal fun isWaterOnlyResource(resource: TileResource, ruleset: Ruleset) = resource.terrainsCanBeFoundOn
|
||||
.all { terrainName -> ruleset.terrains[terrainName]!!.type == TerrainType.Water }
|
||||
|
||||
|
||||
/** @return a fake unique with conditionals that will satisfy the same conditions as terrainsCanBeFoundOn */
|
||||
internal fun getTerrainRule(terrain: Terrain, ruleset: Ruleset): Unique {
|
||||
|
@ -0,0 +1,322 @@
|
||||
package com.unciv.logic.map.mapgenerator.mapregions
|
||||
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.map.MapResources
|
||||
import com.unciv.logic.map.TileMap
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.tile.ResourceType
|
||||
import com.unciv.models.ruleset.tile.TerrainType
|
||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.stats.Stat
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
|
||||
/** Ensures that starting positions of civs have enough yield that they aren't at a disadvantage */
|
||||
object StartNormalizer {
|
||||
|
||||
/** Attempts to improve the start on [startTile] as needed to make it decent.
|
||||
* Relies on startPosition having been set previously.
|
||||
* Assumes unchanged baseline values ie citizens eat 2 food each, similar production costs
|
||||
* If [isMinorCiv] is true, different weightings will be used. */
|
||||
fun normalizeStart(startTile: Tile, tileMap: TileMap, tileData: TileDataMap, ruleset: Ruleset, isMinorCiv: Boolean) {
|
||||
// Remove ice-like features adjacent to start
|
||||
for (tile in startTile.neighbors) {
|
||||
val lastTerrain = tile.terrainFeatureObjects.lastOrNull { it.impassable }
|
||||
if (lastTerrain != null) {
|
||||
tile.removeTerrainFeature(lastTerrain.name)
|
||||
}
|
||||
}
|
||||
|
||||
if (tileMap.mapParameters.mapResources == MapResources.strategicBalance)
|
||||
placeStrategicBalanceResources(startTile, ruleset, tileData)
|
||||
|
||||
normalizeProduction(startTile, isMinorCiv, ruleset, tileData)
|
||||
|
||||
val foodBonusesNeeded = calculateFoodBonusesNeeded(startTile, isMinorCiv, ruleset, tileMap)
|
||||
placeFoodBonuses(isMinorCiv, startTile, ruleset, foodBonusesNeeded)
|
||||
|
||||
// Minor civs are done, go on with grassiness checks for major civs
|
||||
if (isMinorCiv) return
|
||||
|
||||
addProductionBonuses(startTile, ruleset)
|
||||
}
|
||||
|
||||
private fun normalizeProduction(
|
||||
startTile: Tile,
|
||||
isMinorCiv: Boolean,
|
||||
ruleset: Ruleset,
|
||||
tileData: TileDataMap
|
||||
) {
|
||||
// evaluate production potential
|
||||
val innerProduction =
|
||||
startTile.neighbors.sumOf { getPotentialYield(it, Stat.Production).toInt() }
|
||||
val outerProduction =
|
||||
startTile.getTilesAtDistance(2).sumOf { getPotentialYield(it, Stat.Production).toInt() }
|
||||
// for very early production we ideally want tiles that also give food
|
||||
val earlyProduction = startTile.getTilesInDistanceRange(1..2).sumOf {
|
||||
if (getPotentialYield(it, Stat.Food, unimproved = true) > 0f) getPotentialYield(
|
||||
it,
|
||||
Stat.Production,
|
||||
unimproved = true
|
||||
).toInt()
|
||||
else 0
|
||||
}
|
||||
|
||||
// If terrible, try adding a hill to a dry flat tile
|
||||
if (innerProduction == 0 || (innerProduction < 2 && outerProduction < 8) || (isMinorCiv && innerProduction < 4)) {
|
||||
val hillSpot = startTile.neighbors
|
||||
.filter { it.isLand && it.terrainFeatures.isEmpty() && !it.isAdjacentTo(Constants.freshWater) && !it.isImpassible() }
|
||||
.toList().randomOrNull()
|
||||
val hillEquivalent = ruleset.terrains.values
|
||||
.firstOrNull {
|
||||
it.type == TerrainType.TerrainFeature && it.production >= 2 && !it.hasUnique(
|
||||
UniqueType.RareFeature
|
||||
)
|
||||
}?.name
|
||||
if (hillSpot != null && hillEquivalent != null) {
|
||||
hillSpot.addTerrainFeature(hillEquivalent)
|
||||
}
|
||||
}
|
||||
|
||||
// If bad early production, add a small strategic resource to SECOND ring (not for minors)
|
||||
if (!isMinorCiv && innerProduction < 3 && earlyProduction < 6) {
|
||||
val lastEraNumber = ruleset.eras.values.maxOf { it.eraNumber }
|
||||
val earlyEras = ruleset.eras.filterValues { it.eraNumber <= lastEraNumber / 3 }
|
||||
val validResources = ruleset.tileResources.values.filter {
|
||||
it.resourceType == ResourceType.Strategic &&
|
||||
(it.revealedBy == null ||
|
||||
ruleset.technologies[it.revealedBy]!!.era() in earlyEras)
|
||||
}.shuffled()
|
||||
val candidateTiles = startTile.getTilesAtDistance(2).shuffled()
|
||||
for (resource in validResources) {
|
||||
val resourcesAdded = MapRegionResources.tryAddingResourceToTiles(
|
||||
tileData, resource, 1, candidateTiles, majorDeposit = false)
|
||||
if (resourcesAdded > 0) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun placeStrategicBalanceResources(
|
||||
startTile: Tile,
|
||||
ruleset: Ruleset,
|
||||
tileData: TileDataMap
|
||||
) {
|
||||
val candidateTiles =
|
||||
startTile.getTilesInDistanceRange(1..2).shuffled() + startTile.getTilesAtDistance(3)
|
||||
.shuffled()
|
||||
for (resource in ruleset.tileResources.values.filter { it.hasUnique(UniqueType.StrategicBalanceResource) }) {
|
||||
if (MapRegionResources.tryAddingResourceToTiles(
|
||||
tileData,
|
||||
resource,
|
||||
1,
|
||||
candidateTiles,
|
||||
majorDeposit = true
|
||||
) == 0
|
||||
) {
|
||||
// Fallback mode - force placement, even on an otherwise inappropriate terrain. Do still respect water and impassible tiles!
|
||||
val resourceTiles =
|
||||
if (isWaterOnlyResource(
|
||||
resource,
|
||||
ruleset
|
||||
)
|
||||
) candidateTiles.filter { it.isWater && !it.isImpassible() }.toList()
|
||||
else candidateTiles.filter { it.isLand && !it.isImpassible() }.toList()
|
||||
MapRegionResources.placeResourcesInTiles(
|
||||
tileData,
|
||||
999,
|
||||
resourceTiles,
|
||||
listOf(resource),
|
||||
majorDeposit = true,
|
||||
forcePlacement = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Check for very food-heavy starts that might still need some stone to help with production */
|
||||
private fun addProductionBonuses(startTile: Tile, ruleset: Ruleset) {
|
||||
val grassTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
|
||||
it.isLand &&
|
||||
getPotentialYield(it, Stat.Food, unimproved = true) >= 2f && // Food neutral natively
|
||||
getPotentialYield(it, Stat.Production) == 0f // Production can't even be improved
|
||||
}.toMutableList()
|
||||
val plainsTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
|
||||
it.isLand &&
|
||||
getPotentialYield(it, Stat.Food) >= 2f && // Something that can be improved to food neutral
|
||||
getPotentialYield(it, Stat.Production, unimproved = true) >= 1f // Some production natively
|
||||
}.toList()
|
||||
var productionBonusesNeeded = when {
|
||||
grassTypePlots.size >= 9 && plainsTypePlots.isEmpty() -> 2
|
||||
grassTypePlots.size >= 6 && plainsTypePlots.size <= 4 -> 1
|
||||
else -> 0
|
||||
}
|
||||
val productionBonuses =
|
||||
ruleset.tileResources.values.filter { it.resourceType == ResourceType.Bonus && it.production > 0 }
|
||||
|
||||
if (productionBonuses.isNotEmpty()) {
|
||||
while (productionBonusesNeeded > 0 && grassTypePlots.isNotEmpty()) {
|
||||
val plot = grassTypePlots.random()
|
||||
grassTypePlots.remove(plot)
|
||||
|
||||
if (plot.resource != null) continue
|
||||
|
||||
val bonusToPlace =
|
||||
productionBonuses.filter { plot.lastTerrain.name in it.terrainsCanBeFoundOn }
|
||||
.randomOrNull()
|
||||
if (bonusToPlace != null) {
|
||||
plot.resource = bonusToPlace.name
|
||||
productionBonusesNeeded--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateFoodBonusesNeeded(
|
||||
startTile: Tile,
|
||||
minorCiv: Boolean,
|
||||
ruleset: Ruleset,
|
||||
tileMap: TileMap
|
||||
): Int {
|
||||
// evaluate food situation
|
||||
// Food²/4 because excess food is really good and lets us work other tiles or run specialists!
|
||||
// 2F is worth 1, 3F is worth 2, 4F is worth 4, 5F is worth 6 and so on
|
||||
val innerFood =
|
||||
startTile.neighbors.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
|
||||
val outerFood = startTile.getTilesAtDistance(2)
|
||||
.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
|
||||
val totalFood = innerFood + outerFood
|
||||
// we want at least some two-food tiles to keep growing
|
||||
val innerNativeTwoFood =
|
||||
startTile.neighbors.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
|
||||
val outerNativeTwoFood = startTile.getTilesAtDistance(2)
|
||||
.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
|
||||
val totalNativeTwoFood = innerNativeTwoFood + outerNativeTwoFood
|
||||
|
||||
// Determine number of needed bonuses. Different weightings for minor and major civs.
|
||||
var bonusesNeeded = if (minorCiv) {
|
||||
when { // From 2 to 0
|
||||
totalFood < 12 || innerFood < 4 -> 2
|
||||
totalFood < 16 || innerFood < 9 -> 1
|
||||
else -> 0
|
||||
}
|
||||
} else {
|
||||
when { // From 5 to 0
|
||||
innerFood == 0 && totalFood < 4 -> 5
|
||||
totalFood < 6 -> 4
|
||||
totalFood < 8 ||
|
||||
(totalFood < 12 && innerFood < 5) -> 3
|
||||
|
||||
(totalFood < 17 && innerFood < 9) ||
|
||||
totalNativeTwoFood < 2 -> 2
|
||||
|
||||
(totalFood < 24 && innerFood < 11) ||
|
||||
totalNativeTwoFood == 2 ||
|
||||
innerNativeTwoFood == 0 ||
|
||||
totalFood < 20 -> 1
|
||||
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
if (tileMap.mapParameters.mapResources == MapResources.legendaryStart)
|
||||
bonusesNeeded += 2
|
||||
|
||||
// Attempt to place one grassland at a plains-only spot (nor for minors)
|
||||
if (!minorCiv && bonusesNeeded < 3 && totalNativeTwoFood == 0) {
|
||||
val twoFoodTerrain =
|
||||
ruleset.terrains.values.firstOrNull { it.type == TerrainType.Land && it.food >= 2 }?.name
|
||||
val candidateInnerSpots = startTile.neighbors
|
||||
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
|
||||
val candidateOuterSpots = startTile.getTilesAtDistance(2)
|
||||
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
|
||||
val spot =
|
||||
candidateInnerSpots.shuffled().firstOrNull() ?: candidateOuterSpots.shuffled()
|
||||
.firstOrNull()
|
||||
if (twoFoodTerrain != null && spot != null) {
|
||||
spot.baseTerrain = twoFoodTerrain
|
||||
} else
|
||||
bonusesNeeded = 3 // Irredeemable plains situation
|
||||
}
|
||||
return bonusesNeeded
|
||||
}
|
||||
|
||||
private fun placeFoodBonuses(
|
||||
minorCiv: Boolean,
|
||||
startTile: Tile,
|
||||
ruleset: Ruleset,
|
||||
foodBonusesNeeded: Int
|
||||
) {
|
||||
var bonusesStillNeeded = foodBonusesNeeded
|
||||
val oasisEquivalent = ruleset.terrains.values.firstOrNull {
|
||||
it.type == TerrainType.TerrainFeature &&
|
||||
it.hasUnique(UniqueType.RareFeature) &&
|
||||
it.food >= 2 &&
|
||||
it.food + it.production + it.gold >= 3 &&
|
||||
it.occursOn.any { base -> ruleset.terrains[base]!!.type == TerrainType.Land }
|
||||
}
|
||||
var canPlaceOasis =
|
||||
oasisEquivalent != null // One oasis per start is enough. Don't bother finding a place if there is no good oasis equivalent
|
||||
var placedInFirst = 0 // Attempt to put first 2 in inner ring and next 3 in second ring
|
||||
var placedInSecond = 0
|
||||
val rangeForBonuses = if (minorCiv) 2 else 3
|
||||
|
||||
// Start with list of candidate plots sorted in ring order 1,2,3
|
||||
val candidatePlots = startTile.getTilesInDistanceRange(1..rangeForBonuses)
|
||||
.filter { it.resource == null && oasisEquivalent !in it.terrainFeatureObjects }
|
||||
.shuffled().sortedBy { it.aerialDistanceTo(startTile) }.toMutableList()
|
||||
|
||||
// Place food bonuses (and oases) as able
|
||||
while (bonusesStillNeeded > 0 && candidatePlots.isNotEmpty()) {
|
||||
val plot = candidatePlots.first()
|
||||
candidatePlots.remove(plot) // remove the plot as it has now been tried, whether successfully or not
|
||||
if (plot.getBaseTerrain().hasUnique(
|
||||
UniqueType.BlocksResources,
|
||||
StateForConditionals(attackedTile = plot)
|
||||
)
|
||||
)
|
||||
continue // Don't put bonuses on snow hills
|
||||
|
||||
val validBonuses = ruleset.tileResources.values.filter {
|
||||
it.resourceType == ResourceType.Bonus &&
|
||||
it.food >= 1 &&
|
||||
plot.lastTerrain.name in it.terrainsCanBeFoundOn
|
||||
}
|
||||
val goodPlotForOasis =
|
||||
canPlaceOasis && plot.lastTerrain.name in oasisEquivalent!!.occursOn
|
||||
|
||||
if (validBonuses.isNotEmpty() || goodPlotForOasis) {
|
||||
if (goodPlotForOasis) {
|
||||
plot.addTerrainFeature(oasisEquivalent!!.name)
|
||||
canPlaceOasis = false
|
||||
} else {
|
||||
plot.setTileResource(validBonuses.random())
|
||||
}
|
||||
|
||||
if (plot.aerialDistanceTo(startTile) == 1) {
|
||||
placedInFirst++
|
||||
if (placedInFirst == 2) // Resort the list in ring order 2,3,1
|
||||
candidatePlots.sortBy { abs(it.aerialDistanceTo(startTile) * 10 - 22) }
|
||||
} else if (plot.aerialDistanceTo(startTile) == 2) {
|
||||
placedInSecond++
|
||||
if (placedInSecond == 3) // Resort the list in ring order 3,1,2
|
||||
candidatePlots.sortByDescending { abs(it.aerialDistanceTo(startTile) * 10 - 17) }
|
||||
}
|
||||
bonusesStillNeeded--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPotentialYield(tile: Tile, stat: Stat, unimproved: Boolean = false): Float {
|
||||
val baseYield = tile.stats.getTileStats(null)[stat]
|
||||
if (unimproved) return baseYield
|
||||
|
||||
val bestImprovementYield = tile.tileMap.ruleset!!.tileImprovements.values
|
||||
.filter { !it.hasUnique(UniqueType.GreatImprovement) &&
|
||||
it.uniqueTo == null &&
|
||||
tile.lastTerrain.name in it.terrainsCanBeBuiltOn }
|
||||
.maxOfOrNull { it[stat] }
|
||||
return baseYield + (bestImprovementYield ?: 0f)
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user