Map generation simplified, rewritten, and now much MUCH more readable!

This commit is contained in:
Yair Morgenstern 2019-10-29 22:17:07 +02:00
parent df4ca861c3
commit 2ec628a0da
10 changed files with 415 additions and 623 deletions

View file

@ -23,7 +23,7 @@ class UnCivGame(val version: String) : Game() {
* This exists so that when debugging we can see the entire map.
* Remember to turn this to false before commit and upload!
*/
var viewEntireMapForDebug = true
var viewEntireMapForDebug = false
/** For when you need to test something in an advanced game and don't have time to faff around */
val superchargedForDebug = false

View file

@ -3,9 +3,7 @@ package com.unciv.logic
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.BFS
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.logic.map.*
import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.metadata.GameParameters
import java.util.*
@ -18,7 +16,13 @@ class GameStarter{
val gameInfo = GameInfo()
gameInfo.gameParameters = newGameParameters
gameInfo.tileMap = TileMap(newGameParameters)
if(newGameParameters.mapType==MapType.file)
gameInfo.tileMap = MapSaver().loadMap(newGameParameters.mapFileName!!)
else gameInfo.tileMap = MapGenerator().generateMap(newGameParameters)
gameInfo.tileMap.setTransients()
gameInfo.tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map
gameInfo.difficulty = newGameParameters.difficulty
@ -74,7 +78,7 @@ class GameStarter{
.map { it.improvement!!.replace("StartingLocation ", "") }
val availableCityStatesNames = Stack<String>()
// since we shuffle and then order by, we end up with all the city states with starting locations first in a random order,
// since we shuffle and then order by, we end up with all the city states with starting tiles first in a random order,
// and then all the other city states in a random order! Because the sortedBy function is stable!
availableCityStatesNames.addAll(GameBasics.Nations.filter { it.value.isCityState() }.keys
.shuffled().sortedByDescending { it in cityStatesWithStartingLocations })
@ -154,7 +158,7 @@ class GameStarter{
val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement=="StartingLocation "+civ.civName }
if(presetStartingLocation!=null) startingLocation = presetStartingLocation
else {
if (freeTiles.isEmpty()) break // we failed to get all the starting locations with this minimum distance
if (freeTiles.isEmpty()) break // we failed to get all the starting tiles with this minimum distance
var preferredTiles = freeTiles.toList()
for (startBias in civ.nation.startBias) {
@ -175,7 +179,7 @@ class GameStarter{
for(tile in tilesWithStartingLocations) tile.improvement=null // get rid of the starting location improvements
return startingLocations
}
throw Exception("Didn't manage to get starting locations even with distance of 1?")
throw Exception("Didn't manage to get starting tiles even with distance of 1?")
}
private fun vectorIsAtLeastNTilesAwayFromEdge(vector: Vector2, n:Int, tileMap: TileMap): Boolean {

View file

@ -23,7 +23,7 @@ interface NotificationAction {
fun execute(worldScreen: WorldScreen)
}
/** cycle through locations */
/** cycle through tiles */
data class LocationAction(var locations: ArrayList<Vector2> = ArrayList()) : NotificationAction {
constructor(locations: List<Vector2>): this(ArrayList(locations))
@ -31,7 +31,7 @@ data class LocationAction(var locations: ArrayList<Vector2> = ArrayList()) : Not
override fun execute(worldScreen: WorldScreen) {
if (locations.isNotEmpty()) {
var index = locations.indexOf(worldScreen.tileMapHolder.selectedTile?.position)
index = ++index % locations.size // cycle through locations
index = ++index % locations.size // cycle through tiles
worldScreen.tileMapHolder.setCenterPosition(locations[index], selectUnit = false)
}
}

View file

@ -0,0 +1,389 @@
package com.unciv.logic.map
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.HexMath
import com.unciv.models.Counter
import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.gamebasics.tile.ResourceType
import com.unciv.models.gamebasics.tile.TerrainType
import com.unciv.models.metadata.GameParameters
import java.util.*
import kotlin.math.*
// This is no longer an Enum because there were map types that were disabled,
// and when parsing an existing map to an Enum you have to have all the options.
// So either we had to keep the old Enums forever, or change to strings.
class MapType {
companion object{
val default="Default" // Creates a cellular automata map
val perlin="Perlin"
val continents = "Continents"
val pangaea = "Pangaea"
val file = "File"
}
}
class MapGenerator() {
fun generateMap(gameParameters: GameParameters): TileMap {
val mapRadius = gameParameters.mapRadius
val mapType = gameParameters.mapType
val map = TileMap(mapRadius)
// Step one - separate land and water, in form of Grasslands and Oceans
if(mapType == MapType.perlin)
MapLandmassGenerator().generateLandPerlin(map)
else MapLandmassGenerator().generateLandCellularAutomata(map,mapRadius, mapType)
divideIntoBiomes(map,6, 0.05f, mapRadius)
for(tile in map.values) tile.setTransients()
setWaterTiles(map)
for(tile in map.values) randomizeTile(tile)
randomizeResources(map, mapRadius)
return map
}
fun setWaterTiles(map: TileMap) {
//define lakes
var waterTiles = map.values.filter { it.isWater }
val tilesInArea = ArrayList<TileInfo>()
val tilesToCheck = ArrayList<TileInfo>()
while (waterTiles.isNotEmpty()) {
val initialWaterTile = waterTiles.random()
tilesInArea += initialWaterTile
tilesToCheck += initialWaterTile
waterTiles -= initialWaterTile
while (tilesToCheck.isNotEmpty()) {
val tileWeAreChecking = tilesToCheck.random()
for (vector in tileWeAreChecking.neighbors
.filter { !tilesInArea.contains(it) and waterTiles.contains(it) }) {
tilesInArea += vector
tilesToCheck += vector
waterTiles -= vector
}
tilesToCheck -= tileWeAreChecking
}
if (tilesInArea.size <= 10) {
for (tile in tilesInArea) {
tile.baseTerrain = Constants.lakes
tile.setTransients()
}
}
tilesInArea.clear()
}
//Coasts
for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) {
if (tile.getTilesInDistance(2).any { it.isLand }) {
tile.baseTerrain = Constants.coast
tile.setTransients()
}
}
}
fun randomizeTile(tileInfo: TileInfo){
if(tileInfo.getBaseTerrain().type==TerrainType.Land && Math.random()<0.05f){
tileInfo.baseTerrain = Constants.mountain
tileInfo.setTransients()
}
addRandomTerrainFeature(tileInfo)
maybeAddAncientRuins(tileInfo)
}
fun getLatitude(vector: Vector2): Float {
return (sin(3.1416/3) * vector.y).toFloat()
}
fun divideIntoBiomes(map: TileMap, averageTilesPerArea: Int, waterPercent: Float, distance: Int) {
val areas = ArrayList<Area>()
val terrains = GameBasics.Terrains.values
.filter { it.type === TerrainType.Land && it.name != Constants.lakes && it.name != Constants.mountain}
for(tile in map.values.filter { it.baseTerrain==Constants.grassland }) tile.baseTerrain="" // So we know it's not chosen
while(map.values.any { it.baseTerrain=="" }) // the world could be split into lots off tiny islands, and every island deserves land types
{
val emptyTiles = map.values.filter { it.baseTerrain == "" }.toMutableList()
val numberOfSeeds = ceil(emptyTiles.size / averageTilesPerArea.toFloat()).toInt()
val maxLatitude = abs(getLatitude(Vector2(distance.toFloat(), distance.toFloat())))
for (i in 0 until numberOfSeeds) {
var terrain = if (Math.random() > waterPercent) terrains.random().name
else Constants.ocean
val tile = emptyTiles.random()
//change grassland to desert or tundra based on y
if (abs(getLatitude(tile.position)) < maxLatitude * 0.1) {
if (terrain == Constants.grassland || terrain == Constants.tundra)
terrain = Constants.desert
} else if (abs(getLatitude(tile.position)) > maxLatitude * 0.7) {
if (terrain == Constants.grassland || terrain == Constants.plains || terrain == Constants.desert || terrain == Constants.ocean) {
terrain = Constants.tundra
}
} else {
if (terrain == Constants.tundra) terrain = Constants.plains
else if (terrain == Constants.desert) terrain = Constants.grassland
}
val area = Area(terrain)
emptyTiles -= tile
area.addTile(tile)
areas += area
}
expandAreas(areas)
expandAreas(areas)
}
}
fun expandAreas(areas: ArrayList<Area>) {
val expandableAreas = ArrayList<Area>(areas)
while (expandableAreas.isNotEmpty()) {
val areaToExpand = expandableAreas.random()
if(areaToExpand.tiles.size>=20){
expandableAreas -= areaToExpand
continue
}
val availableExpansionTiles = areaToExpand.tiles
.flatMap { it.neighbors }.distinct()
.filter { it.baseTerrain=="" }
if (availableExpansionTiles.isEmpty()) expandableAreas -= areaToExpand
else {
val expansionTile = availableExpansionTiles.random()
areaToExpand.addTile(expansionTile)
val areasToJoin = areas.filter {
it.terrain == areaToExpand.terrain
&& it != areaToExpand
&& it.tiles.any { tile -> tile in expansionTile.neighbors }
}
for (area in areasToJoin) {
areaToExpand.tiles += area.tiles
areas.remove(area)
expandableAreas.remove(area)
}
}
}
}
fun addRandomTerrainFeature(tileInfo: TileInfo) {
if (tileInfo.getBaseTerrain().canHaveOverlay && Math.random() > 0.7f) {
val secondaryTerrains = GameBasics.Terrains.values
.filter { it.type === TerrainType.TerrainFeature && it.occursOn!!.contains(tileInfo.baseTerrain) }
if (secondaryTerrains.any()) tileInfo.terrainFeature = secondaryTerrains.random().name
}
}
fun maybeAddAncientRuins(tile: TileInfo) {
val baseTerrain = tile.getBaseTerrain()
if(baseTerrain.type!=TerrainType.Water && !baseTerrain.impassable && Random().nextDouble() < 1f/100)
tile.improvement = Constants.ancientRuins
}
fun randomizeResources(mapToReturn: TileMap, distance: Int) {
for(tile in mapToReturn.values)
if(tile.resource!=null)
tile.resource=null
randomizeStrategicResources(mapToReturn, distance)
randomizeResource(mapToReturn, distance, ResourceType.Luxury)
randomizeResource(mapToReturn, distance, ResourceType.Bonus)
}
// Here, we need each specific resource to be spread over the map - it matters less if specific resources are near each other
private fun randomizeStrategicResources(mapToReturn: TileMap, distance: Int) {
val resourcesOfType = GameBasics.TileResources.values.filter { it.resourceType == ResourceType.Strategic }
for (resource in resourcesOfType) {
val suitableTiles = mapToReturn.values
.filter { it.resource == null && resource.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) }
val averageTilesPerResource = 15 * resourcesOfType.count()
val numberOfResources = mapToReturn.values.count { it.isLand && !it.getBaseTerrain().impassable } / averageTilesPerResource
val locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance)
for (location in locations) location.resource = resource.name
}
}
// Here, we need there to be some luxury/bonus resource - it matters less what
private fun randomizeResource(mapToReturn: TileMap, distance: Int, resourceType: ResourceType) {
val resourcesOfType = GameBasics.TileResources.values.filter { it.resourceType == resourceType }
val suitableTiles = mapToReturn.values
.filter { it.resource == null && resourcesOfType.any { r->r.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } }
val numberOfResources = mapToReturn.values.count { it.isLand && !it.getBaseTerrain().impassable } / 15
val locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance)
val resourceToNumber = Counter<String>()
for(tile in locations){
val possibleResources = resourcesOfType
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) }
.map { it.name }
if(possibleResources.isEmpty()) continue
val resourceWithLeastAssignments = possibleResources.minBy { resourceToNumber[it]!! }!!
resourceToNumber.add(resourceWithLeastAssignments, 1)
tile.resource = resourceWithLeastAssignments
}
}
fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List<TileInfo>, initialDistance:Int): ArrayList<TileInfo> {
for(distanceBetweenResources in initialDistance downTo 1){
var availableTiles = suitableTiles.toList()
val chosenTiles = ArrayList<TileInfo>()
for(i in 1..numberOfResources){
if(availableTiles.isEmpty()) break
val chosenTile = availableTiles.random()
availableTiles = availableTiles.filter { it.arialDistanceTo(chosenTile)>distanceBetweenResources }
chosenTiles.add(chosenTile)
}
// Either we got them all, or we're not going to get anything better
if(chosenTiles.size == numberOfResources || distanceBetweenResources==1) return chosenTiles
}
throw Exception("Couldn't choose suitable tiles for $numberOfResources resources!")
}
}
class MapLandmassGenerator(){
fun generateLandCellularAutomata(tileMap: TileMap, mapRadius: Int, mapType: String) {
val numSmooth = 4
//init
for (tile in tileMap.values) {
val terrainType = getInitialTerrainCellularAutomata(tile, mapRadius, mapType)
if(terrainType==TerrainType.Land) tile.baseTerrain = Constants.grassland
else tile.baseTerrain = Constants.ocean
tile.setTransients()
}
//smooth
val grassland = Constants.grassland
val ocean = Constants.ocean
for (loop in 0..numSmooth) {
for (tileInfo in tileMap.values) {
if (HexMath().getDistance(Vector2.Zero, tileInfo.position) < mapRadius) {
val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain==grassland }
if (tileInfo.baseTerrain == grassland) { // land tile
if (numberOfLandNeighbors < 3)
tileInfo.baseTerrain = ocean
} else { // water tile
if (numberOfLandNeighbors > 3)
tileInfo.baseTerrain = grassland
}
} else {
tileInfo.baseTerrain = ocean
}
}
if (mapType == MapType.continents) { //keep a ocean column in the middle
for (y in -mapRadius..mapRadius) {
tileMap.get(Vector2((y / 2).toFloat(), y.toFloat())).baseTerrain=ocean
tileMap.get(Vector2((y / 2 +1).toFloat(), y.toFloat())).baseTerrain=ocean
}
}
}
}
private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapRadius: Int, mapType: String):TerrainType {
val landProbability = 0.55f
if (mapType == MapType.pangaea) {
val distanceFactor = (HexMath().getDistance(Vector2.Zero, tileInfo.position) * 1.8 / mapRadius).toFloat()
if (Random().nextDouble() < landProbability.pow(distanceFactor)) return TerrainType.Land
else return TerrainType.Water
}
if (mapType == MapType.continents) {
val distanceWeight = min(getDistanceWeightForContinents(Vector2(mapRadius.toFloat() / 2, 0f), tileInfo.position),
getDistanceWeightForContinents(Vector2(-mapRadius.toFloat() / 2, 0f), tileInfo.position))
val distanceFactor = (distanceWeight * 1.8 / mapRadius).toFloat()
if (Random().nextDouble() < landProbability.pow(distanceFactor)) return TerrainType.Land
else return TerrainType.Water
}
// default
if (HexMath().getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) {
if (Random().nextDouble() < 0.1) return TerrainType.Land else return TerrainType.Water
}
if (HexMath().getDistance(Vector2.Zero, tileInfo.position) > 0.85f * mapRadius) {
if (Random().nextDouble() < 0.2) return TerrainType.Land else return TerrainType.Water
}
if (Random().nextDouble() < landProbability) return TerrainType.Land else return TerrainType.Water
}
private fun getDistanceWeightForContinents(origin: Vector2, destination: Vector2): Float {
val relative_x = 2*(origin.x-destination.x)
val relative_y = origin.y-destination.y
if (relative_x * relative_y >= 0)
return max(abs(relative_x),abs(relative_y))
else
return (abs(relative_x) + abs(relative_y))
}
/**
* This generator simply generates Perlin noise,
* "spreads" it out according to the ratio in generateTile,
* and assigns it as the height of the various tiles.
* Tiles below a certain height threshold (determined in generateTile, currently 50%)
* are considered water tiles, the rest are land tiles
*/
fun generateLandPerlin(tileMap: TileMap){
val mapRandomSeed = Random().nextDouble() // without this, all the "random" maps would look the same
for(tile in tileMap.values){
val ratio = 1/10.0
val vector = tile.position
val height = Perlin.noise(vector.x*ratio,vector.y*ratio,mapRandomSeed)
+ Perlin.noise(vector.x*ratio*2,vector.y*ratio*2,mapRandomSeed)/2
+ Perlin.noise(vector.x*ratio*4,vector.y*ratio*4,mapRandomSeed)/4
when { // If we want to change water levels, we could raise or lower the >0
height>0 -> tile.baseTerrain = Constants.grassland
else -> tile.baseTerrain = Constants.ocean
}
}
}
}
class Area(var terrain: String) {
val tiles = ArrayList<TileInfo>()
fun addTile(tileInfo: TileInfo) {
tiles+=tileInfo
tileInfo.baseTerrain = terrain
}
}

View file

@ -1,589 +0,0 @@
package com.unciv.logic.map
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.HexMath
import com.unciv.models.Counter
import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.gamebasics.tile.ResourceType
import com.unciv.models.gamebasics.tile.TerrainType
import com.unciv.models.gamebasics.tile.TileResource
import java.util.*
import kotlin.collections.HashMap
import kotlin.math.*
enum class MapType {
Perlin,
Default,
Continents,
Pangaea,
File
}
class CelluarAutomataRandomMapGenerator(): SeedRandomMapGenerator() {
var landProb = 0.55f
var numSmooth = 4
var mapType = MapType.Default
constructor(type: MapType): this() {
mapType = type
if (mapType != MapType.Default && mapType !=MapType.Pangaea && mapType !=MapType.Continents) {
mapType = MapType.Default
}
}
override fun generateMap(distance: Int): HashMap<String, TileInfo> {
val mapVectors = HexMath().getVectorsInDistance(Vector2.Zero, distance)
val landscape = HashMap<Vector2, TerrainType>()
//init
for (vector in mapVectors) {
landscape[vector] = generateInitTerrain(vector, distance)
}
//smooth
for (loop in 0..numSmooth) {
for (vector in mapVectors) {
if (HexMath().getDistance(Vector2.Zero, vector) < distance) {
val neighborLands = HexMath().getAdjacentVectors(vector).count {landscape[it] == TerrainType.Land}
if (landscape[vector] == TerrainType.Land) {
if (neighborLands < 3)
landscape[vector] = TerrainType.Water
} else {
if (neighborLands > 3)
landscape[vector] = TerrainType.Land
}
}
else {
landscape[vector] = TerrainType.Water
}
}
if (mapType == MapType.Continents) { //keep a ocean column in the middle
for (y in -distance..distance) {
landscape[Vector2((y/2).toFloat(), y.toFloat())] = TerrainType.Water
landscape[Vector2((y/2+1).toFloat(), y.toFloat())] = TerrainType.Water
}
}
}
val map = HashMap<Vector2, TileInfo>()
for (vector in mapVectors)
map[vector] = generateTile(vector,landscape[vector]!!)
divideIntoAreas2(6, 0.05f, distance, map)
val mapToReturn = HashMap<String, TileInfo>()
for(tile in map) {
tile.value.setTransients()
mapToReturn[tile.key.toString()] = tile.value
}
setWaterTiles(mapToReturn)
for(tile in mapToReturn.values) randomizeTile(tile,mapToReturn)
randomizeResources(mapToReturn,distance)
return mapToReturn
}
private fun getDistanceWeightForContinents(origin: Vector2, destination: Vector2): Float {
val relative_x = 2*(origin.x-destination.x)
val relative_y = origin.y-destination.y
if (relative_x * relative_y >= 0)
return max(abs(relative_x),abs(relative_y))
else
return (abs(relative_x) + abs(relative_y))
}
private fun generateInitTerrain(vector: Vector2, distance: Int): TerrainType {
val type: TerrainType
if (mapType == MapType.Pangaea) {
val distanceFactor = (HexMath().getDistance(Vector2.Zero, vector) * 1.8 / distance).toFloat()
type = if (Random().nextDouble() < landProb.pow(distanceFactor)) TerrainType.Land else TerrainType.Water
} else if (mapType == MapType.Continents) {
val distanceWeight = min(getDistanceWeightForContinents(Vector2(distance.toFloat()/2, 0f), vector),
getDistanceWeightForContinents(Vector2(-distance.toFloat()/2, 0f), vector))
val distanceFactor = (distanceWeight * 1.8 / distance).toFloat()
type = if (Random().nextDouble() < landProb.pow(distanceFactor)) TerrainType.Land else TerrainType.Water
} else { //default
if (HexMath().getDistance(Vector2.Zero, vector) > 0.9f * distance)
type = if (Random().nextDouble() < 0.1) TerrainType.Land else TerrainType.Water
else if (HexMath().getDistance(Vector2.Zero, vector) > 0.85f * distance)
type = if (Random().nextDouble() < 0.2) TerrainType.Land else TerrainType.Water
else
type = if (Random().nextDouble() < landProb) TerrainType.Land else TerrainType.Water
}
return type
}
private fun generateTile(vector: Vector2, type: TerrainType): TileInfo {
val tile=TileInfo()
tile.position=vector
if (type == TerrainType.Land) tile.baseTerrain = ""
else tile.baseTerrain = Constants.ocean
return tile
}
override fun setWaterTiles(map: HashMap<String, TileInfo>) {
//define lakes
var waterTiles = map.values.filter { it.isWater }.map { it.position }
val tilesInArea = ArrayList<Vector2>()
val tilesToCheck = ArrayList<Vector2>()
while (waterTiles.isNotEmpty()) {
val initialWaterTile = waterTiles.random()
tilesInArea += initialWaterTile
tilesToCheck += initialWaterTile
waterTiles -= initialWaterTile
while (tilesToCheck.isNotEmpty()) {
val tileChecking = tilesToCheck.random()
for (vector in HexMath().getVectorsAtDistance(tileChecking,1)
.filter { !tilesInArea.contains(it) and waterTiles.contains(it) }) {
tilesInArea += vector
tilesToCheck += vector
waterTiles -= vector
}
tilesToCheck -= tileChecking
}
if (tilesInArea.size <= 10) {
for (vector in tilesInArea) {
val tile = map[vector.toString()]!!
tile.baseTerrain = Constants.lakes
tile.setTransients()
}
}
tilesInArea.clear()
}
//Coasts
for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) {
if (HexMath().getVectorsInDistance(tile.position,2).any { hasLandTile(map,it) }) {
tile.baseTerrain = Constants.coast
tile.setTransients()
}
}
}
override fun randomizeTile(tileInfo: TileInfo, map: HashMap<String, TileInfo>){
if(tileInfo.getBaseTerrain().type==TerrainType.Land && Math.random()<0.05f){
tileInfo.baseTerrain = Constants.mountain
tileInfo.setTransients()
}
addRandomTerrainFeature(tileInfo)
addRandomResourceToTile(tileInfo)
maybeAddAncientRuins(tileInfo)
}
fun getLatitude(vector: Vector2): Float {
return (sin(3.1416/3) * vector.y).toFloat()
}
fun divideIntoAreas2(averageTilesPerArea: Int, waterPercent: Float, distance: Int, map: HashMap<Vector2, TileInfo>) {
val areas = ArrayList<Area>()
val terrains = GameBasics.Terrains.values.filter { it.type === TerrainType.Land && it.name != Constants.lakes
&& it.name != Constants.mountain}
while(map.values.any { it.baseTerrain=="" }) // the world could be split into lots off tiny islands, and every island deserves land types
{
val emptyTiles = map.values.filter { it.baseTerrain == "" }.toMutableList()
val numberOfSeeds = ceil(emptyTiles.size / averageTilesPerArea.toFloat()).toInt()
val maxLatitude = abs(getLatitude(Vector2(distance.toFloat(), distance.toFloat())))
for (i in 0 until numberOfSeeds) {
var terrain = if (Math.random() > waterPercent) terrains.random().name
else Constants.ocean
val tile = emptyTiles.random()
//change grassland to desert or tundra based on y
if (abs(getLatitude(tile.position)) < maxLatitude * 0.1) {
if (terrain == Constants.grassland || terrain == Constants.tundra)
terrain = Constants.desert
} else if (abs(getLatitude(tile.position)) > maxLatitude * 0.7) {
if (terrain == Constants.grassland || terrain == Constants.plains || terrain == Constants.desert || terrain == Constants.ocean) {
terrain = Constants.tundra
}
} else {
if (terrain == Constants.tundra) terrain = Constants.plains
else if (terrain == Constants.desert) terrain = Constants.grassland
}
val area = Area(terrain)
emptyTiles -= tile
area.addTile(tile)
areas += area
}
expandAreas(areas, map)
expandAreas(areas, map)
}
}
}
/**
* This generator simply generates Perlin noise,
* "spreads" it out according to the ratio in generateTile,
* and assigns it as the height of the various tiles.
* Tiles below a certain height threshold (determined in generateTile, currently 50%)
* are considered water tiles, the rest are land tiles
*/
class PerlinNoiseRandomMapGenerator:SeedRandomMapGenerator(){
override fun generateMap(distance: Int): HashMap<String, TileInfo> {
val map = HashMap<Vector2, TileInfo>()
val mapRandomSeed = Random().nextDouble() // without this, all the "random" maps would look the same
for (vector in HexMath().getVectorsInDistance(Vector2.Zero, distance))
map[vector] = generateTile(vector,mapRandomSeed)
divideIntoAreas(6, 0f, map)
val mapToReturn = HashMap<String, TileInfo>()
for(tile in map) {
tile.value.setTransients()
mapToReturn[tile.key.toString()] = tile.value
}
setWaterTiles(mapToReturn)
for(tile in mapToReturn.values) randomizeTile(tile,mapToReturn)
randomizeResources(mapToReturn,distance)
return mapToReturn
}
private fun generateTile(vector: Vector2, mapRandomSeed: Double): TileInfo {
val tile=TileInfo()
tile.position=vector
val ratio = 1/10.0
val height = Perlin.noise(vector.x*ratio,vector.y*ratio,mapRandomSeed)
+ Perlin.noise(vector.x*ratio*2,vector.y*ratio*2,mapRandomSeed)/2
+ Perlin.noise(vector.x*ratio*4,vector.y*ratio*4,mapRandomSeed)/4
when {
height>0.8 -> tile.baseTerrain = Constants.mountain
height>0 -> tile.baseTerrain = "" // we'll leave this to the area division
else -> tile.baseTerrain = Constants.ocean
}
return tile
}
}
/**
* This generator uses the algorithm from the game "Alexander", outlined here:
* http://www.cartania.com/alexander/generation.html
*/
class AlexanderRandomMapGenerator:RandomMapGenerator(){
fun generateMap(distance: Int, landExpansionChance:Float): HashMap<String, TileInfo> {
val map = HashMap<Vector2, TileInfo?>()
for (vector in HexMath().getVectorsInDistance(Vector2.Zero, distance))
map[vector] = null
val sparkList = ArrayList<Vector2>()
for(i in 0..distance*distance/6){
val location = map.filter { it.value==null }.map { it.key }.random()
map[location] = TileInfo().apply { baseTerrain= Constants.grassland}
sparkList.add(location)
}
while(sparkList.any()){
val currentSpark = sparkList.random()
val emptyTilesAroundSpark = HexMath().getAdjacentVectors(currentSpark)
.filter { map.containsKey(it) && map[it]==null }
if(map[currentSpark]!!.baseTerrain==Constants.grassland){
for(tile in emptyTilesAroundSpark){
if(Math.random()<landExpansionChance) map[tile]=TileInfo().apply { baseTerrain=Constants.grassland }
else map[tile]=TileInfo().apply { baseTerrain=Constants.ocean }
}
}
else{
for(tile in emptyTilesAroundSpark)
map[tile]=TileInfo().apply { baseTerrain=Constants.ocean }
}
sparkList.remove(currentSpark)
sparkList.addAll(emptyTilesAroundSpark)
}
val newmap = HashMap<String,TileInfo>()
for(entry in map){
entry.value!!.position = entry.key
if(entry.value!!.baseTerrain==Constants.ocean
&& HexMath().getAdjacentVectors(entry.key).all { !map.containsKey(it) || map[it]!!.baseTerrain==Constants.grassland })
entry.value!!.baseTerrain=Constants.grassland
newmap[entry.key.toString()] = entry.value!!
}
setWaterTiles(newmap)
return newmap
// now that we've divided them into land and not-land, stage 2 - seeding areas the way we did with the seed generator!
}
}
class Area(var terrain: String) {
val locations = ArrayList<Vector2>()
fun addTile(tileInfo: TileInfo) {
locations+=tileInfo.position
tileInfo.baseTerrain = terrain
}
}
/**
* This generator works by creating a number of seeds of different terrain types in random places,
* and choosing a random one each time to expand in a random direction, until the map is filled.
* With water, this creates canal-like structures.
*/
open class SeedRandomMapGenerator : RandomMapGenerator() {
fun generateMap(distance: Int, waterPercent:Float): HashMap<String, TileInfo> {
val map = HashMap<Vector2, TileInfo>()
for (vector in HexMath().getVectorsInDistance(Vector2.Zero, distance))
map[vector] = TileInfo().apply { position=vector; baseTerrain="" }
divideIntoAreas(6, waterPercent, map)
val mapToReturn = HashMap<String,TileInfo>()
for (entry in map) mapToReturn[entry.key.toString()] = entry.value
for (entry in map) randomizeTile(entry.value, mapToReturn)
setWaterTiles(mapToReturn)
randomizeResources(mapToReturn,distance)
return mapToReturn
}
open fun divideIntoAreas(averageTilesPerArea: Int, waterPercent: Float, map: HashMap<Vector2, TileInfo>) {
val areas = ArrayList<Area>()
val terrains = GameBasics.Terrains.values
.filter { it.type === TerrainType.Land && it.name != Constants.lakes && it.name != Constants.mountain }
while(map.values.any { it.baseTerrain=="" }) // the world could be split into lots off tiny islands, and every island deserves land types
{
val emptyTiles = map.values.filter { it.baseTerrain == "" }.toMutableList()
val numberOfSeeds = ceil(emptyTiles.size / averageTilesPerArea.toFloat()).toInt()
for (i in 0 until numberOfSeeds) {
val terrain = if (Math.random() > waterPercent) terrains.random().name
else Constants.ocean
val area = Area(terrain)
val tile = emptyTiles.random()
emptyTiles -= tile
area.addTile(tile)
areas += area
}
expandAreas(areas, map)
expandAreas(areas, map)
}
for (area in areas.filter { it.terrain == Constants.ocean && it.locations.size <= 10 }) {
// areas with 10 or less tiles are lakes.
for (location in area.locations)
map[location]!!.baseTerrain = Constants.lakes
}
}
fun expandAreas(areas: ArrayList<Area>, map: HashMap<Vector2, TileInfo>) {
val expandableAreas = ArrayList<Area>(areas)
while (expandableAreas.isNotEmpty()) {
val areaToExpand = expandableAreas.random()
if(areaToExpand.locations.size>=20){
expandableAreas -= areaToExpand
continue
}
val availableExpansionVectors = areaToExpand.locations
.flatMap { HexMath().getAdjacentVectors(it) }.asSequence().distinct()
.filter { map.containsKey(it) && map[it]!!.baseTerrain=="" }.toList()
if (availableExpansionVectors.isEmpty()) expandableAreas -= areaToExpand
else {
val expansionVector = availableExpansionVectors.random()
areaToExpand.addTile(map[expansionVector]!!)
val neighbors = HexMath().getAdjacentVectors(expansionVector)
val areasToJoin = areas.filter {
it.terrain == areaToExpand.terrain
&& it != areaToExpand
&& it.locations.any { location -> location in neighbors }
}
for (area in areasToJoin) {
areaToExpand.locations += area.locations
areas.remove(area)
expandableAreas.remove(area)
}
}
}
}
}
/**
* This contains the basic randomizing tasks (add random terrain feature/resource)
* and a basic map generator where every single tile is individually randomized.
* Doesn't look very good TBH.
*/
open class RandomMapGenerator {
private fun addRandomTile(position: Vector2): TileInfo {
val tileInfo = TileInfo()
tileInfo.position = position
val terrains = GameBasics.Terrains.values
val baseTerrain = terrains.filter { it.type === TerrainType.Land }.random()
tileInfo.baseTerrain = baseTerrain.name
addRandomTerrainFeature(tileInfo)
addRandomResourceToTile(tileInfo)
return tileInfo
}
fun addRandomTerrainFeature(tileInfo: TileInfo) {
if (tileInfo.getBaseTerrain().canHaveOverlay && Math.random() > 0.7f) {
val secondaryTerrains = GameBasics.Terrains.values
.filter { it.type === TerrainType.TerrainFeature && it.occursOn!!.contains(tileInfo.baseTerrain) }
if (secondaryTerrains.any()) tileInfo.terrainFeature = secondaryTerrains.random().name
}
}
internal fun addRandomResourceToTile(tileInfo: TileInfo) {
var tileResources = GameBasics.TileResources.values.toList()
// Resources are placed according to TerrainFeature, if exists, otherwise according to BaseLayer.
tileResources = tileResources.filter { it.terrainsCanBeFoundOn.contains(tileInfo.getLastTerrain().name) }
var resource: TileResource? = null
when {
Math.random() < 1 / 15f -> resource = getRandomResource(tileResources, ResourceType.Bonus)
Math.random() < 1 / 15f -> resource = getRandomResource(tileResources, ResourceType.Strategic)
Math.random() < 1 / 15f -> resource = getRandomResource(tileResources, ResourceType.Luxury)
}
if (resource != null) tileInfo.resource = resource.name
}
private fun getRandomResource(resources: List<TileResource>, resourceType: ResourceType): TileResource? {
val filtered = resources.filter { it.resourceType == resourceType }
if (filtered.isEmpty()) return null
else return filtered.random()
}
open fun generateMap(distance: Int): HashMap<String, TileInfo> {
val map = HashMap<String, TileInfo>()
for (vector in HexMath().getVectorsInDistance(Vector2.Zero, distance))
map[vector.toString()] = addRandomTile(vector)
return map
}
fun maybeAddAncientRuins(tile: TileInfo) {
val baseTerrain = tile.getBaseTerrain()
if(baseTerrain.type!=TerrainType.Water && !baseTerrain.impassable && Random().nextDouble() < 1f/100)
tile.improvement = Constants.ancientRuins
}
fun hasLandTile(map: HashMap<String, TileInfo>, vector: Vector2): Boolean {
return map.containsKey(vector.toString()) && map[vector.toString()]!!.getBaseTerrain().type == TerrainType.Land
}
open fun setWaterTiles(map: HashMap<String, TileInfo>) {
for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) {
if (HexMath().getVectorsInDistance(tile.position,2).any { hasLandTile(map,it) }) {
tile.baseTerrain = Constants.coast
tile.setTransients()
}
}
}
open fun randomizeTile(tileInfo: TileInfo, map: HashMap<String, TileInfo>){
if(tileInfo.getBaseTerrain().type==TerrainType.Land && Math.random()<0.05f){
tileInfo.baseTerrain = Constants.mountain
tileInfo.setTransients()
}
if(tileInfo.getBaseTerrain().type==TerrainType.Land && Math.random()<0.05f
&& HexMath().getVectorsInDistance(tileInfo.position,1).all { hasLandTile(map,it) }){
tileInfo.baseTerrain = Constants.lakes
tileInfo.setTransients()
}
addRandomTerrainFeature(tileInfo)
addRandomResourceToTile(tileInfo)
maybeAddAncientRuins(tileInfo)
}
fun randomizeResources(mapToReturn: HashMap<String, TileInfo>, distance: Int) {
for(tile in mapToReturn.values)
if(tile.resource!=null)
tile.resource=null
randomizeStrategicResources(mapToReturn, distance, ResourceType.Strategic)
randomizeResource(mapToReturn, distance, ResourceType.Luxury)
randomizeResource(mapToReturn, distance, ResourceType.Bonus)
}
// Here, we need each specific resource to be spread over the map - it matters less if specific resources are near each other
private fun randomizeStrategicResources(mapToReturn: HashMap<String, TileInfo>, distance: Int, resourceType: ResourceType) {
val resourcesOfType = GameBasics.TileResources.values.filter { it.resourceType == resourceType }
for (resource in resourcesOfType) {
val suitableTiles = mapToReturn.values
.filter { it.resource == null && resource.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) }
val averageTilesPerResource = 15 * resourcesOfType.count()
val numberOfResources = mapToReturn.values.count { it.isLand && !it.getBaseTerrain().impassable } / averageTilesPerResource
val locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance)
for (location in locations) location.resource = resource.name
}
}
// Here, we need there to be some luxury/bonus resource - it matters less what
private fun randomizeResource(mapToReturn: HashMap<String, TileInfo>, distance: Int, resourceType: ResourceType) {
val resourcesOfType = GameBasics.TileResources.values.filter { it.resourceType == resourceType }
val suitableTiles = mapToReturn.values
.filter { it.resource == null && resourcesOfType.any { r->r.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } }
val numberOfResources = mapToReturn.values.count { it.isLand && !it.getBaseTerrain().impassable } / 15
val locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance)
val resourceToNumber = Counter<String>()
for(tile in locations){
val possibleResources = resourcesOfType
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) }
.map { it.name }
if(possibleResources.isEmpty()) continue
val resourceWithLeastAssignments = possibleResources.minBy { resourceToNumber[it]!! }!!
resourceToNumber.add(resourceWithLeastAssignments, 1)
tile.resource = resourceWithLeastAssignments
}
}
fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List<TileInfo>, initialDistance:Int): ArrayList<TileInfo> {
for(distanceBetweenResources in initialDistance downTo 1){
var availableTiles = suitableTiles.toList()
val chosenTiles = ArrayList<TileInfo>()
for(i in 1..numberOfResources){
if(availableTiles.isEmpty()) break
val chosenTile = availableTiles.random()
availableTiles = availableTiles.filter { it.arialDistanceTo(chosenTile)>distanceBetweenResources }
chosenTiles.add(chosenTile)
}
if(chosenTiles.size == numberOfResources) return chosenTiles
}
throw Exception("ArgleBargle")
}
}

View file

@ -1,12 +1,11 @@
package com.unciv.logic.map
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.GameInfo
import com.unciv.logic.HexMath
import com.unciv.logic.MapSaver
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.metadata.GameParameters
class TileMap {
@ -32,18 +31,10 @@ class TileMap {
get() = tileList
constructor(newGameParameters: GameParameters) {
val mapValues:Collection<TileInfo>
if(newGameParameters.mapType == MapType.File)
mapValues = MapSaver().loadMap(newGameParameters.mapFileName!!).values
else if(newGameParameters.mapType==MapType.Perlin)
mapValues = PerlinNoiseRandomMapGenerator().generateMap(newGameParameters.mapRadius).values
else
mapValues = CelluarAutomataRandomMapGenerator(newGameParameters.mapType).generateMap(newGameParameters.mapRadius).values
tileList.addAll(mapValues)
constructor(radius:Int){
for(vector in HexMath().getVectorsInDistance(Vector2.Zero, radius))
tileList.add(TileInfo().apply { position = vector; baseTerrain= Constants.grassland })
setTransients()
}

View file

@ -14,7 +14,7 @@ class GameParameters { // Default values are the default new game
for (i in 1..3) add(Player())
}
var numberOfCityStates = 0
var mapType = MapType.Perlin
var mapType = MapType.pangaea
var noBarbarians = false
var mapFileName: String? = null
var victoryTypes: ArrayList<VictoryType> = VictoryType.values().toCollection(ArrayList()) // By default, all victory types

View file

@ -6,7 +6,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.unciv.logic.MapSaver
import com.unciv.logic.map.TileMap
import com.unciv.models.gamebasics.tr
import com.unciv.models.metadata.GameParameters
import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.utils.CameraStageBaseScreen
@ -15,7 +14,7 @@ import com.unciv.ui.utils.setFontSize
import com.unciv.ui.worldscreen.TileGroupMap
class MapEditorScreen(): CameraStageBaseScreen(){
var tileMap = TileMap(GameParameters())
var tileMap = TileMap()
var mapName = "My first map"
lateinit var mapHolder: TileGroupMap<TileGroup>
private val tileEditorOptions = TileEditorOptionsTable(this)

View file

@ -85,6 +85,7 @@ class NewGameScreen: PickerScreen(){
cantMakeThatMapPopup.addGoodSizedLabel("Maybe you put too many players into too small a map?".tr()).row()
cantMakeThatMapPopup.addCloseButton()
cantMakeThatMapPopup.open()
Gdx.input.inputProcessor = stage
}
}
}

View file

@ -60,26 +60,23 @@ class NewGameScreenOptionsTable(val newGameParameters: GameParameters, val onMul
private fun addMapTypeSizeAndFile() {
add("{Map type}:".tr())
val mapTypes = LinkedHashMap<String, MapType>()
for (type in MapType.values()) {
if (type == MapType.File && MapSaver().getMaps().isEmpty()) continue
mapTypes[type.toString()] = type
}
val mapTypes = arrayListOf(MapType.default,MapType.continents,MapType.perlin,MapType.pangaea)
if(MapSaver().getMaps().isNotEmpty()) mapTypes.add(MapType.file)
val mapFileLabel = "{Map file}:".toLabel()
val mapFileSelectBox = getMapFileSelectBox()
mapFileLabel.isVisible = false
mapFileSelectBox.isVisible = false
val mapTypeSelectBox = TranslatedSelectBox(mapTypes.keys, newGameParameters.mapType.toString(), CameraStageBaseScreen.skin)
val mapTypeSelectBox = TranslatedSelectBox(mapTypes, newGameParameters.mapType, CameraStageBaseScreen.skin)
val worldSizeSelectBox = getWorldSizeSelectBox()
val worldSizeLabel = "{World size}:".toLabel()
mapTypeSelectBox.addListener(object : ChangeListener() {
override fun changed(event: ChangeEvent?, actor: Actor?) {
newGameParameters.mapType = mapTypes[mapTypeSelectBox.selected.value]!!
if (newGameParameters.mapType == MapType.File) {
newGameParameters.mapType = mapTypeSelectBox.selected.value
if (newGameParameters.mapType == MapType.file) {
worldSizeSelectBox.isVisible = false
worldSizeLabel.isVisible = false
mapFileSelectBox.isVisible = true