mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-30 22:58:50 +07:00
Map generation and start locations (#4588)
* cellular automata for mountains and hills * cellular automata for mountains and hills * tweaks * spawn location algorithm * consolation prizes * improve city state spawns * AI settle in place if possible * make heightmap generation mod agnostic
This commit is contained in:
@ -75,4 +75,7 @@ object Constants {
|
||||
const val barbarians = "Barbarians"
|
||||
const val spectator = "Spectator"
|
||||
const val custom = "Custom"
|
||||
|
||||
const val rising = "Rising"
|
||||
const val lowering = "Lowering"
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import com.unciv.models.metadata.GameParameters
|
||||
import com.unciv.models.ruleset.Era
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.ruleset.tile.ResourceType
|
||||
import com.unciv.ui.newgamescreen.GameSetupInfo
|
||||
import java.util.*
|
||||
import kotlin.NoSuchElementException
|
||||
@ -180,7 +181,24 @@ object GameStarter {
|
||||
val startingEra = gameInfo.gameParameters.startingEra
|
||||
var startingUnits: MutableList<String>
|
||||
var eraUnitReplacement: String
|
||||
|
||||
|
||||
// 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 cityStatesWithStartingLocations =
|
||||
gameInfo.tileMap.values
|
||||
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
|
||||
.map { it.improvement!!.replace("StartingLocation ", "") }
|
||||
val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in cityStatesWithStartingLocations) }
|
||||
val bestLocations = getStartingLocations(bestCivs, gameInfo.tileMap)
|
||||
for (civ in bestCivs)
|
||||
{
|
||||
if (civ.isCityState()) // Already have explicit starting locations
|
||||
continue
|
||||
|
||||
// Mark the best start locations so we remember them for the second pass
|
||||
bestLocations[civ]!!.improvement = "StartingLocation " + civ.civName
|
||||
}
|
||||
|
||||
val startingLocations = getStartingLocations(
|
||||
gameInfo.civilizations.filter { !it.isBarbarian() },
|
||||
gameInfo.tileMap)
|
||||
@ -188,10 +206,18 @@ object GameStarter {
|
||||
val settlerLikeUnits = ruleSet.units.filter {
|
||||
it.value.uniqueObjects.any { it.placeholderText == Constants.settlerUnique }
|
||||
}
|
||||
|
||||
|
||||
// no starting units for Barbarians and Spectators
|
||||
for (civ in gameInfo.civilizations.filter { !it.isBarbarian() && !it.isSpectator() }) {
|
||||
val startingLocation = startingLocations[civ]!!
|
||||
|
||||
if(civ.isMajorCiv() && startingLocation.getTileStartScore() < 45) {
|
||||
// An unusually bad spawning location
|
||||
addConsolationPrize(gameInfo, startingLocation, 45 - startingLocation.getTileStartScore().toInt())
|
||||
}
|
||||
if(civ.isCityState())
|
||||
addCityStateLuxury(gameInfo, startingLocation)
|
||||
|
||||
for (tile in startingLocation.getTilesInDistance(3))
|
||||
if (tile.improvement == Constants.ancientRuins)
|
||||
tile.improvement = null // Remove ancient ruins in immediate vicinity
|
||||
@ -296,27 +322,36 @@ object GameStarter {
|
||||
val tilesWithStartingLocations = tileMap.values
|
||||
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
|
||||
|
||||
val civsOrderedByAvailableLocations = civs.sortedBy { civ ->
|
||||
|
||||
val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start
|
||||
.sortedBy { civ ->
|
||||
when {
|
||||
tilesWithStartingLocations.any { it.improvement == "StartingLocation " + civ.civName } -> 1 // harshest requirements
|
||||
civ.nation.startBias.isNotEmpty() -> 2 // less harsh
|
||||
else -> 3
|
||||
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
|
||||
}
|
||||
|
||||
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 3 downTo 0) {
|
||||
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) {
|
||||
val freeTiles = landTilesInBigEnoughGroup
|
||||
.filter { vectorIsAtLeastNTilesAwayFromEdge(it.position, minimumDistanceBetweenStartingLocations, tileMap) }
|
||||
.filter { vectorIsAtLeastNTilesAwayFromEdge(it.position, (minimumDistanceBetweenStartingLocations * 2) /3, tileMap) }
|
||||
.toMutableList()
|
||||
|
||||
val startingLocations = HashMap<CivilizationInfo, TileInfo>()
|
||||
|
||||
for (civ in civsOrderedByAvailableLocations) {
|
||||
var startingLocation: TileInfo
|
||||
val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement == "StartingLocation " + civ.civName }
|
||||
var distanceToNext = minimumDistanceBetweenStartingLocations
|
||||
|
||||
if (presetStartingLocation != null) startingLocation = 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 { it.getTileStartScore() }
|
||||
|
||||
var preferredTiles = freeTiles.toList()
|
||||
|
||||
for (startBias in civ.nation.startBias) {
|
||||
@ -327,10 +362,10 @@ object GameStarter {
|
||||
else preferredTiles = preferredTiles.filter { it.matchesTerrainFilter(startBias) }
|
||||
}
|
||||
|
||||
startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.random() else freeTiles.random()
|
||||
startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.last() else freeTiles.last()
|
||||
}
|
||||
startingLocations[civ] = startingLocation
|
||||
freeTiles.removeAll(tileMap.getTilesInDistance(startingLocation.position, minimumDistanceBetweenStartingLocations))
|
||||
freeTiles.removeAll(tileMap.getTilesInDistance(startingLocation.position, distanceToNext))
|
||||
}
|
||||
if (startingLocations.size < civs.size) continue // let's try again with less minimum distance!
|
||||
|
||||
@ -339,6 +374,53 @@ object GameStarter {
|
||||
throw Exception("Didn't manage to get starting tiles even with distance of 1?")
|
||||
}
|
||||
|
||||
private fun addConsolationPrize(gameInfo: GameInfo, spawn: TileInfo, points: Int) {
|
||||
val relevantTiles = spawn.getTilesInDistanceRange(1..2).shuffled()
|
||||
var addedPoints = 0
|
||||
var addedBonuses = 0
|
||||
|
||||
for (tile in relevantTiles) {
|
||||
if (addedPoints >= points || addedBonuses >= 4) // At some point enough is enough
|
||||
break
|
||||
if (tile.resource != null || tile.baseTerrain == Constants.snow) // Snow is quite irredeemable
|
||||
continue
|
||||
|
||||
val bonusToAdd = gameInfo.ruleSet.tileResources.values
|
||||
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) && it.resourceType == ResourceType.Bonus }
|
||||
.randomOrNull()
|
||||
|
||||
if (bonusToAdd != null) {
|
||||
tile.resource = bonusToAdd.name
|
||||
addedPoints += (bonusToAdd.food + bonusToAdd.production + bonusToAdd.gold + 1).toInt() // +1 because resources can be improved
|
||||
addedBonuses++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCityStateLuxury(gameInfo: GameInfo, spawn: TileInfo) {
|
||||
// Every city state should have at least one luxury to trade
|
||||
val relevantTiles = spawn.getTilesInDistance(2).shuffled()
|
||||
|
||||
for (tile in relevantTiles) {
|
||||
if(tile.resource != null && tile.getTileResource().resourceType == ResourceType.Luxury)
|
||||
return // At least one luxury; all set
|
||||
}
|
||||
|
||||
for (tile in relevantTiles) {
|
||||
// Add a luxury to the first eligible tile
|
||||
if (tile.resource != null)
|
||||
continue
|
||||
|
||||
val luxuryToAdd = gameInfo.ruleSet.tileResources.values
|
||||
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) && it.resourceType == ResourceType.Luxury }
|
||||
.randomOrNull()
|
||||
if (luxuryToAdd != null) {
|
||||
tile.resource = luxuryToAdd.name
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun vectorIsAtLeastNTilesAwayFromEdge(vector: Vector2, n: Int, tileMap: TileMap): Boolean {
|
||||
// Since all maps are HEXAGONAL, the easiest way of checking if a tile is n steps away from the
|
||||
// edge is checking the distance to the CENTER POINT
|
||||
|
@ -146,6 +146,14 @@ object SpecificUnitAutomation {
|
||||
}
|
||||
|
||||
fun automateSettlerActions(unit: MapUnit) {
|
||||
if (unit.civInfo.gameInfo.turns == 0) { // Special case, we want AI to settle in place on turn 1.
|
||||
val foundCityAction = UnitActions.getFoundCityAction(unit, unit.getTile())
|
||||
if(foundCityAction?.action != null) {
|
||||
foundCityAction.action.invoke()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (unit.getTile().militaryUnit == null) return // Don't move until you're accompanied by a military unit
|
||||
|
||||
val tilesNearCities = unit.civInfo.gameInfo.getCities().asSequence()
|
||||
|
@ -278,6 +278,41 @@ open class TileInfo {
|
||||
return stats
|
||||
}
|
||||
|
||||
fun getTileStartScore(): Float {
|
||||
var sum = 0f
|
||||
for (tile in getTilesInDistance(2)) {
|
||||
if (tile == this)
|
||||
continue
|
||||
sum += tile.getTileStartYield()
|
||||
if (tile in neighbors)
|
||||
sum += tile.getTileStartYield()
|
||||
}
|
||||
|
||||
if (isHill())
|
||||
sum -= 2
|
||||
if (isAdjacentToRiver())
|
||||
sum += 2
|
||||
if (neighbors.any { it.baseTerrain == Constants.mountain })
|
||||
sum += 2
|
||||
|
||||
return sum
|
||||
}
|
||||
|
||||
private fun getTileStartYield(): Float {
|
||||
var stats = getBaseTerrain().clone()
|
||||
|
||||
for (terrainFeatureBase in getTerrainFeatures()) {
|
||||
if (terrainFeatureBase.overrideStats)
|
||||
stats = terrainFeatureBase.clone()
|
||||
else
|
||||
stats.add(terrainFeatureBase)
|
||||
}
|
||||
if (resource != null) stats.add(getTileResource())
|
||||
if (stats.production < 0) stats.production = 0f
|
||||
|
||||
return stats.food + stats.production + stats.gold
|
||||
}
|
||||
|
||||
fun getImprovementStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats {
|
||||
val stats = improvement.clone() // clones the stats of the improvement, not the improvement itself
|
||||
if (hasViewableResource(observingCiv) && getTileResource().improvement == improvement.name)
|
||||
|
@ -8,10 +8,7 @@ import com.unciv.models.Counter
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.tile.ResourceType
|
||||
import com.unciv.models.ruleset.tile.TerrainType
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sign
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
@ -179,6 +176,20 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
* [MapParameters.elevationExponent] favors high elevation
|
||||
*/
|
||||
private fun raiseMountainsAndHills(tileMap: TileMap) {
|
||||
val mountain = ruleset.terrains.values.filter { it.uniques.contains("Occurs in chains at high elevations") }.firstOrNull()?.name
|
||||
val hill = ruleset.terrains.values.filter { it.uniques.contains("Occurs in groups around high elevations") }.firstOrNull()?.name
|
||||
val flat = ruleset.terrains.values.filter { !it.impassable && it.type == TerrainType.Land && !it.uniques.contains("Rough Terrain") }.firstOrNull()?.name
|
||||
|
||||
if (flat == null) {
|
||||
println("Ruleset seems to contain no flat terrain - can't generate heightmap")
|
||||
return
|
||||
}
|
||||
|
||||
if (mountain != null)
|
||||
println("Mountainlike generation for " + mountain)
|
||||
if (hill != null)
|
||||
println("Hill-like generation for " + hill)
|
||||
|
||||
val elevationSeed = randomness.RNG.nextInt().toDouble()
|
||||
tileMap.setTransients(ruleset)
|
||||
for (tile in tileMap.values.filter { !it.isWater }) {
|
||||
@ -186,12 +197,102 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
elevation = abs(elevation).pow(1.0 - tileMap.mapParameters.elevationExponent.toDouble()) * elevation.sign
|
||||
|
||||
when {
|
||||
elevation <= 0.5 -> tile.baseTerrain = Constants.plains
|
||||
elevation <= 0.7 -> tile.terrainFeatures.add(Constants.hill)
|
||||
elevation <= 1.0 -> tile.baseTerrain = Constants.mountain
|
||||
elevation <= 0.5 -> tile.baseTerrain = flat
|
||||
elevation <= 0.7 && hill != null -> tile.terrainFeatures.add(hill)
|
||||
elevation <= 0.7 && hill == null -> tile.baseTerrain = flat // otherwise would be hills become mountains
|
||||
elevation <= 1.0 && mountain != null -> tile.baseTerrain = mountain
|
||||
}
|
||||
tile.setTerrainTransients()
|
||||
}
|
||||
|
||||
if (mountain != null)
|
||||
cellularMountainRanges(tileMap, mountain, hill, flat)
|
||||
if (hill != null)
|
||||
cellularHills(tileMap, mountain, hill)
|
||||
}
|
||||
|
||||
private fun cellularMountainRanges(tileMap: TileMap, mountain: String, hill: String?, flat: String) {
|
||||
val targetMountains = tileMap.values.count { it.baseTerrain == mountain } * 2
|
||||
|
||||
for (i in 1..5) {
|
||||
var totalMountains = tileMap.values.count { it.baseTerrain == mountain }
|
||||
|
||||
for (tile in tileMap.values.filter { !it.isWater }) {
|
||||
val adjacentMountains =
|
||||
tile.neighbors.count { it.baseTerrain == mountain }
|
||||
val adjacentImpassible =
|
||||
tile.neighbors.count { ruleset.terrains[it.baseTerrain]?.impassable == true }
|
||||
|
||||
if (adjacentMountains == 0 && tile.baseTerrain == mountain) {
|
||||
if (randomness.RNG.nextInt(until = 4) == 0)
|
||||
tile.terrainFeatures.add(Constants.lowering)
|
||||
} else if (adjacentMountains == 1) {
|
||||
if (randomness.RNG.nextInt(until = 10) == 0)
|
||||
tile.terrainFeatures.add(Constants.rising)
|
||||
} else if (adjacentImpassible == 3) {
|
||||
if (randomness.RNG.nextInt(until = 2) == 0)
|
||||
tile.terrainFeatures.add(Constants.lowering)
|
||||
} else if (adjacentImpassible > 3) {
|
||||
tile.terrainFeatures.add(Constants.lowering)
|
||||
}
|
||||
}
|
||||
|
||||
for (tile in tileMap.values.filter { !it.isWater }) {
|
||||
if (tile.terrainFeatures.remove(Constants.rising) && totalMountains < targetMountains) {
|
||||
if (hill != null)
|
||||
tile.terrainFeatures.remove(hill)
|
||||
tile.baseTerrain = mountain
|
||||
totalMountains++
|
||||
}
|
||||
if (tile.terrainFeatures.remove(Constants.lowering) && totalMountains > targetMountains * 0.5f) {
|
||||
if (tile.baseTerrain == mountain) {
|
||||
if (hill != null && !tile.terrainFeatures.contains(hill))
|
||||
tile.terrainFeatures.add(hill)
|
||||
totalMountains--
|
||||
}
|
||||
tile.baseTerrain = flat
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cellularHills(tileMap: TileMap, mountain: String?, hill: String) {
|
||||
val targetHills = tileMap.values.count { it.terrainFeatures.contains(hill) }
|
||||
|
||||
for (i in 1..5) {
|
||||
var totalHills = tileMap.values.count { it.terrainFeatures.contains(hill) }
|
||||
|
||||
for (tile in tileMap.values.filter { !it.isWater && (mountain == null || it.baseTerrain != mountain) }) {
|
||||
val adjacentMountains = if (mountain == null) 0 else
|
||||
tile.neighbors.count { it.baseTerrain == mountain }
|
||||
val adjacentHills =
|
||||
tile.neighbors.count { it.terrainFeatures.contains(hill) }
|
||||
|
||||
if (adjacentHills <= 1 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) {
|
||||
tile.terrainFeatures.add(Constants.lowering)
|
||||
} else if (adjacentHills > 3 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) {
|
||||
tile.terrainFeatures.add(Constants.lowering)
|
||||
} else if (adjacentHills + adjacentMountains in 2..3 && randomness.RNG.nextInt(until = 2) == 0) {
|
||||
tile.terrainFeatures.add(Constants.rising)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (tile in tileMap.values.filter { !it.isWater && (mountain == null || it.baseTerrain != mountain) }) {
|
||||
if (tile.terrainFeatures.remove(Constants.rising) && (totalHills <= targetHills || i == 1) ) {
|
||||
if (!tile.terrainFeatures.contains(hill)) {
|
||||
tile.terrainFeatures.add(hill)
|
||||
totalHills++
|
||||
}
|
||||
}
|
||||
if (tile.terrainFeatures.remove(Constants.lowering) && (totalHills >= targetHills * 0.9f || i == 1)) {
|
||||
if (tile.terrainFeatures.contains(hill)) {
|
||||
tile.terrainFeatures.remove(hill)
|
||||
totalHills--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user