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 package com.unciv.logic
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.civilization.* 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.TileMap
import com.unciv.logic.map.mapgenerator.MapGenerator import com.unciv.logic.map.mapgenerator.MapGenerator
import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameParameters
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.Era import com.unciv.models.ruleset.Era
import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.metadata.GameSetupInfo
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
@ -167,7 +166,7 @@ object GameStarter {
availableCivNames.addAll(ruleset.nations.filter { it.value.isMajorCiv() }.keys.shuffled()) availableCivNames.addAll(ruleset.nations.filter { it.value.isMajorCiv() }.keys.shuffled())
availableCivNames.removeAll(newGameParameters.players.map { it.chosenCiv }) availableCivNames.removeAll(newGameParameters.players.map { it.chosenCiv })
availableCivNames.remove(Constants.barbarians) availableCivNames.remove(Constants.barbarians)
val startingTechs = ruleset.technologies.values.filter { it.uniques.contains("Starting tech") } val startingTechs = ruleset.technologies.values.filter { it.uniques.contains("Starting tech") }
if (!newGameParameters.noBarbarians && ruleset.nations.containsKey(Constants.barbarians)) { if (!newGameParameters.noBarbarians && ruleset.nations.containsKey(Constants.barbarians)) {
@ -256,7 +255,7 @@ object GameStarter {
// An unusually bad spawning location // An unusually bad spawning location
addConsolationPrize(gameInfo, startingLocation, 45 - startingLocation.getTileStartScore().toInt()) addConsolationPrize(gameInfo, startingLocation, 45 - startingLocation.getTileStartScore().toInt())
} }
if(civ.isCityState()) if(civ.isCityState())
addCityStateLuxury(gameInfo, startingLocation) addCityStateLuxury(gameInfo, startingLocation)
@ -271,19 +270,19 @@ object GameStarter {
fun placeNearStartingPosition(unitName: String) { fun placeNearStartingPosition(unitName: String) {
civ.placeUnitNearTile(startingLocation.position, unitName) civ.placeUnitNearTile(startingLocation.position, unitName)
} }
// Determine starting units based on starting era // Determine starting units based on starting era
startingUnits = ruleSet.eras[startingEra]!!.getStartingUnits().toMutableList() startingUnits = ruleSet.eras[startingEra]!!.getStartingUnits().toMutableList()
eraUnitReplacement = ruleSet.eras[startingEra]!!.startingMilitaryUnit eraUnitReplacement = ruleSet.eras[startingEra]!!.startingMilitaryUnit
// Add extra units granted by difficulty // Add extra units granted by difficulty
startingUnits.addAll(when { startingUnits.addAll(when {
civ.isPlayerCivilization() -> gameInfo.getDifficulty().playerBonusStartingUnits civ.isPlayerCivilization() -> gameInfo.getDifficulty().playerBonusStartingUnits
civ.isMajorCiv() -> gameInfo.getDifficulty().aiMajorCivBonusStartingUnits civ.isMajorCiv() -> gameInfo.getDifficulty().aiMajorCivBonusStartingUnits
else -> gameInfo.getDifficulty().aiCityStateBonusStartingUnits else -> gameInfo.getDifficulty().aiCityStateBonusStartingUnits
}) })
fun getEquivalentUnit(civ: CivilizationInfo, unitParam: String): String? { fun getEquivalentUnit(civ: CivilizationInfo, unitParam: String): String? {
var unit = unitParam // We want to change it and this is the easiest way to do so var unit = unitParam // We want to change it and this is the easiest way to do so
if (unit == Constants.eraSpecificUnit) unit = eraUnitReplacement if (unit == Constants.eraSpecificUnit) unit = eraUnitReplacement
@ -315,11 +314,11 @@ object GameStarter {
startingUnits.clear() startingUnits.clear()
startingUnits.add(startingSettlers.random()) startingUnits.add(startingSettlers.random())
} }
// One city challengers should spawn with one settler only regardless of era and difficulty // One city challengers should spawn with one settler only regardless of era and difficulty
if (civ.playerType == PlayerType.Human && gameInfo.gameParameters.oneCityChallenge) { if (civ.playerType == PlayerType.Human && gameInfo.gameParameters.oneCityChallenge) {
val startingSettlers = startingUnits.filter { settlerLikeUnits.contains(it) } val startingSettlers = startingUnits.filter { settlerLikeUnits.contains(it) }
startingUnits.removeAll(startingSettlers) startingUnits.removeAll(startingSettlers)
startingUnits.add(startingSettlers.random()) startingUnits.add(startingSettlers.random())
} }
@ -358,8 +357,10 @@ object GameStarter {
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) { for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) {
val freeTiles = landTilesInBigEnoughGroup val freeTiles = landTilesInBigEnoughGroup
.filter { vectorIsAtLeastNTilesAwayFromEdge(it.position, (minimumDistanceBetweenStartingLocations * 2) /3, tileMap) } .filter {
.toMutableList() HexMath.getDistanceFromEdge(it.position, tileMap.mapParameters) >=
(minimumDistanceBetweenStartingLocations * 2) /3
}.toMutableList()
val startingLocations = HashMap<CivilizationInfo, TileInfo>() val startingLocations = HashMap<CivilizationInfo, TileInfo>()
for (civ in civsOrderedByAvailableLocations) { 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.Vector2
import com.badlogic.gdx.math.Vector3 import com.badlogic.gdx.math.Vector3
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapShape
import kotlin.math.* import kotlin.math.*
@Suppress("MemberVisibilityCanBePrivate", "unused") // this is a library offering optional services @Suppress("MemberVisibilityCanBePrivate", "unused") // this is a library offering optional services
@ -20,7 +22,7 @@ object HexMath {
if (size < 0) return 0 if (size < 0) return 0
return 1 + 6 * size * (size + 1) / 2 return 1 + 6 * size * (size + 1) / 2
} }
/** Almost inverse of [getNumberOfTilesInHexagon] - get equivalent fractional Hexagon radius for an Area */ /** Almost inverse of [getNumberOfTilesInHexagon] - get equivalent fractional Hexagon radius for an Area */
fun getHexagonalRadiusForArea(numberOfTiles: Int) = fun getHexagonalRadiusForArea(numberOfTiles: Int) =
if (numberOfTiles < 1) 0f else ((sqrt(12f * numberOfTiles - 3) - 3) / 6) if (numberOfTiles < 1) 0f else ((sqrt(12f * numberOfTiles - 3) - 3) / 6)
@ -194,4 +196,34 @@ object HexMath {
fun getClockDirectionToWorldVector(clockDirection: Int): Vector2 = fun getClockDirectionToWorldVector(clockDirection: Int): Vector2 =
clockToWorldVectors[clockDirection] ?: Vector2.Zero 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 { fun getTileStartScore(): Float {
var sum = 0f var sum = 0f
for (tile in getTilesInDistance(2)) { for (tile in getTilesInDistance(2)) {
if (tile == this) val tileYield = tile.getTileStartYield(tile == this)
continue sum += tileYield
sum += tile.getTileStartYield()
if (tile in neighbors) if (tile in neighbors)
sum += tile.getTileStartYield() sum += tileYield
} }
if (isHill()) if (isHill())
sum -= 2 sum -= 2f
if (isAdjacentToRiver()) if (isAdjacentToRiver())
sum += 2 sum += 2f
if (neighbors.any { it.baseTerrain == Constants.mountain }) 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 return sum
} }
private fun getTileStartYield(): Float { private fun getTileStartYield(isCenter: Boolean): Float {
var stats = getBaseTerrain().clone() var stats = getBaseTerrain().clone()
for (terrainFeatureBase in getTerrainFeatures()) { for (terrainFeatureBase in getTerrainFeatures()) {
@ -303,7 +306,12 @@ open class TileInfo {
stats.add(terrainFeatureBase) stats.add(terrainFeatureBase)
} }
if (resource != null) stats.add(getTileResource()) if (resource != null) stats.add(getTileResource())
if (stats.production < 0) stats.production = 0f 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 return stats.food + stats.production + stats.gold
} }

View File

@ -195,7 +195,7 @@ class TileMap {
}.filterNotNull() }.filterNotNull()
/** @return tile at hex coordinates ([x],[y]) or null if they are outside the map. Respects map edges and world wrap. */ /** @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)) if (contains(x, y))
return get(x, y) return get(x, y)

View File

@ -1,6 +1,5 @@
package com.unciv.logic.map.mapgenerator package com.unciv.logic.map.mapgenerator
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.HexMath import com.unciv.logic.HexMath
import com.unciv.logic.map.* 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.ResourceType
import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.Terrain
import com.unciv.models.ruleset.tile.TerrainType 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 import kotlin.random.Random
@ -19,7 +21,7 @@ class MapGenerator(val ruleset: Ruleset) {
const val consoleOutput = false const val consoleOutput = false
private const val consoleTimings = false private const val consoleTimings = false
} }
private var randomness = MapGenerationRandomness() private var randomness = MapGenerationRandomness()
fun generateMap(mapParameters: MapParameters): TileMap { fun generateMap(mapParameters: MapParameters): TileMap {
@ -73,7 +75,7 @@ class MapGenerator(val ruleset: Ruleset) {
NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map) NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map)
} }
runAndMeasure("RiverGenerator") { runAndMeasure("RiverGenerator") {
RiverGenerator(randomness).spawnRivers(map) RiverGenerator(map, randomness).spawnRivers()
} }
runAndMeasure("spreadResources") { runAndMeasure("spreadResources") {
spreadResources(map) spreadResources(map)
@ -459,7 +461,7 @@ class MapGenerator(val ruleset: Ruleset) {
} }
class MapGenerationRandomness{ class MapGenerationRandomness {
var RNG = Random(42) var RNG = Random(42)
fun seedRNG(seed: Long = 42) { fun seedRNG(seed: Long = 42) {
@ -532,30 +534,3 @@ class MapGenerationRandomness{
throw Exception() 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 package com.unciv.logic.map.mapgenerator
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap 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) { fun spawnRivers() {
val numberOfRivers = map.values.count { it.isLand } / 100 val numberOfRivers = tileMap.values.count { it.isLand } / MAP_TILES_PER_RIVER
var optionalTiles = map.values.asSequence() var optionalTiles = tileMap.values.asSequence()
.filter { it.baseTerrain == Constants.mountain && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 }.toMutableList() .filter { it.baseTerrain == Constants.mountain && it.isFarEnoughFromWater() }.toMutableList()
if (optionalTiles.size < numberOfRivers) 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) 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) 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.isAdjacentToRiver()) {
if (tile.baseTerrain == Constants.desert && tile.terrainFeatures.isEmpty()) tile.terrainFeatures.add(Constants.floodPlains) when {
else if (tile.baseTerrain == Constants.snow) tile.baseTerrain = Constants.tundra tile.baseTerrain == Constants.desert && tile.terrainFeatures.isEmpty() ->
else if (tile.baseTerrain == Constants.tundra) tile.baseTerrain = Constants.plains tile.terrainFeatures.add(Constants.floodPlains)
tile.baseTerrain == Constants.snow -> tile.baseTerrain = Constants.tundra
tile.baseTerrain == Constants.tundra -> tile.baseTerrain = Constants.plains
}
tile.setTerrainTransients() tile.setTerrainTransients()
} }
} }
} }
private fun getClosestWaterTile(tile: TileInfo): TileInfo { private fun TileInfo.isFarEnoughFromWater(): Boolean {
var distance = 1 for (distance in 1 until MIN_RIVER_LENGTH) {
while (true) { if (getTilesAtDistance(distance).any { it.isWater }) return false
val waterTiles = tile.getTilesAtDistance(distance).filter { it.isWater }
if (waterTiles.none()) {
distance++
continue
}
return waterTiles.toList().random(randomness.RNG)
} }
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! // Recommendation: Draw a bunch of hexagons on paper before trying to understand this, it's super helpful!
val endPosition = getClosestWaterTile(initialPosition) val endPosition = getClosestWaterTile(initialPosition)
var riverCoordinate = RiverCoordinate(initialPosition.position, var riverCoordinate = RiverCoordinate(initialPosition.position,
RiverCoordinate.BottomRightOrLeft.values().random(randomness.RNG)) RiverCoordinate.BottomRightOrLeft.values().random(randomness.RNG))
for (step in 1..MAX_RIVER_LENGTH) { // Arbitrary max on river length, otherwise this will go in circles - rarely
while (getAdjacentTiles(riverCoordinate, map).none { it.isWater }) { if (riverCoordinate.getAdjacentTiles(tileMap).any { it.isWater }) return
val possibleCoordinates = riverCoordinate.getAdjacentPositions() val possibleCoordinates = riverCoordinate.getAdjacentPositions(tileMap)
.filter { map.contains(it.position) }
if (possibleCoordinates.none()) return // end of the line if (possibleCoordinates.none()) return // end of the line
val newCoordinate = possibleCoordinates val newCoordinate = possibleCoordinates
.groupBy { .groupBy { newCoordinate ->
getAdjacentTiles(it, map).map { it.aerialDistanceTo(endPosition) } newCoordinate.getAdjacentTiles(tileMap).map { it.aerialDistanceTo(endPosition) }
.minOrNull()!! .minOrNull()!!
} }
.minByOrNull { it.key }!! .minByOrNull { it.key }!!
.component2().random(randomness.RNG) .component2().random(randomness.RNG)
// set new rivers in place // 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 if (newCoordinate.position == riverCoordinate.position) // same tile, switched right-to-left
riverCoordinateTile.hasBottomRiver = true riverCoordinateTile.hasBottomRiver = true
else if (riverCoordinate.bottomRightOrLeft == RiverCoordinate.BottomRightOrLeft.BottomRight) { 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 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! 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 } 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 riverCoordinateTile.hasBottomLeftRiver = true
else // moved from our 7 O'Clock down in the 7 O'Clock direction 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 riverCoordinate = newCoordinate
} }
println("River reached max length!")
} }
fun getAdjacentTiles(riverCoordinate: RiverCoordinate, map: TileMap): Sequence<TileInfo> { /*
val potentialPositions = sequenceOf( fun numberOfConnectedRivers(riverCoordinate: RiverCoordinate): Int {
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 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 (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) 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 { } 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) 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 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.civilization.CivilizationInfo
import com.unciv.logic.map.RoadStatus import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.ui.cityscreen.YieldGroup import com.unciv.ui.cityscreen.YieldGroup
import com.unciv.ui.utils.ImageGetter import com.unciv.ui.utils.ImageGetter
import com.unciv.ui.utils.center import com.unciv.ui.utils.center