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 =
Hide advanced settings =
RNG Seed =
Map Height =
Temperature extremeness =
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 */
var mods = LinkedHashSet<String>()
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

View File

@ -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
*

View File

@ -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)

View File

@ -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<TileInfo>, 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!!

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.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 = 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 {
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()
}