GameStarter wrap and shape aware (#5107)

- Starting locations code aware of wrap and map shape
- RiverGenerator wrap aware
- RiverGenerator guard against endless loop
This commit is contained in:
SomeTroglodyte 2021-09-06 17:32:12 +02:00 committed by GitHub
parent 7f386da2bc
commit 7118e9779c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 179 additions and 117 deletions

View File

@ -1,6 +1,5 @@
package com.unciv.logic
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.civilization.*
@ -9,12 +8,12 @@ import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.logic.map.mapgenerator.MapGenerator
import com.unciv.models.metadata.GameParameters
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.Era
import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.metadata.GameSetupInfo
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
@ -358,8 +357,10 @@ object GameStarter {
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) {
val freeTiles = landTilesInBigEnoughGroup
.filter { vectorIsAtLeastNTilesAwayFromEdge(it.position, (minimumDistanceBetweenStartingLocations * 2) /3, tileMap) }
.toMutableList()
.filter {
HexMath.getDistanceFromEdge(it.position, tileMap.mapParameters) >=
(minimumDistanceBetweenStartingLocations * 2) /3
}.toMutableList()
val startingLocations = HashMap<CivilizationInfo, TileInfo>()
for (civ in civsOrderedByAvailableLocations) {
@ -446,13 +447,4 @@ object GameStarter {
}
}
}
private fun vectorIsAtLeastNTilesAwayFromEdge(vector: Vector2, n: Int, tileMap: TileMap): Boolean {
// Since all maps are HEXAGONAL, the easiest way of checking if a tile is n steps away from the
// edge is checking the distance to the CENTER POINT
// Can't believe we used a dumb way of calculating this before!
val hexagonalRadius = -tileMap.leftX
val distanceFromCenter = HexMath.getDistance(vector, Vector2.Zero)
return hexagonalRadius - distanceFromCenter >= n
}
}

View File

@ -2,6 +2,8 @@ package com.unciv.logic
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.math.Vector3
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapShape
import kotlin.math.*
@Suppress("MemberVisibilityCanBePrivate", "unused") // this is a library offering optional services
@ -194,4 +196,34 @@ object HexMath {
fun getClockDirectionToWorldVector(clockDirection: Int): Vector2 =
clockToWorldVectors[clockDirection] ?: Vector2.Zero
fun getDistanceFromEdge(vector: Vector2, mapParameters: MapParameters): Int {
val x = vector.x.toInt()
val y = vector.y.toInt()
if (mapParameters.shape == MapShape.rectangular) {
val height = mapParameters.mapSize.height
val width = mapParameters.mapSize.width
val left = if (mapParameters.worldWrap) Int.MAX_VALUE else width / 2 - (x - y)
val right = if (mapParameters.worldWrap) Int.MAX_VALUE else (width - 1) / 2 - (y - x)
val top = height / 2 - (x + y) / 2
// kotlin's Int division rounds in different directions depending on sign! Thus 1 extra `-1`
val bottom = (x + y - 1) / 2 + (height - 1) / 2
return min(min(left, right), min(top, bottom))
} else {
val radius = mapParameters.mapSize.radius
if (mapParameters.worldWrap) {
// The non-wrapping method holds in the upper two and lower two 'triangles' of the hexagon
// but needs special casing for left and right 'wedges', where only distance from the
// 'choke points' counts (upper and lower hex at the 'seam' where height is smallest).
// These are at (radius,0) and (0,-radius)
if (x.sign == y.sign) return radius - getDistance(vector, Vector2.Zero)
// left wedge - the 'choke points' are not wrapped relative to us
if (x > 0) return min(getDistance(vector, Vector2(radius.toFloat(),0f)), getDistance(vector, Vector2(0f, -radius.toFloat())))
// right wedge - compensate wrap by using a hex 1 off along the edge - same result
return min(getDistance(vector, Vector2(1f, radius.toFloat())), getDistance(vector, Vector2(-radius.toFloat(), -1f)))
} else {
return radius - getDistance(vector, Vector2.Zero)
}
}
}
}

View File

@ -276,24 +276,27 @@ open class TileInfo {
fun getTileStartScore(): Float {
var sum = 0f
for (tile in getTilesInDistance(2)) {
if (tile == this)
continue
sum += tile.getTileStartYield()
val tileYield = tile.getTileStartYield(tile == this)
sum += tileYield
if (tile in neighbors)
sum += tile.getTileStartYield()
sum += tileYield
}
if (isHill())
sum -= 2
sum -= 2f
if (isAdjacentToRiver())
sum += 2
sum += 2f
if (neighbors.any { it.baseTerrain == Constants.mountain })
sum += 2
sum += 2f
if (isCoastalTile())
sum += 3f
if (!isCoastalTile() && neighbors.any { it.isCoastalTile() })
sum -= 7f
return sum
}
private fun getTileStartYield(): Float {
private fun getTileStartYield(isCenter: Boolean): Float {
var stats = getBaseTerrain().clone()
for (terrainFeatureBase in getTerrainFeatures()) {
@ -303,7 +306,12 @@ open class TileInfo {
stats.add(terrainFeatureBase)
}
if (resource != null) stats.add(getTileResource())
if (stats.production < 0) stats.production = 0f
if (isCenter) {
if (stats.food < 2) stats.food = 2f
if (stats.production < 1) stats.production = 1f
}
return stats.food + stats.production + stats.gold
}

View File

@ -195,7 +195,7 @@ class TileMap {
}.filterNotNull()
/** @return tile at hex coordinates ([x],[y]) or null if they are outside the map. Respects map edges and world wrap. */
private fun getIfTileExistsOrNull(x: Int, y: Int): TileInfo? {
fun getIfTileExistsOrNull(x: Int, y: Int): TileInfo? {
if (contains(x, y))
return get(x, y)

View File

@ -1,6 +1,5 @@
package com.unciv.logic.map.mapgenerator
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.HexMath
import com.unciv.logic.map.*
@ -9,7 +8,10 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.Terrain
import com.unciv.models.ruleset.tile.TerrainType
import kotlin.math.*
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.sign
import kotlin.random.Random
@ -73,7 +75,7 @@ class MapGenerator(val ruleset: Ruleset) {
NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map)
}
runAndMeasure("RiverGenerator") {
RiverGenerator(randomness).spawnRivers(map)
RiverGenerator(map, randomness).spawnRivers()
}
runAndMeasure("spreadResources") {
spreadResources(map)
@ -459,7 +461,7 @@ class MapGenerator(val ruleset: Ruleset) {
}
class MapGenerationRandomness{
class MapGenerationRandomness {
var RNG = Random(42)
fun seedRNG(seed: Long = 42) {
@ -532,30 +534,3 @@ class MapGenerationRandomness{
throw Exception()
}
}
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
return if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft) {
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 {
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

@ -1,111 +1,167 @@
package com.unciv.logic.map.mapgenerator
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
class RiverGenerator(val randomness: MapGenerationRandomness) {
class RiverGenerator(
private val tileMap: TileMap,
private val randomness: MapGenerationRandomness
) {
companion object{
const val MAP_TILES_PER_RIVER = 100
const val MIN_RIVER_LENGTH = 5
const val MAX_RIVER_LENGTH = 666 // Do not set < max map radius
}
fun spawnRivers(map: TileMap) {
val numberOfRivers = map.values.count { it.isLand } / 100
fun spawnRivers() {
val numberOfRivers = tileMap.values.count { it.isLand } / MAP_TILES_PER_RIVER
var optionalTiles = map.values.asSequence()
.filter { it.baseTerrain == Constants.mountain && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 }.toMutableList()
var optionalTiles = tileMap.values.asSequence()
.filter { it.baseTerrain == Constants.mountain && it.isFarEnoughFromWater() }.toMutableList()
if (optionalTiles.size < numberOfRivers)
optionalTiles.addAll(map.values.filter { it.isHill() && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 })
optionalTiles.addAll(tileMap.values.filter { it.isHill() && it.isFarEnoughFromWater() })
if (optionalTiles.size < numberOfRivers)
optionalTiles = map.values.filter { it.isLand && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 }.toMutableList()
optionalTiles = tileMap.values.filter { it.isLand && it.isFarEnoughFromWater() }.toMutableList()
val mapRadius = map.mapParameters.mapSize.radius
val mapRadius = tileMap.mapParameters.mapSize.radius
val riverStarts = randomness.chooseSpreadOutLocations(numberOfRivers, optionalTiles, mapRadius)
for (tile in riverStarts) spawnRiver(tile, map)
for (tile in riverStarts) spawnRiver(tile)
for (tile in map.values) {
for (tile in tileMap.values) {
if (tile.isAdjacentToRiver()) {
if (tile.baseTerrain == Constants.desert && tile.terrainFeatures.isEmpty()) tile.terrainFeatures.add(Constants.floodPlains)
else if (tile.baseTerrain == Constants.snow) tile.baseTerrain = Constants.tundra
else if (tile.baseTerrain == Constants.tundra) tile.baseTerrain = Constants.plains
when {
tile.baseTerrain == Constants.desert && tile.terrainFeatures.isEmpty() ->
tile.terrainFeatures.add(Constants.floodPlains)
tile.baseTerrain == Constants.snow -> tile.baseTerrain = Constants.tundra
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 TileInfo.isFarEnoughFromWater(): Boolean {
for (distance in 1 until MIN_RIVER_LENGTH) {
if (getTilesAtDistance(distance).any { it.isWater }) return false
}
return true
}
private fun spawnRiver(initialPosition: TileInfo, map: TileMap) {
private fun getClosestWaterTile(tile: TileInfo): TileInfo {
for (distance in 1..MAX_RIVER_LENGTH) {
val waterTiles = tile.getTilesAtDistance(distance).filter { it.isWater }
if (waterTiles.any())
return waterTiles.toList().random(randomness.RNG)
}
throw IllegalStateException()
}
private fun spawnRiver(initialPosition: TileInfo) {
// 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()
.filter { map.contains(it.position) }
for (step in 1..MAX_RIVER_LENGTH) { // Arbitrary max on river length, otherwise this will go in circles - rarely
if (riverCoordinate.getAdjacentTiles(tileMap).any { it.isWater }) return
val possibleCoordinates = riverCoordinate.getAdjacentPositions(tileMap)
if (possibleCoordinates.none()) return // end of the line
val newCoordinate = possibleCoordinates
.groupBy {
getAdjacentTiles(it, map).map { it.aerialDistanceTo(endPosition) }
.groupBy { newCoordinate ->
newCoordinate.getAdjacentTiles(tileMap).map { it.aerialDistanceTo(endPosition) }
.minOrNull()!!
}
.minByOrNull { it.key }!!
.component2().random(randomness.RNG)
// set new rivers in place
val riverCoordinateTile = map[riverCoordinate.position]
val riverCoordinateTile = tileMap[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
if (newCoordinate.getAdjacentTiles(tileMap).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
tileMap[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
if (newCoordinate.getAdjacentTiles(tileMap).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
tileMap[newCoordinate.position].hasBottomRightRiver = true
}
riverCoordinate = newCoordinate
}
println("River reached max length!")
}
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 {
/*
fun numberOfConnectedRivers(riverCoordinate: RiverCoordinate): Int {
var sum = 0
if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomRiver) sum += 1
if (tileMap.contains(riverCoordinate.position) && tileMap[riverCoordinate.position].hasBottomRiver) sum += 1
if (riverCoordinate.bottomRightOrLeft == RiverCoordinate.BottomRightOrLeft.BottomLeft) {
if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomLeftRiver) sum += 1
if (tileMap.contains(riverCoordinate.position) && tileMap[riverCoordinate.position].hasBottomLeftRiver) sum += 1
val bottomLeftTilePosition = riverCoordinate.position.cpy().add(0f, -1f)
if (map.contains(bottomLeftTilePosition) && map[bottomLeftTilePosition].hasBottomRightRiver) sum += 1
if (tileMap.contains(bottomLeftTilePosition) && tileMap[bottomLeftTilePosition].hasBottomRightRiver) sum += 1
} else {
if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomRightRiver) sum += 1
if (tileMap.contains(riverCoordinate.position) && tileMap[riverCoordinate.position].hasBottomRightRiver) sum += 1
val bottomLeftTilePosition = riverCoordinate.position.cpy().add(-1f, 0f)
if (map.contains(bottomLeftTilePosition) && map[bottomLeftTilePosition].hasBottomLeftRiver) sum += 1
if (tileMap.contains(bottomLeftTilePosition) && tileMap[bottomLeftTilePosition].hasBottomLeftRiver) sum += 1
}
return sum
}
*/
/** Describes a _Vertex_ on our hexagonal grid via a neighboring hex and clock direction, normalized
* such that always the north-most hex and one of the two clock directions 5 / 7 o'clock are used. */
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
}
/** Lists the three neighboring vertices which have their anchor hex on the map
* (yes some positions on the map's outer border will be included, some not) */
fun getAdjacentPositions(tileMap: TileMap): Sequence<RiverCoordinate> = sequence {
// 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
val x = position.x.toInt()
val y = position.y.toInt()
if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft) {
yield(RiverCoordinate(position, BottomRightOrLeft.BottomRight)) // same tile, other side
val myTopLeft = tileMap.getIfTileExistsOrNull(x + 1, y)
if (myTopLeft != null)
yield(RiverCoordinate(myTopLeft.position, BottomRightOrLeft.BottomRight)) // tile to MY top-left, take its bottom right corner
val myBottomLeft = tileMap.getIfTileExistsOrNull(x, y - 1)
if (myBottomLeft != null)
yield(RiverCoordinate(myBottomLeft.position, BottomRightOrLeft.BottomRight)) // Tile to MY bottom-left, take its bottom right
} else {
yield(RiverCoordinate(position, BottomRightOrLeft.BottomLeft)) // same tile, other side
val myTopRight = tileMap.getIfTileExistsOrNull(x, y + 1)
if (myTopRight != null)
yield(RiverCoordinate(myTopRight.position, BottomRightOrLeft.BottomLeft)) // tile to MY top-right, take its bottom left
val myBottomRight = tileMap.getIfTileExistsOrNull(x - 1, y)
if (myBottomRight != null)
yield(RiverCoordinate(myBottomRight.position, BottomRightOrLeft.BottomLeft)) // tile to MY bottom-right, take its bottom left
}
}
/** Lists the three neighboring hexes to this vertex which are on the map */
fun getAdjacentTiles(tileMap: TileMap): Sequence<TileInfo> = sequence {
val x = position.x.toInt()
val y = position.y.toInt()
yield(tileMap[x, y])
val below = tileMap.getIfTileExistsOrNull(x - 1, y - 1) // tile directly below us,
if (below != null) yield(below)
val leftOrRight = if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft)
tileMap.getIfTileExistsOrNull(x, y - 1) // tile to our bottom-left
else tileMap.getIfTileExistsOrNull(x - 1, y) // tile to our bottom-right
if (leftOrRight != null) yield(leftOrRight)
}
}
}

View File

@ -11,7 +11,6 @@ import com.unciv.UncivGame
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.ui.cityscreen.YieldGroup
import com.unciv.ui.utils.ImageGetter
import com.unciv.ui.utils.center