chore: Separated luxury resource placement logic into a separate object

This commit is contained in:
Yair Morgenstern 2023-10-03 11:42:07 +03:00
parent a2a5cb7c2c
commit 2582d913d3
2 changed files with 186 additions and 112 deletions

View File

@ -0,0 +1,163 @@
package com.unciv.logic.map.mapgenerator.mapregions
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.tile.TileResource
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.randomWeighted
import kotlin.math.min
import kotlin.math.pow
object LuxuryResourcePlacementLogic {
/** Assigns a luxury to each region. No luxury can be assigned to too many regions.
* Some luxuries are earmarked for city states. The rest are randomly distributed or
* don't occur att all in the map */
fun assignLuxuries(regions: ArrayList<Region>, tileData: TileDataMap, ruleset: Ruleset): Pair<List<String>, List<String>> {
// If there are any weightings defined in json, assume they are complete. If there are none, use flat weightings instead
val fallbackWeightings = ruleset.tileResources.values.none {
it.resourceType == ResourceType.Luxury &&
(it.uniqueObjects.any { unique -> unique.isOfType(UniqueType.ResourceWeighting) } || it.hasUnique(
UniqueType.LuxuryWeightingForCityStates)) }
val maxRegionsWithLuxury = when {
regions.size > 12 -> 3
regions.size > 8 -> 2
else -> 1
}
val targetCityStateLuxuries = 3 // was probably intended to be "if (tileData.size > 5000) 4 else 3"
val assignableLuxuries = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Luxury &&
!it.hasUnique(UniqueType.LuxurySpecialPlacement) &&
!it.hasUnique(UniqueType.CityStateOnlyResource) }
val amountRegionsWithLuxury = HashMap<String, Int>()
// Init map
ruleset.tileResources.values
.forEach { amountRegionsWithLuxury[it.name] = 0 }
for (region in regions.sortedBy { getRegionPriority(ruleset.terrains[it.type]) } ) {
val candidateLuxuries = getCandidateLuxuries(
assignableLuxuries,
amountRegionsWithLuxury,
maxRegionsWithLuxury,
fallbackWeightings,
region,
ruleset
)
// If there are no candidates (mad modders???) just skip this region
if (candidateLuxuries.isEmpty()) continue
// Pick a luxury at random. Weight is reduced if the luxury has been picked before
val regionConditional = StateForConditionals(region = region)
val modifiedWeights = candidateLuxuries.map {
val weightingUnique = it.getMatchingUniques(UniqueType.ResourceWeighting, regionConditional).firstOrNull()
val relativeWeight = if (weightingUnique == null) 1f else weightingUnique.params[0].toFloat()
relativeWeight / (1f + amountRegionsWithLuxury[it.name]!!)
}.shuffled()
region.luxury = candidateLuxuries.randomWeighted(modifiedWeights).name
amountRegionsWithLuxury[region.luxury!!] = amountRegionsWithLuxury[region.luxury]!! + 1
}
val cityStateLuxuries = assignCityStateLuxuries(
targetCityStateLuxuries,
assignableLuxuries,
amountRegionsWithLuxury,
fallbackWeightings
)
val randomLuxuries = getLuxuriesForRandomPlacement(assignableLuxuries, amountRegionsWithLuxury, tileData, ruleset)
return Pair(cityStateLuxuries, randomLuxuries)
}
private fun getLuxuriesForRandomPlacement(
assignableLuxuries: List<TileResource>,
amountRegionsWithLuxury: HashMap<String, Int>,
tileData: TileDataMap,
ruleset: Ruleset
): List<String> {
val remainingLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name] == 0
}.map { it.name }.shuffled()
val disabledPercent =
100 - min(tileData.size.toFloat().pow(0.2f) * 16, 100f).toInt() // Approximately
val targetDisabledLuxuries = (ruleset.tileResources.values
.count { it.resourceType == ResourceType.Luxury } * disabledPercent) / 100
val randomLuxuries = remainingLuxuries.drop(targetDisabledLuxuries)
return randomLuxuries
}
private fun getCandidateLuxuries(
assignableLuxuries: List<TileResource>,
amountRegionsWithLuxury: HashMap<String, Int>,
maxRegionsWithLuxury: Int,
fallbackWeightings: Boolean,
region: Region,
ruleset: Ruleset
): List<TileResource> {
val regionConditional = StateForConditionals(region = region)
var candidateLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury &&
// Check that it has a weight for this region type
(fallbackWeightings ||
it.hasUnique(UniqueType.ResourceWeighting, regionConditional)) &&
// Check that there is enough coast if it is a water based resource
((region.terrainCounts["Coastal"] ?: 0) >= 12 ||
it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water })
}
// If we couldn't find any options, pick from all luxuries. First try to not pick water luxuries on land regions
if (candidateLuxuries.isEmpty()) {
candidateLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury &&
// Ignore weightings for this pass
// Check that there is enough coast if it is a water based resource
((region.terrainCounts["Coastal"] ?: 0) >= 12 ||
it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water })
}
}
// If there are still no candidates, ignore water restrictions
if (candidateLuxuries.isEmpty()) {
candidateLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury
// Ignore weightings and water for this pass
}
}
return candidateLuxuries
}
private fun assignCityStateLuxuries(
targetCityStateLuxuries: Int,
assignableLuxuries: List<TileResource>,
amountRegionsWithLuxury: HashMap<String, Int>,
fallbackWeightings: Boolean
): ArrayList<String> {
val cityStateLuxuries = ArrayList<String>()
repeat(targetCityStateLuxuries) {
val candidateLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name] == 0 &&
(fallbackWeightings || it.hasUnique(UniqueType.LuxuryWeightingForCityStates))
}
if (candidateLuxuries.isEmpty()) return@repeat
val weights = candidateLuxuries.map {
val weightingUnique =
it.getMatchingUniques(UniqueType.LuxuryWeightingForCityStates).firstOrNull()
if (weightingUnique == null)
1f
else
weightingUnique.params[0].toFloat()
}
val luxury = candidateLuxuries.randomWeighted(weights).name
cityStateLuxuries.add(luxury)
amountRegionsWithLuxury[luxury] = 1
}
return cityStateLuxuries
}
}

View File

@ -88,8 +88,6 @@ class MapRegions (val ruleset: Ruleset){
private val regions = ArrayList<Region>()
private var usingArchipelagoRegions = false
private val tileData = TileDataMap()
private val cityStateLuxuries = ArrayList<String>()
private val randomLuxuries = ArrayList<String>()
/** Creates [numRegions] number of balanced regions for civ starting locations. */
fun generateRegions(tileMap: TileMap, numRegions: Int) {
@ -426,18 +424,6 @@ class MapRegions (val ruleset: Ruleset){
Log.debug(Tag("assignRegions"), msg, startBiasType, logCiv, region)
}
private fun getRegionPriority(terrain: Terrain?): Int? {
if (terrain == null) // ie "hybrid"
return 99999 // a big number
return if (!terrain.hasUnique(UniqueType.RegionRequirePercentSingleType)
&& !terrain.hasUnique(UniqueType.RegionRequirePercentTwoTypes))
null
else
if (terrain.hasUnique(UniqueType.RegionRequirePercentSingleType))
terrain.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).first().params[2].toInt()
else
terrain.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt()
}
private fun assignCivToRegion(civ: Civilization, region: Region) {
val tile = region.tileMap[region.startPosition!!]
@ -671,9 +657,10 @@ class MapRegions (val ruleset: Ruleset){
fun placeResourcesAndMinorCivs(tileMap: TileMap, minorCivs: List<Civilization>) {
placeNaturalWonderImpacts(tileMap)
assignLuxuries()
val (cityStateLuxuries, randomLuxuries) = LuxuryResourcePlacementLogic.assignLuxuries(regions, tileData, ruleset)
MinorCivPlacer.placeMinorCivs(regions, tileMap, minorCivs, usingArchipelagoRegions, tileData, ruleset)
placeLuxuries(tileMap)
placeLuxuries(tileMap, cityStateLuxuries, randomLuxuries)
placeStrategicAndBonuses(tileMap)
}
@ -687,104 +674,14 @@ class MapRegions (val ruleset: Ruleset){
}
}
/** Assigns a luxury to each region. No luxury can be assigned to too many regions.
* Some luxuries are earmarked for city states. The rest are randomly distributed or
* don't occur att all in the map */
private fun assignLuxuries() {
// If there are any weightings defined in json, assume they are complete. If there are none, use flat weightings instead
val fallbackWeightings = ruleset.tileResources.values.none {
it.resourceType == ResourceType.Luxury &&
(it.uniqueObjects.any { unique -> unique.isOfType(UniqueType.ResourceWeighting) } || it.hasUnique(UniqueType.LuxuryWeightingForCityStates)) }
val maxRegionsWithLuxury = when {
regions.size > 12 -> 3
regions.size > 8 -> 2
else -> 1
}
val targetCityStateLuxuries = 3 // was probably intended to be "if (tileData.size > 5000) 4 else 3"
val disabledPercent = 100 - min(tileData.size.toFloat().pow(0.2f) * 16, 100f).toInt() // Approximately
val targetDisabledLuxuries = (ruleset.tileResources.values
.count { it.resourceType == ResourceType.Luxury } * disabledPercent) / 100
val assignableLuxuries = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Luxury &&
!it.hasUnique(UniqueType.LuxurySpecialPlacement) &&
!it.hasUnique(UniqueType.CityStateOnlyResource) }
val amountRegionsWithLuxury = HashMap<String, Int>()
// Init map
ruleset.tileResources.values
.forEach { amountRegionsWithLuxury[it.name] = 0 }
for (region in regions.sortedBy { getRegionPriority(ruleset.terrains[it.type]) } ) {
val regionConditional = StateForConditionals(region = region)
var candidateLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury &&
// Check that it has a weight for this region type
(fallbackWeightings ||
it.hasUnique(UniqueType.ResourceWeighting, regionConditional)) &&
// Check that there is enough coast if it is a water based resource
((region.terrainCounts["Coastal"] ?: 0) >= 12 ||
it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water } )
}
// If we couldn't find any options, pick from all luxuries. First try to not pick water luxuries on land regions
if (candidateLuxuries.isEmpty()) {
candidateLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury &&
// Ignore weightings for this pass
// Check that there is enough coast if it is a water based resource
((region.terrainCounts["Coastal"] ?: 0) >= 12 ||
it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water })
}
}
// If there are still no candidates, ignore water restrictions
if (candidateLuxuries.isEmpty()) {
candidateLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury
// Ignore weightings and water for this pass
}
}
// If there are still no candidates (mad modders???) just skip this region
if (candidateLuxuries.isEmpty()) continue
// Pick a luxury at random. Weight is reduced if the luxury has been picked before
val modifiedWeights = candidateLuxuries.map {
val weightingUnique = it.getMatchingUniques(UniqueType.ResourceWeighting, regionConditional).firstOrNull()
val relativeWeight = if (weightingUnique == null) 1f else weightingUnique.params[0].toFloat()
relativeWeight / (1f + amountRegionsWithLuxury[it.name]!!)
}.shuffled()
region.luxury = candidateLuxuries.randomWeighted(modifiedWeights).name
amountRegionsWithLuxury[region.luxury!!] = amountRegionsWithLuxury[region.luxury]!! + 1
}
// Assign luxuries to City States
repeat(targetCityStateLuxuries) {
val candidateLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name] == 0 &&
(fallbackWeightings || it.hasUnique(UniqueType.LuxuryWeightingForCityStates))
}
if (candidateLuxuries.isEmpty()) return@repeat
val weights = candidateLuxuries.map {
val weightingUnique = it.getMatchingUniques(UniqueType.LuxuryWeightingForCityStates).firstOrNull()
if (weightingUnique == null)
1f
else
weightingUnique.params[0].toFloat()
}
val luxury = candidateLuxuries.randomWeighted(weights).name
cityStateLuxuries.add(luxury)
amountRegionsWithLuxury[luxury] = 1
}
// Assign some resources as random placement.
val remainingLuxuries = assignableLuxuries.filter {
amountRegionsWithLuxury[it.name] == 0
}.map { it.name }.shuffled()
randomLuxuries.addAll(remainingLuxuries.drop(targetDisabledLuxuries))
}
/** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */
private fun placeLuxuries(tileMap: TileMap) {
private fun placeLuxuries(
tileMap: TileMap,
cityStateLuxuries: List<String>,
randomLuxuries: List<String>
) {
// First place luxuries at major civ start locations
val averageFertilityDensity = regions.sumOf { it.totalFertility } / regions.sumOf { it.tiles.size }.toFloat()
for (region in regions) {
@ -1124,6 +1021,20 @@ class MapRegions (val ruleset: Ruleset){
}
fun getRegionPriority(terrain: Terrain?): Int? {
if (terrain == null) // ie "hybrid"
return 99999 // a big number
return if (!terrain.hasUnique(UniqueType.RegionRequirePercentSingleType)
&& !terrain.hasUnique(UniqueType.RegionRequirePercentTwoTypes))
null
else
if (terrain.hasUnique(UniqueType.RegionRequirePercentSingleType))
terrain.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).first().params[2].toInt()
else
terrain.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt()
}
/** @return a fake unique with the same conditionals, but sorted alphabetically.
* Used to save some memory and time when building resource lists. */
internal fun anonymizeUnique(unique: Unique) = Unique(