City-States Influence rates; Wary status; Proximity calculations (#5198)

* Rates for natural influence change

* Minor civ wariness, proximity calculation

* CS can declare permanent war

* CS can in fact not declare permanent war

* adjustments, template.properties

* neater code

* fix failing test? .

* move proximity code, for reals fix failing check

* now?

* revisions

* BFS only once, better check for water map

* assign continents on pre-made maps as well

* now works on all pre-made maps
This commit is contained in:
SimonCeder
2021-09-14 10:01:43 +02:00
committed by GitHub
parent 297618706c
commit 7bd555ac95
11 changed files with 297 additions and 48 deletions

View File

@ -182,6 +182,7 @@ Diplomatic Marriage ([amount] Gold) =
We have married into the ruling family of [civName], bringing them under our control. =
[civName] has married into the ruling family of [civName2], bringing them under their control. =
You have broken your Pledge to Protect [civName]! =
City-States grow wary of your aggression. The resting point for Influence has decreased by [amount] for [civName]. =
Cultured =
Maritime =

View File

@ -33,23 +33,29 @@ object GameStarter {
// In the case where we used to have a mod, and now we don't, we cannot "unselect" it in the UI.
// We need to remove the dead mods so there aren't problems later.
gameSetupInfo.gameParameters.mods.removeAll{ !RulesetCache.containsKey(it) }
gameSetupInfo.gameParameters.mods.removeAll { !RulesetCache.containsKey(it) }
gameInfo.gameParameters = gameSetupInfo.gameParameters
val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods)
val mapGen = MapGenerator(ruleset)
if (gameSetupInfo.mapParameters.name != "") runAndMeasure("loadMap") {
tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!)
// Don't override the map parameters - this can include if we world wrap or not!
} else runAndMeasure("generateMap") {
tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters)
tileMap = mapGen.generateMap(gameSetupInfo.mapParameters)
tileMap.mapParameters = gameSetupInfo.mapParameters
}
runAndMeasure("addCivilizations") {
gameInfo.tileMap = tileMap
tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map
addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics
tileMap.gameInfo =
gameInfo // need to set this transient before placing units in the map
addCivilizations(
gameSetupInfo.gameParameters,
gameInfo,
ruleset
) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics
}
runAndMeasure("Remove units") {
@ -78,6 +84,11 @@ object GameStarter {
addCivStats(gameInfo)
}
runAndMeasure("assignContinents?") {
if (tileMap.continentSizes.isEmpty()) // Probably saved map without continent data
mapGen.assignContinents(tileMap)
}
runAndMeasure("addCivStartingUnits") {
// and only now do we add units for everyone, because otherwise both the gameInfo.setTransients() and the placeUnit will both add the unit to the civ's unit list!
addCivStartingUnits(gameInfo)
@ -334,19 +345,23 @@ object GameStarter {
}
}
private fun getStartingLocations(civs: List<CivilizationInfo>, tileMap: TileMap, startScores: HashMap<TileInfo, Float>): HashMap<CivilizationInfo, TileInfo> {
var landTiles = tileMap.values
val landTilesInBigEnoughGroup = tileMap.landTilesInBigEnoughGroup
if (landTilesInBigEnoughGroup.isEmpty()) {
// Worst case - a pre-made map with continent data. This means we didn't re-run assignContinents,
// so we don't have a cached landTilesInBigEnoughGroup. So we need to do it the hard way.
var landTiles = tileMap.values
// Games starting on snow might as well start over...
.filter { it.isLand && !it.isImpassible() && it.baseTerrain != Constants.snow }
val landTilesInBigEnoughGroup = ArrayList<TileInfo>()
while (landTiles.any()) {
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
bfs.stepToEnd()
val tilesInGroup = bfs.getReachedTiles()
landTiles = landTiles.filter { it !in tilesInGroup }
if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on
landTilesInBigEnoughGroup.addAll(tilesInGroup)
while (landTiles.any()) {
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
bfs.stepToEnd()
val tilesInGroup = bfs.getReachedTiles()
landTiles = landTiles.filter { it !in tilesInGroup }
if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on
landTilesInBigEnoughGroup.addAll(tilesInGroup)
}
}
val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start

View File

@ -3,6 +3,7 @@ package com.unciv.logic.city
import com.badlogic.gdx.math.Vector2
import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.Proximity
import com.unciv.logic.civilization.ReligionState
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.map.RoadStatus
@ -129,6 +130,18 @@ class CityInfo {
population.autoAssignPopulation()
cityStats.update()
// Update proximity rankings for all civs
for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) {
if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors
civInfo.updateProximity(otherCiv,
otherCiv.updateProximity(civInfo))
}
for (otherCiv in civInfo.gameInfo.getAliveCityStates()) {
if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors
civInfo.updateProximity(otherCiv,
otherCiv.updateProximity(civInfo))
}
triggerCitiesSettledNearOtherCiv()
}
@ -556,6 +569,16 @@ class CityInfo {
if (isCapital() && civInfo.cities.isNotEmpty()) { // Move the capital if destroyed (by a nuke or by razing)
civInfo.cities.first().cityConstructions.addBuilding(capitalCityIndicator())
}
// Update proximity rankings for all civs
for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) {
civInfo.updateProximity(otherCiv,
otherCiv.updateProximity(civInfo))
}
for (otherCiv in civInfo.gameInfo.getAliveCityStates()) {
civInfo.updateProximity(otherCiv,
otherCiv.updateProximity(civInfo))
}
}
fun annexCity() = CityInfoConquestFunctions(this).annexCity()

View File

@ -276,6 +276,10 @@ class CityInfoConquestFunctions(val city: CityInfo){
tryUpdateRoadStatus()
cityStats.update()
// Update proximity rankings
civInfo.updateProximity(oldCiv,
oldCiv.updateProximity(civInfo))
}
}

View File

@ -2,17 +2,16 @@ package com.unciv.logic.civilization
import com.unciv.Constants
import com.unciv.logic.automation.NextTurnAutomation
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.civilization.diplomacy.*
import com.unciv.models.metadata.GameSpeed
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.stats.Stat
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText
import com.unciv.ui.victoryscreen.RankingType
import java.util.*
import kotlin.collections.HashMap
import kotlin.collections.HashSet
import kotlin.collections.LinkedHashMap
import kotlin.math.max
import kotlin.math.min
@ -506,10 +505,58 @@ class CityStateFunctions(val civInfo: CivilizationInfo) {
}
}
/** A city state was attacked. What are its protectors going to do about it??? */
/** A city state was attacked. What are its protectors going to do about it??? Also checks for Wary */
fun cityStateAttacked(attacker: CivilizationInfo) {
if (!civInfo.isCityState()) return // What are we doing here?
// We might become wary!
if (attacker.isMinorCivWarmonger()) { // They've attacked a lot of city-states
civInfo.getDiplomacyManager(attacker).becomeWary()
}
else if (attacker.isMinorCivAggressor()) { // They've attacked a few
if (Random().nextBoolean()) { // 50% chance
civInfo.getDiplomacyManager(attacker).becomeWary()
}
}
// Others might become wary!
if (attacker.isMinorCivAggressor()) {
for (cityState in civInfo.gameInfo.getAliveCityStates()) {
if (cityState == civInfo) // Must be a different minor
continue
if (cityState.getAllyCiv() == attacker.civName) // Must not be allied to the attacker
continue
if (!cityState.knows(attacker)) // Must have met
continue
var probability: Int
if (attacker.isMinorCivWarmonger()) {
// High probability if very aggressive
probability = when (cityState.getProximity(attacker)) {
Proximity.Neighbors -> 100
Proximity.Close -> 75
Proximity.Far -> 50
Proximity.Distant -> 25
else -> 0
}
} else {
// Lower probability if only somewhat aggressive
probability = when (cityState.getProximity(attacker)) {
Proximity.Neighbors -> 50
Proximity.Close -> 20
else -> 0
}
}
// Higher probability if already at war
if (cityState.isAtWarWith(attacker))
probability += 50
if (Random().nextInt(100) <= probability) {
cityState.getDiplomacyManager(attacker).becomeWary()
}
}
}
for (protector in civInfo.getProtectorCivs()) {
if (!protector.knows(attacker)) // Who?
continue

View File

@ -10,9 +10,7 @@ import com.unciv.logic.civilization.RuinsManager.RuinsManager
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomacyManager
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.UnitMovementAlgorithms
import com.unciv.logic.map.*
import com.unciv.logic.trade.TradeEvaluation
import com.unciv.logic.trade.TradeRequest
import com.unciv.models.Counter
@ -34,6 +32,14 @@ import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
enum class Proximity {
None, // ie no cities
Neighbors,
Close,
Far,
Distant
}
class CivilizationInfo {
@Transient
@ -109,6 +115,7 @@ class CivilizationInfo {
var victoryManager = VictoryManager()
var ruinsManager = RuinsManager()
var diplomacy = HashMap<String, DiplomacyManager>()
var proximity = HashMap<String, Proximity>()
var notifications = ArrayList<Notification>()
val popupAlerts = ArrayList<PopupAlert>()
private var allyCivName: String? = null
@ -143,6 +150,9 @@ class CivilizationInfo {
// default false once we no longer want legacy save-game compatibility
var hasEverOwnedOriginalCapital: Boolean? = null
// For Aggressor, Warmonger status
private var numMinorCivsAttacked = 0
constructor()
constructor(civName: String) {
@ -167,6 +177,7 @@ class CivilizationInfo {
toReturn.allyCivName = allyCivName
for (diplomacyManager in diplomacy.values.map { it.clone() })
toReturn.diplomacy[diplomacyManager.otherCivName] = diplomacyManager
toReturn.proximity.putAll(proximity)
toReturn.cities = cities.map { it.clone() }
// This is the only thing that is NOT switched out, which makes it a source of ConcurrentModification errors.
@ -187,6 +198,7 @@ class CivilizationInfo {
toReturn.boughtConstructionsWithGloballyIncreasingPrice.putAll(boughtConstructionsWithGloballyIncreasingPrice)
//
toReturn.hasEverOwnedOriginalCapital = hasEverOwnedOriginalCapital
toReturn.numMinorCivsAttacked = numMinorCivsAttacked
return toReturn
}
@ -199,6 +211,9 @@ class CivilizationInfo {
fun getDiplomacyManager(civInfo: CivilizationInfo) = getDiplomacyManager(civInfo.civName)
fun getDiplomacyManager(civName: String) = diplomacy[civName]!!
fun getProximity(civInfo: CivilizationInfo) = getProximity(civInfo.civName)
fun getProximity(civName: String) = proximity[civName] ?: Proximity.None
/** Returns only undefeated civs, aka the ones we care about */
fun getKnownCivs() = diplomacy.values.map { it.otherCiv() }.filter { !it.isDefeated() }
fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName)
@ -556,6 +571,9 @@ class CivilizationInfo {
fun hasTechOrPolicy(techOrPolicyName: String) =
tech.isResearched(techOrPolicyName) || policies.isAdopted(techOrPolicyName)
fun isMinorCivAggressor() = numMinorCivsAttacked >= 2
fun isMinorCivWarmonger() = numMinorCivsAttacked >= 4
//endregion
//region state-changing functions
@ -612,6 +630,10 @@ class CivilizationInfo {
updateDetailedCivResources()
}
fun changeMinorCivsAttacked(count: Int) {
numMinorCivsAttacked += count
}
// implementation in a separate class, to not clog up CivInfo
fun initialSetCitiesConnectedToCapitalTransients() = transients().updateCitiesConnectedToCapital(true)
fun updateHasActiveGreatWall() = transients().updateHasActiveGreatWall()
@ -912,6 +934,81 @@ class CivilizationInfo {
).toInt()
}
fun updateProximity(otherCiv: CivilizationInfo, preCalculated: Proximity? = null): Proximity {
if (otherCiv == this) return Proximity.None
if (preCalculated != null) {
// We usually want to update this for a pair of civs at the same time
// Since this function *should* be symmetrical for both civs, we can just do it once
this.proximity[otherCiv.civName] = preCalculated
return preCalculated
}
if (cities.isEmpty() || otherCiv.cities.isEmpty()) {
proximity[otherCiv.civName] = Proximity.None
return Proximity.None
}
val mapParams = gameInfo.tileMap.mapParameters
var minDistance = 100000 // a long distance
var totalDistance = 0
var connections = 0
var proximity = Proximity.None
for (ourCity in cities) {
for (theirCity in otherCiv.cities) {
val distance = ourCity.getCenterTile().aerialDistanceTo(theirCity.getCenterTile())
totalDistance += distance
connections++
if (minDistance > distance) minDistance = distance
}
}
if (minDistance <= 7) {
proximity = Proximity.Neighbors
} else if (connections > 0) {
val averageDistance = totalDistance / connections
val mapFactor = if (mapParams.shape == MapShape.rectangular)
(mapParams.mapSize.height + mapParams.mapSize.width) / 2
else (mapParams.mapSize.radius * 3) / 2 // slightly less area than equal size rect
val closeDistance = ((mapFactor * 25) / 100).coerceIn(10, 20)
val farDistance = ((mapFactor * 45) / 100).coerceIn(20, 50)
proximity = if (minDistance <= 11 && averageDistance <= closeDistance)
Proximity.Close
else if (averageDistance <= farDistance)
Proximity.Far
else
Proximity.Distant
}
// Check if different continents (unless already max distance, or water map)
if (connections > 0 && proximity != Proximity.Distant
&& !gameInfo.tileMap.isWaterMap()) {
if (getCapital().getCenterTile().getContinent() != otherCiv.getCapital().getCenterTile().getContinent()) {
// Different continents - increase separation by one step
proximity = when (proximity) {
Proximity.Far -> Proximity.Distant
Proximity.Close -> Proximity.Far
Proximity.Neighbors -> Proximity.Close
else -> proximity
}
}
}
// If there aren't many players (left) we can't be that far
val numMajors = gameInfo.getAliveMajorCivs().count()
if (numMajors <= 2 && proximity > Proximity.Close)
proximity = Proximity.Close
if (numMajors <= 4 && proximity > Proximity.Far)
proximity = Proximity.Far
this.proximity[otherCiv.civName] = proximity
return proximity
}
//////////////////////// City State wrapper functions ////////////////////////
fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection<String>)

View File

@ -9,6 +9,7 @@ import com.unciv.logic.trade.TradeType
import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText
import com.unciv.ui.utils.toPercent
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
@ -49,7 +50,8 @@ enum class DiplomacyFlags {
RememberAttackedProtectedMinor,
RememberBulliedProtectedMinor,
RememberSidedWithProtectedMinor,
Denunciation
Denunciation,
WaryOf,
}
enum class DiplomaticModifiers {
@ -238,6 +240,9 @@ class DiplomacyManager() {
restingPoint += unique.params[0].toInt()
if (diplomaticStatus == DiplomaticStatus.Protector) restingPoint += 10
if (hasFlag(DiplomacyFlags.WaryOf)) restingPoint -= 20
return restingPoint
}
@ -245,47 +250,47 @@ class DiplomacyManager() {
if (influence < getCityStateInfluenceRestingPoint())
return 0f
val decrement = when (civInfo.cityStatePersonality) {
CityStatePersonality.Hostile -> 1.5f
else -> 1f
}
var modifier = when (civInfo.cityStatePersonality) {
CityStatePersonality.Hostile -> 2f
CityStatePersonality.Irrational -> 1.5f
CityStatePersonality.Friendly -> .5f
val decrement = when {
civInfo.cityStatePersonality == CityStatePersonality.Hostile -> 1.5f
otherCiv().isMinorCivAggressor() -> 2f
else -> 1f
}
var modifierPercent = 0f
for (unique in otherCiv().getMatchingUniques("City-State Influence degrades []% slower"))
modifier *= 1f - unique.params[0].toFloat() / 100f
modifierPercent -= unique.params[0].toFloat()
val religion = if (civInfo.cities.isEmpty()) null
else civInfo.getCapital().religion.getMajorityReligionName()
if (religion != null && religion == otherCiv().religionManager.religion?.name)
modifierPercent -= 25f // 25% slower degrade when sharing a religion
for (civ in civInfo.gameInfo.civilizations.filter { it.isMajorCiv() && it != otherCiv()}) {
for (unique in civ.getMatchingUniques("Influence of all other civilizations with all city-states degrades []% faster")) {
modifier *= 1f + unique.params[0].toFloat() / 100f
modifierPercent += unique.params[0].toFloat()
}
}
return max(0f, decrement) * max(0f, modifier)
return max(0f, decrement) * max(-100f, modifierPercent).toPercent()
}
private fun getCityStateInfluenceRecovery(): Float {
if (influence > getCityStateInfluenceRestingPoint())
return 0f
val increment = 1f
val increment = 1f // sic: personality does not matter here
var modifier = when (civInfo.cityStatePersonality) {
CityStatePersonality.Friendly -> 2f
CityStatePersonality.Irrational -> 1.5f
CityStatePersonality.Hostile -> .5f
else -> 1f
}
var modifierPercent = 0f
if (otherCiv().hasUnique("City-State Influence recovers at twice the normal rate"))
modifier *= 2f
modifierPercent += 100f
return max(0f, increment) * max(0f, modifier)
val religion = if (civInfo.cities.isEmpty()) null
else civInfo.getCapital().religion.getMajorityReligionName()
if (religion != null && religion == otherCiv().religionManager.religion?.name)
modifierPercent += 50f // 50% quicker recovery when sharing a religion
return max(0f, increment) * max(0f, modifierPercent).toPercent()
}
fun canDeclareWar() = turnsToPeaceTreaty() == 0 && diplomaticStatus != DiplomaticStatus.War
@ -642,6 +647,7 @@ class DiplomacyManager() {
otherCivDiplomacy.setModifier(DiplomaticModifiers.DeclaredWarOnUs, -20f)
if (otherCiv.isCityState()) {
otherCivDiplomacy.setInfluence(-60f)
civInfo.changeMinorCivsAttacked(1)
otherCiv.cityStateAttacked(civInfo)
}
@ -832,5 +838,11 @@ class DiplomacyManager() {
otherCivDiplomacy().setFlag(DiplomacyFlags.RememberSidedWithProtectedMinor, 25)
}
fun becomeWary() {
if (hasFlag(DiplomacyFlags.WaryOf)) return // once is enough
setFlag(DiplomacyFlags.WaryOf, -1) // Never expires
otherCiv().addNotification("City-States grow wary of your aggression. The resting point for Influence has decreased by [20] for [${civInfo.civName}].", civInfo.civName)
}
//endregion
}

View File

@ -23,10 +23,13 @@ class BFS(
tilesReached[startingPoint] = startingPoint
}
/** Process fully until there's nowhere left to check */
fun stepToEnd() {
/** Process fully until there's nowhere left to check
* Optionally assigns a continent ID as it goes */
fun stepToEnd(continent: Int? = null) {
if (continent != null)
startingPoint.setContinent(continent)
while (!hasEnded())
nextStep()
nextStep(continent)
}
/**
@ -46,13 +49,15 @@ class BFS(
*
* Will do nothing when [hasEnded] returns `true`
*/
fun nextStep() {
fun nextStep(continent: Int? = null) {
if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return }
val current = tilesToCheck.removeFirstOrNull() ?: return
for (neighbor in current.neighbors) {
if (neighbor !in tilesReached && predicate(neighbor)) {
tilesReached[neighbor] = current
tilesToCheck.add(neighbor)
if (continent != null)
neighbor.setContinent(continent)
}
}
}

View File

@ -70,6 +70,8 @@ open class TileInfo {
var hasBottomRiver = false
var hasBottomLeftRiver = false
private var continent = -1
val latitude: Float
get() = HexMath.getLatitude(position)
val longitude: Float
@ -92,6 +94,7 @@ open class TileInfo {
toReturn.hasBottomLeftRiver = hasBottomLeftRiver
toReturn.hasBottomRightRiver = hasBottomRightRiver
toReturn.hasBottomRiver = hasBottomRiver
toReturn.continent = continent
return toReturn
}
@ -654,6 +657,7 @@ open class TileInfo {
return out
}
fun getContinent() = continent
//endregion
@ -773,5 +777,12 @@ open class TileInfo {
}
}
// Should only be set once at map generation
fun setContinent(continent: Int) {
if (this.continent != -1)
throw Exception("Continent already assigned @ $position")
this.continent = continent
}
//endregion
}

View File

@ -34,6 +34,7 @@ class TileMap {
var mapParameters = MapParameters()
private var tileList = ArrayList<TileInfo>()
val continentSizes = HashMap<Int, Int>() // Continent ID, Continent size
/** Structure geared for simple serialization by Gdx.Json (which is a little blind to kotlin collections, especially HashSet)
* @param position [Vector2] of the location
@ -78,6 +79,9 @@ class TileMap {
@Transient
val startingLocationsByNation = HashMap<String,HashSet<TileInfo>>()
@Transient
val landTilesInBigEnoughGroup = ArrayList<TileInfo>() // cached at map gen
//endregion
//region Constructors
@ -123,6 +127,7 @@ class TileMap {
toReturn.startingLocations.clear()
toReturn.startingLocations.ensureCapacity(startingLocations.size)
toReturn.startingLocations.addAll(startingLocations)
toReturn.continentSizes.putAll(continentSizes)
return toReturn
}
@ -344,6 +349,12 @@ class TileMap {
return rulesetIncompatibilities
}
fun isWaterMap(): Boolean {
val bigIslands = continentSizes.count { it.value > 20 }
val players = gameInfo.gameParameters.players.count()
return bigIslands >= players
}
//endregion
//region State-Changing Methods

View File

@ -73,6 +73,9 @@ class MapGenerator(val ruleset: Ruleset) {
runAndMeasure("spawnIce") {
spawnIce(map)
}
runAndMeasure("assignContinents") {
assignContinents(map)
}
runAndMeasure("NaturalWonderGenerator") {
NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map)
}
@ -461,6 +464,26 @@ class MapGenerator(val ruleset: Ruleset) {
}
}
// Set a continent id for each tile, so we can quickly see which tiles are connected.
// Can also be called on saved maps
fun assignContinents(tileMap: TileMap) {
var landTiles = tileMap.values
.filter { it.isLand && !it.isImpassible()}
var currentContinent = 0
while (landTiles.any()) {
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
bfs.stepToEnd(currentContinent)
val continent = bfs.getReachedTiles()
tileMap.continentSizes[currentContinent] = continent.size
if (continent.size > 20) {
tileMap.landTilesInBigEnoughGroup.addAll(continent)
}
currentContinent++
landTiles = landTiles.filter { it !in continent }
}
}
}
class MapGenerationRandomness {