mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-05 21:11:35 +07:00
Fix startBias regional assignments (#9171)
This commit is contained in:
parent
9ea135fba8
commit
e0533e994f
@ -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}" }})"
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user