mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-08 23:08:35 +07:00
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:
@ -230,6 +230,7 @@ Rectangular =
|
||||
|
||||
Show advanced settings =
|
||||
Hide advanced settings =
|
||||
RNG Seed =
|
||||
Map Height =
|
||||
Temperature extremeness =
|
||||
Resource richness =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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)
|
||||
|
@ -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!!
|
||||
|
@ -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()
|
||||
}
|
||||
|
Reference in New Issue
Block a user