Map generation and start locations (#4588)

* cellular automata for mountains and hills

* cellular automata for mountains and hills

* tweaks

* spawn location algorithm

* consolation prizes

* improve city state spawns

* AI settle in place if possible

* make heightmap generation mod agnostic
This commit is contained in:
SimonCeder
2021-07-21 13:41:48 +02:00
committed by GitHub
parent 08954c1bd1
commit 2971e6bba3
6 changed files with 249 additions and 19 deletions

View File

@ -75,4 +75,7 @@ object Constants {
const val barbarians = "Barbarians"
const val spectator = "Spectator"
const val custom = "Custom"
const val rising = "Rising"
const val lowering = "Lowering"
}

View File

@ -11,6 +11,7 @@ import com.unciv.models.metadata.GameParameters
import com.unciv.models.ruleset.Era
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.ui.newgamescreen.GameSetupInfo
import java.util.*
import kotlin.NoSuchElementException
@ -180,7 +181,24 @@ object GameStarter {
val startingEra = gameInfo.gameParameters.startingEra
var startingUnits: MutableList<String>
var eraUnitReplacement: String
// First we get start locations for the major civs, on the second pass the city states (without predetermined starts) can squeeze in wherever
// I hear copying code is good
val cityStatesWithStartingLocations =
gameInfo.tileMap.values
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
.map { it.improvement!!.replace("StartingLocation ", "") }
val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in cityStatesWithStartingLocations) }
val bestLocations = getStartingLocations(bestCivs, gameInfo.tileMap)
for (civ in bestCivs)
{
if (civ.isCityState()) // Already have explicit starting locations
continue
// Mark the best start locations so we remember them for the second pass
bestLocations[civ]!!.improvement = "StartingLocation " + civ.civName
}
val startingLocations = getStartingLocations(
gameInfo.civilizations.filter { !it.isBarbarian() },
gameInfo.tileMap)
@ -188,10 +206,18 @@ object GameStarter {
val settlerLikeUnits = ruleSet.units.filter {
it.value.uniqueObjects.any { it.placeholderText == Constants.settlerUnique }
}
// no starting units for Barbarians and Spectators
for (civ in gameInfo.civilizations.filter { !it.isBarbarian() && !it.isSpectator() }) {
val startingLocation = startingLocations[civ]!!
if(civ.isMajorCiv() && startingLocation.getTileStartScore() < 45) {
// An unusually bad spawning location
addConsolationPrize(gameInfo, startingLocation, 45 - startingLocation.getTileStartScore().toInt())
}
if(civ.isCityState())
addCityStateLuxury(gameInfo, startingLocation)
for (tile in startingLocation.getTilesInDistance(3))
if (tile.improvement == Constants.ancientRuins)
tile.improvement = null // Remove ancient ruins in immediate vicinity
@ -296,27 +322,36 @@ object GameStarter {
val tilesWithStartingLocations = tileMap.values
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
val civsOrderedByAvailableLocations = civs.sortedBy { civ ->
val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start
.sortedBy { civ ->
when {
tilesWithStartingLocations.any { it.improvement == "StartingLocation " + civ.civName } -> 1 // harshest requirements
civ.nation.startBias.isNotEmpty() -> 2 // less harsh
else -> 3
civ.nation.startBias.contains("Tundra") -> 2 // Tundra starts are hard to find, so let's do them first
civ.nation.startBias.isNotEmpty() -> 3 // less harsh
else -> 4
} // no requirements
}
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 3 downTo 0) {
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) {
val freeTiles = landTilesInBigEnoughGroup
.filter { vectorIsAtLeastNTilesAwayFromEdge(it.position, minimumDistanceBetweenStartingLocations, tileMap) }
.filter { vectorIsAtLeastNTilesAwayFromEdge(it.position, (minimumDistanceBetweenStartingLocations * 2) /3, tileMap) }
.toMutableList()
val startingLocations = HashMap<CivilizationInfo, TileInfo>()
for (civ in civsOrderedByAvailableLocations) {
var startingLocation: TileInfo
val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement == "StartingLocation " + civ.civName }
var distanceToNext = minimumDistanceBetweenStartingLocations
if (presetStartingLocation != null) startingLocation = presetStartingLocation
else {
if (freeTiles.isEmpty()) break // we failed to get all the starting tiles with this minimum distance
if (civ.isCityState())
distanceToNext = minimumDistanceBetweenStartingLocations / 2 // We allow random city states to squeeze in tighter
freeTiles.sortBy { it.getTileStartScore() }
var preferredTiles = freeTiles.toList()
for (startBias in civ.nation.startBias) {
@ -327,10 +362,10 @@ object GameStarter {
else preferredTiles = preferredTiles.filter { it.matchesTerrainFilter(startBias) }
}
startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.random() else freeTiles.random()
startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.last() else freeTiles.last()
}
startingLocations[civ] = startingLocation
freeTiles.removeAll(tileMap.getTilesInDistance(startingLocation.position, minimumDistanceBetweenStartingLocations))
freeTiles.removeAll(tileMap.getTilesInDistance(startingLocation.position, distanceToNext))
}
if (startingLocations.size < civs.size) continue // let's try again with less minimum distance!
@ -339,6 +374,53 @@ object GameStarter {
throw Exception("Didn't manage to get starting tiles even with distance of 1?")
}
private fun addConsolationPrize(gameInfo: GameInfo, spawn: TileInfo, points: Int) {
val relevantTiles = spawn.getTilesInDistanceRange(1..2).shuffled()
var addedPoints = 0
var addedBonuses = 0
for (tile in relevantTiles) {
if (addedPoints >= points || addedBonuses >= 4) // At some point enough is enough
break
if (tile.resource != null || tile.baseTerrain == Constants.snow) // Snow is quite irredeemable
continue
val bonusToAdd = gameInfo.ruleSet.tileResources.values
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) && it.resourceType == ResourceType.Bonus }
.randomOrNull()
if (bonusToAdd != null) {
tile.resource = bonusToAdd.name
addedPoints += (bonusToAdd.food + bonusToAdd.production + bonusToAdd.gold + 1).toInt() // +1 because resources can be improved
addedBonuses++
}
}
}
private fun addCityStateLuxury(gameInfo: GameInfo, spawn: TileInfo) {
// Every city state should have at least one luxury to trade
val relevantTiles = spawn.getTilesInDistance(2).shuffled()
for (tile in relevantTiles) {
if(tile.resource != null && tile.getTileResource().resourceType == ResourceType.Luxury)
return // At least one luxury; all set
}
for (tile in relevantTiles) {
// Add a luxury to the first eligible tile
if (tile.resource != null)
continue
val luxuryToAdd = gameInfo.ruleSet.tileResources.values
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) && it.resourceType == ResourceType.Luxury }
.randomOrNull()
if (luxuryToAdd != null) {
tile.resource = luxuryToAdd.name
return
}
}
}
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

View File

@ -146,6 +146,14 @@ object SpecificUnitAutomation {
}
fun automateSettlerActions(unit: MapUnit) {
if (unit.civInfo.gameInfo.turns == 0) { // Special case, we want AI to settle in place on turn 1.
val foundCityAction = UnitActions.getFoundCityAction(unit, unit.getTile())
if(foundCityAction?.action != null) {
foundCityAction.action.invoke()
return
}
}
if (unit.getTile().militaryUnit == null) return // Don't move until you're accompanied by a military unit
val tilesNearCities = unit.civInfo.gameInfo.getCities().asSequence()

View File

@ -278,6 +278,41 @@ open class TileInfo {
return stats
}
fun getTileStartScore(): Float {
var sum = 0f
for (tile in getTilesInDistance(2)) {
if (tile == this)
continue
sum += tile.getTileStartYield()
if (tile in neighbors)
sum += tile.getTileStartYield()
}
if (isHill())
sum -= 2
if (isAdjacentToRiver())
sum += 2
if (neighbors.any { it.baseTerrain == Constants.mountain })
sum += 2
return sum
}
private fun getTileStartYield(): Float {
var stats = getBaseTerrain().clone()
for (terrainFeatureBase in getTerrainFeatures()) {
if (terrainFeatureBase.overrideStats)
stats = terrainFeatureBase.clone()
else
stats.add(terrainFeatureBase)
}
if (resource != null) stats.add(getTileResource())
if (stats.production < 0) stats.production = 0f
return stats.food + stats.production + stats.gold
}
fun getImprovementStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats {
val stats = improvement.clone() // clones the stats of the improvement, not the improvement itself
if (hasViewableResource(observingCiv) && getTileResource().improvement == improvement.name)

View File

@ -8,10 +8,7 @@ import com.unciv.models.Counter
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TerrainType
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.sign
import kotlin.math.*
import kotlin.random.Random
@ -179,6 +176,20 @@ class MapGenerator(val ruleset: Ruleset) {
* [MapParameters.elevationExponent] favors high elevation
*/
private fun raiseMountainsAndHills(tileMap: TileMap) {
val mountain = ruleset.terrains.values.filter { it.uniques.contains("Occurs in chains at high elevations") }.firstOrNull()?.name
val hill = ruleset.terrains.values.filter { it.uniques.contains("Occurs in groups around high elevations") }.firstOrNull()?.name
val flat = ruleset.terrains.values.filter { !it.impassable && it.type == TerrainType.Land && !it.uniques.contains("Rough Terrain") }.firstOrNull()?.name
if (flat == null) {
println("Ruleset seems to contain no flat terrain - can't generate heightmap")
return
}
if (mountain != null)
println("Mountainlike generation for " + mountain)
if (hill != null)
println("Hill-like generation for " + hill)
val elevationSeed = randomness.RNG.nextInt().toDouble()
tileMap.setTransients(ruleset)
for (tile in tileMap.values.filter { !it.isWater }) {
@ -186,12 +197,102 @@ class MapGenerator(val ruleset: Ruleset) {
elevation = abs(elevation).pow(1.0 - tileMap.mapParameters.elevationExponent.toDouble()) * elevation.sign
when {
elevation <= 0.5 -> tile.baseTerrain = Constants.plains
elevation <= 0.7 -> tile.terrainFeatures.add(Constants.hill)
elevation <= 1.0 -> tile.baseTerrain = Constants.mountain
elevation <= 0.5 -> tile.baseTerrain = flat
elevation <= 0.7 && hill != null -> tile.terrainFeatures.add(hill)
elevation <= 0.7 && hill == null -> tile.baseTerrain = flat // otherwise would be hills become mountains
elevation <= 1.0 && mountain != null -> tile.baseTerrain = mountain
}
tile.setTerrainTransients()
}
if (mountain != null)
cellularMountainRanges(tileMap, mountain, hill, flat)
if (hill != null)
cellularHills(tileMap, mountain, hill)
}
private fun cellularMountainRanges(tileMap: TileMap, mountain: String, hill: String?, flat: String) {
val targetMountains = tileMap.values.count { it.baseTerrain == mountain } * 2
for (i in 1..5) {
var totalMountains = tileMap.values.count { it.baseTerrain == mountain }
for (tile in tileMap.values.filter { !it.isWater }) {
val adjacentMountains =
tile.neighbors.count { it.baseTerrain == mountain }
val adjacentImpassible =
tile.neighbors.count { ruleset.terrains[it.baseTerrain]?.impassable == true }
if (adjacentMountains == 0 && tile.baseTerrain == mountain) {
if (randomness.RNG.nextInt(until = 4) == 0)
tile.terrainFeatures.add(Constants.lowering)
} else if (adjacentMountains == 1) {
if (randomness.RNG.nextInt(until = 10) == 0)
tile.terrainFeatures.add(Constants.rising)
} else if (adjacentImpassible == 3) {
if (randomness.RNG.nextInt(until = 2) == 0)
tile.terrainFeatures.add(Constants.lowering)
} else if (adjacentImpassible > 3) {
tile.terrainFeatures.add(Constants.lowering)
}
}
for (tile in tileMap.values.filter { !it.isWater }) {
if (tile.terrainFeatures.remove(Constants.rising) && totalMountains < targetMountains) {
if (hill != null)
tile.terrainFeatures.remove(hill)
tile.baseTerrain = mountain
totalMountains++
}
if (tile.terrainFeatures.remove(Constants.lowering) && totalMountains > targetMountains * 0.5f) {
if (tile.baseTerrain == mountain) {
if (hill != null && !tile.terrainFeatures.contains(hill))
tile.terrainFeatures.add(hill)
totalMountains--
}
tile.baseTerrain = flat
}
}
}
}
private fun cellularHills(tileMap: TileMap, mountain: String?, hill: String) {
val targetHills = tileMap.values.count { it.terrainFeatures.contains(hill) }
for (i in 1..5) {
var totalHills = tileMap.values.count { it.terrainFeatures.contains(hill) }
for (tile in tileMap.values.filter { !it.isWater && (mountain == null || it.baseTerrain != mountain) }) {
val adjacentMountains = if (mountain == null) 0 else
tile.neighbors.count { it.baseTerrain == mountain }
val adjacentHills =
tile.neighbors.count { it.terrainFeatures.contains(hill) }
if (adjacentHills <= 1 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) {
tile.terrainFeatures.add(Constants.lowering)
} else if (adjacentHills > 3 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) {
tile.terrainFeatures.add(Constants.lowering)
} else if (adjacentHills + adjacentMountains in 2..3 && randomness.RNG.nextInt(until = 2) == 0) {
tile.terrainFeatures.add(Constants.rising)
}
}
for (tile in tileMap.values.filter { !it.isWater && (mountain == null || it.baseTerrain != mountain) }) {
if (tile.terrainFeatures.remove(Constants.rising) && (totalHills <= targetHills || i == 1) ) {
if (!tile.terrainFeatures.contains(hill)) {
tile.terrainFeatures.add(hill)
totalHills++
}
}
if (tile.terrainFeatures.remove(Constants.lowering) && (totalHills >= targetHills * 0.9f || i == 1)) {
if (tile.terrainFeatures.contains(hill)) {
tile.terrainFeatures.remove(hill)
totalHills--
}
}
}
}
}
/**