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. * This exists so that when debugging we can see the entire map.
* Remember to turn this to false before commit and upload! * 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 */ /** For when you need to test something in an advanced game and don't have time to faff around */
val superchargedForDebug = false val superchargedForDebug = false

View File

@ -3,9 +3,7 @@ package com.unciv.logic
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.BFS import com.unciv.logic.map.*
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.gamebasics.GameBasics import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameParameters
import java.util.* import java.util.*
@ -18,7 +16,13 @@ class GameStarter{
val gameInfo = GameInfo() val gameInfo = GameInfo()
gameInfo.gameParameters = newGameParameters 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.tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map
gameInfo.difficulty = newGameParameters.difficulty gameInfo.difficulty = newGameParameters.difficulty
@ -74,7 +78,7 @@ class GameStarter{
.map { it.improvement!!.replace("StartingLocation ", "") } .map { it.improvement!!.replace("StartingLocation ", "") }
val availableCityStatesNames = Stack<String>() 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! // 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 availableCityStatesNames.addAll(GameBasics.Nations.filter { it.value.isCityState() }.keys
.shuffled().sortedByDescending { it in cityStatesWithStartingLocations }) .shuffled().sortedByDescending { it in cityStatesWithStartingLocations })
@ -154,7 +158,7 @@ class GameStarter{
val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement=="StartingLocation "+civ.civName } val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement=="StartingLocation "+civ.civName }
if(presetStartingLocation!=null) startingLocation = presetStartingLocation if(presetStartingLocation!=null) startingLocation = presetStartingLocation
else { 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() var preferredTiles = freeTiles.toList()
for (startBias in civ.nation.startBias) { 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 for(tile in tilesWithStartingLocations) tile.improvement=null // get rid of the starting location improvements
return startingLocations 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 { private fun vectorIsAtLeastNTilesAwayFromEdge(vector: Vector2, n:Int, tileMap: TileMap): Boolean {

View File

@ -23,7 +23,7 @@ interface NotificationAction {
fun execute(worldScreen: WorldScreen) fun execute(worldScreen: WorldScreen)
} }
/** cycle through locations */ /** cycle through tiles */
data class LocationAction(var locations: ArrayList<Vector2> = ArrayList()) : NotificationAction { data class LocationAction(var locations: ArrayList<Vector2> = ArrayList()) : NotificationAction {
constructor(locations: List<Vector2>): this(ArrayList(locations)) 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) { override fun execute(worldScreen: WorldScreen) {
if (locations.isNotEmpty()) { if (locations.isNotEmpty()) {
var index = locations.indexOf(worldScreen.tileMapHolder.selectedTile?.position) 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) 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 package com.unciv.logic.map
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.HexMath import com.unciv.logic.HexMath
import com.unciv.logic.MapSaver
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.gamebasics.GameBasics import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.metadata.GameParameters
class TileMap { class TileMap {
@ -32,18 +31,10 @@ class TileMap {
get() = tileList 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() setTransients()
} }

View File

@ -14,7 +14,7 @@ class GameParameters { // Default values are the default new game
for (i in 1..3) add(Player()) for (i in 1..3) add(Player())
} }
var numberOfCityStates = 0 var numberOfCityStates = 0
var mapType = MapType.Perlin var mapType = MapType.pangaea
var noBarbarians = false var noBarbarians = false
var mapFileName: String? = null var mapFileName: String? = null
var victoryTypes: ArrayList<VictoryType> = VictoryType.values().toCollection(ArrayList()) // By default, all victory types 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.MapSaver
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.models.gamebasics.tr import com.unciv.models.gamebasics.tr
import com.unciv.models.metadata.GameParameters
import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.utils.CameraStageBaseScreen import com.unciv.ui.utils.CameraStageBaseScreen
@ -15,7 +14,7 @@ import com.unciv.ui.utils.setFontSize
import com.unciv.ui.worldscreen.TileGroupMap import com.unciv.ui.worldscreen.TileGroupMap
class MapEditorScreen(): CameraStageBaseScreen(){ class MapEditorScreen(): CameraStageBaseScreen(){
var tileMap = TileMap(GameParameters()) var tileMap = TileMap()
var mapName = "My first map" var mapName = "My first map"
lateinit var mapHolder: TileGroupMap<TileGroup> lateinit var mapHolder: TileGroupMap<TileGroup>
private val tileEditorOptions = TileEditorOptionsTable(this) 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.addGoodSizedLabel("Maybe you put too many players into too small a map?".tr()).row()
cantMakeThatMapPopup.addCloseButton() cantMakeThatMapPopup.addCloseButton()
cantMakeThatMapPopup.open() cantMakeThatMapPopup.open()
Gdx.input.inputProcessor = stage
} }
} }
} }

View File

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