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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.getPlaceholderParameters
import com.unciv.ui.components.extensions.randomWeighted
import com.unciv.utils.Log
import com.unciv.utils.Tag
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@ -210,6 +212,9 @@ class MapRegions (val ruleset: Ruleset){
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) {
if (civilizations.isEmpty()) return
@ -256,28 +261,43 @@ class MapRegions (val ruleset: Ruleset){
normalizeStart(tileMap[region.startPosition!!], tileMap, minorCiv = false)
}
val coastBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.contains("Coast") }
val negativeBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.any { bias -> bias.equalsPlaceholderText("Avoid []") } }
.sortedByDescending { ruleset.nations[it.civName]!!.startBias.size } // Civs with more complex avoids go first
val randomCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.isEmpty() }.toMutableList() // We might fill this up as we go
// The rest are positive bias
val positiveBiasCivs = civilizations.filterNot { it in coastBiasCivs || it in negativeBiasCivs || it in randomCivs }
.sortedBy { ruleset.nations[it.civName]!!.startBias.size } // civs with only one desired region go first
val positiveBiasFallbackCivs = ArrayList<Civilization>() // Civs who couldn't get their desired region at first pass
val civBiases = civilizations.associateWith { ruleset.nations[it.civName]!!.startBias }
// This ensures each civ can only be in one of the buckets
val civsByBiasType = civBiases.entries.groupBy(
keySelector = {
(_, startBias) ->
when {
gameParameters.noStartBias -> BiasTypes.Random
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()
// First assign coast bias civs
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
var startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].isCoastalTile() }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) {
logAssignRegion(true, BiasTypes.Coastal, civ, startRegion)
assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue
@ -286,6 +306,7 @@ class MapRegions (val ruleset: Ruleset){
startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.getBaseTerrain().hasUnique(UniqueType.FreshWater) } }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) {
logAssignRegion(true, BiasTypes.Coastal, civ, startRegion)
assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue
@ -294,6 +315,7 @@ class MapRegions (val ruleset: Ruleset){
startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].isAdjacentToRiver() }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) {
logAssignRegion(true, BiasTypes.Coastal, civ, startRegion)
assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue
@ -302,72 +324,85 @@ class MapRegions (val ruleset: Ruleset){
startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.isAdjacentToRiver() } }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) {
logAssignRegion(true, BiasTypes.Coastal, civ, startRegion)
assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue
}
// Else pick a random region at the end
logAssignRegion(false, BiasTypes.Coastal, civ)
randomCivs.add(civ)
}
// Next do positive bias civs
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
val preferred = ruleset.nations[civ.civName]!!.startBias
val preferred = civBiases[civ]!!
val startRegion = unpickedRegions.filter { it.type in preferred }
.maxByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in preferred }.values.sum() }
if (startRegion != null) {
logAssignRegion(true, BiasTypes.Positive, civ, startRegion)
assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
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)
} else { // Others get random starts
logAssignRegion(false, BiasTypes.Positive, civ)
randomCivs.add(civ)
}
}
// Do a second pass for fallback civs, choosing the region most similar to the desired type
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)
unpickedRegions.remove(startRegion)
}
// Next do negative bias ones (ie "Avoid []")
for (civ in negativeBiasCivs) {
// If noStartBias is enabled consider these to be randomCivs
if (gameParameters.noStartBias) {
randomCivs.addAll(negativeBiasCivs)
break
}
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 (avoidBias, preferred) = civBiases[civ]!!
.partition { bias -> bias.equalsPlaceholderText("Avoid []") }
val avoided = avoidBias.map { it.getPlaceholderParameters()[0] }
// 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 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) {
logAssignRegion(true, BiasTypes.Negative, civ, startRegion)
assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue
} else
} else {
logAssignRegion(false, BiasTypes.Negative, civ)
randomCivs.add(civ) // else pick a random region at the end
}
}
// Finally assign the remaining civs randomly
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()
logAssignRegion(true, BiasTypes.Random, civ, startRegion)
assignCivToRegion(civ, 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? {
if (terrain == null) // ie "hybrid"
return 99999 // a big number
@ -381,9 +416,9 @@ class MapRegions (val ruleset: Ruleset){
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!!]
region.tileMap.addStartingLocation(civInfo.civName, tile)
region.tileMap.addStartingLocation(civ.civName, tile)
// Place impacts to keep city states etc at appropriate distance
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] */
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 = (
System.getProperty("noLog")
?: "Battle,Music,Sounds,Translations,WorkerAutomation"
?: "Battle,Music,Sounds,Translations,WorkerAutomation,assignRegions"
).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.
| Attribute | Type | Optional | Notes |
| --------- | ---- | -------- | ----- |
| 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. |
| style | String | Default empty | Modifier appended to pixel unit image names |
| adjective | String | Default empty | Currently unused |
| 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). |
| preferredVictoryType | Enum | Default Neutral | Neutral, Cultural, Diplomatic, Domination or Scientific |
| 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. |
| declaringWar | String | Default empty | another greeting |
| attacked | String | Default empty | another greeting |
| defeated | String | Default empty | another greeting |
| introduction | String | Default empty | another greeting |
| neutralHello | String | Default empty | another greeting |
| hateHello | 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 |
| 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 |
| 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) |
| 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) |
| Attribute | Type | Optional | Notes |
|----------------------|------------|------------------|------------------------------------------------------------------------------------------------------------------|
| 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. |
| style | String | Default empty | Modifier appended to pixel unit image names |
| adjective | String | Default empty | Currently unused |
| 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]". [^S] |
| preferredVictoryType | Enum | Default Neutral | Neutral, Cultural, Diplomatic, Domination or Scientific |
| 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. |
| declaringWar | String | Default empty | another greeting |
| attacked | String | Default empty | another greeting |
| defeated | String | Default empty | another greeting |
| introduction | String | Default empty | another greeting |
| neutralHello | String | Default empty | another greeting |
| hateHello | 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 |
| 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 |
| 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) |
| 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) |
[^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