Separated Landmass and Natural Wonder generation from the MapGenerator class - now it's much easier to understand what's going on where.

This commit is contained in:
Yair Morgenstern
2020-05-31 17:21:15 +03:00
parent 844cdcb821
commit 60aeebd3bb
7 changed files with 796 additions and 771 deletions

View File

@ -9,11 +9,10 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.logic.GameStarter import com.unciv.logic.GameStarter
import com.unciv.logic.map.MapGenerator import com.unciv.logic.map.mapgenerator.MapGenerator
import com.unciv.logic.map.MapParameters import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapSize import com.unciv.logic.map.MapSize
import com.unciv.logic.map.MapType import com.unciv.logic.map.MapType
import com.unciv.models.metadata.GameParameters
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.MultiplayerScreen import com.unciv.ui.MultiplayerScreen
import com.unciv.ui.mapeditor.EditorMapHolder import com.unciv.ui.mapeditor.EditorMapHolder

View File

@ -4,6 +4,7 @@ import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.* import com.unciv.logic.map.*
import com.unciv.logic.map.mapgenerator.MapGenerator
import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameParameters
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache

View File

@ -1,768 +0,0 @@
package com.unciv.logic.map
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.HexMath
import com.unciv.models.Counter
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.Terrain
import com.unciv.models.ruleset.tile.TerrainType
import kotlin.math.*
import kotlin.random.Random
class MapGenerator(val ruleset: Ruleset) {
fun generateMap(mapParameters: MapParameters, seed: Long = System.currentTimeMillis()): TileMap {
val mapRadius = mapParameters.size.radius
val mapType = mapParameters.type
val map: TileMap
if (mapParameters.shape == MapShape.rectangular) {
val size = HexMath.getEquivalentRectangularSize(mapRadius)
map = TileMap(size.x.toInt(), size.y.toInt(), ruleset)
}
else
map = TileMap(mapRadius, ruleset)
map.mapParameters = mapParameters
map.mapParameters.seed = seed
if (mapType == MapType.empty)
return map
seedRNG(seed)
generateLand(map,ruleset)
raiseMountainsAndHills(map)
applyHumidityAndTemperature(map)
spawnLakesAndCoasts(map)
spawnVegetation(map)
spawnRareFeatures(map)
spawnIce(map)
spreadResources(map)
spreadAncientRuins(map)
spawnNaturalWonders(map)
return map
}
private fun seedRNG(seed: Long) {
RNG = Random(seed)
println("RNG seeded with $seed")
}
private fun spawnLakesAndCoasts(map: TileMap) {
//define lakes
var waterTiles = map.values.filter { it.isWater }
val tilesInArea = ArrayList<TileInfo>()
val tilesToCheck = ArrayList<TileInfo>()
while (waterTiles.isNotEmpty()) {
val initialWaterTile = waterTiles.random(RNG)
tilesInArea += initialWaterTile
tilesToCheck += initialWaterTile
waterTiles -= initialWaterTile
// Floodfill to cluster water tiles
while (tilesToCheck.isNotEmpty()) {
val tileWeAreChecking = tilesToCheck.random(RNG)
for (vector in tileWeAreChecking.neighbors
.filter { !tilesInArea.contains(it) and waterTiles.contains(it) }) {
tilesInArea += vector
tilesToCheck += vector
waterTiles -= vector
}
tilesToCheck -= tileWeAreChecking
}
if (tilesInArea.size <= 10) {
for (tile in tilesInArea) {
tile.baseTerrain = Constants.lakes
tile.setTransients()
}
}
tilesInArea.clear()
}
//Coasts
for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) {
val coastLength = max(1, RNG.nextInt(max(1, map.mapParameters.maxCoastExtension)))
if (tile.getTilesInDistance(coastLength).any { it.isLand }) {
tile.baseTerrain = Constants.coast
tile.setTransients()
}
}
}
private fun spreadAncientRuins(map: TileMap) {
if(map.mapParameters.noRuins)
return
val suitableTiles = map.values.filter { it.isLand && !it.getBaseTerrain().impassable }
val locations = chooseSpreadOutLocations(suitableTiles.size/100,
suitableTiles, 10)
for(tile in locations)
tile.improvement = Constants.ancientRuins
}
private fun spreadResources(tileMap: TileMap) {
val distance = tileMap.mapParameters.size.radius
for (tile in tileMap.values)
if (tile.resource != null)
tile.resource = null
spreadStrategicResources(tileMap, distance)
spreadResources(tileMap, distance, ResourceType.Luxury)
spreadResources(tileMap, distance, ResourceType.Bonus)
}
//region natural-wonders
/*
https://gaming.stackexchange.com/questions/95095/do-natural-wonders-spawn-more-closely-to-city-states/96479
https://www.reddit.com/r/civ/comments/1jae5j/information_on_the_occurrence_of_natural_wonders/
*/
private fun spawnNaturalWonders(tileMap: TileMap) {
if (tileMap.mapParameters.noNaturalWonders)
return
val mapRadius = tileMap.mapParameters.size.radius
// number of Natural Wonders scales linearly with mapRadius as #wonders = mapRadius * 0.13133208 - 0.56128831
val numberToSpawn = round(mapRadius * 0.13133208f - 0.56128831f).toInt()
val toBeSpawned = ArrayList<Terrain>()
val allNaturalWonders = ruleset.terrains.values
.filter { it.type == TerrainType.NaturalWonder }.toMutableList()
while (allNaturalWonders.isNotEmpty() && toBeSpawned.size < numberToSpawn) {
val totalWeight = allNaturalWonders.map { it.weight }.sum().toFloat()
val random = RNG.nextDouble()
var sum = 0f
for (wonder in allNaturalWonders) {
sum += wonder.weight/totalWeight
if (random <= sum) {
toBeSpawned.add(wonder)
allNaturalWonders.remove(wonder)
break
}
}
}
println("Natural Wonders for this game: $toBeSpawned")
for (wonder in toBeSpawned) {
when (wonder.name) {
Constants.barringerCrater -> spawnBarringerCrater(tileMap)
Constants.mountFuji -> spawnMountFuji(tileMap)
Constants.grandMesa -> spawnGrandMesa(tileMap)
Constants.greatBarrierReef -> spawnGreatBarrierReef(tileMap)
Constants.krakatoa -> spawnKrakatoa(tileMap)
Constants.rockOfGibraltar -> spawnRockOfGibraltar(tileMap)
Constants.oldFaithful -> spawnOldFaithful(tileMap)
Constants.cerroDePotosi -> spawnCerroDePotosi(tileMap)
Constants.elDorado -> spawnElDorado(tileMap)
Constants.fountainOfYouth -> spawnFountainOfYouth(tileMap)
}
}
}
private fun trySpawnOnSuitableLocation(suitableLocations: List<TileInfo>, wonder: Terrain): TileInfo? {
if (suitableLocations.isNotEmpty()) {
val location = suitableLocations.random()
location.naturalWonder = wonder.name
location.baseTerrain = wonder.turnsInto!!
location.terrainFeature = null
return location
}
println("No suitable location for ${wonder.name}")
return null
}
/*
Must be in tundra or desert; cannot be adjacent to grassland; can be adjacent to a maximum
of 2 mountains and a maximum of 4 hills and mountains; avoids oceans; becomes mountain
*/
private fun spawnBarringerCrater(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.barringerCrater]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.grassland }
&& it.neighbors.count{ neighbor -> neighbor.getBaseTerrain().name == Constants.mountain } <= 2
&& it.neighbors.count{ neighbor -> neighbor.getBaseTerrain().name == Constants.mountain || neighbor.getBaseTerrain().name == Constants.hill} <= 4
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Mt. Fuji: Must be in grass or plains; cannot be adjacent to tundra, desert, marsh, or mountains;
can be adjacent to a maximum of 2 hills; becomes mountain
*/
private fun spawnMountFuji(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.mountFuji]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.tundra }
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.desert }
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain }
&& it.neighbors.none { neighbor -> neighbor.getLastTerrain().name == Constants.marsh }
&& it.neighbors.count{ neighbor -> neighbor.getBaseTerrain().name == Constants.hill } <= 2
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Grand Mesa: Must be in plains, desert, or tundra, and must be adjacent to at least 2 hills;
cannot be adjacent to grass; can be adjacent to a maximum of 2 mountains; avoids oceans; becomes mountain
*/
private fun spawnGrandMesa(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.grandMesa]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.count{ neighbor -> neighbor.getBaseTerrain().name == Constants.hill } >= 2
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.grassland }
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain } <= 2
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Great Barrier Reef: Specifics currently unknown;
Assumption: at least 1 neighbour not water; no tundra; at least 1 neighbour coast; becomes coast
*/
private fun spawnGreatBarrierReef(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.greatBarrierReef]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& abs(it.latitude) > tileMap.maxLatitude * 0.1
&& abs(it.latitude) < tileMap.maxLatitude * 0.7
&& it.neighbors.all {neighbor -> neighbor.isWater}
&& it.neighbors.any {neighbor ->
neighbor.resource == null && neighbor.improvement == null
&& wonder.occursOn!!.contains(neighbor.getLastTerrain().name)
&& neighbor.neighbors.all{ it.isWater } }
}
val location = trySpawnOnSuitableLocation(suitableLocations, wonder)
if (location != null) {
val location2 = location.neighbors
.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.all{ it.isWater } }
.toList().random()
location2.naturalWonder = wonder.name
location2.baseTerrain = wonder.turnsInto!!
location2.terrainFeature = null
}
}
/*
Krakatoa: Must spawn in the ocean next to at least 1 shallow water tile; cannot be adjacent
to ice; changes tiles around it to shallow water; mountain
*/
private fun spawnKrakatoa(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.krakatoa]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.any { neighbor -> neighbor.getBaseTerrain().name == Constants.coast }
&& it.neighbors.none { neighbor -> neighbor.getLastTerrain().name == Constants.ice}
}
val location = trySpawnOnSuitableLocation(suitableLocations, wonder)
if (location != null) {
for (tile in location.neighbors) {
if (tile.baseTerrain == Constants.coast) continue
tile.baseTerrain = Constants.coast
tile.terrainFeature = null
tile.resource = null
tile.improvement = null
}
}
}
/*
Rock of Gibraltar: Specifics currently unknown
Assumption: spawn on grassland, at least 1 coast and 1 mountain adjacent;
turn neighbours into coast)
*/
private fun spawnRockOfGibraltar(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.rockOfGibraltar]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.any { neighbor -> neighbor.getBaseTerrain().name == Constants.coast }
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain } == 1
}
val location = trySpawnOnSuitableLocation(suitableLocations, wonder)
if (location != null) {
for (tile in location.neighbors) {
if (tile.baseTerrain == Constants.coast) continue
if (tile.baseTerrain == Constants.mountain) continue
tile.baseTerrain = Constants.coast
tile.terrainFeature = null
tile.resource = null
tile.improvement = null
}
}
}
/*
Old Faithful: Must be adjacent to at least 3 hills and mountains; cannot be adjacent to
more than 4 mountains, and cannot be adjacent to more than 3 desert or 3 tundra tiles;
avoids oceans; becomes mountain
*/
private fun spawnOldFaithful(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.oldFaithful]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain } <= 4
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain ||
neighbor.getBaseTerrain().name == Constants.hill} >= 3
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.desert } <= 3
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.tundra } <= 3
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Cerro de Potosi: Must be adjacent to at least 1 hill; avoids oceans; becomes mountain
*/
private fun spawnCerroDePotosi(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.cerroDePotosi]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.any { neighbor -> neighbor.getBaseTerrain().name == Constants.hill }
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
El Dorado: Must be next to at least 1 jungle tile; avoids oceans; becomes flatland plains
*/
private fun spawnElDorado(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.elDorado]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.any { neighbor -> neighbor.getLastTerrain().name == Constants.jungle }
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Fountain of Youth: Avoids oceans; becomes flatland plains
*/
private fun spawnFountainOfYouth(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.fountainOfYouth]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name) }
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
//endregion
// Here, we need each specific resource to be spread over the map - it matters less if specific resources are near each other
private fun spreadStrategicResources(tileMap: TileMap, distance: Int) {
val strategicResources = ruleset.tileResources.values.filter { it.resourceType == ResourceType.Strategic }
// passable land tiles (no mountains, no wonders) without resources yet
val candidateTiles = tileMap.values.filter { it.resource == null && it.isLand && !it.getLastTerrain().impassable }
val totalNumberOfResources = candidateTiles.size * tileMap.mapParameters.resourceRichness
val resourcesPerType = (totalNumberOfResources/strategicResources.size).toInt()
for (resource in strategicResources) {
// remove the tiles where previous resources have been placed
val suitableTiles = candidateTiles
.filter { it.resource == null
&& resource.terrainsCanBeFoundOn.contains(it.getBaseTerrain().name)
&& (it.terrainFeature==null || ruleset.tileImprovements.containsKey("Remove "+it.terrainFeature)) }
val locations = chooseSpreadOutLocations(resourcesPerType, suitableTiles, distance)
for (location in locations) location.resource = resource.name
}
}
/**
* Spreads resources of type [resourceType] picking locations at [distance] from each other.
* [MapParameters.resourceRichness] used to control how many resources to spawn.
*/
private fun spreadResources(tileMap: TileMap, distance: Int, resourceType: ResourceType) {
val resourcesOfType = ruleset.tileResources.values.filter { it.resourceType == resourceType }
val suitableTiles = tileMap.values
.filter { it.resource == null && resourcesOfType.any { r -> r.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } }
val numberOfResources = tileMap.values.count { it.isLand && !it.getBaseTerrain().impassable } *
tileMap.mapParameters.resourceRichness
val locations = chooseSpreadOutLocations(numberOfResources.toInt(), suitableTiles, distance)
val resourceToNumber = Counter<String>()
for (tile in locations) {
val possibleResources = resourcesOfType
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) }
.map { it.name }
if (possibleResources.isEmpty()) continue
val resourceWithLeastAssignments = possibleResources.minBy { resourceToNumber[it]!! }!!
resourceToNumber.add(resourceWithLeastAssignments, 1)
tile.resource = resourceWithLeastAssignments
}
}
private fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List<TileInfo>, initialDistance: Int): ArrayList<TileInfo> {
for (distanceBetweenResources in initialDistance downTo 1) {
var availableTiles = suitableTiles.toList()
val chosenTiles = ArrayList<TileInfo>()
// If possible, we want to equalize the base terrains upon which
// the resources are found, so we save how many have been
// found for each base terrain and try to get one from the lowerst
val baseTerrainsToChosenTiles = HashMap<String, Int>()
for(tileInfo in availableTiles){
if(tileInfo.baseTerrain !in baseTerrainsToChosenTiles)
baseTerrainsToChosenTiles[tileInfo.baseTerrain] = 0
}
for (i in 1..numberOfResources) {
if (availableTiles.isEmpty()) break
val orderedKeys = baseTerrainsToChosenTiles.entries
.sortedBy { it.value }.map { it.key }
val firstKeyWithTilesLeft = orderedKeys
.first { availableTiles.any { tile -> tile.baseTerrain== it} }
val chosenTile = availableTiles.filter { it.baseTerrain==firstKeyWithTilesLeft }.random()
availableTiles = availableTiles.filter { it.aerialDistanceTo(chosenTile) > distanceBetweenResources }
chosenTiles.add(chosenTile)
baseTerrainsToChosenTiles[firstKeyWithTilesLeft] = baseTerrainsToChosenTiles[firstKeyWithTilesLeft]!!+1
}
// Either we got them all, or we're not going to get anything better
if (chosenTiles.size == numberOfResources || distanceBetweenResources == 1) return chosenTiles
}
throw Exception("Couldn't choose suitable tiles for $numberOfResources resources!")
}
/**
* [MapParameters.elevationExponent] favors high elevation
*/
private fun raiseMountainsAndHills(tileMap: TileMap) {
val elevationSeed = RNG.nextInt().toDouble()
tileMap.setTransients(ruleset)
for (tile in tileMap.values.filter { !it.isWater }) {
var elevation = getPerlinNoise(tile, elevationSeed, scale = 2.0)
elevation = abs(elevation).pow(1.0 - tileMap.mapParameters.elevationExponent.toDouble()) * elevation.sign
if (elevation <= 0.5) tile.baseTerrain = Constants.plains
else if (elevation <= 0.7) tile.baseTerrain = Constants.hill
else if (elevation <= 1.0) tile.baseTerrain = Constants.mountain
}
}
/**
* [MapParameters.tilesPerBiomeArea] to set biomes size
* [MapParameters.temperatureExtremeness] to favor very high and very low temperatures
*/
private fun applyHumidityAndTemperature(tileMap: TileMap) {
val humiditySeed = RNG.nextInt().toDouble()
val temperatureSeed = RNG.nextInt().toDouble()
tileMap.setTransients(ruleset)
val scale = tileMap.mapParameters.tilesPerBiomeArea.toDouble()
for (tile in tileMap.values) {
if (tile.isWater || tile.baseTerrain in arrayOf(Constants.mountain, Constants.hill))
continue
val humidity = (getPerlinNoise(tile, humiditySeed, scale = scale, nOctaves = 1) + 1.0) / 2.0
val randomTemperature = getPerlinNoise(tile, temperatureSeed, scale = scale, nOctaves = 1)
val latitudeTemperature = 1.0 - 2.0 * abs(tile.latitude) / tileMap.maxLatitude
var temperature = ((5.0 * latitudeTemperature + randomTemperature) / 6.0)
temperature = abs(temperature).pow(1.0 - tileMap.mapParameters.temperatureExtremeness) * temperature.sign
tile.baseTerrain = when {
temperature < -0.4 -> {
when {
humidity < 0.5 -> Constants.snow
else -> Constants.tundra
}
}
temperature < 0.8 -> {
when {
humidity < 0.5 -> Constants.plains
else -> Constants.grassland
}
}
temperature <= 1.0 -> {
when {
humidity < 0.7 -> Constants.desert
else -> Constants.plains
}
}
else -> {
println(temperature)
Constants.lakes
}
}
}
}
/**
* [MapParameters.vegetationOccurrance] is the threshold for vegetation spawn
*/
private fun spawnVegetation(tileMap: TileMap) {
val vegetationSeed = RNG.nextInt().toDouble()
val candidateTerrains = Constants.vegetation.flatMap{ ruleset.terrains[it]!!.occursOn!! }
for (tile in tileMap.values.asSequence().filter { it.baseTerrain in candidateTerrains && it.terrainFeature == null}) {
val vegetation = (getPerlinNoise(tile, vegetationSeed, scale = 3.0, nOctaves = 1) + 1.0) / 2.0
if (vegetation <= tileMap.mapParameters.vegetationRichness)
tile.terrainFeature = Constants.vegetation.filter { ruleset.terrains[it]!!.occursOn!!.contains(tile.baseTerrain) }.random(RNG)
}
}
/**
* [MapParameters.rareFeaturesProbability] is the probability of spawning a rare feature
*/
private fun spawnRareFeatures(tileMap: TileMap) {
val rareFeatures = ruleset.terrains.values.filter {
it.type == TerrainType.TerrainFeature &&
it.name !in Constants.vegetation &&
it.name != Constants.ice
}
for (tile in tileMap.values.asSequence().filter { it.terrainFeature == null }) {
if (RNG.nextDouble() <= tileMap.mapParameters.rareFeaturesRichness) {
val possibleFeatures = rareFeatures.filter { it.occursOn != null && it.occursOn.contains(tile.baseTerrain) }
if (possibleFeatures.any())
tile.terrainFeature = possibleFeatures.random(RNG).name
}
}
}
/**
* [MapParameters.temperatureExtremeness] as in [applyHumidityAndTemperature]
*/
private fun spawnIce(tileMap: TileMap) {
tileMap.setTransients(ruleset)
val temperatureSeed = RNG.nextInt().toDouble()
for (tile in tileMap.values) {
if (tile.baseTerrain !in Constants.sea || tile.terrainFeature != null)
continue
val randomTemperature = getPerlinNoise(tile, temperatureSeed, scale = tileMap.mapParameters.tilesPerBiomeArea.toDouble(), nOctaves = 1)
val latitudeTemperature = 1.0 - 2.0 * abs(tile.latitude) / tileMap.maxLatitude
var temperature = ((latitudeTemperature + randomTemperature) / 2.0)
temperature = abs(temperature).pow(1.0 - tileMap.mapParameters.temperatureExtremeness) * temperature.sign
if (temperature < -0.8)
tile.terrainFeature = Constants.ice
}
}
companion object MapLandmassGenerator {
var RNG = Random(42)
fun generateLand(tileMap: TileMap, ruleset: Ruleset) {
if(ruleset.terrains.values.none { it.type==TerrainType.Water }) {
for (tile in tileMap.values)
tile.baseTerrain = Constants.grassland
return
}
when (tileMap.mapParameters.type) {
MapType.pangaea -> createPangea(tileMap)
MapType.continents -> createTwoContinents(tileMap)
MapType.perlin -> createPerlin(tileMap)
MapType.archipelago -> createArchipelago(tileMap)
MapType.default -> generateLandCellularAutomata(tileMap)
}
}
private fun spawnLandOrWater(tile: TileInfo, elevation: Double, threshold: Double) {
when {
elevation < threshold -> tile.baseTerrain = Constants.ocean
else -> tile.baseTerrain = Constants.grassland
}
}
private fun smooth(tileMap: TileMap) {
for (tileInfo in tileMap.values) {
val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == Constants.grassland }
if (RNG.nextFloat() < 0.5f)
continue
if (numberOfLandNeighbors > 3)
tileInfo.baseTerrain = Constants.grassland
else if (numberOfLandNeighbors < 3)
tileInfo.baseTerrain = Constants.ocean
}
}
private fun createPerlin(tileMap: TileMap) {
val elevationSeed = RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = getPerlinNoise(tile, elevationSeed)
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
}
}
private fun createArchipelago(tileMap: TileMap) {
val elevationSeed = RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = getRidgedPerlinNoise(tile, elevationSeed)
spawnLandOrWater(tile, elevation, 0.25 + tileMap.mapParameters.waterThreshold.toDouble())
}
}
private fun createPangea(tileMap: TileMap) {
val elevationSeed = RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getCircularNoise(tile, tileMap) ) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
}
}
private fun createTwoContinents(tileMap: TileMap) {
val elevationSeed = RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getTwoContinentsTransform(tile, tileMap)) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
}
}
private fun getCircularNoise(tileInfo: TileInfo, tileMap: TileMap): Double {
val randomScale = RNG.nextDouble()
val distanceFactor = percentualDistanceToCenter(tileInfo, tileMap)
return min(0.3, 1.0 - (5.0 * distanceFactor * distanceFactor + randomScale) / 3.0)
}
private fun getTwoContinentsTransform(tileInfo: TileInfo, tileMap: TileMap): Double {
val randomScale = RNG.nextDouble()
val longitudeFactor = abs(tileInfo.longitude) / tileMap.maxLongitude
return min(0.2,-1.0 + (5.0 * longitudeFactor.pow(0.6f) + randomScale) / 3.0)
}
private fun percentualDistanceToCenter(tileInfo: TileInfo, tileMap: TileMap): Double {
val mapRadius = tileMap.mapParameters.size.radius
if (tileMap.mapParameters.shape == MapShape.hexagonal)
return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble()/mapRadius
else {
val size = HexMath.getEquivalentRectangularSize(mapRadius)
return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble() / HexMath.getDistance(Vector2.Zero, Vector2(size.x/2, size.y/2))
}
}
/**
* Generates a perlin noise channel combining multiple octaves
*
* [nOctaves] is the number of octaves
* [persistence] is the scaling factor of octave amplitudes
* [lacunarity] is the scaling factor of octave frequencies
* [scale] is the distance the noise is observed from
*/
private fun getPerlinNoise(tile: TileInfo, seed: Double,
nOctaves: Int = 6,
persistence: Double = 0.5,
lacunarity: Double = 2.0,
scale: Double = 10.0): Double {
val worldCoords = HexMath.hex2WorldCoords(tile.position)
return Perlin.noise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale)
}
/**
* Generates ridged perlin noise. As for parameters see [getPerlinNoise]
*/
private fun getRidgedPerlinNoise(tile: TileInfo, seed: Double,
nOctaves: Int = 10,
persistence: Double = 0.5,
lacunarity: Double = 2.0,
scale: Double = 15.0): Double {
val worldCoords = HexMath.hex2WorldCoords(tile.position)
return Perlin.ridgedNoise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale)
}
// region Cellular automata
private fun generateLandCellularAutomata(tileMap: TileMap) {
val mapRadius = tileMap.mapParameters.size.radius
val mapType = tileMap.mapParameters.type
val numSmooth = 4
//init
for (tile in tileMap.values) {
val terrainType = getInitialTerrainCellularAutomata(tile, tileMap.mapParameters)
if (terrainType == TerrainType.Land) tile.baseTerrain = Constants.grassland
else tile.baseTerrain = Constants.ocean
tile.setTransients()
}
//smooth
val grassland = Constants.grassland
val ocean = Constants.ocean
for (loop in 0..numSmooth) {
for (tileInfo in tileMap.values) {
//if (HexMath.getDistance(Vector2.Zero, tileInfo.position) < mapRadius) {
val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == grassland }
if (tileInfo.baseTerrain == grassland) { // land tile
if (numberOfLandNeighbors < 3)
tileInfo.baseTerrain = ocean
} else { // water tile
if (numberOfLandNeighbors > 3)
tileInfo.baseTerrain = grassland
}
/*} else {
tileInfo.baseTerrain = ocean
}*/
}
}
}
private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapParameters: MapParameters): TerrainType {
val mapRadius = mapParameters.size.radius
// default
if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) {
if (RNG.nextDouble() < 0.1) return TerrainType.Land else return TerrainType.Water
}
if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.85f * mapRadius) {
if (RNG.nextDouble() < 0.2) return TerrainType.Land else return TerrainType.Water
}
if (RNG.nextDouble() < 0.55) return TerrainType.Land else return TerrainType.Water
}
// endregion
}
}
class RiverGenerator(){
public class RiverCoordinate(val position: Vector2, val bottomRightOrLeft:BottomRightOrLeft){
enum class BottomRightOrLeft{
BottomLeft, BottomRight
}
fun getAdjacentPositions(): Sequence<RiverCoordinate> {
// What's nice is that adjacents are always the OPPOSITE in terms of right-left - rights are adjacent to only lefts, and vice-versa
// This means that a lot of obviously-wrong assignments are simple to spot
if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft) {
return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomRight), // same tile, other side
RiverCoordinate(position.cpy().add(1f,0f), BottomRightOrLeft.BottomRight), // tile to MY top-left, take its bottom right corner
RiverCoordinate(position.cpy().add(0f,-1f), BottomRightOrLeft.BottomRight) // Tile to MY bottom-left, take its bottom right
)
} else {
return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomLeft), // same tile, other side
RiverCoordinate(position.cpy().add(0f,1f), BottomRightOrLeft.BottomLeft), // tile to MY top-right, take its bottom left
RiverCoordinate(position.cpy().add(-1f,0f), BottomRightOrLeft.BottomLeft) // tile to MY bottom-right, take its bottom left
)
}
}
}
}

View File

@ -0,0 +1,359 @@
package com.unciv.logic.map.mapgenerator
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.HexMath
import com.unciv.logic.map.*
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.random.Random
class MapGenerator(val ruleset: Ruleset) {
var randomness = MapGenerationRandomness()
fun generateMap(mapParameters: MapParameters, seed: Long = System.currentTimeMillis()): TileMap {
val mapRadius = mapParameters.size.radius
val mapType = mapParameters.type
val map: TileMap
if (mapParameters.shape == MapShape.rectangular) {
val size = HexMath.getEquivalentRectangularSize(mapRadius)
map = TileMap(size.x.toInt(), size.y.toInt(), ruleset)
}
else
map = TileMap(mapRadius, ruleset)
map.mapParameters = mapParameters
map.mapParameters.seed = seed
if (mapType == MapType.empty)
return map
seedRNG(seed)
MapLandmassGenerator(randomness).generateLand(map,ruleset)
raiseMountainsAndHills(map)
applyHumidityAndTemperature(map)
spawnLakesAndCoasts(map)
spawnVegetation(map)
spawnRareFeatures(map)
spawnIce(map)
spreadResources(map)
spreadAncientRuins(map)
NaturalWonderGenerator(ruleset).spawnNaturalWonders(map, randomness)
return map
}
private fun seedRNG(seed: Long) {
randomness.RNG = Random(seed)
println("RNG seeded with $seed")
}
private fun spawnLakesAndCoasts(map: TileMap) {
//define lakes
var waterTiles = map.values.filter { it.isWater }
val tilesInArea = ArrayList<TileInfo>()
val tilesToCheck = ArrayList<TileInfo>()
while (waterTiles.isNotEmpty()) {
val initialWaterTile = waterTiles.random(randomness.RNG)
tilesInArea += initialWaterTile
tilesToCheck += initialWaterTile
waterTiles -= initialWaterTile
// Floodfill to cluster water tiles
while (tilesToCheck.isNotEmpty()) {
val tileWeAreChecking = tilesToCheck.random(randomness.RNG)
for (vector in tileWeAreChecking.neighbors
.filter { !tilesInArea.contains(it) and waterTiles.contains(it) }) {
tilesInArea += vector
tilesToCheck += vector
waterTiles -= vector
}
tilesToCheck -= tileWeAreChecking
}
if (tilesInArea.size <= 10) {
for (tile in tilesInArea) {
tile.baseTerrain = Constants.lakes
tile.setTransients()
}
}
tilesInArea.clear()
}
//Coasts
for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) {
val coastLength = max(1, randomness.RNG.nextInt(max(1, map.mapParameters.maxCoastExtension)))
if (tile.getTilesInDistance(coastLength).any { it.isLand }) {
tile.baseTerrain = Constants.coast
tile.setTransients()
}
}
}
private fun spreadAncientRuins(map: TileMap) {
if(map.mapParameters.noRuins)
return
val suitableTiles = map.values.filter { it.isLand && !it.getBaseTerrain().impassable }
val locations = chooseSpreadOutLocations(suitableTiles.size/100,
suitableTiles, 10)
for(tile in locations)
tile.improvement = Constants.ancientRuins
}
private fun spreadResources(tileMap: TileMap) {
val distance = tileMap.mapParameters.size.radius
for (tile in tileMap.values)
if (tile.resource != null)
tile.resource = null
spreadStrategicResources(tileMap, distance)
spreadResources(tileMap, distance, ResourceType.Luxury)
spreadResources(tileMap, distance, ResourceType.Bonus)
}
// Here, we need each specific resource to be spread over the map - it matters less if specific resources are near each other
private fun spreadStrategicResources(tileMap: TileMap, distance: Int) {
val strategicResources = ruleset.tileResources.values.filter { it.resourceType == ResourceType.Strategic }
// passable land tiles (no mountains, no wonders) without resources yet
val candidateTiles = tileMap.values.filter { it.resource == null && it.isLand && !it.getLastTerrain().impassable }
val totalNumberOfResources = candidateTiles.size * tileMap.mapParameters.resourceRichness
val resourcesPerType = (totalNumberOfResources/strategicResources.size).toInt()
for (resource in strategicResources) {
// remove the tiles where previous resources have been placed
val suitableTiles = candidateTiles
.filter { it.resource == null
&& resource.terrainsCanBeFoundOn.contains(it.getBaseTerrain().name)
&& (it.terrainFeature==null || ruleset.tileImprovements.containsKey("Remove "+it.terrainFeature)) }
val locations = chooseSpreadOutLocations(resourcesPerType, suitableTiles, distance)
for (location in locations) location.resource = resource.name
}
}
/**
* Spreads resources of type [resourceType] picking locations at [distance] from each other.
* [MapParameters.resourceRichness] used to control how many resources to spawn.
*/
private fun spreadResources(tileMap: TileMap, distance: Int, resourceType: ResourceType) {
val resourcesOfType = ruleset.tileResources.values.filter { it.resourceType == resourceType }
val suitableTiles = tileMap.values
.filter { it.resource == null && resourcesOfType.any { r -> r.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } }
val numberOfResources = tileMap.values.count { it.isLand && !it.getBaseTerrain().impassable } *
tileMap.mapParameters.resourceRichness
val locations = chooseSpreadOutLocations(numberOfResources.toInt(), suitableTiles, distance)
val resourceToNumber = Counter<String>()
for (tile in locations) {
val possibleResources = resourcesOfType
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) }
.map { it.name }
if (possibleResources.isEmpty()) continue
val resourceWithLeastAssignments = possibleResources.minBy { resourceToNumber[it]!! }!!
resourceToNumber.add(resourceWithLeastAssignments, 1)
tile.resource = resourceWithLeastAssignments
}
}
private fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List<TileInfo>, initialDistance: Int): ArrayList<TileInfo> {
for (distanceBetweenResources in initialDistance downTo 1) {
var availableTiles = suitableTiles.toList()
val chosenTiles = ArrayList<TileInfo>()
// If possible, we want to equalize the base terrains upon which
// the resources are found, so we save how many have been
// found for each base terrain and try to get one from the lowerst
val baseTerrainsToChosenTiles = HashMap<String, Int>()
for(tileInfo in availableTiles){
if(tileInfo.baseTerrain !in baseTerrainsToChosenTiles)
baseTerrainsToChosenTiles[tileInfo.baseTerrain] = 0
}
for (i in 1..numberOfResources) {
if (availableTiles.isEmpty()) break
val orderedKeys = baseTerrainsToChosenTiles.entries
.sortedBy { it.value }.map { it.key }
val firstKeyWithTilesLeft = orderedKeys
.first { availableTiles.any { tile -> tile.baseTerrain== it} }
val chosenTile = availableTiles.filter { it.baseTerrain==firstKeyWithTilesLeft }.random()
availableTiles = availableTiles.filter { it.aerialDistanceTo(chosenTile) > distanceBetweenResources }
chosenTiles.add(chosenTile)
baseTerrainsToChosenTiles[firstKeyWithTilesLeft] = baseTerrainsToChosenTiles[firstKeyWithTilesLeft]!!+1
}
// Either we got them all, or we're not going to get anything better
if (chosenTiles.size == numberOfResources || distanceBetweenResources == 1) return chosenTiles
}
throw Exception("Couldn't choose suitable tiles for $numberOfResources resources!")
}
/**
* [MapParameters.elevationExponent] favors high elevation
*/
private fun raiseMountainsAndHills(tileMap: TileMap) {
val elevationSeed = randomness.RNG.nextInt().toDouble()
tileMap.setTransients(ruleset)
for (tile in tileMap.values.filter { !it.isWater }) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed, scale = 2.0)
elevation = abs(elevation).pow(1.0 - tileMap.mapParameters.elevationExponent.toDouble()) * elevation.sign
if (elevation <= 0.5) tile.baseTerrain = Constants.plains
else if (elevation <= 0.7) tile.baseTerrain = Constants.hill
else if (elevation <= 1.0) tile.baseTerrain = Constants.mountain
}
}
/**
* [MapParameters.tilesPerBiomeArea] to set biomes size
* [MapParameters.temperatureExtremeness] to favor very high and very low temperatures
*/
private fun applyHumidityAndTemperature(tileMap: TileMap) {
val humiditySeed = randomness.RNG.nextInt().toDouble()
val temperatureSeed = randomness.RNG.nextInt().toDouble()
tileMap.setTransients(ruleset)
val scale = tileMap.mapParameters.tilesPerBiomeArea.toDouble()
for (tile in tileMap.values) {
if (tile.isWater || tile.baseTerrain in arrayOf(Constants.mountain, Constants.hill))
continue
val humidity = (randomness.getPerlinNoise(tile, humiditySeed, scale = scale, nOctaves = 1) + 1.0) / 2.0
val randomTemperature = randomness.getPerlinNoise(tile, temperatureSeed, scale = scale, nOctaves = 1)
val latitudeTemperature = 1.0 - 2.0 * abs(tile.latitude) / tileMap.maxLatitude
var temperature = ((5.0 * latitudeTemperature + randomTemperature) / 6.0)
temperature = abs(temperature).pow(1.0 - tileMap.mapParameters.temperatureExtremeness) * temperature.sign
tile.baseTerrain = when {
temperature < -0.4 -> {
if (humidity < 0.5) Constants.snow
else Constants.tundra
}
temperature < 0.8 -> {
if (humidity < 0.5) Constants.plains
else Constants.grassland
}
temperature <= 1.0 -> {
if (humidity < 0.7) Constants.desert
else Constants.plains
}
else -> {
println(temperature)
Constants.lakes
}
}
}
}
/**
* [MapParameters.vegetationOccurrance] is the threshold for vegetation spawn
*/
private fun spawnVegetation(tileMap: TileMap) {
val vegetationSeed = randomness.RNG.nextInt().toDouble()
val candidateTerrains = Constants.vegetation.flatMap{ ruleset.terrains[it]!!.occursOn!! }
for (tile in tileMap.values.asSequence().filter { it.baseTerrain in candidateTerrains && it.terrainFeature == null}) {
val vegetation = (randomness.getPerlinNoise(tile, vegetationSeed, scale = 3.0, nOctaves = 1) + 1.0) / 2.0
if (vegetation <= tileMap.mapParameters.vegetationRichness)
tile.terrainFeature = Constants.vegetation.filter { ruleset.terrains[it]!!.occursOn!!.contains(tile.baseTerrain) }.random(randomness.RNG)
}
}
/**
* [MapParameters.rareFeaturesProbability] is the probability of spawning a rare feature
*/
private fun spawnRareFeatures(tileMap: TileMap) {
val rareFeatures = ruleset.terrains.values.filter {
it.type == TerrainType.TerrainFeature &&
it.name !in Constants.vegetation &&
it.name != Constants.ice
}
for (tile in tileMap.values.asSequence().filter { it.terrainFeature == null }) {
if (randomness.RNG.nextDouble() <= tileMap.mapParameters.rareFeaturesRichness) {
val possibleFeatures = rareFeatures.filter { it.occursOn != null && it.occursOn.contains(tile.baseTerrain) }
if (possibleFeatures.any())
tile.terrainFeature = possibleFeatures.random(randomness.RNG).name
}
}
}
/**
* [MapParameters.temperatureExtremeness] as in [applyHumidityAndTemperature]
*/
private fun spawnIce(tileMap: TileMap) {
tileMap.setTransients(ruleset)
val temperatureSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
if (tile.baseTerrain !in Constants.sea || tile.terrainFeature != null)
continue
val randomTemperature = randomness.getPerlinNoise(tile, temperatureSeed, scale = tileMap.mapParameters.tilesPerBiomeArea.toDouble(), nOctaves = 1)
val latitudeTemperature = 1.0 - 2.0 * abs(tile.latitude) / tileMap.maxLatitude
var temperature = ((latitudeTemperature + randomTemperature) / 2.0)
temperature = abs(temperature).pow(1.0 - tileMap.mapParameters.temperatureExtremeness) * temperature.sign
if (temperature < -0.8)
tile.terrainFeature = Constants.ice
}
}
}
class MapGenerationRandomness{
var RNG = Random(42)
/**
* Generates a perlin noise channel combining multiple octaves
*
* [nOctaves] is the number of octaves
* [persistence] is the scaling factor of octave amplitudes
* [lacunarity] is the scaling factor of octave frequencies
* [scale] is the distance the noise is observed from
*/
fun getPerlinNoise(tile: TileInfo, seed: Double,
nOctaves: Int = 6,
persistence: Double = 0.5,
lacunarity: Double = 2.0,
scale: Double = 10.0): Double {
val worldCoords = HexMath.hex2WorldCoords(tile.position)
return Perlin.noise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale)
}
}
class RiverGenerator(){
public class RiverCoordinate(val position: Vector2, val bottomRightOrLeft: BottomRightOrLeft){
enum class BottomRightOrLeft{
BottomLeft, BottomRight
}
fun getAdjacentPositions(): Sequence<RiverCoordinate> {
// What's nice is that adjacents are always the OPPOSITE in terms of right-left - rights are adjacent to only lefts, and vice-versa
// This means that a lot of obviously-wrong assignments are simple to spot
if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft) {
return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomRight), // same tile, other side
RiverCoordinate(position.cpy().add(1f, 0f), BottomRightOrLeft.BottomRight), // tile to MY top-left, take its bottom right corner
RiverCoordinate(position.cpy().add(0f, -1f), BottomRightOrLeft.BottomRight) // Tile to MY bottom-left, take its bottom right
)
} else {
return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomLeft), // same tile, other side
RiverCoordinate(position.cpy().add(0f, 1f), BottomRightOrLeft.BottomLeft), // tile to MY top-right, take its bottom left
RiverCoordinate(position.cpy().add(-1f, 0f), BottomRightOrLeft.BottomLeft) // tile to MY bottom-right, take its bottom left
)
}
}
}
}

View File

@ -0,0 +1,171 @@
package com.unciv.logic.map.mapgenerator
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.HexMath
import com.unciv.logic.map.*
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.TerrainType
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.pow
class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
fun generateLand(tileMap: TileMap, ruleset: Ruleset) {
if(ruleset.terrains.values.none { it.type== TerrainType.Water }) {
for (tile in tileMap.values)
tile.baseTerrain = Constants.grassland
return
}
when (tileMap.mapParameters.type) {
MapType.pangaea -> createPangea(tileMap)
MapType.continents -> createTwoContinents(tileMap)
MapType.perlin -> createPerlin(tileMap)
MapType.archipelago -> createArchipelago(tileMap)
MapType.default -> generateLandCellularAutomata(tileMap)
}
}
private fun spawnLandOrWater(tile: TileInfo, elevation: Double, threshold: Double) {
when {
elevation < threshold -> tile.baseTerrain = Constants.ocean
else -> tile.baseTerrain = Constants.grassland
}
}
private fun smooth(tileMap: TileMap, randomness: MapGenerationRandomness) {
for (tileInfo in tileMap.values) {
val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == Constants.grassland }
if (randomness.RNG.nextFloat() < 0.5f)
continue
if (numberOfLandNeighbors > 3)
tileInfo.baseTerrain = Constants.grassland
else if (numberOfLandNeighbors < 3)
tileInfo.baseTerrain = Constants.ocean
}
}
private fun createPerlin(tileMap: TileMap) {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
}
}
private fun createArchipelago(tileMap: TileMap) {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = getRidgedPerlinNoise(tile, elevationSeed)
spawnLandOrWater(tile, elevation, 0.25 + tileMap.mapParameters.waterThreshold.toDouble())
}
}
private fun createPangea(tileMap: TileMap) {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getCircularNoise(tile, tileMap) ) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
}
}
private fun createTwoContinents(tileMap: TileMap) {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getTwoContinentsTransform(tile, tileMap)) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
}
}
private fun getCircularNoise(tileInfo: TileInfo, tileMap: TileMap): Double {
val randomScale = randomness.RNG.nextDouble()
val distanceFactor = percentualDistanceToCenter(tileInfo, tileMap)
return min(0.3, 1.0 - (5.0 * distanceFactor * distanceFactor + randomScale) / 3.0)
}
private fun getTwoContinentsTransform(tileInfo: TileInfo, tileMap: TileMap): Double {
val randomScale = randomness.RNG.nextDouble()
val longitudeFactor = abs(tileInfo.longitude) / tileMap.maxLongitude
return min(0.2, -1.0 + (5.0 * longitudeFactor.pow(0.6f) + randomScale) / 3.0)
}
private fun percentualDistanceToCenter(tileInfo: TileInfo, tileMap: TileMap): Double {
val mapRadius = tileMap.mapParameters.size.radius
if (tileMap.mapParameters.shape == MapShape.hexagonal)
return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble()/mapRadius
else {
val size = HexMath.getEquivalentRectangularSize(mapRadius)
return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble() / HexMath.getDistance(Vector2.Zero, Vector2(size.x / 2, size.y / 2))
}
}
/**
* Generates ridged perlin noise. As for parameters see [getPerlinNoise]
*/
private fun getRidgedPerlinNoise(tile: TileInfo, seed: Double,
nOctaves: Int = 10,
persistence: Double = 0.5,
lacunarity: Double = 2.0,
scale: Double = 15.0): Double {
val worldCoords = HexMath.hex2WorldCoords(tile.position)
return Perlin.ridgedNoise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale)
}
// region Cellular automata
private fun generateLandCellularAutomata(tileMap: TileMap) {
val mapRadius = tileMap.mapParameters.size.radius
val mapType = tileMap.mapParameters.type
val numSmooth = 4
//init
for (tile in tileMap.values) {
val terrainType = getInitialTerrainCellularAutomata(tile, tileMap.mapParameters)
if (terrainType == TerrainType.Land) tile.baseTerrain = Constants.grassland
else tile.baseTerrain = Constants.ocean
tile.setTransients()
}
//smooth
val grassland = Constants.grassland
val ocean = Constants.ocean
for (loop in 0..numSmooth) {
for (tileInfo in tileMap.values) {
//if (HexMath.getDistance(Vector2.Zero, tileInfo.position) < mapRadius) {
val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == grassland }
if (tileInfo.baseTerrain == grassland) { // land tile
if (numberOfLandNeighbors < 3)
tileInfo.baseTerrain = ocean
} else { // water tile
if (numberOfLandNeighbors > 3)
tileInfo.baseTerrain = grassland
}
/*} else {
tileInfo.baseTerrain = ocean
}*/
}
}
}
private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapParameters: MapParameters): TerrainType {
val mapRadius = mapParameters.size.radius
// default
if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) {
if (randomness.RNG.nextDouble() < 0.1) return TerrainType.Land else return TerrainType.Water
}
if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.85f * mapRadius) {
if (randomness.RNG.nextDouble() < 0.2) return TerrainType.Land else return TerrainType.Water
}
if (randomness.RNG.nextDouble() < 0.55) return TerrainType.Land else return TerrainType.Water
}
// endregion
}

View File

@ -0,0 +1,263 @@
package com.unciv.logic.map.mapgenerator
import com.unciv.Constants
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.Terrain
import com.unciv.models.ruleset.tile.TerrainType
import kotlin.math.abs
import kotlin.math.round
class NaturalWonderGenerator(val ruleset: Ruleset){
/*
https://gaming.stackexchange.com/questions/95095/do-natural-wonders-spawn-more-closely-to-city-states/96479
https://www.reddit.com/r/civ/comments/1jae5j/information_on_the_occurrence_of_natural_wonders/
*/
fun spawnNaturalWonders(tileMap: TileMap, randomness: MapGenerationRandomness) {
if (tileMap.mapParameters.noNaturalWonders)
return
val mapRadius = tileMap.mapParameters.size.radius
// number of Natural Wonders scales linearly with mapRadius as #wonders = mapRadius * 0.13133208 - 0.56128831
val numberToSpawn = round(mapRadius * 0.13133208f - 0.56128831f).toInt()
val toBeSpawned = ArrayList<Terrain>()
val allNaturalWonders = ruleset.terrains.values
.filter { it.type == TerrainType.NaturalWonder }.toMutableList()
while (allNaturalWonders.isNotEmpty() && toBeSpawned.size < numberToSpawn) {
val totalWeight = allNaturalWonders.map { it.weight }.sum().toFloat()
val random = randomness.RNG.nextDouble()
var sum = 0f
for (wonder in allNaturalWonders) {
sum += wonder.weight/totalWeight
if (random <= sum) {
toBeSpawned.add(wonder)
allNaturalWonders.remove(wonder)
break
}
}
}
println("Natural Wonders for this game: $toBeSpawned")
for (wonder in toBeSpawned) {
when (wonder.name) {
Constants.barringerCrater -> spawnBarringerCrater(tileMap)
Constants.mountFuji -> spawnMountFuji(tileMap)
Constants.grandMesa -> spawnGrandMesa(tileMap)
Constants.greatBarrierReef -> spawnGreatBarrierReef(tileMap)
Constants.krakatoa -> spawnKrakatoa(tileMap)
Constants.rockOfGibraltar -> spawnRockOfGibraltar(tileMap)
Constants.oldFaithful -> spawnOldFaithful(tileMap)
Constants.cerroDePotosi -> spawnCerroDePotosi(tileMap)
Constants.elDorado -> spawnElDorado(tileMap)
Constants.fountainOfYouth -> spawnFountainOfYouth(tileMap)
}
}
}
private fun trySpawnOnSuitableLocation(suitableLocations: List<TileInfo>, wonder: Terrain): TileInfo? {
if (suitableLocations.isNotEmpty()) {
val location = suitableLocations.random()
location.naturalWonder = wonder.name
location.baseTerrain = wonder.turnsInto!!
location.terrainFeature = null
return location
}
println("No suitable location for ${wonder.name}")
return null
}
/*
Must be in tundra or desert; cannot be adjacent to grassland; can be adjacent to a maximum
of 2 mountains and a maximum of 4 hills and mountains; avoids oceans; becomes mountain
*/
private fun spawnBarringerCrater(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.barringerCrater]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.grassland }
&& it.neighbors.count{ neighbor -> neighbor.getBaseTerrain().name == Constants.mountain } <= 2
&& it.neighbors.count{ neighbor -> neighbor.getBaseTerrain().name == Constants.mountain || neighbor.getBaseTerrain().name == Constants.hill } <= 4
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Mt. Fuji: Must be in grass or plains; cannot be adjacent to tundra, desert, marsh, or mountains;
can be adjacent to a maximum of 2 hills; becomes mountain
*/
private fun spawnMountFuji(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.mountFuji]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.tundra }
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.desert }
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain }
&& it.neighbors.none { neighbor -> neighbor.getLastTerrain().name == Constants.marsh }
&& it.neighbors.count{ neighbor -> neighbor.getBaseTerrain().name == Constants.hill } <= 2
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Grand Mesa: Must be in plains, desert, or tundra, and must be adjacent to at least 2 hills;
cannot be adjacent to grass; can be adjacent to a maximum of 2 mountains; avoids oceans; becomes mountain
*/
private fun spawnGrandMesa(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.grandMesa]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.count{ neighbor -> neighbor.getBaseTerrain().name == Constants.hill } >= 2
&& it.neighbors.none { neighbor -> neighbor.getBaseTerrain().name == Constants.grassland }
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain } <= 2
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Great Barrier Reef: Specifics currently unknown;
Assumption: at least 1 neighbour not water; no tundra; at least 1 neighbour coast; becomes coast
*/
private fun spawnGreatBarrierReef(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.greatBarrierReef]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& abs(it.latitude) > tileMap.maxLatitude * 0.1
&& abs(it.latitude) < tileMap.maxLatitude * 0.7
&& it.neighbors.all {neighbor -> neighbor.isWater}
&& it.neighbors.any {neighbor ->
neighbor.resource == null && neighbor.improvement == null
&& wonder.occursOn!!.contains(neighbor.getLastTerrain().name)
&& neighbor.neighbors.all{ it.isWater } }
}
val location = trySpawnOnSuitableLocation(suitableLocations, wonder)
if (location != null) {
val location2 = location.neighbors
.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.all{ it.isWater } }
.toList().random()
location2.naturalWonder = wonder.name
location2.baseTerrain = wonder.turnsInto!!
location2.terrainFeature = null
}
}
/*
Krakatoa: Must spawn in the ocean next to at least 1 shallow water tile; cannot be adjacent
to ice; changes tiles around it to shallow water; mountain
*/
private fun spawnKrakatoa(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.krakatoa]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.any { neighbor -> neighbor.getBaseTerrain().name == Constants.coast }
&& it.neighbors.none { neighbor -> neighbor.getLastTerrain().name == Constants.ice }
}
val location = trySpawnOnSuitableLocation(suitableLocations, wonder)
if (location != null) {
for (tile in location.neighbors) {
if (tile.baseTerrain == Constants.coast) continue
tile.baseTerrain = Constants.coast
tile.terrainFeature = null
tile.resource = null
tile.improvement = null
}
}
}
/*
Rock of Gibraltar: Specifics currently unknown
Assumption: spawn on grassland, at least 1 coast and 1 mountain adjacent;
turn neighbours into coast)
*/
private fun spawnRockOfGibraltar(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.rockOfGibraltar]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.any { neighbor -> neighbor.getBaseTerrain().name == Constants.coast }
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain } == 1
}
val location = trySpawnOnSuitableLocation(suitableLocations, wonder)
if (location != null) {
for (tile in location.neighbors) {
if (tile.baseTerrain == Constants.coast) continue
if (tile.baseTerrain == Constants.mountain) continue
tile.baseTerrain = Constants.coast
tile.terrainFeature = null
tile.resource = null
tile.improvement = null
}
}
}
/*
Old Faithful: Must be adjacent to at least 3 hills and mountains; cannot be adjacent to
more than 4 mountains, and cannot be adjacent to more than 3 desert or 3 tundra tiles;
avoids oceans; becomes mountain
*/
private fun spawnOldFaithful(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.oldFaithful]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain } <= 4
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.mountain ||
neighbor.getBaseTerrain().name == Constants.hill
} >= 3
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.desert } <= 3
&& it.neighbors.count { neighbor -> neighbor.getBaseTerrain().name == Constants.tundra } <= 3
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Cerro de Potosi: Must be adjacent to at least 1 hill; avoids oceans; becomes mountain
*/
private fun spawnCerroDePotosi(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.cerroDePotosi]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.any { neighbor -> neighbor.getBaseTerrain().name == Constants.hill }
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
El Dorado: Must be next to at least 1 jungle tile; avoids oceans; becomes flatland plains
*/
private fun spawnElDorado(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.elDorado]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name)
&& it.neighbors.any { neighbor -> neighbor.getLastTerrain().name == Constants.jungle }
}
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
/*
Fountain of Youth: Avoids oceans; becomes flatland plains
*/
private fun spawnFountainOfYouth(tileMap: TileMap) {
val wonder = ruleset.terrains[Constants.fountainOfYouth]!!
val suitableLocations = tileMap.values.filter { it.resource == null && it.improvement == null
&& wonder.occursOn!!.contains(it.getLastTerrain().name) }
trySpawnOnSuitableLocation(suitableLocations, wonder)
}
}

View File

@ -4,7 +4,7 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.MainMenuScreen import com.unciv.MainMenuScreen
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.map.MapGenerator import com.unciv.logic.map.mapgenerator.MapGenerator
import com.unciv.logic.map.MapParameters import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache