River generation is go! =D

This commit is contained in:
Yair Morgenstern 2020-05-31 19:14:16 +03:00
parent 60aeebd3bb
commit ba9329963f
3 changed files with 170 additions and 54 deletions

View File

@ -24,6 +24,7 @@ object Constants {
const val oasis = "Oasis"
const val atoll = "Atoll"
const val ice = "Ice"
const val floodPlains = "Flood plains"
val vegetation = arrayOf(forest, jungle)
val sea = arrayOf(ocean, coast)

View File

@ -44,6 +44,7 @@ class MapGenerator(val ruleset: Ruleset) {
spawnVegetation(map)
spawnRareFeatures(map)
spawnIce(map)
RiverGenerator(randomness).spawnRivers(map)
spreadResources(map)
spreadAncientRuins(map)
NaturalWonderGenerator(ruleset).spawnNaturalWonders(map, randomness)
@ -104,7 +105,7 @@ class MapGenerator(val ruleset: Ruleset) {
if(map.mapParameters.noRuins)
return
val suitableTiles = map.values.filter { it.isLand && !it.getBaseTerrain().impassable }
val locations = chooseSpreadOutLocations(suitableTiles.size/100,
val locations = randomness.chooseSpreadOutLocations(suitableTiles.size/100,
suitableTiles, 10)
for(tile in locations)
tile.improvement = Constants.ancientRuins
@ -135,7 +136,7 @@ class MapGenerator(val ruleset: Ruleset) {
&& resource.terrainsCanBeFoundOn.contains(it.getBaseTerrain().name)
&& (it.terrainFeature==null || ruleset.tileImprovements.containsKey("Remove "+it.terrainFeature)) }
val locations = chooseSpreadOutLocations(resourcesPerType, suitableTiles, distance)
val locations = randomness.chooseSpreadOutLocations(resourcesPerType, suitableTiles, distance)
for (location in locations) location.resource = resource.name
}
@ -152,7 +153,7 @@ class MapGenerator(val ruleset: Ruleset) {
.filter { it.resource == null && resourcesOfType.any { r -> r.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } }
val numberOfResources = tileMap.values.count { it.isLand && !it.getBaseTerrain().impassable } *
tileMap.mapParameters.resourceRichness
val locations = chooseSpreadOutLocations(numberOfResources.toInt(), suitableTiles, distance)
val locations = randomness.chooseSpreadOutLocations(numberOfResources.toInt(), suitableTiles, distance)
val resourceToNumber = Counter<String>()
@ -167,37 +168,6 @@ class MapGenerator(val ruleset: Ruleset) {
}
}
private 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>()
// If possible, we want to equalize the base terrains upon which
// the resources are found, so we save how many have been
// found for each base terrain and try to get one from the lowerst
val baseTerrainsToChosenTiles = HashMap<String, Int>()
for(tileInfo in availableTiles){
if(tileInfo.baseTerrain !in baseTerrainsToChosenTiles)
baseTerrainsToChosenTiles[tileInfo.baseTerrain] = 0
}
for (i in 1..numberOfResources) {
if (availableTiles.isEmpty()) break
val orderedKeys = baseTerrainsToChosenTiles.entries
.sortedBy { it.value }.map { it.key }
val firstKeyWithTilesLeft = orderedKeys
.first { availableTiles.any { tile -> tile.baseTerrain== it} }
val chosenTile = availableTiles.filter { it.baseTerrain==firstKeyWithTilesLeft }.random()
availableTiles = availableTiles.filter { it.aerialDistanceTo(chosenTile) > distanceBetweenResources }
chosenTiles.add(chosenTile)
baseTerrainsToChosenTiles[firstKeyWithTilesLeft] = baseTerrainsToChosenTiles[firstKeyWithTilesLeft]!!+1
}
// 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!")
}
/**
* [MapParameters.elevationExponent] favors high elevation
@ -280,6 +250,7 @@ class MapGenerator(val ruleset: Ruleset) {
val rareFeatures = ruleset.terrains.values.filter {
it.type == TerrainType.TerrainFeature &&
it.name !in Constants.vegetation &&
it.name != Constants.floodPlains &&
it.name != Constants.ice
}
for (tile in tileMap.values.asSequence().filter { it.terrainFeature == null }) {
@ -331,29 +302,63 @@ class MapGenerationRandomness{
val worldCoords = HexMath.hex2WorldCoords(tile.position)
return Perlin.noise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale)
}
fun chooseSpreadOutLocations(number: Int, suitableTiles: List<TileInfo>, initialDistance: Int): ArrayList<TileInfo> {
for (distanceBetweenResources in initialDistance downTo 1) {
var availableTiles = suitableTiles.toList()
val chosenTiles = ArrayList<TileInfo>()
// If possible, we want to equalize the base terrains upon which
// the resources are found, so we save how many have been
// found for each base terrain and try to get one from the lowerst
val baseTerrainsToChosenTiles = HashMap<String, Int>()
for(tileInfo in availableTiles){
if(tileInfo.baseTerrain !in baseTerrainsToChosenTiles)
baseTerrainsToChosenTiles[tileInfo.baseTerrain] = 0
}
for (i in 1..number) {
if (availableTiles.isEmpty()) break
val orderedKeys = baseTerrainsToChosenTiles.entries
.sortedBy { it.value }.map { it.key }
val firstKeyWithTilesLeft = orderedKeys
.first { availableTiles.any { tile -> tile.baseTerrain== it} }
val chosenTile = availableTiles.filter { it.baseTerrain==firstKeyWithTilesLeft }.random()
availableTiles = availableTiles.filter { it.aerialDistanceTo(chosenTile) > distanceBetweenResources }
chosenTiles.add(chosenTile)
baseTerrainsToChosenTiles[firstKeyWithTilesLeft] = baseTerrainsToChosenTiles[firstKeyWithTilesLeft]!!+1
}
// Either we got them all, or we're not going to get anything better
if (chosenTiles.size == number || distanceBetweenResources == 1) return chosenTiles
}
throw Exception("Couldn't choose suitable tiles for $number resources!")
}
}
class RiverGenerator(){
public class RiverCoordinate(val position: Vector2, val bottomRightOrLeft: BottomRightOrLeft){
enum class BottomRightOrLeft{
BottomLeft, BottomRight
}
class RiverCoordinate(val position: Vector2, val bottomRightOrLeft: BottomRightOrLeft){
enum class BottomRightOrLeft{
/** 7 O'Clock of the tile */
BottomLeft,
/** 5 O'Clock of the tile */
BottomRight
}
fun getAdjacentPositions(): Sequence<RiverCoordinate> {
// What's nice is that adjacents are always the OPPOSITE in terms of right-left - rights are adjacent to only lefts, and vice-versa
// This means that a lot of obviously-wrong assignments are simple to spot
if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft) {
return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomRight), // same tile, other side
RiverCoordinate(position.cpy().add(1f, 0f), BottomRightOrLeft.BottomRight), // tile to MY top-left, take its bottom right corner
RiverCoordinate(position.cpy().add(0f, -1f), BottomRightOrLeft.BottomRight) // Tile to MY bottom-left, take its bottom right
)
} else {
return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomLeft), // same tile, other side
RiverCoordinate(position.cpy().add(0f, 1f), BottomRightOrLeft.BottomLeft), // tile to MY top-right, take its bottom left
RiverCoordinate(position.cpy().add(-1f, 0f), BottomRightOrLeft.BottomLeft) // tile to MY bottom-right, take its bottom left
)
}
fun getAdjacentPositions(): Sequence<RiverCoordinate> {
// What's nice is that adjacents are always the OPPOSITE in terms of right-left - rights are adjacent to only lefts, and vice-versa
// This means that a lot of obviously-wrong assignments are simple to spot
if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft) {
return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomRight), // same tile, other side
RiverCoordinate(position.cpy().add(1f, 0f), BottomRightOrLeft.BottomRight), // tile to MY top-left, take its bottom right corner
RiverCoordinate(position.cpy().add(0f, -1f), BottomRightOrLeft.BottomRight) // Tile to MY bottom-left, take its bottom right
)
} else {
return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomLeft), // same tile, other side
RiverCoordinate(position.cpy().add(0f, 1f), BottomRightOrLeft.BottomLeft), // tile to MY top-right, take its bottom left
RiverCoordinate(position.cpy().add(-1f, 0f), BottomRightOrLeft.BottomLeft) // tile to MY bottom-right, take its bottom left
)
}
}
}

View File

@ -0,0 +1,110 @@
package com.unciv.logic.map.mapgenerator
import com.unciv.Constants
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
class RiverGenerator(val randomness: MapGenerationRandomness){
fun spawnRivers(map: TileMap){
val numberOfRivers = map.values.count { it.isLand } / 100
var optionalTiles = map.values
.filter { it.baseTerrain== Constants.mountain && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 }
if(optionalTiles.size < numberOfRivers)
optionalTiles += map.values.filter { it.baseTerrain== Constants.hill && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 }
if(optionalTiles.size < numberOfRivers)
optionalTiles = map.values.filter { it.isLand && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 }
val riverStarts = randomness.chooseSpreadOutLocations(numberOfRivers, optionalTiles, 10)
for(tile in riverStarts) spawnRiver(tile, map)
for(tile in map.values){
if(tile.isAdjacentToRiver()){
if(tile.baseTerrain== Constants.desert) tile.terrainFeature= Constants.floodPlains
else if(tile.baseTerrain== Constants.snow) tile.baseTerrain = Constants.tundra
else if(tile.baseTerrain== Constants.tundra) tile.baseTerrain = Constants.plains
tile.setTerrainTransients()
}
}
}
private fun getClosestWaterTile(tile: TileInfo): TileInfo {
var distance = 1
while(true){
val waterTiles = tile.getTilesAtDistance(distance).filter { it.isWater }
if(waterTiles.none()) {
distance++
continue
}
return waterTiles.toList().random(randomness.RNG)
}
}
private fun spawnRiver(initialPosition: TileInfo, map: TileMap) {
// Recommendation: Draw a bunch of hexagons on paper before trying to understand this, it's super helpful!
val endPosition = getClosestWaterTile(initialPosition)
var riverCoordinate = RiverCoordinate(initialPosition.position,
RiverCoordinate.BottomRightOrLeft.values().random(randomness.RNG))
while(getAdjacentTiles(riverCoordinate,map).none { it.isWater }){
val possibleCoordinates = riverCoordinate.getAdjacentPositions()
if(possibleCoordinates.none()) return // end of the line
val newCoordinate = possibleCoordinates
// .sortedBy { numberOfConnectedRivers(it,map) }
.groupBy { getAdjacentTiles(it,map).map { it.aerialDistanceTo(endPosition) }.min()!! }
.minBy { it.key }!!
.component2().random(randomness.RNG)
// .minBy { getAdjacentTiles(it,map).map { it.aerialDistanceTo(endPosition) }.min()!! }!!
// set new rivers in place
val riverCoordinateTile = map[riverCoordinate.position]
if(newCoordinate.position == riverCoordinate.position) // same tile, switched right-to-left
riverCoordinateTile.hasBottomRiver=true
else if(riverCoordinate.bottomRightOrLeft== RiverCoordinate.BottomRightOrLeft.BottomRight){
if(getAdjacentTiles(newCoordinate,map).contains(riverCoordinateTile)) // moved from our 5 O'Clock to our 3 O'Clock
riverCoordinateTile.hasBottomRightRiver = true
else // moved from our 5 O'Clock down in the 5 O'Clock direction - this is the 8 O'Clock river of the tile to our 4 O'Clock!
map[newCoordinate.position].hasBottomLeftRiver = true
}
else { // riverCoordinate.bottomRightOrLeft==RiverCoordinate.BottomRightOrLeft.Left
if(getAdjacentTiles(newCoordinate,map).contains(riverCoordinateTile)) // moved from our 7 O'Clock to our 9 O'Clock
riverCoordinateTile.hasBottomLeftRiver = true
else // moved from our 7 O'Clock down in the 7 O'Clock direction
map[newCoordinate.position].hasBottomRightRiver = true
}
riverCoordinate = newCoordinate
}
}
fun getAdjacentTiles(riverCoordinate: RiverCoordinate, map: TileMap): Sequence<TileInfo> {
val potentialPositions = sequenceOf(
riverCoordinate.position,
riverCoordinate.position.cpy().add(-1f, -1f), // tile directly below us,
if (riverCoordinate.bottomRightOrLeft == RiverCoordinate.BottomRightOrLeft.BottomLeft)
riverCoordinate.position.cpy().add(0f, -1f) // tile to our bottom-left
else riverCoordinate.position.cpy().add(-1f, 0f) // tile to our bottom-right
)
return potentialPositions.map { if (map.contains(it)) map[it] else null }.filterNotNull()
}
fun numberOfConnectedRivers(riverCoordinate: RiverCoordinate, map: TileMap): Int {
var sum = 0
if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomRiver) sum += 1
if (riverCoordinate.bottomRightOrLeft == RiverCoordinate.BottomRightOrLeft.BottomLeft) {
if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomLeftRiver) sum += 1
val bottomLeftTilePosition = riverCoordinate.position.cpy().add(0f, -1f)
if (map.contains(bottomLeftTilePosition) && map[bottomLeftTilePosition].hasBottomRightRiver) sum += 1
} else {
if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomRightRiver) sum += 1
val bottomLeftTilePosition = riverCoordinate.position.cpy().add(-1f, 0f)
if (map.contains(bottomLeftTilePosition) && map[bottomLeftTilePosition].hasBottomLeftRiver) sum += 1
}
return sum
}
}