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