Added random land\sea division using Perlin noise

Can now divide undecided parts of maps into random terrain regions
This commit is contained in:
Yair Morgenstern 2018-10-11 13:00:23 +03:00
parent f07521b843
commit 64359a794e
12 changed files with 583 additions and 382 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 885 KiB

After

Width:  |  Height:  |  Size: 892 KiB

View file

@ -3,14 +3,15 @@
{
name:"Ocean",
type:"Water",
food:2,
food:1,
gold:1,
movementCost:1,
RGB: [100,100,255]
},
{
name:"Coast",
type:"Water",
food:2,
food:1,
movementCost:1,
RGB: [150,150,255]
},

View file

@ -53,7 +53,16 @@
improvement:"Quarry",
improvementStats:{production:1},
building:"Stone Works"
},
},
{
name:"Fish",
resourceType:"Bonus",
terrainsCanBeFoundOn:["Coast"],
food:1,
improvement:"Fishing Boats",
improvementStats:{food:1},
building:"Lighthouse"
},
// Strategic resources
{
@ -222,5 +231,24 @@
improvement:"Quarry",
improvementStats:{gold:1,production:1},
building:"Stone Works"
},
{
name:"Whales",
resourceType:"Luxury",
terrainsCanBeFoundOn:["Coast"],
food:1,
gold:1,
improvement:"Fishing Boats",
improvementStats:{food:1},
building:"Harbor"
},
{
name:"Pearls",
resourceType:"Luxury",
terrainsCanBeFoundOn:["Coast"],
gold:2,
improvement:"Fishing Boats",
improvementStats:{food:1},
building:"Harbor"
}
]

View file

@ -6,6 +6,7 @@ import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.models.gamebasics.GameBasics
import com.unciv.models.gamebasics.tile.TerrainType
import com.unciv.models.gamebasics.tile.TileImprovement
class WorkerAutomation(val unit: MapUnit) {
@ -79,6 +80,7 @@ class WorkerAutomation(val unit: MapUnit) {
.filter {
(it.civilianUnit== null || it == currentTile)
&& it.improvement == null
&& it.getBaseTerrain().type==TerrainType.Land
&& it.canBuildImprovement(chooseImprovement(it), unit.civInfo)
&& {val city=it.getCity(); city==null || it.getCity()?.civInfo == unit.civInfo}() // don't work tiles belonging to another civ
}.sortedByDescending { getPriority(it, unit.civInfo) }.toMutableList()

View file

@ -85,7 +85,8 @@ class CityConstructions {
if(special!=null) return special
}
throw Exception("$constructionName is not a building or a unit!")
class NotBuildingOrUnitException(message:String):Exception(message)
throw NotBuildingOrUnitException("$constructionName is not a building or a unit!")
}
internal fun getBuiltBuildings(): List<Building> = builtBuildings.map { GameBasics.Buildings[it]!! }

View file

@ -0,0 +1,80 @@
package com.unciv.logic.map
// version 1.1.3
// From https://rosettacode.org/wiki/Perlin_noise#Kotlin
object Perlin {
private val permutation = intArrayOf(
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225,
140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148,
247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32,
57 , 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175,
74 , 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122,
60 , 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54,
65 , 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169,
200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64,
52 , 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212,
207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213,
119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104,
218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241,
81 , 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180
)
private val p = IntArray(512) {
if (it < 256) permutation[it] else permutation[it - 256]
}
fun noise(x: Double, y: Double, z: Double): Double {
// Find unit cube that contains point
val xi = Math.floor(x).toInt() and 255
val yi = Math.floor(y).toInt() and 255
val zi = Math.floor(z).toInt() and 255
// Find relative x, y, z of point in cube
val xx = x - Math.floor(x)
val yy = y - Math.floor(y)
val zz = z - Math.floor(z)
// Compute fade curves for each of xx, yy, zz
val u = fade(xx)
val v = fade(yy)
val w = fade(zz)
// Hash co-ordinates of the 8 cube corners
// and add blended results from 8 corners of cube
val a = p[xi] + yi
val aa = p[a] + zi
val ab = p[a + 1] + zi
val b = p[xi + 1] + yi
val ba = p[b] + zi
val bb = p[b + 1] + zi
return lerp(w, lerp(v, lerp(u, grad(p[aa], xx, yy, zz),
grad(p[ba], xx - 1, yy, zz)),
lerp(u, grad(p[ab], xx, yy - 1, zz),
grad(p[bb], xx - 1, yy - 1, zz))),
lerp(v, lerp(u, grad(p[aa + 1], xx, yy, zz - 1),
grad(p[ba + 1], xx - 1, yy, zz - 1)),
lerp(u, grad(p[ab + 1], xx, yy - 1, zz - 1),
grad(p[bb + 1], xx - 1, yy - 1, zz - 1))))
}
private fun fade(t: Double) = t * t * t * (t * (t * 6 - 15) + 10)
private fun lerp(t: Double, a: Double, b: Double) = a + t * (b - a)
private fun grad(hash: Int, x: Double, y: Double, z: Double): Double {
// Convert low 4 bits of hash code into 12 gradient directions
val h = hash and 15
val u = if (h < 8) x else y
val v = if (h < 4) y else if (h == 12 || h == 14) x else z
return (if ((h and 1) == 0) u else -u) +
(if ((h and 2) == 0) v else -v)
}
}

View file

@ -8,9 +8,46 @@ import com.unciv.models.gamebasics.tile.TerrainType
import com.unciv.models.gamebasics.tile.TileResource
import com.unciv.ui.utils.getRandom
import java.util.*
import kotlin.collections.HashMap
import kotlin.math.ceil
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)
mapToReturn[tile.key.toString()] = tile.value
setWaterTiles(mapToReturn)
for(tile in mapToReturn.values) randomizeTile(tile)
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.4 -> tile.baseTerrain = "Hill"
height>0 -> tile.baseTerrain = "" // we'll leave this to the area division
else -> tile.baseTerrain = "Ocean"
}
return tile
}
}
class AlexanderRandomMapGenerator:RandomMapGenerator(){
fun generateMap(distance: Int, landExpansionChange:Float){
fun generateMap(distance: Int, landExpansionChance:Float): HashMap<String, TileInfo> {
val map = HashMap<Vector2, TileInfo?>()
for (vector in HexMath().GetVectorsInDistance(Vector2.Zero, distance))
@ -31,7 +68,7 @@ class AlexanderRandomMapGenerator:RandomMapGenerator(){
.filter { map.containsKey(it) && map[it]==null }
if(map[currentSpark]!!.baseTerrain==grassland){
for(tile in emptyTilesAroundSpark){
if(Math.random()<landExpansionChange) map[tile]=TileInfo().apply { baseTerrain=grassland }
if(Math.random()<landExpansionChance) map[tile]=TileInfo().apply { baseTerrain=grassland }
else map[tile]=TileInfo().apply { baseTerrain=ocean }
}
}
@ -43,23 +80,29 @@ class AlexanderRandomMapGenerator:RandomMapGenerator(){
sparkList.addAll(emptyTilesAroundSpark)
}
// now that we've divided them into land and not-land, stage 2 - seeding areas the way we did with the seed generator!
val newmap = HashMap<String,TileInfo>()
for(entry in map){
entry.value!!.position = entry.key
if(entry.value!!.baseTerrain==ocean
&& HexMath().GetAdjacentVectors(entry.key).all { !map.containsKey(it) || map[it]!!.baseTerrain==grassland })
entry.value!!.baseTerrain=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(position: Vector2) : TileInfo{
locations+=position
val tile = TileInfo()
tile.position = position
tile.baseTerrain = terrain
RandomMapGenerator().addRandomTerrainFeature(tile)
RandomMapGenerator().addRandomResourceToTile(tile)
RandomMapGenerator().maybeAddAncientRuins(tile)
return tile
fun addTile(tileInfo: TileInfo) {
locations+=tileInfo.position
tileInfo.baseTerrain = terrain
}
}
@ -68,101 +111,106 @@ class Area(var terrain: String) {
* 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.
*/
class SeedRandomMapGenerator : RandomMapGenerator() {
open class SeedRandomMapGenerator : RandomMapGenerator() {
fun generateMap(distance: Int, waterPercent:Float): HashMap<String, TileInfo> {
val map = HashMap<Vector2, TileInfo?>()
val map = HashMap<Vector2, TileInfo>()
for (vector in HexMath().GetVectorsInDistance(Vector2.Zero, distance))
map[vector] = null
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!!
mapToReturn[entry.key.toString()] = entry.value
}
setWaterTiles(mapToReturn)
return mapToReturn
}
private fun divideIntoAreas(averageTilesPerArea: Int, waterPercent: Float, map: HashMap<Vector2, TileInfo?>) {
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 != "Lakes" }
for (i in 0..(map.count { it.value==null } / averageTilesPerArea)) {
val terrain = if (Math.random() > waterPercent) terrains.getRandom().name
else "Ocean"
val area = Area(terrain)
val location = map.filter { it.value == null }.map { it.key }.getRandom()
map[location] = area.addTile(location)
areas += area
}
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()
fun expandAreas() {
val expandableAreas = ArrayList<Area>(areas)
while (expandableAreas.isNotEmpty()) {
val areaToExpand = expandableAreas.getRandom()
val availableExpansionVectors = areaToExpand.locations
.flatMap { HexMath().GetAdjacentVectors(it) }.distinct()
.filter { map.containsKey(it) && map[it] == null }
if (availableExpansionVectors.isEmpty()) expandableAreas -= areaToExpand
else {
val expansionVector = availableExpansionVectors.getRandom()
map[expansionVector] = areaToExpand.addTile(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)
}
}
for (i in 0 until numberOfSeeds) {
val terrain = if (Math.random() > waterPercent) terrains.getRandom().name
else "Ocean"
val area = Area(terrain)
val tile = emptyTiles.getRandom()
emptyTiles -= tile
area.addTile(tile)
areas += area
}
}
expandAreas()
// After we've assigned all the tiles, there will be some areas that contain only 1 or 2 tiles.
// So, we kill those areas, and have the world expand on and cover them too
for (area in areas.toList()) {
if (area.locations.size < 3) {
areas -= area
for (location in area.locations) map[location] = null
}
expandAreas(areas, map)
// After we've assigned all the tiles, there will be some areas that contain only 1 or 2 tiles.
// So, we kill those areas, and have the world expand on and cover them too
// for (area in areas.toList()) {
// if (area.locations.size < 3) {
// areas -= area
// for (location in area.locations) map[location]!!.baseTerrain = ""
// }
// }
expandAreas(areas, map)
}
expandAreas()
// Once our map has all its tiles, we'll want to change the water tiles to Coasts, Oceans and Lakes
for (area in areas.filter { it.terrain == "Ocean" && it.locations.size <= 10 }) {
// areas with 10 or less tiles are lakes.
for (location in area.locations)
map[location]!!.baseTerrain = "Lakes"
}
for (tile in map.values.filter { it != null && it.baseTerrain == "Ocean" }) {
if (HexMath().GetAdjacentVectors(tile!!.position)
.any { map.containsKey(it) && map[it]!!.getBaseTerrain().type == TerrainType.Land })
tile.baseTerrain = "Coast"
}
fun expandAreas(areas: ArrayList<Area>, map: HashMap<Vector2, TileInfo>) {
val expandableAreas = ArrayList<Area>(areas)
while (expandableAreas.isNotEmpty()) {
val areaToExpand = expandableAreas.getRandom()
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.getRandom()
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.
* DDoeesn't look very good TBH.
* Doesn't look very good TBH.
*/
open class RandomMapGenerator {
@ -198,8 +246,8 @@ open class RandomMapGenerator {
var resource: TileResource? = null
when {
Math.random() < 1 / 5f -> resource = getRandomResource(tileResources, ResourceType.Bonus)
Math.random() < 1 / 7f -> resource = getRandomResource(tileResources, ResourceType.Strategic)
Math.random() < 1 / 5f -> resource = getRandomResource(tileResources, ResourceType.Bonus)
Math.random() < 1 / 7f -> resource = getRandomResource(tileResources, ResourceType.Strategic)
Math.random() < 1 / 15f -> resource = getRandomResource(tileResources, ResourceType.Luxury)
}
if (resource != null) tileInfo.resource = resource.name
@ -219,6 +267,25 @@ open class RandomMapGenerator {
}
fun maybeAddAncientRuins(tile: TileInfo) {
if(Random().nextDouble() < 1f/100) tile.improvement = "Ancient ruins"
if(tile.getBaseTerrain().type!=TerrainType.Water && Random().nextDouble() < 1f/100)
tile.improvement = "Ancient ruins"
}
}
private fun hasWaterTile(map: HashMap<String, TileInfo>, vector: Vector2): Boolean {
return map.containsKey(vector.toString()) && map[vector.toString()]!!.getBaseTerrain().type == TerrainType.Land
}
fun setWaterTiles(map: HashMap<String, TileInfo>) {
for (tile in map.values.filter { it.baseTerrain == "Ocean" }) {
if (HexMath().GetVectorsInDistance(tile.position,2).any { hasWaterTile(map,it) })
tile.baseTerrain = "Coast"
}
}
fun randomizeTile(tileInfo: TileInfo){
RandomMapGenerator().addRandomTerrainFeature(tileInfo)
RandomMapGenerator().addRandomResourceToTile(tileInfo)
RandomMapGenerator().maybeAddAncientRuins(tileInfo)
}
}

View file

@ -31,7 +31,8 @@ class TileMap {
constructor(distance: Int) {
tileList.addAll(SeedRandomMapGenerator().generateMap(distance,0f).values)
// tileList.addAll(AlexanderRandomMapGenerator().generateMap(distance,0.75f).values)
tileList.addAll(PerlinNoiseRandomMapGenerator().generateMap(distance).values)
setTransients()
}