mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-30 22:58:50 +07:00
Rework game start again (continents) (#5335)
Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
@ -3,21 +3,18 @@ package com.unciv.logic
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.civilization.*
|
||||
import com.unciv.logic.map.BFS
|
||||
import com.unciv.logic.map.TileInfo
|
||||
import com.unciv.logic.map.TileMap
|
||||
import com.unciv.logic.map.mapgenerator.MapGenerator
|
||||
import com.unciv.models.metadata.GameParameters
|
||||
import com.unciv.models.metadata.GameSetupInfo
|
||||
import com.unciv.models.ruleset.Era
|
||||
import com.unciv.models.ruleset.ModOptionsConstants
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.ruleset.tile.ResourceType
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.math.max
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
object GameStarter {
|
||||
// temporary instrumentation while tuning/debugging
|
||||
@ -86,7 +83,7 @@ object GameStarter {
|
||||
|
||||
if (tileMap.continentSizes.isEmpty()) // Probably saved map without continent data
|
||||
runAndMeasure("assignContinents") {
|
||||
mapGen.assignContinents(tileMap)
|
||||
tileMap.assignContinents()
|
||||
}
|
||||
|
||||
runAndMeasure("addCivStartingUnits") {
|
||||
@ -244,23 +241,21 @@ object GameStarter {
|
||||
for (tile in tileMap.values) {
|
||||
startScores[tile] = tile.getTileStartScore()
|
||||
}
|
||||
val allCivs = gameInfo.civilizations.filter { !it.isBarbarian() }
|
||||
val landTilesInBigEnoughGroup = getCandidateLand(allCivs.size, tileMap, startScores)
|
||||
|
||||
// First we get start locations for the major civs, on the second pass the city states (without predetermined starts) can squeeze in wherever
|
||||
// I hear copying code is good
|
||||
val civNamesWithStartingLocations = tileMap.startingLocationsByNation.keys
|
||||
val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in civNamesWithStartingLocations) }
|
||||
val bestLocations = getStartingLocations(bestCivs, tileMap, startScores)
|
||||
val bestCivs = allCivs.filter { !it.isCityState() || it.civName in civNamesWithStartingLocations }
|
||||
val bestLocations = getStartingLocations(bestCivs, tileMap, landTilesInBigEnoughGroup, startScores)
|
||||
for ((civ, tile) in bestLocations) {
|
||||
if (civ.civName in civNamesWithStartingLocations) // Already have explicit starting locations
|
||||
continue
|
||||
|
||||
// A nation can have multiple marked starting locations, of which the first pass may have chosen one
|
||||
tileMap.removeStartingLocations(civ.civName)
|
||||
// Mark the best start locations so we remember them for the second pass
|
||||
tileMap.addStartingLocation(civ.civName, tile)
|
||||
}
|
||||
|
||||
val startingLocations = getStartingLocations(
|
||||
gameInfo.civilizations.filter { !it.isBarbarian() },
|
||||
tileMap, startScores)
|
||||
val startingLocations = getStartingLocations(allCivs, tileMap, landTilesInBigEnoughGroup, startScores)
|
||||
|
||||
val settlerLikeUnits = ruleSet.units.filter {
|
||||
it.value.uniqueObjects.any { unique -> unique.placeholderText == Constants.settlerUnique }
|
||||
@ -349,70 +344,66 @@ object GameStarter {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCandidateLand(
|
||||
civCount: Int,
|
||||
tileMap: TileMap,
|
||||
startScores: HashMap<TileInfo, Float>
|
||||
): Map<TileInfo, Float> {
|
||||
if (tileMap.continentSizes.isEmpty()) tileMap.assignContinents()
|
||||
|
||||
private fun getStartingLocations(civs: List<CivilizationInfo>, tileMap: TileMap, startScores: HashMap<TileInfo, Float>): HashMap<CivilizationInfo, TileInfo> {
|
||||
val landTilesInBigEnoughGroup = tileMap.landTilesInBigEnoughGroup
|
||||
if (landTilesInBigEnoughGroup.isEmpty()) {
|
||||
// Worst case - a pre-made map with continent data. This means we didn't re-run assignContinents,
|
||||
// so we don't have a cached landTilesInBigEnoughGroup. So we need to do it the hard way.
|
||||
var landTiles = tileMap.values
|
||||
// Games starting on snow might as well start over...
|
||||
.filter { it.isLand && !it.isImpassible() && it.baseTerrain != Constants.snow }
|
||||
while (landTiles.any()) {
|
||||
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
|
||||
bfs.stepToEnd()
|
||||
val tilesInGroup = bfs.getReachedTiles()
|
||||
landTiles = landTiles.filter { it !in tilesInGroup }
|
||||
if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on
|
||||
landTilesInBigEnoughGroup.addAll(tilesInGroup)
|
||||
}
|
||||
// We want to distribute starting locations fairly, and thus not place anybody on a small island
|
||||
// - unless necessary. Old code would only consider landmasses >= 20 tiles.
|
||||
// Instead, take continents until >=75% total area or everybody can get their own island
|
||||
val orderedContinents = tileMap.continentSizes.asSequence().sortedByDescending { it.value }.toList()
|
||||
val totalArea = tileMap.continentSizes.values.sum()
|
||||
var candidateArea = 0
|
||||
val candidateContinents = HashSet<Int>()
|
||||
for ((index, continentSize) in orderedContinents.withIndex()) {
|
||||
candidateArea += continentSize.value
|
||||
candidateContinents.add(continentSize.key)
|
||||
if (candidateArea * 4 >= totalArea * 3) break
|
||||
if (index >= civCount) break
|
||||
}
|
||||
|
||||
return startScores.filter { it.key.getContinent() in candidateContinents }
|
||||
}
|
||||
|
||||
private fun getStartingLocations(
|
||||
civs: List<CivilizationInfo>,
|
||||
tileMap: TileMap,
|
||||
landTilesInBigEnoughGroup: Map<TileInfo, Float>,
|
||||
startScores: HashMap<TileInfo, Float>
|
||||
): HashMap<CivilizationInfo, TileInfo> {
|
||||
|
||||
val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start
|
||||
.sortedBy { civ ->
|
||||
when {
|
||||
civ.civName in tileMap.startingLocationsByNation -> 1 // harshest requirements
|
||||
civ.nation.startBias.contains("Tundra") -> 2 // Tundra starts are hard to find, so let's do them first
|
||||
civ.nation.startBias.isNotEmpty() -> 3 // less harsh
|
||||
else -> 4 // no requirements
|
||||
civ.nation.startBias.any { it in tileMap.naturalWonders } -> 2
|
||||
civ.nation.startBias.contains("Tundra") -> 3 // Tundra starts are hard to find, so let's do them first
|
||||
civ.nation.startBias.isNotEmpty() -> 4 // less harsh
|
||||
else -> 5 // no requirements
|
||||
}
|
||||
}
|
||||
|
||||
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) {
|
||||
val freeTiles = landTilesInBigEnoughGroup
|
||||
.filter {
|
||||
HexMath.getDistanceFromEdge(it.position, tileMap.mapParameters) >=
|
||||
(minimumDistanceBetweenStartingLocations * 2) /3
|
||||
}.toMutableList()
|
||||
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 6 downTo 0) {
|
||||
val freeTiles = landTilesInBigEnoughGroup.asSequence()
|
||||
.filter {
|
||||
HexMath.getDistanceFromEdge(it.key.position, tileMap.mapParameters) >=
|
||||
(minimumDistanceBetweenStartingLocations * 2) / 3
|
||||
}.sortedBy { it.value }
|
||||
.map { it.key }
|
||||
.toMutableList()
|
||||
|
||||
val startingLocations = HashMap<CivilizationInfo, TileInfo>()
|
||||
for (civ in civsOrderedByAvailableLocations) {
|
||||
var startingLocation: TileInfo
|
||||
val presetStartingLocation = tileMap.startingLocationsByNation[civ.civName]?.randomOrNull() // in case map editor is extended to allow alternate starting locations for a nation
|
||||
var distanceToNext = minimumDistanceBetweenStartingLocations
|
||||
|
||||
if (presetStartingLocation != null) startingLocation = presetStartingLocation
|
||||
val distanceToNext = minimumDistanceBetweenStartingLocations /
|
||||
(if (civ.isCityState()) 2 else 1) // We allow city states to squeeze in tighter
|
||||
val presetStartingLocation = tileMap.startingLocationsByNation[civ.civName]?.randomOrNull()
|
||||
val startingLocation = if (presetStartingLocation != null) presetStartingLocation
|
||||
else {
|
||||
if (freeTiles.isEmpty()) break // we failed to get all the starting tiles with this minimum distance
|
||||
if (civ.isCityState())
|
||||
distanceToNext = minimumDistanceBetweenStartingLocations / 2 // We allow random city states to squeeze in tighter
|
||||
|
||||
freeTiles.sortBy { startScores[it] }
|
||||
|
||||
var preferredTiles = freeTiles.toList()
|
||||
|
||||
for (startBias in civ.nation.startBias) {
|
||||
preferredTiles = when {
|
||||
startBias.startsWith("Avoid [") -> {
|
||||
val tileToAvoid = startBias.removePrefix("Avoid [").removeSuffix("]")
|
||||
preferredTiles.filter { !it.matchesTerrainFilter(tileToAvoid) }
|
||||
}
|
||||
startBias == Constants.coast -> preferredTiles.filter { it.isCoastalTile() }
|
||||
else -> preferredTiles.filter { it.matchesTerrainFilter(startBias) }
|
||||
}
|
||||
}
|
||||
|
||||
startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.last() else freeTiles.last()
|
||||
getOneStartingLocation(civ, tileMap, freeTiles, startScores)
|
||||
}
|
||||
startingLocations[civ] = startingLocation
|
||||
freeTiles.removeAll(tileMap.getTilesInDistance(startingLocation.position, distanceToNext))
|
||||
@ -424,6 +415,36 @@ object GameStarter {
|
||||
throw Exception("Didn't manage to get starting tiles even with distance of 1?")
|
||||
}
|
||||
|
||||
private fun getOneStartingLocation(
|
||||
civ: CivilizationInfo,
|
||||
tileMap: TileMap,
|
||||
freeTiles: MutableList<TileInfo>,
|
||||
startScores: HashMap<TileInfo, Float>
|
||||
): TileInfo {
|
||||
if (civ.nation.startBias.any { it in tileMap.naturalWonders }) {
|
||||
// startPref wants Natural wonder neighbor: Rare and very likely to be outside getDistanceFromEdge
|
||||
val wonderNeighbor = tileMap.values.asSequence()
|
||||
.filter { it.isNaturalWonder() && it.naturalWonder!! in civ.nation.startBias }
|
||||
.sortedByDescending { startScores[it] }
|
||||
.firstOrNull()
|
||||
if (wonderNeighbor != null) return wonderNeighbor
|
||||
}
|
||||
|
||||
var preferredTiles = freeTiles.toList()
|
||||
for (startBias in civ.nation.startBias) {
|
||||
preferredTiles = when {
|
||||
startBias.startsWith("Avoid [") -> {
|
||||
val tileToAvoid = startBias.removePrefix("Avoid [").removeSuffix("]")
|
||||
preferredTiles.filter { !it.matchesTerrainFilter(tileToAvoid) }
|
||||
}
|
||||
startBias == Constants.coast -> preferredTiles.filter { it.isCoastalTile() }
|
||||
startBias in tileMap.naturalWonders -> preferredTiles // passthrough: already failed
|
||||
else -> preferredTiles.filter { it.matchesTerrainFilter(startBias) }
|
||||
}
|
||||
}
|
||||
return preferredTiles.lastOrNull() ?: freeTiles.last()
|
||||
}
|
||||
|
||||
private fun addConsolationPrize(gameInfo: GameInfo, spawn: TileInfo, points: Int) {
|
||||
val relevantTiles = spawn.getTilesInDistanceRange(1..2).shuffled()
|
||||
var addedPoints = 0
|
||||
|
@ -2,7 +2,6 @@ package com.unciv.logic.map
|
||||
|
||||
import com.badlogic.gdx.math.Vector2
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.HexMath
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
@ -79,9 +78,6 @@ class TileMap {
|
||||
@Transient
|
||||
val startingLocationsByNation = HashMap<String,HashSet<TileInfo>>()
|
||||
|
||||
@Transient
|
||||
val landTilesInBigEnoughGroup = ArrayList<TileInfo>() // cached at map gen
|
||||
|
||||
//endregion
|
||||
//region Constructors
|
||||
|
||||
@ -546,11 +542,42 @@ class TileMap {
|
||||
// we do not clean up an empty startingLocationsByNation[nationName] set - not worth it
|
||||
}
|
||||
|
||||
/** Removes all starting positions for [nationName], maintaining the transients */
|
||||
fun removeStartingLocations(nationName: String) {
|
||||
if (startingLocationsByNation[nationName] == null) return
|
||||
for (tile in startingLocationsByNation[nationName]!!) {
|
||||
startingLocations.remove(StartingLocation(tile.position, nationName))
|
||||
}
|
||||
startingLocationsByNation[nationName]!!.clear()
|
||||
}
|
||||
|
||||
/** Clears starting positions, e.g. after GameStarter is done with them. Does not clear the pseudo-improvements. */
|
||||
fun clearStartingLocations() {
|
||||
startingLocations.clear()
|
||||
startingLocationsByNation.clear()
|
||||
}
|
||||
|
||||
/** Set a continent id for each tile, so we can quickly see which tiles are connected.
|
||||
* Can also be called on saved maps.
|
||||
* @throws Exception when any land tile already has a continent ID
|
||||
*/
|
||||
fun assignContinents() {
|
||||
var landTiles = values.filter { it.isLand && !it.isImpassible() }
|
||||
var currentContinent = 0
|
||||
|
||||
while (landTiles.any()) {
|
||||
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
|
||||
bfs.stepToEnd()
|
||||
bfs.getReachedTiles().forEach {
|
||||
it.setContinent(currentContinent)
|
||||
}
|
||||
val continent = bfs.getReachedTiles()
|
||||
continentSizes[currentContinent] = continent.size
|
||||
|
||||
currentContinent++
|
||||
landTiles = landTiles.filter { it !in continent }
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
spawnIce(map)
|
||||
}
|
||||
runAndMeasure("assignContinents") {
|
||||
assignContinents(map)
|
||||
map.assignContinents()
|
||||
}
|
||||
runAndMeasure("NaturalWonderGenerator") {
|
||||
NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map)
|
||||
@ -463,32 +463,6 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
tile.terrainFeatures.add(Constants.ice)
|
||||
}
|
||||
}
|
||||
|
||||
/** Set a continent id for each tile, so we can quickly see which tiles are connected.
|
||||
* Can also be called on saved maps.
|
||||
* @throws Exception when any land tile already has a continent ID
|
||||
*/
|
||||
fun assignContinents(tileMap: TileMap) {
|
||||
var landTiles = tileMap.values
|
||||
.filter { it.isLand && !it.isImpassible()}
|
||||
var currentContinent = 0
|
||||
|
||||
while (landTiles.any()) {
|
||||
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
|
||||
bfs.stepToEnd()
|
||||
bfs.getReachedTiles().forEach {
|
||||
it.setContinent(currentContinent)
|
||||
}
|
||||
val continent = bfs.getReachedTiles()
|
||||
tileMap.continentSizes[currentContinent] = continent.size
|
||||
if (continent.size > 20) {
|
||||
tileMap.landTilesInBigEnoughGroup.addAll(continent)
|
||||
}
|
||||
|
||||
currentContinent++
|
||||
landTiles = landTiles.filter { it !in continent }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MapGenerationRandomness {
|
||||
|
@ -54,6 +54,15 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration
|
||||
private fun Unique.getIntParam(index: Int) = params[index].toInt()
|
||||
|
||||
private fun spawnSpecificWonder(tileMap: TileMap, wonder: Terrain): Boolean {
|
||||
val continentsRelevant = wonder.hasUnique(UniqueType.NaturalWonderLargerLandmass) ||
|
||||
wonder.hasUnique(UniqueType.NaturalWonderSmallerLandmass)
|
||||
val sortedContinents = if (continentsRelevant)
|
||||
tileMap.continentSizes.asSequence()
|
||||
.sortedByDescending { it.value }
|
||||
.map { it.key }
|
||||
.toList()
|
||||
else listOf()
|
||||
|
||||
val suitableLocations = tileMap.values.filter { tile->
|
||||
tile.resource == null &&
|
||||
wonder.occursOn.contains(tile.getLastTerrain().name) &&
|
||||
@ -71,13 +80,12 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration
|
||||
}
|
||||
count in unique.getIntParam(0)..unique.getIntParam(1)
|
||||
}
|
||||
UniqueType.NaturalWonderLandmass -> {
|
||||
val sortedContinents = tileMap.continentSizes.asSequence()
|
||||
.sortedByDescending { it.value }
|
||||
.map { it.key }
|
||||
.toList()
|
||||
UniqueType.NaturalWonderSmallerLandmass -> {
|
||||
tile.getContinent() !in sortedContinents.take(unique.getIntParam(0))
|
||||
}
|
||||
UniqueType.NaturalWonderLargerLandmass -> {
|
||||
tile.getContinent() in sortedContinents.take(unique.getIntParam(0))
|
||||
}
|
||||
UniqueType.NaturalWonderLatitude -> {
|
||||
val lower = tileMap.maxLatitude * unique.getIntParam(0) * 0.01f
|
||||
val upper = tileMap.maxLatitude * unique.getIntParam(1) * 0.01f
|
||||
@ -168,7 +176,9 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration
|
||||
private fun TileInfo.matchesWonderFilter(filter: String) = when (filter) {
|
||||
"Elevated" -> baseTerrain == Constants.mountain || isHill()
|
||||
"Water" -> isWater
|
||||
"Land" -> isLand
|
||||
"Hill" -> isHill()
|
||||
naturalWonder -> true
|
||||
in allTerrainFeatures -> getLastTerrain().name == filter
|
||||
else -> baseTerrain == filter
|
||||
}
|
||||
|
@ -92,14 +92,11 @@ enum class UniqueParameterType(val parameterName:String) {
|
||||
},
|
||||
/** Used by NaturalWonderGenerator, only tests base terrain or a feature */
|
||||
SimpleTerrain("simpleTerrain") {
|
||||
private val knownValues = setOf("Elevated", "Water", "Land")
|
||||
override fun getErrorSeverity(parameterText: String, ruleset: Ruleset):
|
||||
UniqueType.UniqueComplianceErrorSeverity? {
|
||||
if (parameterText == "Elevated") return null
|
||||
if (ruleset.terrains.values.any {
|
||||
it.name == parameterText &&
|
||||
(it.type.isBaseTerrain || it.type == TerrainType.TerrainFeature)
|
||||
})
|
||||
return null
|
||||
if (parameterText in knownValues) return null
|
||||
if (ruleset.terrains.containsKey(parameterText)) return null
|
||||
return UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific
|
||||
}
|
||||
},
|
||||
|
@ -103,14 +103,16 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) {
|
||||
CityStateMilitaryUnits("Provides military units every ≈[amount] turns", UniqueTarget.CityState), // No conditional support as of yet
|
||||
CityStateUniqueLuxury("Provides a unique luxury", UniqueTarget.CityState), // No conditional support as of yet
|
||||
|
||||
NaturalWonderNeighborCount("Must be adjacent to [amount] [terrainFilter] tiles", UniqueTarget.Terrain),
|
||||
NaturalWonderNeighborsRange("Must be adjacent to [amount] to [amount] [terrainFilter] tiles", UniqueTarget.Terrain),
|
||||
NaturalWonderLandmass("Must not be on [amount] largest landmasses", UniqueTarget.Terrain),
|
||||
NaturalWonderNeighborCount("Must be adjacent to [amount] [simpleTerrain] tiles", UniqueTarget.Terrain),
|
||||
NaturalWonderNeighborsRange("Must be adjacent to [amount] to [amount] [simpleTerrain] tiles", UniqueTarget.Terrain),
|
||||
NaturalWonderSmallerLandmass("Must not be on [amount] largest landmasses", UniqueTarget.Terrain),
|
||||
NaturalWonderLargerLandmass("Must be on [amount] largest landmasses", UniqueTarget.Terrain),
|
||||
NaturalWonderLatitude("Occurs on latitudes from [amount] to [amount] percent of distance equator to pole", UniqueTarget.Terrain),
|
||||
NaturalWonderGroups("Occurs in groups of [amount] to [amount] tiles", UniqueTarget.Terrain),
|
||||
NaturalWonderConvertNeighbors("Neighboring tiles will convert to [baseTerrain]", UniqueTarget.Terrain),
|
||||
|
||||
// The "Except [terrainFilter]" could theoretically be implemented with a conditional
|
||||
NaturalWonderConvertNeighborsExcept("Neighboring tiles except [terrainFilter] will convert to [baseTerrain]", UniqueTarget.Terrain),
|
||||
NaturalWonderConvertNeighborsExcept("Neighboring tiles except [baseTerrain] will convert to [baseTerrain]", UniqueTarget.Terrain),
|
||||
|
||||
TerrainGrantsPromotion("Grants [promotion] ([comment]) to adjacent [mapUnitFilter] units for the rest of the game", UniqueTarget.Terrain),
|
||||
|
||||
|
Reference in New Issue
Block a user