From 0b696451cee716cf13ca2f0832b3869a3bd5ddfb Mon Sep 17 00:00:00 2001 From: Federico Luongo Date: Tue, 8 Jun 2021 05:42:27 +0200 Subject: [PATCH] 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 --- .../jsons/translations/template.properties | 1 + core/src/com/unciv/logic/map/MapParameters.kt | 3 +- .../logic/map/mapgenerator/MapGenerator.kt | 17 +++-- .../map/mapgenerator/MapLandmassGenerator.kt | 4 +- .../mapgenerator/NaturalWonderGenerator.kt | 6 +- .../ui/newgamescreen/MapParametersTable.kt | 62 ++++++++++++------- 6 files changed, 59 insertions(+), 34 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index bc8628e1b1..544e3b8d75 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -230,6 +230,7 @@ Rectangular = Show advanced settings = Hide advanced settings = +RNG Seed = Map Height = Temperature extremeness = Resource richness = diff --git a/core/src/com/unciv/logic/map/MapParameters.kt b/core/src/com/unciv/logic/map/MapParameters.kt index eadad355bc..04593ce674 100644 --- a/core/src/com/unciv/logic/map/MapParameters.kt +++ b/core/src/com/unciv/logic/map/MapParameters.kt @@ -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 */ var mods = LinkedHashSet() - var seed: Long = 0 + var seed: Long = System.currentTimeMillis() var tilesPerBiomeArea = 6 var maxCoastExtension = 2 var elevationExponent = 0.7f @@ -141,6 +141,7 @@ class MapParameters { var waterThreshold = 0f fun resetAdvancedSettings() { + seed = System.currentTimeMillis() tilesPerBiomeArea = 6 maxCoastExtension = 2 elevationExponent = 0.7f diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 544a27b4fd..16984646b7 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -18,17 +18,21 @@ import kotlin.random.Random class MapGenerator(val ruleset: Ruleset) { private var randomness = MapGenerationRandomness() - fun generateMap(mapParameters: MapParameters, seed: Long = System.currentTimeMillis()): TileMap { + fun generateMap(mapParameters: MapParameters): TileMap { val mapSize = mapParameters.mapSize val mapType = mapParameters.type + if (mapParameters.seed == 0L) + mapParameters.seed = System.currentTimeMillis() + + randomness.seedRNG(mapParameters.seed) + val map: TileMap = if (mapParameters.shape == MapShape.rectangular) TileMap(mapSize.width, mapSize.height, ruleset, mapParameters.worldWrap) else TileMap(mapSize.radius, ruleset, mapParameters.worldWrap) map.mapParameters = mapParameters - map.mapParameters.seed = seed if (mapType == MapType.empty) { for (tile in map.values) { @@ -39,15 +43,14 @@ class MapGenerator(val ruleset: Ruleset) { return map } - seedRNG(seed) - MapLandmassGenerator(randomness).generateLand(map,ruleset) + MapLandmassGenerator(ruleset, randomness).generateLand(map) raiseMountainsAndHills(map) applyHumidityAndTemperature(map) spawnLakesAndCoasts(map) spawnVegetation(map) spawnRareFeatures(map) spawnIce(map) - NaturalWonderGenerator(ruleset).spawnNaturalWonders(map, randomness) + NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map) RiverGenerator(randomness).spawnRivers(map) spreadResources(map) spreadAncientRuins(map) @@ -303,6 +306,10 @@ class MapGenerator(val ruleset: Ruleset) { class MapGenerationRandomness{ var RNG = Random(42) + fun seedRNG(seed: Long = 42) { + RNG = Random(seed) + } + /** * Generates a perlin noise channel combining multiple octaves * diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt index 0565c1304d..a9a68efb3f 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt @@ -9,9 +9,9 @@ import kotlin.math.abs import kotlin.math.min 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 if (ruleset.terrains.values.none { it.type == TerrainType.Water }) { for (tile in tileMap.values) diff --git a/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt index 7334784c85..a5da1ef3b8 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt @@ -9,13 +9,13 @@ import com.unciv.models.ruleset.tile.TerrainType import kotlin.math.abs 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://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) return val mapRadius = tileMap.mapParameters.mapSize.radius @@ -60,7 +60,7 @@ class NaturalWonderGenerator(val ruleset: Ruleset) { private fun trySpawnOnSuitableLocation(suitableLocations: List, wonder: Terrain): TileInfo? { if (suitableLocations.isNotEmpty()) { - val location = suitableLocations.random() + val location = suitableLocations.random(randomness.RNG) clearTile(location) location.naturalWonder = wonder.name location.baseTerrain = wonder.turnsInto!! diff --git a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt index 1c6a42aaf2..85a0775fa3 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt @@ -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.Table 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.UncivGame import com.unciv.logic.map.* @@ -40,23 +41,22 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed addWorldSizeTable() addNoRuinsCheckbox() addNoNaturalWondersCheckbox() - if (UncivGame.Current.settings.showExperimentalWorldWrap) { + if (UncivGame.Current.settings.showExperimentalWorldWrap) addWorldWrapCheckbox() - } addAdvancedSettings() } private fun addMapShapeSelectBox() { val mapShapes = listOfNotNull( - MapShape.hexagonal, - MapShape.rectangular + MapShape.hexagonal, + MapShape.rectangular ) val mapShapeSelectBox = - TranslatedSelectBox(mapShapes, mapParameters.shape, skin) + TranslatedSelectBox(mapShapes, mapParameters.shape, skin) mapShapeSelectBox.onChange { - mapParameters.shape = mapShapeSelectBox.selected.value - updateWorldSizeTable() - } + mapParameters.shape = mapShapeSelectBox.selected.value + updateWorldSizeTable() + } add ("{Map Shape}:".toLabel()).left() add(mapShapeSelectBox).fillX().row() @@ -77,12 +77,12 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed mapTypeSelectBox = TranslatedSelectBox(mapTypes, mapParameters.type, skin) mapTypeSelectBox.onChange { - mapParameters.type = mapTypeSelectBox.selected.value + mapParameters.type = mapTypeSelectBox.selected.value - // If the map won't be generated, these options are irrelevant and are hidden - noRuinsCheckbox.isVisible = mapParameters.type != MapType.empty - noNaturalWondersCheckbox.isVisible = mapParameters.type != MapType.empty - } + // If the map won't be generated, these options are irrelevant and are hidden + noRuinsCheckbox.isVisible = mapParameters.type != MapType.empty + noNaturalWondersCheckbox.isVisible = mapParameters.type != MapType.empty + } add("{Map Generation Type}:".toLabel()).left() add(mapTypeSelectBox).fillX().row() @@ -106,7 +106,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed private fun addHexagonalSizeTable() { val defaultRadius = mapParameters.mapSize.radius.toString() customMapSizeRadius = TextField(defaultRadius, skin).apply { - textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter() + textFieldFilter = DigitsOnlyFilter() } customMapSizeRadius.onChange { mapParameters.mapSize = MapSizeNew(customMapSizeRadius.text.toIntOrNull() ?: 0 ) @@ -114,18 +114,18 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed hexagonalSizeTable.add("{Radius}:".toLabel()).grow().left() hexagonalSizeTable.add(customMapSizeRadius).right().row() hexagonalSizeTable.add("Anything above 40 may work very slowly on Android!".toLabel(Color.RED) - .apply { wrap=true }).width(prefWidth).colspan(hexagonalSizeTable.columns) + .apply { wrap=true }).width(prefWidth).colspan(hexagonalSizeTable.columns) } private fun addRectangularSizeTable() { val defaultWidth = mapParameters.mapSize.width.toString() customMapWidth = TextField(defaultWidth, skin).apply { - textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter() + textFieldFilter = DigitsOnlyFilter() } val defaultHeight = mapParameters.mapSize.height.toString() customMapHeight = TextField(defaultHeight, skin).apply { - textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter() + textFieldFilter = DigitsOnlyFilter() } customMapWidth.onChange { @@ -141,7 +141,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed rectangularSizeTable.add("{Height}:".toLabel()).grow().left() rectangularSizeTable.add(customMapHeight).right().row() rectangularSizeTable.add("Anything above 80 by 50 may work very slowly on Android!".toLabel(Color.RED) - .apply { wrap=true }).width(prefWidth).colspan(hexagonalSizeTable.columns) + .apply { wrap = true }).width(prefWidth).colspan(hexagonalSizeTable.columns) } private fun updateWorldSizeTable() { @@ -159,7 +159,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed noRuinsCheckbox = CheckBox("No Ancient Ruins".tr(), skin) noRuinsCheckbox.isChecked = mapParameters.noRuins noRuinsCheckbox.onChange { mapParameters.noRuins = noRuinsCheckbox.isChecked } - add(noRuinsCheckbox).colspan(2).row() + add(noRuinsCheckbox).colspan(2).left().row() } private fun addNoNaturalWondersCheckbox() { @@ -168,7 +168,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed noNaturalWondersCheckbox.onChange { mapParameters.noNaturalWonders = noNaturalWondersCheckbox.isChecked } - add(noNaturalWondersCheckbox).colspan(2).row() + add(noNaturalWondersCheckbox).colspan(2).left().row() } private fun addWorldWrapCheckbox() { @@ -177,7 +177,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed worldWrapCheckbox.onChange { 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!" .toLabel(fontSize = 14).apply { wrap=true }).colspan(2).fillX().row() } @@ -209,11 +209,26 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed private fun getAdvancedSettingsTable(): 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 = HashMapFloat>() - 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 { val slider = Slider(min, max, (max - min) / 20, false, skin) slider.value = getValue() slider.onChange { onChange(slider.value) } @@ -250,6 +265,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed val resetToDefaultButton = "Reset to default".toTextButton() resetToDefaultButton.onClick { mapParameters.resetAdvancedSettings() + seedTextField.text = mapParameters.seed.toString() for(entry in sliders) entry.key.value = entry.value() }