From f52e7d37f4798bee3a21fdd52a2ddf4345b3295b Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Mon, 2 Oct 2023 12:06:16 +0300 Subject: [PATCH] chore: Split normalizeStart into subfunctions for easier parsing --- .../mapregions/MapRegionResources.kt | 10 +- .../map/mapgenerator/mapregions/MapRegions.kt | 185 +++++++++++------- 2 files changed, 119 insertions(+), 76 deletions(-) diff --git a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegionResources.kt b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegionResources.kt index c2ba1b5b32..8cbf4a9876 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegionResources.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegionResources.kt @@ -100,6 +100,7 @@ object MapRegionResources { tile.lastTerrain.name in resource.terrainsCanBeFoundOn && !tile.getBaseTerrain().hasUnique(UniqueType.BlocksResources, conditionalTerrain) && !resource.hasUnique(UniqueType.NoNaturalGeneration, conditionalTerrain) && + resource.getMatchingUniques(UniqueType.TileGenerationConditions).none { tile.temperature!! !in it.params[0].toDouble() .. it.params[1].toDouble() || tile.humidity!! !in it.params[2].toDouble() .. it.params[3].toDouble() @@ -127,16 +128,19 @@ object MapRegionResources { * Lifted out of the main function to allow postponing water resources. * @return a map of resource types to placed deposits. */ fun placeMajorDeposits(tileData: TileDataMap, ruleset: Ruleset, tileList: List, terrain: Terrain, fallbackWeightings: Boolean, baseImpact: Int, randomImpact: Int): Map { - if (tileList.isEmpty()) - return mapOf() + if (tileList.isEmpty()) return mapOf() + val frequency = if (terrain.hasUnique(UniqueType.MajorStrategicFrequency)) terrain.getMatchingUniques(UniqueType.MajorStrategicFrequency).first().params[0].toInt() else 25 + + val terrainRule = getTerrainRule(terrain, ruleset) val resourceOptions = ruleset.tileResources.values.filter { it.resourceType == ResourceType.Strategic && ((fallbackWeightings && terrain.name in it.terrainsCanBeFoundOn) || - it.uniqueObjects.any { unique -> anonymizeUnique(unique).text == getTerrainRule(terrain,ruleset).text }) + it.uniqueObjects.any { unique -> anonymizeUnique(unique).text == terrainRule.text }) } + return if (resourceOptions.isNotEmpty()) placeResourcesInTiles(tileData, frequency, tileList, resourceOptions, baseImpact, randomImpact, true) else diff --git a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt index 848c8d6bc5..7f960017d5 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt @@ -279,7 +279,7 @@ class MapRegions (val ruleset: Ruleset){ } // Normalize starts for (region in regions) { - normalizeStart(tileMap[region.startPosition!!], tileMap, minorCiv = false) + normalizeStart(tileMap[region.startPosition!!], tileMap, isMinorCiv = false) } val civBiases = civilizations.associateWith { ruleset.nations[it.civName]!!.startBias } @@ -564,8 +564,8 @@ class MapRegions (val ruleset: Ruleset){ /** 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 [minorCiv] is true, different weightings will be used. */ - private fun normalizeStart(startTile: Tile, tileMap: TileMap, minorCiv: Boolean) { + * 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 } @@ -583,7 +583,7 @@ class MapRegions (val ruleset: Ruleset){ else 0 } // If terrible, try adding a hill to a dry flat tile - if (innerProduction == 0 || (innerProduction < 2 && outerProduction < 8) || (minorCiv && innerProduction < 4)) { + 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() @@ -600,17 +600,16 @@ class MapRegions (val ruleset: Ruleset){ 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! - if (isWaterOnlyResource(resource)) - MapRegionResources.placeResourcesInTiles(tileData, 999, candidateTiles.filter { it.isWater && !it.isImpassible() }.toList(), listOf(resource), majorDeposit = true, forcePlacement = true) - else - MapRegionResources.placeResourcesInTiles(tileData, 999, candidateTiles.filter { it.isLand && !it.isImpassible() }.toList(), listOf(resource), majorDeposit = true, forcePlacement = true) - + 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 (!minorCiv && innerProduction < 3 && earlyProduction < 6) { + 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 { @@ -625,15 +624,71 @@ class MapRegions (val ruleset: Ruleset){ } } - // Now evaluate food situation + 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 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 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. @@ -648,13 +703,16 @@ class MapRegions (val ruleset: Ruleset){ innerFood == 0 && totalFood < 4 -> 5 totalFood < 6 -> 4 totalFood < 8 || - (totalFood < 12 && innerFood < 5) -> 3 + (totalFood < 12 && innerFood < 5) -> 3 + (totalFood < 17 && innerFood < 9) || - totalNativeTwoFood < 2 -> 2 + totalNativeTwoFood < 2 -> 2 + (totalFood < 24 && innerFood < 11) || - totalNativeTwoFood == 2 || - innerNativeTwoFood == 0 || - totalFood < 20 -> 1 + totalNativeTwoFood == 2 || + innerNativeTwoFood == 0 || + totalFood < 20 -> 1 + else -> 0 } } @@ -663,48 +721,65 @@ class MapRegions (val ruleset: Ruleset){ // 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 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 } + .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() + .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.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 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() + .filter { it.resource == null && oasisEquivalent !in it.terrainFeatureObjects } + .shuffled().sortedBy { it.aerialDistanceTo(startTile) }.toMutableList() // Place food bonuses (and oases) as able - while (bonusesNeeded > 0 && candidatePlots.isNotEmpty()) { + 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))) + 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 + it.food >= 1 && + plot.lastTerrain.name in it.terrainsCanBeFoundOn } - val goodPlotForOasis = canPlaceOasis && plot.lastTerrain.name in oasisEquivalent!!.occursOn + val goodPlotForOasis = + canPlaceOasis && plot.lastTerrain.name in oasisEquivalent!!.occursOn if (validBonuses.isNotEmpty() || goodPlotForOasis) { if (goodPlotForOasis) { @@ -717,49 +792,13 @@ class MapRegions (val ruleset: Ruleset){ 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 ) } + 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) } } - bonusesNeeded-- - } - } - - // Minor civs are done, go on with grassiness checks for major civs - if (minorCiv) return - - // Check for very grass-heavy starts that might still need some stone to help with production - 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 stoneNeeded = when { - grassTypePlots.size >= 9 && plainsTypePlots.isEmpty() -> 2 - grassTypePlots.size >= 6 && plainsTypePlots.size <= 4 -> 1 - else -> 0 - } - val stoneTypeBonuses = ruleset.tileResources.values.filter { it.resourceType == ResourceType.Bonus && it.production > 0 } - - if(stoneTypeBonuses.isNotEmpty()) { - while (stoneNeeded > 0 && grassTypePlots.isNotEmpty()) { - val plot = grassTypePlots.random() - grassTypePlots.remove(plot) - - if (plot.resource != null) continue - - val bonusToPlace = stoneTypeBonuses.filter { plot.lastTerrain.name in it.terrainsCanBeFoundOn }.randomOrNull() - if (bonusToPlace != null) { - plot.resource = bonusToPlace.name - stoneNeeded-- - } + bonusesStillNeeded-- } } } @@ -1144,7 +1183,7 @@ class MapRegions (val ruleset: Ruleset){ tileData.placeImpact(ImpactType.Strategic,tile, 0) tileData.placeImpact(ImpactType.Bonus, tile, 3) - normalizeStart(tile, tileMap, minorCiv = true) + normalizeStart(tile, tileMap, isMinorCiv = true) } /** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */