diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index 546659c532..830e7493ed 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -9,11 +9,10 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.logic.GameSaver 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.MapSize import com.unciv.logic.map.MapType -import com.unciv.models.metadata.GameParameters import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.MultiplayerScreen import com.unciv.ui.mapeditor.EditorMapHolder diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index e7318bbcf4..28c009301c 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.* +import com.unciv.logic.map.mapgenerator.MapGenerator import com.unciv.models.metadata.GameParameters import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache diff --git a/core/src/com/unciv/logic/map/MapGenerator.kt b/core/src/com/unciv/logic/map/MapGenerator.kt deleted file mode 100644 index b5b04500a5..0000000000 --- a/core/src/com/unciv/logic/map/MapGenerator.kt +++ /dev/null @@ -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() - val tilesToCheck = ArrayList() - - 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() - 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, 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() - - 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, initialDistance: Int): ArrayList { - - for (distanceBetweenResources in initialDistance downTo 1) { - var availableTiles = suitableTiles.toList() - val chosenTiles = ArrayList() - - // 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() - 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 { - // 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 - ) - } - } - } -} \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt new file mode 100644 index 0000000000..02b924f22b --- /dev/null +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -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() + val tilesToCheck = ArrayList() + + 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() + + 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, initialDistance: Int): ArrayList { + + for (distanceBetweenResources in initialDistance downTo 1) { + var availableTiles = suitableTiles.toList() + val chosenTiles = ArrayList() + + // 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() + 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 { + // 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 + ) + } + } + } +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt new file mode 100644 index 0000000000..adbdf8fb07 --- /dev/null +++ b/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt @@ -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 +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt new file mode 100644 index 0000000000..899a6aa520 --- /dev/null +++ b/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt @@ -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() + 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, 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) + } +} \ No newline at end of file diff --git a/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt b/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt index 53bf19f92d..abc0b817fa 100644 --- a/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt @@ -4,7 +4,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.MainMenuScreen 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.TileMap import com.unciv.models.ruleset.RulesetCache