Map Generation Seedable (#4072)

* Map Generation Seedable
* Added Seed editable field in MapParametersTable
Previously, using the same set of parameters, one could not get the same map twice (i.e. negligible probability for it to happen). With this commit players can specify, alongside the usual map parameters, a long integer used to seed the RNG and get replicable results.

* Fixed Natural Wonder Spawn was not using MapGenerationRandomness hence giving not reproducible maps

* Translation strings
This commit is contained in:
Federico Luongo
2021-06-08 05:42:27 +02:00
committed by GitHub
parent 7c9b0e04b4
commit 0b696451ce
6 changed files with 59 additions and 34 deletions

View File

@ -230,6 +230,7 @@ Rectangular =
Show advanced settings = Show advanced settings =
Hide advanced settings = Hide advanced settings =
RNG Seed =
Map Height = Map Height =
Temperature extremeness = Temperature extremeness =
Resource richness = Resource richness =

View File

@ -130,7 +130,7 @@ class MapParameters {
/** This is used mainly for the map editor, so you can continue editing a map under the ame ruleset you started with */ /** This is used mainly for the map editor, so you can continue editing a map under the ame ruleset you started with */
var mods = LinkedHashSet<String>() var mods = LinkedHashSet<String>()
var seed: Long = 0 var seed: Long = System.currentTimeMillis()
var tilesPerBiomeArea = 6 var tilesPerBiomeArea = 6
var maxCoastExtension = 2 var maxCoastExtension = 2
var elevationExponent = 0.7f var elevationExponent = 0.7f
@ -141,6 +141,7 @@ class MapParameters {
var waterThreshold = 0f var waterThreshold = 0f
fun resetAdvancedSettings() { fun resetAdvancedSettings() {
seed = System.currentTimeMillis()
tilesPerBiomeArea = 6 tilesPerBiomeArea = 6
maxCoastExtension = 2 maxCoastExtension = 2
elevationExponent = 0.7f elevationExponent = 0.7f

View File

@ -18,17 +18,21 @@ import kotlin.random.Random
class MapGenerator(val ruleset: Ruleset) { class MapGenerator(val ruleset: Ruleset) {
private var randomness = MapGenerationRandomness() private var randomness = MapGenerationRandomness()
fun generateMap(mapParameters: MapParameters, seed: Long = System.currentTimeMillis()): TileMap { fun generateMap(mapParameters: MapParameters): TileMap {
val mapSize = mapParameters.mapSize val mapSize = mapParameters.mapSize
val mapType = mapParameters.type val mapType = mapParameters.type
if (mapParameters.seed == 0L)
mapParameters.seed = System.currentTimeMillis()
randomness.seedRNG(mapParameters.seed)
val map: TileMap = if (mapParameters.shape == MapShape.rectangular) val map: TileMap = if (mapParameters.shape == MapShape.rectangular)
TileMap(mapSize.width, mapSize.height, ruleset, mapParameters.worldWrap) TileMap(mapSize.width, mapSize.height, ruleset, mapParameters.worldWrap)
else else
TileMap(mapSize.radius, ruleset, mapParameters.worldWrap) TileMap(mapSize.radius, ruleset, mapParameters.worldWrap)
map.mapParameters = mapParameters map.mapParameters = mapParameters
map.mapParameters.seed = seed
if (mapType == MapType.empty) { if (mapType == MapType.empty) {
for (tile in map.values) { for (tile in map.values) {
@ -39,15 +43,14 @@ class MapGenerator(val ruleset: Ruleset) {
return map return map
} }
seedRNG(seed) MapLandmassGenerator(ruleset, randomness).generateLand(map)
MapLandmassGenerator(randomness).generateLand(map,ruleset)
raiseMountainsAndHills(map) raiseMountainsAndHills(map)
applyHumidityAndTemperature(map) applyHumidityAndTemperature(map)
spawnLakesAndCoasts(map) spawnLakesAndCoasts(map)
spawnVegetation(map) spawnVegetation(map)
spawnRareFeatures(map) spawnRareFeatures(map)
spawnIce(map) spawnIce(map)
NaturalWonderGenerator(ruleset).spawnNaturalWonders(map, randomness) NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map)
RiverGenerator(randomness).spawnRivers(map) RiverGenerator(randomness).spawnRivers(map)
spreadResources(map) spreadResources(map)
spreadAncientRuins(map) spreadAncientRuins(map)
@ -303,6 +306,10 @@ class MapGenerator(val ruleset: Ruleset) {
class MapGenerationRandomness{ class MapGenerationRandomness{
var RNG = Random(42) var RNG = Random(42)
fun seedRNG(seed: Long = 42) {
RNG = Random(seed)
}
/** /**
* Generates a perlin noise channel combining multiple octaves * Generates a perlin noise channel combining multiple octaves
* *

View File

@ -9,9 +9,9 @@ import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
class MapLandmassGenerator(val randomness: MapGenerationRandomness) { class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRandomness) {
fun generateLand(tileMap: TileMap, ruleset: Ruleset) { fun generateLand(tileMap: TileMap) {
// This is to accommodate land-only mods // This is to accommodate land-only mods
if (ruleset.terrains.values.none { it.type == TerrainType.Water }) { if (ruleset.terrains.values.none { it.type == TerrainType.Water }) {
for (tile in tileMap.values) for (tile in tileMap.values)

View File

@ -9,13 +9,13 @@ import com.unciv.models.ruleset.tile.TerrainType
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.round import kotlin.math.round
class NaturalWonderGenerator(val ruleset: Ruleset) { class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGenerationRandomness) {
/* /*
https://gaming.stackexchange.com/questions/95095/do-natural-wonders-spawn-more-closely-to-city-states/96479 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/ https://www.reddit.com/r/civ/comments/1jae5j/information_on_the_occurrence_of_natural_wonders/
*/ */
fun spawnNaturalWonders(tileMap: TileMap, randomness: MapGenerationRandomness) { fun spawnNaturalWonders(tileMap: TileMap) {
if (tileMap.mapParameters.noNaturalWonders) if (tileMap.mapParameters.noNaturalWonders)
return return
val mapRadius = tileMap.mapParameters.mapSize.radius val mapRadius = tileMap.mapParameters.mapSize.radius
@ -60,7 +60,7 @@ class NaturalWonderGenerator(val ruleset: Ruleset) {
private fun trySpawnOnSuitableLocation(suitableLocations: List<TileInfo>, wonder: Terrain): TileInfo? { private fun trySpawnOnSuitableLocation(suitableLocations: List<TileInfo>, wonder: Terrain): TileInfo? {
if (suitableLocations.isNotEmpty()) { if (suitableLocations.isNotEmpty()) {
val location = suitableLocations.random() val location = suitableLocations.random(randomness.RNG)
clearTile(location) clearTile(location)
location.naturalWonder = wonder.name location.naturalWonder = wonder.name
location.baseTerrain = wonder.turnsInto!! location.baseTerrain = wonder.turnsInto!!

View File

@ -5,6 +5,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
import com.badlogic.gdx.scenes.scene2d.ui.Slider import com.badlogic.gdx.scenes.scene2d.ui.Slider
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.scenes.scene2d.ui.TextField.TextFieldFilter.DigitsOnlyFilter
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.map.* import com.unciv.logic.map.*
@ -40,9 +41,8 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
addWorldSizeTable() addWorldSizeTable()
addNoRuinsCheckbox() addNoRuinsCheckbox()
addNoNaturalWondersCheckbox() addNoNaturalWondersCheckbox()
if (UncivGame.Current.settings.showExperimentalWorldWrap) { if (UncivGame.Current.settings.showExperimentalWorldWrap)
addWorldWrapCheckbox() addWorldWrapCheckbox()
}
addAdvancedSettings() addAdvancedSettings()
} }
@ -106,7 +106,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
private fun addHexagonalSizeTable() { private fun addHexagonalSizeTable() {
val defaultRadius = mapParameters.mapSize.radius.toString() val defaultRadius = mapParameters.mapSize.radius.toString()
customMapSizeRadius = TextField(defaultRadius, skin).apply { customMapSizeRadius = TextField(defaultRadius, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter() textFieldFilter = DigitsOnlyFilter()
} }
customMapSizeRadius.onChange { customMapSizeRadius.onChange {
mapParameters.mapSize = MapSizeNew(customMapSizeRadius.text.toIntOrNull() ?: 0 ) mapParameters.mapSize = MapSizeNew(customMapSizeRadius.text.toIntOrNull() ?: 0 )
@ -120,12 +120,12 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
private fun addRectangularSizeTable() { private fun addRectangularSizeTable() {
val defaultWidth = mapParameters.mapSize.width.toString() val defaultWidth = mapParameters.mapSize.width.toString()
customMapWidth = TextField(defaultWidth, skin).apply { customMapWidth = TextField(defaultWidth, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter() textFieldFilter = DigitsOnlyFilter()
} }
val defaultHeight = mapParameters.mapSize.height.toString() val defaultHeight = mapParameters.mapSize.height.toString()
customMapHeight = TextField(defaultHeight, skin).apply { customMapHeight = TextField(defaultHeight, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter() textFieldFilter = DigitsOnlyFilter()
} }
customMapWidth.onChange { customMapWidth.onChange {
@ -159,7 +159,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
noRuinsCheckbox = CheckBox("No Ancient Ruins".tr(), skin) noRuinsCheckbox = CheckBox("No Ancient Ruins".tr(), skin)
noRuinsCheckbox.isChecked = mapParameters.noRuins noRuinsCheckbox.isChecked = mapParameters.noRuins
noRuinsCheckbox.onChange { mapParameters.noRuins = noRuinsCheckbox.isChecked } noRuinsCheckbox.onChange { mapParameters.noRuins = noRuinsCheckbox.isChecked }
add(noRuinsCheckbox).colspan(2).row() add(noRuinsCheckbox).colspan(2).left().row()
} }
private fun addNoNaturalWondersCheckbox() { private fun addNoNaturalWondersCheckbox() {
@ -168,7 +168,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
noNaturalWondersCheckbox.onChange { noNaturalWondersCheckbox.onChange {
mapParameters.noNaturalWonders = noNaturalWondersCheckbox.isChecked mapParameters.noNaturalWonders = noNaturalWondersCheckbox.isChecked
} }
add(noNaturalWondersCheckbox).colspan(2).row() add(noNaturalWondersCheckbox).colspan(2).left().row()
} }
private fun addWorldWrapCheckbox() { private fun addWorldWrapCheckbox() {
@ -177,7 +177,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
worldWrapCheckbox.onChange { worldWrapCheckbox.onChange {
mapParameters.worldWrap = worldWrapCheckbox.isChecked mapParameters.worldWrap = worldWrapCheckbox.isChecked
} }
add(worldWrapCheckbox).colspan(2).row() add(worldWrapCheckbox).colspan(2).left().row()
add("World wrap maps are very memory intensive - creating large world wrap maps on Android can lead to crashes!" add("World wrap maps are very memory intensive - creating large world wrap maps on Android can lead to crashes!"
.toLabel(fontSize = 14).apply { wrap=true }).colspan(2).fillX().row() .toLabel(fontSize = 14).apply { wrap=true }).colspan(2).fillX().row()
} }
@ -211,6 +211,21 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
val advancedSettingsTable = Table() val advancedSettingsTable = Table()
.apply {isVisible = false; defaults().pad(5f)} .apply {isVisible = false; defaults().pad(5f)}
val seedTextField = TextField(mapParameters.seed.toString(), skin)
seedTextField.textFieldFilter = DigitsOnlyFilter()
// If the field is empty, fallback seed value to 0
seedTextField.onChange {
mapParameters.seed = try {
seedTextField.text.toLong()
} catch (e: Exception) {
0L
}
}
advancedSettingsTable.add("RNG Seed".toLabel()).left()
advancedSettingsTable.add(seedTextField).fillX().row()
val sliders = HashMap<Slider, ()->Float>() val sliders = HashMap<Slider, ()->Float>()
fun addSlider(text: String, getValue:()->Float, min:Float, max:Float, onChange: (value:Float)->Unit): Slider { fun addSlider(text: String, getValue:()->Float, min:Float, max:Float, onChange: (value:Float)->Unit): Slider {
@ -250,6 +265,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
val resetToDefaultButton = "Reset to default".toTextButton() val resetToDefaultButton = "Reset to default".toTextButton()
resetToDefaultButton.onClick { resetToDefaultButton.onClick {
mapParameters.resetAdvancedSettings() mapParameters.resetAdvancedSettings()
seedTextField.text = mapParameters.seed.toString()
for(entry in sliders) for(entry in sliders)
entry.key.value = entry.value() entry.key.value = entry.value()
} }