Fix startBias regional assignments (#9171)

This commit is contained in:
SomeTroglodyte
2023-04-13 14:38:19 +02:00
committed by GitHub
parent 9ea135fba8
commit e0533e994f
3 changed files with 105 additions and 61 deletions

View File

@ -21,6 +21,8 @@ import com.unciv.models.stats.Stat
import com.unciv.models.translations.equalsPlaceholderText import com.unciv.models.translations.equalsPlaceholderText
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.ui.components.extensions.randomWeighted import com.unciv.ui.components.extensions.randomWeighted
import com.unciv.utils.Log
import com.unciv.utils.Tag
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -210,6 +212,9 @@ class MapRegions (val ruleset: Ruleset){
return Pair(splitOffRegion, regionToSplit) return Pair(splitOffRegion, regionToSplit)
} }
/** Buckets for startBias to region assignments, used only in [assignRegions]. [PositiveFallback] is only for logging. */
private enum class BiasTypes { Coastal, Positive, Negative, Random, PositiveFallback }
fun assignRegions(tileMap: TileMap, civilizations: List<Civilization>, gameParameters: GameParameters) { fun assignRegions(tileMap: TileMap, civilizations: List<Civilization>, gameParameters: GameParameters) {
if (civilizations.isEmpty()) return if (civilizations.isEmpty()) return
@ -256,28 +261,43 @@ class MapRegions (val ruleset: Ruleset){
normalizeStart(tileMap[region.startPosition!!], tileMap, minorCiv = false) normalizeStart(tileMap[region.startPosition!!], tileMap, minorCiv = false)
} }
val coastBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.contains("Coast") } val civBiases = civilizations.associateWith { ruleset.nations[it.civName]!!.startBias }
val negativeBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.any { bias -> bias.equalsPlaceholderText("Avoid []") } } // This ensures each civ can only be in one of the buckets
.sortedByDescending { ruleset.nations[it.civName]!!.startBias.size } // Civs with more complex avoids go first val civsByBiasType = civBiases.entries.groupBy(
val randomCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.isEmpty() }.toMutableList() // We might fill this up as we go keySelector = {
// The rest are positive bias (_, startBias) ->
val positiveBiasCivs = civilizations.filterNot { it in coastBiasCivs || it in negativeBiasCivs || it in randomCivs } when {
.sortedBy { ruleset.nations[it.civName]!!.startBias.size } // civs with only one desired region go first gameParameters.noStartBias -> BiasTypes.Random
val positiveBiasFallbackCivs = ArrayList<Civilization>() // Civs who couldn't get their desired region at first pass startBias.any { bias -> bias.equalsPlaceholderText("Avoid []") } -> BiasTypes.Negative
"Coast" in startBias -> BiasTypes.Coastal
startBias.isNotEmpty() -> BiasTypes.Positive
else -> BiasTypes.Random
}
},
valueTransform = { (civ, _) -> civ }
)
val coastBiasCivs = civsByBiasType[BiasTypes.Coastal]
?: emptyList()
val positiveBiasCivs = civsByBiasType[BiasTypes.Positive]
?.sortedBy { civBiases[it]?.size } // civs with only one desired region go first
?: emptyList()
val negativeBiasCivs = civsByBiasType[BiasTypes.Negative]
?.sortedByDescending { civBiases[it]?.size } // Civs with more complex avoids go first
?: emptyList()
val randomCivs = civsByBiasType[BiasTypes.Random]
?.toMutableList() // We might fill this up as we go
?: mutableListOf()
val positiveBiasFallbackCivs = mutableListOf<Civilization>() // Civs who couldn't get their desired region at first pass
val unpickedRegions = regions.toMutableList() val unpickedRegions = regions.toMutableList()
// First assign coast bias civs // First assign coast bias civs
for (civ in coastBiasCivs) { for (civ in coastBiasCivs) {
// If noStartBias is enabled consider these to be randomCivs
if (gameParameters.noStartBias) {
randomCivs.addAll(coastBiasCivs)
break
}
// Try to find a coastal start, preferably a really coastal one // Try to find a coastal start, preferably a really coastal one
var startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].isCoastalTile() } var startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].isCoastalTile() }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) { if (startRegion != null) {
logAssignRegion(true, BiasTypes.Coastal, civ, startRegion)
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion) unpickedRegions.remove(startRegion)
continue continue
@ -286,6 +306,7 @@ class MapRegions (val ruleset: Ruleset){
startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.getBaseTerrain().hasUnique(UniqueType.FreshWater) } } startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.getBaseTerrain().hasUnique(UniqueType.FreshWater) } }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) { if (startRegion != null) {
logAssignRegion(true, BiasTypes.Coastal, civ, startRegion)
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion) unpickedRegions.remove(startRegion)
continue continue
@ -294,6 +315,7 @@ class MapRegions (val ruleset: Ruleset){
startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].isAdjacentToRiver() } startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].isAdjacentToRiver() }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) { if (startRegion != null) {
logAssignRegion(true, BiasTypes.Coastal, civ, startRegion)
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion) unpickedRegions.remove(startRegion)
continue continue
@ -302,72 +324,85 @@ class MapRegions (val ruleset: Ruleset){
startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.isAdjacentToRiver() } } startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.isAdjacentToRiver() } }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) { if (startRegion != null) {
logAssignRegion(true, BiasTypes.Coastal, civ, startRegion)
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion) unpickedRegions.remove(startRegion)
continue continue
} }
// Else pick a random region at the end // Else pick a random region at the end
logAssignRegion(false, BiasTypes.Coastal, civ)
randomCivs.add(civ) randomCivs.add(civ)
} }
// Next do positive bias civs // Next do positive bias civs
for (civ in positiveBiasCivs) { for (civ in positiveBiasCivs) {
// If noStartBias is enabled consider these to be randomCivs
if (gameParameters.noStartBias) {
randomCivs.addAll(positiveBiasCivs)
break
}
// Try to find a start that matches any of the desired regions, ideally with lots of desired terrain // Try to find a start that matches any of the desired regions, ideally with lots of desired terrain
val preferred = ruleset.nations[civ.civName]!!.startBias val preferred = civBiases[civ]!!
val startRegion = unpickedRegions.filter { it.type in preferred } val startRegion = unpickedRegions.filter { it.type in preferred }
.maxByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in preferred }.values.sum() } .maxByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in preferred }.values.sum() }
if (startRegion != null) { if (startRegion != null) {
logAssignRegion(true, BiasTypes.Positive, civ, startRegion)
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion) unpickedRegions.remove(startRegion)
continue continue
} else if (ruleset.nations[civ.civName]!!.startBias.size == 1) { // Civs with a single bias (only) get to look for a fallback region } else if (preferred.size == 1) { // Civs with a single bias (only) get to look for a fallback region
positiveBiasFallbackCivs.add(civ) positiveBiasFallbackCivs.add(civ)
} else { // Others get random starts } else { // Others get random starts
logAssignRegion(false, BiasTypes.Positive, civ)
randomCivs.add(civ) randomCivs.add(civ)
} }
} }
// Do a second pass for fallback civs, choosing the region most similar to the desired type // Do a second pass for fallback civs, choosing the region most similar to the desired type
for (civ in positiveBiasFallbackCivs) { for (civ in positiveBiasFallbackCivs) {
val startRegion = getFallbackRegion(ruleset.nations[civ.civName]!!.startBias.first(), unpickedRegions) val startRegion = getFallbackRegion(civBiases[civ]!!.first(), unpickedRegions)
logAssignRegion(true, BiasTypes.PositiveFallback, civ, startRegion)
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion) unpickedRegions.remove(startRegion)
} }
// Next do negative bias ones (ie "Avoid []") // Next do negative bias ones (ie "Avoid []")
for (civ in negativeBiasCivs) { for (civ in negativeBiasCivs) {
// If noStartBias is enabled consider these to be randomCivs val (avoidBias, preferred) = civBiases[civ]!!
if (gameParameters.noStartBias) { .partition { bias -> bias.equalsPlaceholderText("Avoid []") }
randomCivs.addAll(negativeBiasCivs) val avoided = avoidBias.map { it.getPlaceholderParameters()[0] }
break // Try to find a region not of the avoided types, secondary sort by
} // least number of undesired terrains (weighed double) / most number of desired terrains
val avoided = ruleset.nations[civ.civName]!!.startBias.map { it.getPlaceholderParameters()[0] }
// Try to find a region not of the avoided types, secondary sort by least number of undesired terrains
val startRegion = unpickedRegions.filterNot { it.type in avoided } val startRegion = unpickedRegions.filterNot { it.type in avoided }
.minByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in avoided }.values.sum() } .minByOrNull {
2 * it.terrainCounts.filterKeys { terrain -> terrain in avoided }.values.sum()
- it.terrainCounts.filterKeys { terrain -> terrain in preferred }.values.sum()
}
if (startRegion != null) { if (startRegion != null) {
logAssignRegion(true, BiasTypes.Negative, civ, startRegion)
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion) unpickedRegions.remove(startRegion)
continue continue
} else } else {
logAssignRegion(false, BiasTypes.Negative, civ)
randomCivs.add(civ) // else pick a random region at the end randomCivs.add(civ) // else pick a random region at the end
}
} }
// Finally assign the remaining civs randomly // Finally assign the remaining civs randomly
for (civ in randomCivs) { for (civ in randomCivs) {
// throws if regions.size < civilizations.size or if the assigning mismatched - leads to popup on newgame screen
val startRegion = unpickedRegions.random() val startRegion = unpickedRegions.random()
logAssignRegion(true, BiasTypes.Random, civ, startRegion)
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion) unpickedRegions.remove(startRegion)
} }
} }
private fun logAssignRegion(success: Boolean, startBiasType: BiasTypes, civ: Civilization, region: Region? = null) {
if (Log.backend.isRelease()) return
val logCiv = { civ.civName + " " + ruleset.nations[civ.civName]!!.startBias.joinToString(",", "(", ")") }
val msg = if (success) "(%s): %s to %s"
else "no region (%s) found for %s"
Log.debug(Tag("assignRegions"), msg, startBiasType, logCiv, region)
}
private fun getRegionPriority(terrain: Terrain?): Int? { private fun getRegionPriority(terrain: Terrain?): Int? {
if (terrain == null) // ie "hybrid" if (terrain == null) // ie "hybrid"
return 99999 // a big number return 99999 // a big number
@ -381,9 +416,9 @@ class MapRegions (val ruleset: Ruleset){
terrain.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt() terrain.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt()
} }
private fun assignCivToRegion(civInfo: Civilization, region: Region) { private fun assignCivToRegion(civ: Civilization, region: Region) {
val tile = region.tileMap[region.startPosition!!] val tile = region.tileMap[region.startPosition!!]
region.tileMap.addStartingLocation(civInfo.civName, tile) region.tileMap.addStartingLocation(civ.civName, tile)
// Place impacts to keep city states etc at appropriate distance // Place impacts to keep city states etc at appropriate distance
placeImpact(ImpactType.MinorCiv,tile, 6) placeImpact(ImpactType.MinorCiv,tile, 6)
@ -1743,4 +1778,6 @@ class Region (val tileMap: TileMap, val rect: Rectangle, val continentID: Int =
/** Returns number terrains with [name] */ /** Returns number terrains with [name] */
fun getTerrainAmount(name: String) = terrainCounts[name] ?: 0 fun getTerrainAmount(name: String) = terrainCounts[name] ?: 0
override fun toString() = "Region($type, ${tiles.size} tiles, ${terrainCounts.entries.joinToString { "${it.value} ${it.key}" }})"
} }

View File

@ -23,7 +23,7 @@ object Log {
*/ */
val disableLogsFrom = ( val disableLogsFrom = (
System.getProperty("noLog") System.getProperty("noLog")
?: "Battle,Music,Sounds,Translations,WorkerAutomation" ?: "Battle,Music,Sounds,Translations,WorkerAutomation,assignRegions"
).split(',').filterNot { it.isEmpty() }.toMutableSet() ).split(',').filterNot { it.isEmpty() }.toMutableSet()
/** /**

View File

@ -63,31 +63,38 @@ Each building can have the following attributes:
This file contains all the nations and city states, including Barbarians and Spectator. This file contains all the nations and city states, including Barbarians and Spectator.
| Attribute | Type | Optional | Notes | | Attribute | Type | Optional | Notes |
| --------- | ---- | -------- | ----- | |----------------------|------------|------------------|------------------------------------------------------------------------------------------------------------------|
| name | String | Required | | | name | String | Required | |
| leaderName | String | Default empty | Omit only for city states! If you want LeaderPortraits, the image file names must match exactly, including case. | | leaderName | String | Default empty | Omit only for city states! If you want LeaderPortraits, the image file names must match exactly, including case. |
| style | String | Default empty | Modifier appended to pixel unit image names | | style | String | Default empty | Modifier appended to pixel unit image names |
| adjective | String | Default empty | Currently unused | | adjective | String | Default empty | Currently unused |
| cityStateType | Enum | Default absent | Distinguishes Major Civilizations from City States (Cultured, Maritime, Mercantile, Militaristic) | | cityStateType | Enum | Default absent | Distinguishes Major Civilizations from City States (Cultured, Maritime, Mercantile, Militaristic) |
| startBias | List | Default empty | Zero or more of: terrainFilter or "Avoid [terrainFilter]". Two or more will be logically "and"-ed, and if the filters result in no choices, the entire attribute is ignored (e.g. `"startBias": ["Snow","Tundra"]` will _never_ work). | | startBias | List | Default empty | Zero or more of: terrainFilter or "Avoid [terrainFilter]". [^S] |
| preferredVictoryType | Enum | Default Neutral | Neutral, Cultural, Diplomatic, Domination or Scientific | | preferredVictoryType | Enum | Default Neutral | Neutral, Cultural, Diplomatic, Domination or Scientific |
| startIntroPart1 | String | Default empty | Introductory blurb shown to Player on game start... | | startIntroPart1 | String | Default empty | Introductory blurb shown to Player on game start... |
| startIntroPart2 | String | Default empty | ... second paragraph. ***NO*** "TBD"!!! Leave empty to skip that alert. | | startIntroPart2 | String | Default empty | ... second paragraph. ***NO*** "TBD"!!! Leave empty to skip that alert. |
| declaringWar | String | Default empty | another greeting | | declaringWar | String | Default empty | another greeting |
| attacked | String | Default empty | another greeting | | attacked | String | Default empty | another greeting |
| defeated | String | Default empty | another greeting | | defeated | String | Default empty | another greeting |
| introduction | String | Default empty | another greeting | | introduction | String | Default empty | another greeting |
| neutralHello | String | Default empty | another greeting | | neutralHello | String | Default empty | another greeting |
| hateHello | String | Default empty | another greeting | | hateHello | String | Default empty | another greeting |
| tradeRequest | String | Default empty | another greeting | | tradeRequest | String | Default empty | another greeting |
| innerColor | 3x Integer | Default black | R, G, B for outer ring of nation icon | | innerColor | 3x Integer | Default black | R, G, B for outer ring of nation icon |
| outerColor | 3x Integer | Required | R, G, B for inner circle of nation icon | | outerColor | 3x Integer | Required | R, G, B for inner circle of nation icon |
| uniqueName | String | Default empty | Decorative name for the special characteristic of this Nation | | uniqueName | String | Default empty | Decorative name for the special characteristic of this Nation |
| uniqueText | String | Default empty | Replacement text for "uniques". If empty, uniques are listed individually. | | uniqueText | String | Default empty | Replacement text for "uniques". If empty, uniques are listed individually. |
| uniques | List | Default empty | Properties of the civilization - see [here](../Modders/Unique-parameters.md#general-uniques) | | uniques | List | Default empty | Properties of the civilization - see [here](../Modders/Unique-parameters.md#general-uniques) |
| cities | List | Default empty | City names used sequentially for newly founded cities. | | cities | List | Default empty | City names used sequentially for newly founded cities. |
| civilopediaText | List | Default empty | see [civilopediaText chapter](Miscellaneous-JSON-files.md#civilopedia-text) | | civilopediaText | List | Default empty | see [civilopediaText chapter](Miscellaneous-JSON-files.md#civilopedia-text) |
[^S]: A "Coast" preference (_unless_ combined with "Avoid") is translated to a complex test for coastal land tiles, tiles next to Lakes, river tiles or near-river tiles, and such civs are processed first. Other startBias entries are ignored in that case.
Other positive (no "Avoid") startBias are processed next. Multiple positive preferences are treated equally, but get no "fallback".
Single positive startBias can get a "fallback" region if there is no (or no more) region with that primary type: any leftover region with as much of the specified terrain as possible will do.
Multiple "Avoid" entries are treated equally (and reduce chance for success - if no region is left avoiding _all_ specified types that civ gets a random one).
When combining preferred terrain with "Avoid", the latter takes precedence, and preferred terrain only has minor weight when choosing between regions that are not of a type to avoid.
These notes are **only** valid when playing on generated maps, loaded maps from map editor get no "regions" and startBias is processed differently (but you can expect single-entry startBias to work best).
## Policies.json ## Policies.json