chore: Separated start normalization into a separate object

This commit is contained in:
Yair Morgenstern 2023-10-02 12:26:35 +03:00
parent f52e7d37f4
commit e1a33f2116
2 changed files with 363 additions and 293 deletions

View File

@ -18,7 +18,6 @@ import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.models.translations.equalsPlaceholderText
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.ui.components.extensions.randomWeighted
@ -240,30 +239,7 @@ class MapRegions (val ruleset: Ruleset){
fun assignRegions(tileMap: TileMap, civilizations: List<Civilization>, gameParameters: GameParameters) {
if (civilizations.isEmpty()) return
// first assign region types
val regionTypes = ruleset.terrains.values.filter { getRegionPriority(it) != null }
.sortedBy { getRegionPriority(it) }
for (region in regions) {
region.countTerrains()
for (type in regionTypes) {
// Test exclusion criteria first
if (type.getMatchingUniques(UniqueType.RegionRequireFirstLessThanSecond).any {
region.getTerrainAmount(it.params[0]) >= region.getTerrainAmount(it.params[1]) } ) {
continue
}
// Test inclusion criteria
if (type.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).any {
region.getTerrainAmount(it.params[1]) >= (it.params[0].toInt() * region.tiles.size) / 100 }
|| type.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).any {
region.getTerrainAmount(it.params[1]) + region.getTerrainAmount(it.params[2]) >= (it.params[0].toInt() * region.tiles.size) / 100 }
) {
region.type = type.name
break
}
}
}
assignRegionTypes()
// Generate tile data for all tiles
for (tile in tileMap.values) {
@ -273,13 +249,9 @@ class MapRegions (val ruleset: Ruleset){
// Sort regions by fertility so the worse regions get to pick first
val sortedRegions = regions.sortedBy { it.totalFertility }
// Find a start for each region
for (region in sortedRegions) {
findStart(region)
}
// Normalize starts
for (region in sortedRegions) findStart(region)
for (region in regions) {
normalizeStart(tileMap[region.startPosition!!], tileMap, isMinorCiv = false)
StartNormalizer.normalizeStart(tileMap[region.startPosition!!], tileMap, tileData, ruleset, isMinorCiv = false)
}
val civBiases = civilizations.associateWith { ruleset.nations[it.civName]!!.startBias }
@ -415,6 +387,36 @@ class MapRegions (val ruleset: Ruleset){
}
}
/** Sets region.type */
private fun assignRegionTypes() {
val regionTypes = ruleset.terrains.values.filter { getRegionPriority(it) != null }
.sortedBy { getRegionPriority(it) }
for (region in regions) {
region.countTerrains()
for (type in regionTypes) {
// Test exclusion criteria first
if (type.getMatchingUniques(UniqueType.RegionRequireFirstLessThanSecond).any {
region.getTerrainAmount(it.params[0]) >= region.getTerrainAmount(it.params[1])
}) {
continue
}
// Test inclusion criteria
if (type.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).any {
region.getTerrainAmount(it.params[1]) >= (it.params[0].toInt() * region.tiles.size) / 100
}
|| type.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).any {
region.getTerrainAmount(it.params[1]) + region.getTerrainAmount(it.params[2]) >= (it.params[0].toInt() * region.tiles.size) / 100
}
) {
region.type = type.name
break
}
}
}
}
private fun logAssignRegion(success: Boolean, startBiasType: BiasTypes, civ: Civilization, region: Region? = null) {
if (Log.backend.isRelease()) return
@ -561,260 +563,6 @@ class MapRegions (val ruleset: Ruleset){
setRegionStart(region, panicPosition)
}
/** Attempts to improve the start on [startTile] as needed to make it decent.
* Relies on startPosition having been set previously.
* Assumes unchanged baseline values ie citizens eat 2 food each, similar production costs
* If [isMinorCiv] is true, different weightings will be used. */
private fun normalizeStart(startTile: Tile, tileMap: TileMap, isMinorCiv: Boolean) {
// Remove ice-like features adjacent to start
for (tile in startTile.neighbors) {
val lastTerrain = tile.terrainFeatureObjects.lastOrNull { it.impassable }
if (lastTerrain != null) {
tile.removeTerrainFeature(lastTerrain.name)
}
}
// evaluate production potential
val innerProduction = startTile.neighbors.sumOf { getPotentialYield(it, Stat.Production).toInt() }
val outerProduction = startTile.getTilesAtDistance(2).sumOf { getPotentialYield(it, Stat.Production).toInt() }
// for very early production we ideally want tiles that also give food
val earlyProduction = startTile.getTilesInDistanceRange(1..2).sumOf {
if (getPotentialYield(it, Stat.Food, unimproved = true) > 0f) getPotentialYield(it, Stat.Production, unimproved = true).toInt()
else 0 }
// If terrible, try adding a hill to a dry flat tile
if (innerProduction == 0 || (innerProduction < 2 && outerProduction < 8) || (isMinorCiv && innerProduction < 4)) {
val hillSpot = startTile.neighbors
.filter { it.isLand && it.terrainFeatures.isEmpty() && !it.isAdjacentTo(Constants.freshWater) && !it.isImpassible() }
.toList().randomOrNull()
val hillEquivalent = ruleset.terrains.values
.firstOrNull { it.type == TerrainType.TerrainFeature && it.production >= 2 && !it.hasUnique(UniqueType.RareFeature) }?.name
if (hillSpot != null && hillEquivalent != null) {
hillSpot.addTerrainFeature(hillEquivalent)
}
}
// Place Strategic Balance Resources
if (tileMap.mapParameters.mapResources == MapResources.strategicBalance) {
val candidateTiles = startTile.getTilesInDistanceRange(1..2).shuffled() + startTile.getTilesAtDistance(3).shuffled()
for (resource in ruleset.tileResources.values.filter { it.hasUnique(UniqueType.StrategicBalanceResource) }) {
if (MapRegionResources.tryAddingResourceToTiles(tileData, resource, 1, candidateTiles, majorDeposit = true) == 0) {
// Fallback mode - force placement, even on an otherwise inappropriate terrain. Do still respect water and impassible tiles!
val resourceTiles =
if (isWaterOnlyResource(resource)) candidateTiles.filter { it.isWater && !it.isImpassible() }.toList()
else candidateTiles.filter { it.isLand && !it.isImpassible() }.toList()
MapRegionResources.placeResourcesInTiles(tileData, 999, resourceTiles, listOf(resource), majorDeposit = true, forcePlacement = true)
}
}
}
// If bad early production, add a small strategic resource to SECOND ring (not for minors)
if (!isMinorCiv && innerProduction < 3 && earlyProduction < 6) {
val lastEraNumber = ruleset.eras.values.maxOf { it.eraNumber }
val earlyEras = ruleset.eras.filterValues { it.eraNumber <= lastEraNumber / 3 }
val validResources = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Strategic &&
(it.revealedBy == null ||
ruleset.technologies[it.revealedBy]!!.era() in earlyEras)
}.shuffled()
val candidateTiles = startTile.getTilesAtDistance(2).shuffled()
for (resource in validResources) {
if (MapRegionResources.tryAddingResourceToTiles(tileData, resource, 1, candidateTiles, majorDeposit = false) > 0)
break
}
}
val foodBonusesNeeded = calculateFoodBonusesNeeded(startTile, isMinorCiv, tileMap)
placeFoodBonuses(isMinorCiv, startTile, foodBonusesNeeded)
// Minor civs are done, go on with grassiness checks for major civs
if (isMinorCiv) return
addProductionBonuses(startTile)
}
/** Check for very food-heavy starts that might still need some stone to help with production */
private fun addProductionBonuses(startTile: Tile) {
val grassTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
it.isLand &&
getPotentialYield(it, Stat.Food, unimproved = true) >= 2f && // Food neutral natively
getPotentialYield(it, Stat.Production) == 0f // Production can't even be improved
}.toMutableList()
val plainsTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
it.isLand &&
getPotentialYield(it, Stat.Food) >= 2f && // Something that can be improved to food neutral
getPotentialYield(it, Stat.Production, unimproved = true) >= 1f // Some production natively
}.toList()
var productionBonusesNeeded = when {
grassTypePlots.size >= 9 && plainsTypePlots.isEmpty() -> 2
grassTypePlots.size >= 6 && plainsTypePlots.size <= 4 -> 1
else -> 0
}
val productionBonuses =
ruleset.tileResources.values.filter { it.resourceType == ResourceType.Bonus && it.production > 0 }
if (productionBonuses.isNotEmpty()) {
while (productionBonusesNeeded > 0 && grassTypePlots.isNotEmpty()) {
val plot = grassTypePlots.random()
grassTypePlots.remove(plot)
if (plot.resource != null) continue
val bonusToPlace =
productionBonuses.filter { plot.lastTerrain.name in it.terrainsCanBeFoundOn }
.randomOrNull()
if (bonusToPlace != null) {
plot.resource = bonusToPlace.name
productionBonusesNeeded--
}
}
}
}
private fun calculateFoodBonusesNeeded(
startTile: Tile,
minorCiv: Boolean,
tileMap: TileMap
): Int {
// evaluate food situation
// Food²/4 because excess food is really good and lets us work other tiles or run specialists!
// 2F is worth 1, 3F is worth 2, 4F is worth 4, 5F is worth 6 and so on
val innerFood =
startTile.neighbors.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
val outerFood = startTile.getTilesAtDistance(2)
.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
val totalFood = innerFood + outerFood
// we want at least some two-food tiles to keep growing
val innerNativeTwoFood =
startTile.neighbors.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
val outerNativeTwoFood = startTile.getTilesAtDistance(2)
.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
val totalNativeTwoFood = innerNativeTwoFood + outerNativeTwoFood
// Determine number of needed bonuses. Different weightings for minor and major civs.
var bonusesNeeded = if (minorCiv) {
when { // From 2 to 0
totalFood < 12 || innerFood < 4 -> 2
totalFood < 16 || innerFood < 9 -> 1
else -> 0
}
} else {
when { // From 5 to 0
innerFood == 0 && totalFood < 4 -> 5
totalFood < 6 -> 4
totalFood < 8 ||
(totalFood < 12 && innerFood < 5) -> 3
(totalFood < 17 && innerFood < 9) ||
totalNativeTwoFood < 2 -> 2
(totalFood < 24 && innerFood < 11) ||
totalNativeTwoFood == 2 ||
innerNativeTwoFood == 0 ||
totalFood < 20 -> 1
else -> 0
}
}
if (tileMap.mapParameters.mapResources == MapResources.legendaryStart)
bonusesNeeded += 2
// Attempt to place one grassland at a plains-only spot (nor for minors)
if (!minorCiv && bonusesNeeded < 3 && totalNativeTwoFood == 0) {
val twoFoodTerrain =
ruleset.terrains.values.firstOrNull { it.type == TerrainType.Land && it.food >= 2 }?.name
val candidateInnerSpots = startTile.neighbors
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
val candidateOuterSpots = startTile.getTilesAtDistance(2)
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
val spot =
candidateInnerSpots.shuffled().firstOrNull() ?: candidateOuterSpots.shuffled()
.firstOrNull()
if (twoFoodTerrain != null && spot != null) {
spot.baseTerrain = twoFoodTerrain
} else
bonusesNeeded = 3 // Irredeemable plains situation
}
return bonusesNeeded
}
private fun placeFoodBonuses(
minorCiv: Boolean,
startTile: Tile,
foodBonusesNeeded: Int
) {
var bonusesStillNeeded = foodBonusesNeeded
val oasisEquivalent = ruleset.terrains.values.firstOrNull {
it.type == TerrainType.TerrainFeature &&
it.hasUnique(UniqueType.RareFeature) &&
it.food >= 2 &&
it.food + it.production + it.gold >= 3 &&
it.occursOn.any { base -> ruleset.terrains[base]!!.type == TerrainType.Land }
}
var canPlaceOasis =
oasisEquivalent != null // One oasis per start is enough. Don't bother finding a place if there is no good oasis equivalent
var placedInFirst = 0 // Attempt to put first 2 in inner ring and next 3 in second ring
var placedInSecond = 0
val rangeForBonuses = if (minorCiv) 2 else 3
// Start with list of candidate plots sorted in ring order 1,2,3
val candidatePlots = startTile.getTilesInDistanceRange(1..rangeForBonuses)
.filter { it.resource == null && oasisEquivalent !in it.terrainFeatureObjects }
.shuffled().sortedBy { it.aerialDistanceTo(startTile) }.toMutableList()
// Place food bonuses (and oases) as able
while (bonusesStillNeeded > 0 && candidatePlots.isNotEmpty()) {
val plot = candidatePlots.first()
candidatePlots.remove(plot) // remove the plot as it has now been tried, whether successfully or not
if (plot.getBaseTerrain().hasUnique(
UniqueType.BlocksResources,
StateForConditionals(attackedTile = plot)
)
)
continue // Don't put bonuses on snow hills
val validBonuses = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Bonus &&
it.food >= 1 &&
plot.lastTerrain.name in it.terrainsCanBeFoundOn
}
val goodPlotForOasis =
canPlaceOasis && plot.lastTerrain.name in oasisEquivalent!!.occursOn
if (validBonuses.isNotEmpty() || goodPlotForOasis) {
if (goodPlotForOasis) {
plot.addTerrainFeature(oasisEquivalent!!.name)
canPlaceOasis = false
} else {
plot.setTileResource(validBonuses.random())
}
if (plot.aerialDistanceTo(startTile) == 1) {
placedInFirst++
if (placedInFirst == 2) // Resort the list in ring order 2,3,1
candidatePlots.sortBy { abs(it.aerialDistanceTo(startTile) * 10 - 22) }
} else if (plot.aerialDistanceTo(startTile) == 2) {
placedInSecond++
if (placedInSecond == 3) // Resort the list in ring order 3,1,2
candidatePlots.sortByDescending { abs(it.aerialDistanceTo(startTile) * 10 - 17) }
}
bonusesStillNeeded--
}
}
}
private fun getPotentialYield(tile: Tile, stat: Stat, unimproved: Boolean = false): Float {
val baseYield = tile.stats.getTileStats(null)[stat]
if (unimproved) return baseYield
val bestImprovementYield = tile.tileMap.ruleset!!.tileImprovements.values
.filter { !it.hasUnique(UniqueType.GreatImprovement) &&
it.uniqueTo == null &&
tile.lastTerrain.name in it.terrainsCanBeBuiltOn }
.maxOfOrNull { it[stat] }
return baseYield + (bestImprovementYield ?: 0f)
}
/** @returns the region most similar to a region of [type] */
private fun getFallbackRegion(type: String, candidates: List<Region>): Region {
return candidates.maxByOrNull { it.terrainCounts[type] ?: 0 }!!
@ -1183,7 +931,7 @@ class MapRegions (val ruleset: Ruleset){
tileData.placeImpact(ImpactType.Strategic,tile, 0)
tileData.placeImpact(ImpactType.Bonus, tile, 3)
normalizeStart(tile, tileMap, isMinorCiv = true)
StartNormalizer.normalizeStart(tile, tileMap, tileData, ruleset, isMinorCiv = true)
}
/** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */
@ -1247,7 +995,7 @@ class MapRegions (val ruleset: Ruleset){
for (region in regions) {
val resource = ruleset.tileResources[region.luxury] ?: continue
fun Tile.isShoreOfContinent(continent: Int) = isWater && neighbors.any { it.getContinent() == continent }
val candidates = if (isWaterOnlyResource(resource))
val candidates = if (isWaterOnlyResource(resource, ruleset))
tileMap.getTilesInRectangle(region.rect).filter { it.isShoreOfContinent(region.continentID) }
else region.tiles.asSequence()
MapRegionResources.tryAddingResourceToTiles(tileData, resource, regionTargetNumber, candidates.shuffled(), 0.4f, true, 4, 2)
@ -1460,7 +1208,7 @@ class MapRegions (val ruleset: Ruleset){
for (resource in strategicResources) {
val extraNeeded = min(2, regions.size - totalPlaced[resource]!!)
if (extraNeeded > 0) {
if (isWaterOnlyResource(resource))
if (isWaterOnlyResource(resource, ruleset))
MapRegionResources.tryAddingResourceToTiles(tileData, resource, extraNeeded, tileMap.values.asSequence().filter { it.isWater }.shuffled(), respectImpacts = true)
else
MapRegionResources.tryAddingResourceToTiles(tileData, resource, extraNeeded, landList.asSequence(), respectImpacts = true)
@ -1472,9 +1220,9 @@ class MapRegions (val ruleset: Ruleset){
// Sixth place bonus resources (and other resources that might have been assigned frequency-based generation).
// Water-based bonuses go last and have extra impact, because coasts are very common and we don't want too much clustering
val sortedResourceList = ruleset.tileResources.values.sortedBy { isWaterOnlyResource(it) }
val sortedResourceList = ruleset.tileResources.values.sortedBy { isWaterOnlyResource(it, ruleset) }
for (resource in sortedResourceList) {
val extraImpact = if (isWaterOnlyResource(resource)) 1 else 0
val extraImpact = if (isWaterOnlyResource(resource, ruleset)) 1 else 0
for (rule in resource.uniqueObjects.filter { it.type == UniqueType.ResourceFrequency }) {
// Figure out which list applies, if any
val simpleRule = anonymizeUnique(rule)
@ -1518,9 +1266,6 @@ class MapRegions (val ruleset: Ruleset){
}
}
private fun isWaterOnlyResource(resource: TileResource) = resource.terrainsCanBeFoundOn
.all { terrainName -> ruleset.terrains[terrainName]!!.type == TerrainType.Water }
enum class ImpactType {
Strategic,
Luxury,
@ -1535,6 +1280,9 @@ class MapRegions (val ruleset: Ruleset){
internal fun anonymizeUnique(unique: Unique) = Unique(
"RULE" + unique.conditionals.sortedBy { it.text }.joinToString(prefix = " ", separator = " ") { "<" + it.text + ">" })
internal fun isWaterOnlyResource(resource: TileResource, ruleset: Ruleset) = resource.terrainsCanBeFoundOn
.all { terrainName -> ruleset.terrains[terrainName]!!.type == TerrainType.Water }
/** @return a fake unique with conditionals that will satisfy the same conditions as terrainsCanBeFoundOn */
internal fun getTerrainRule(terrain: Terrain, ruleset: Ruleset): Unique {

View File

@ -0,0 +1,322 @@
package com.unciv.logic.map.mapgenerator.mapregions
import com.unciv.Constants
import com.unciv.logic.map.MapResources
import com.unciv.logic.map.TileMap
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import kotlin.math.abs
import kotlin.math.pow
/** Ensures that starting positions of civs have enough yield that they aren't at a disadvantage */
object StartNormalizer {
/** Attempts to improve the start on [startTile] as needed to make it decent.
* Relies on startPosition having been set previously.
* Assumes unchanged baseline values ie citizens eat 2 food each, similar production costs
* If [isMinorCiv] is true, different weightings will be used. */
fun normalizeStart(startTile: Tile, tileMap: TileMap, tileData: TileDataMap, ruleset: Ruleset, isMinorCiv: Boolean) {
// Remove ice-like features adjacent to start
for (tile in startTile.neighbors) {
val lastTerrain = tile.terrainFeatureObjects.lastOrNull { it.impassable }
if (lastTerrain != null) {
tile.removeTerrainFeature(lastTerrain.name)
}
}
if (tileMap.mapParameters.mapResources == MapResources.strategicBalance)
placeStrategicBalanceResources(startTile, ruleset, tileData)
normalizeProduction(startTile, isMinorCiv, ruleset, tileData)
val foodBonusesNeeded = calculateFoodBonusesNeeded(startTile, isMinorCiv, ruleset, tileMap)
placeFoodBonuses(isMinorCiv, startTile, ruleset, foodBonusesNeeded)
// Minor civs are done, go on with grassiness checks for major civs
if (isMinorCiv) return
addProductionBonuses(startTile, ruleset)
}
private fun normalizeProduction(
startTile: Tile,
isMinorCiv: Boolean,
ruleset: Ruleset,
tileData: TileDataMap
) {
// evaluate production potential
val innerProduction =
startTile.neighbors.sumOf { getPotentialYield(it, Stat.Production).toInt() }
val outerProduction =
startTile.getTilesAtDistance(2).sumOf { getPotentialYield(it, Stat.Production).toInt() }
// for very early production we ideally want tiles that also give food
val earlyProduction = startTile.getTilesInDistanceRange(1..2).sumOf {
if (getPotentialYield(it, Stat.Food, unimproved = true) > 0f) getPotentialYield(
it,
Stat.Production,
unimproved = true
).toInt()
else 0
}
// If terrible, try adding a hill to a dry flat tile
if (innerProduction == 0 || (innerProduction < 2 && outerProduction < 8) || (isMinorCiv && innerProduction < 4)) {
val hillSpot = startTile.neighbors
.filter { it.isLand && it.terrainFeatures.isEmpty() && !it.isAdjacentTo(Constants.freshWater) && !it.isImpassible() }
.toList().randomOrNull()
val hillEquivalent = ruleset.terrains.values
.firstOrNull {
it.type == TerrainType.TerrainFeature && it.production >= 2 && !it.hasUnique(
UniqueType.RareFeature
)
}?.name
if (hillSpot != null && hillEquivalent != null) {
hillSpot.addTerrainFeature(hillEquivalent)
}
}
// If bad early production, add a small strategic resource to SECOND ring (not for minors)
if (!isMinorCiv && innerProduction < 3 && earlyProduction < 6) {
val lastEraNumber = ruleset.eras.values.maxOf { it.eraNumber }
val earlyEras = ruleset.eras.filterValues { it.eraNumber <= lastEraNumber / 3 }
val validResources = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Strategic &&
(it.revealedBy == null ||
ruleset.technologies[it.revealedBy]!!.era() in earlyEras)
}.shuffled()
val candidateTiles = startTile.getTilesAtDistance(2).shuffled()
for (resource in validResources) {
val resourcesAdded = MapRegionResources.tryAddingResourceToTiles(
tileData, resource, 1, candidateTiles, majorDeposit = false)
if (resourcesAdded > 0) break
}
}
}
private fun placeStrategicBalanceResources(
startTile: Tile,
ruleset: Ruleset,
tileData: TileDataMap
) {
val candidateTiles =
startTile.getTilesInDistanceRange(1..2).shuffled() + startTile.getTilesAtDistance(3)
.shuffled()
for (resource in ruleset.tileResources.values.filter { it.hasUnique(UniqueType.StrategicBalanceResource) }) {
if (MapRegionResources.tryAddingResourceToTiles(
tileData,
resource,
1,
candidateTiles,
majorDeposit = true
) == 0
) {
// Fallback mode - force placement, even on an otherwise inappropriate terrain. Do still respect water and impassible tiles!
val resourceTiles =
if (isWaterOnlyResource(
resource,
ruleset
)
) candidateTiles.filter { it.isWater && !it.isImpassible() }.toList()
else candidateTiles.filter { it.isLand && !it.isImpassible() }.toList()
MapRegionResources.placeResourcesInTiles(
tileData,
999,
resourceTiles,
listOf(resource),
majorDeposit = true,
forcePlacement = true
)
}
}
}
/** Check for very food-heavy starts that might still need some stone to help with production */
private fun addProductionBonuses(startTile: Tile, ruleset: Ruleset) {
val grassTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
it.isLand &&
getPotentialYield(it, Stat.Food, unimproved = true) >= 2f && // Food neutral natively
getPotentialYield(it, Stat.Production) == 0f // Production can't even be improved
}.toMutableList()
val plainsTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
it.isLand &&
getPotentialYield(it, Stat.Food) >= 2f && // Something that can be improved to food neutral
getPotentialYield(it, Stat.Production, unimproved = true) >= 1f // Some production natively
}.toList()
var productionBonusesNeeded = when {
grassTypePlots.size >= 9 && plainsTypePlots.isEmpty() -> 2
grassTypePlots.size >= 6 && plainsTypePlots.size <= 4 -> 1
else -> 0
}
val productionBonuses =
ruleset.tileResources.values.filter { it.resourceType == ResourceType.Bonus && it.production > 0 }
if (productionBonuses.isNotEmpty()) {
while (productionBonusesNeeded > 0 && grassTypePlots.isNotEmpty()) {
val plot = grassTypePlots.random()
grassTypePlots.remove(plot)
if (plot.resource != null) continue
val bonusToPlace =
productionBonuses.filter { plot.lastTerrain.name in it.terrainsCanBeFoundOn }
.randomOrNull()
if (bonusToPlace != null) {
plot.resource = bonusToPlace.name
productionBonusesNeeded--
}
}
}
}
private fun calculateFoodBonusesNeeded(
startTile: Tile,
minorCiv: Boolean,
ruleset: Ruleset,
tileMap: TileMap
): Int {
// evaluate food situation
// Food²/4 because excess food is really good and lets us work other tiles or run specialists!
// 2F is worth 1, 3F is worth 2, 4F is worth 4, 5F is worth 6 and so on
val innerFood =
startTile.neighbors.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
val outerFood = startTile.getTilesAtDistance(2)
.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
val totalFood = innerFood + outerFood
// we want at least some two-food tiles to keep growing
val innerNativeTwoFood =
startTile.neighbors.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
val outerNativeTwoFood = startTile.getTilesAtDistance(2)
.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
val totalNativeTwoFood = innerNativeTwoFood + outerNativeTwoFood
// Determine number of needed bonuses. Different weightings for minor and major civs.
var bonusesNeeded = if (minorCiv) {
when { // From 2 to 0
totalFood < 12 || innerFood < 4 -> 2
totalFood < 16 || innerFood < 9 -> 1
else -> 0
}
} else {
when { // From 5 to 0
innerFood == 0 && totalFood < 4 -> 5
totalFood < 6 -> 4
totalFood < 8 ||
(totalFood < 12 && innerFood < 5) -> 3
(totalFood < 17 && innerFood < 9) ||
totalNativeTwoFood < 2 -> 2
(totalFood < 24 && innerFood < 11) ||
totalNativeTwoFood == 2 ||
innerNativeTwoFood == 0 ||
totalFood < 20 -> 1
else -> 0
}
}
if (tileMap.mapParameters.mapResources == MapResources.legendaryStart)
bonusesNeeded += 2
// Attempt to place one grassland at a plains-only spot (nor for minors)
if (!minorCiv && bonusesNeeded < 3 && totalNativeTwoFood == 0) {
val twoFoodTerrain =
ruleset.terrains.values.firstOrNull { it.type == TerrainType.Land && it.food >= 2 }?.name
val candidateInnerSpots = startTile.neighbors
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
val candidateOuterSpots = startTile.getTilesAtDistance(2)
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
val spot =
candidateInnerSpots.shuffled().firstOrNull() ?: candidateOuterSpots.shuffled()
.firstOrNull()
if (twoFoodTerrain != null && spot != null) {
spot.baseTerrain = twoFoodTerrain
} else
bonusesNeeded = 3 // Irredeemable plains situation
}
return bonusesNeeded
}
private fun placeFoodBonuses(
minorCiv: Boolean,
startTile: Tile,
ruleset: Ruleset,
foodBonusesNeeded: Int
) {
var bonusesStillNeeded = foodBonusesNeeded
val oasisEquivalent = ruleset.terrains.values.firstOrNull {
it.type == TerrainType.TerrainFeature &&
it.hasUnique(UniqueType.RareFeature) &&
it.food >= 2 &&
it.food + it.production + it.gold >= 3 &&
it.occursOn.any { base -> ruleset.terrains[base]!!.type == TerrainType.Land }
}
var canPlaceOasis =
oasisEquivalent != null // One oasis per start is enough. Don't bother finding a place if there is no good oasis equivalent
var placedInFirst = 0 // Attempt to put first 2 in inner ring and next 3 in second ring
var placedInSecond = 0
val rangeForBonuses = if (minorCiv) 2 else 3
// Start with list of candidate plots sorted in ring order 1,2,3
val candidatePlots = startTile.getTilesInDistanceRange(1..rangeForBonuses)
.filter { it.resource == null && oasisEquivalent !in it.terrainFeatureObjects }
.shuffled().sortedBy { it.aerialDistanceTo(startTile) }.toMutableList()
// Place food bonuses (and oases) as able
while (bonusesStillNeeded > 0 && candidatePlots.isNotEmpty()) {
val plot = candidatePlots.first()
candidatePlots.remove(plot) // remove the plot as it has now been tried, whether successfully or not
if (plot.getBaseTerrain().hasUnique(
UniqueType.BlocksResources,
StateForConditionals(attackedTile = plot)
)
)
continue // Don't put bonuses on snow hills
val validBonuses = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Bonus &&
it.food >= 1 &&
plot.lastTerrain.name in it.terrainsCanBeFoundOn
}
val goodPlotForOasis =
canPlaceOasis && plot.lastTerrain.name in oasisEquivalent!!.occursOn
if (validBonuses.isNotEmpty() || goodPlotForOasis) {
if (goodPlotForOasis) {
plot.addTerrainFeature(oasisEquivalent!!.name)
canPlaceOasis = false
} else {
plot.setTileResource(validBonuses.random())
}
if (plot.aerialDistanceTo(startTile) == 1) {
placedInFirst++
if (placedInFirst == 2) // Resort the list in ring order 2,3,1
candidatePlots.sortBy { abs(it.aerialDistanceTo(startTile) * 10 - 22) }
} else if (plot.aerialDistanceTo(startTile) == 2) {
placedInSecond++
if (placedInSecond == 3) // Resort the list in ring order 3,1,2
candidatePlots.sortByDescending { abs(it.aerialDistanceTo(startTile) * 10 - 17) }
}
bonusesStillNeeded--
}
}
}
private fun getPotentialYield(tile: Tile, stat: Stat, unimproved: Boolean = false): Float {
val baseYield = tile.stats.getTileStats(null)[stat]
if (unimproved) return baseYield
val bestImprovementYield = tile.tileMap.ruleset!!.tileImprovements.values
.filter { !it.hasUnique(UniqueType.GreatImprovement) &&
it.uniqueTo == null &&
tile.lastTerrain.name in it.terrainsCanBeBuiltOn }
.maxOfOrNull { it[stat] }
return baseYield + (bestImprovementYield ?: 0f)
}
}