Skip to content

Commit 7bd555a

Browse files
authored
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
1 parent 2976187 commit 7bd555a

File tree

11 files changed

+297
-48
lines changed

11 files changed

+297
-48
lines changed

android/assets/jsons/translations/template.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Diplomatic Marriage ([amount] Gold) =
182182
We have married into the ruling family of [civName], bringing them under our control. =
183183
[civName] has married into the ruling family of [civName2], bringing them under their control. =
184184
You have broken your Pledge to Protect [civName]! =
185+
City-States grow wary of your aggression. The resting point for Influence has decreased by [amount] for [civName]. =
185186
186187
Cultured =
187188
Maritime =

core/src/com/unciv/logic/GameStarter.kt

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,29 @@ object GameStarter {
3333

3434
// In the case where we used to have a mod, and now we don't, we cannot "unselect" it in the UI.
3535
// We need to remove the dead mods so there aren't problems later.
36-
gameSetupInfo.gameParameters.mods.removeAll{ !RulesetCache.containsKey(it) }
36+
gameSetupInfo.gameParameters.mods.removeAll { !RulesetCache.containsKey(it) }
3737

3838
gameInfo.gameParameters = gameSetupInfo.gameParameters
3939
val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods)
40+
val mapGen = MapGenerator(ruleset)
4041

4142
if (gameSetupInfo.mapParameters.name != "") runAndMeasure("loadMap") {
4243
tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!)
4344
// Don't override the map parameters - this can include if we world wrap or not!
4445
} else runAndMeasure("generateMap") {
45-
tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters)
46+
tileMap = mapGen.generateMap(gameSetupInfo.mapParameters)
4647
tileMap.mapParameters = gameSetupInfo.mapParameters
4748
}
4849

4950
runAndMeasure("addCivilizations") {
5051
gameInfo.tileMap = tileMap
51-
tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map
52-
addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics
52+
tileMap.gameInfo =
53+
gameInfo // need to set this transient before placing units in the map
54+
addCivilizations(
55+
gameSetupInfo.gameParameters,
56+
gameInfo,
57+
ruleset
58+
) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics
5359
}
5460

5561
runAndMeasure("Remove units") {
@@ -78,6 +84,11 @@ object GameStarter {
7884
addCivStats(gameInfo)
7985
}
8086

87+
runAndMeasure("assignContinents?") {
88+
if (tileMap.continentSizes.isEmpty()) // Probably saved map without continent data
89+
mapGen.assignContinents(tileMap)
90+
}
91+
8192
runAndMeasure("addCivStartingUnits") {
8293
// 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!
8394
addCivStartingUnits(gameInfo)
@@ -334,19 +345,23 @@ object GameStarter {
334345
}
335346
}
336347

348+
337349
private fun getStartingLocations(civs: List<CivilizationInfo>, tileMap: TileMap, startScores: HashMap<TileInfo, Float>): HashMap<CivilizationInfo, TileInfo> {
338-
var landTiles = tileMap.values
350+
val landTilesInBigEnoughGroup = tileMap.landTilesInBigEnoughGroup
351+
if (landTilesInBigEnoughGroup.isEmpty()) {
352+
// Worst case - a pre-made map with continent data. This means we didn't re-run assignContinents,
353+
// so we don't have a cached landTilesInBigEnoughGroup. So we need to do it the hard way.
354+
var landTiles = tileMap.values
339355
// Games starting on snow might as well start over...
340356
.filter { it.isLand && !it.isImpassible() && it.baseTerrain != Constants.snow }
341-
342-
val landTilesInBigEnoughGroup = ArrayList<TileInfo>()
343-
while (landTiles.any()) {
344-
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
345-
bfs.stepToEnd()
346-
val tilesInGroup = bfs.getReachedTiles()
347-
landTiles = landTiles.filter { it !in tilesInGroup }
348-
if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on
349-
landTilesInBigEnoughGroup.addAll(tilesInGroup)
357+
while (landTiles.any()) {
358+
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
359+
bfs.stepToEnd()
360+
val tilesInGroup = bfs.getReachedTiles()
361+
landTiles = landTiles.filter { it !in tilesInGroup }
362+
if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on
363+
landTilesInBigEnoughGroup.addAll(tilesInGroup)
364+
}
350365
}
351366

352367
val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start

core/src/com/unciv/logic/city/CityInfo.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.unciv.logic.city
33
import com.badlogic.gdx.math.Vector2
44
import com.unciv.logic.battle.CityCombatant
55
import com.unciv.logic.civilization.CivilizationInfo
6+
import com.unciv.logic.civilization.Proximity
67
import com.unciv.logic.civilization.ReligionState
78
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
89
import com.unciv.logic.map.RoadStatus
@@ -129,6 +130,18 @@ class CityInfo {
129130
population.autoAssignPopulation()
130131
cityStats.update()
131132

133+
// Update proximity rankings for all civs
134+
for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) {
135+
if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors
136+
civInfo.updateProximity(otherCiv,
137+
otherCiv.updateProximity(civInfo))
138+
}
139+
for (otherCiv in civInfo.gameInfo.getAliveCityStates()) {
140+
if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors
141+
civInfo.updateProximity(otherCiv,
142+
otherCiv.updateProximity(civInfo))
143+
}
144+
132145
triggerCitiesSettledNearOtherCiv()
133146
}
134147

@@ -556,6 +569,16 @@ class CityInfo {
556569
if (isCapital() && civInfo.cities.isNotEmpty()) { // Move the capital if destroyed (by a nuke or by razing)
557570
civInfo.cities.first().cityConstructions.addBuilding(capitalCityIndicator())
558571
}
572+
573+
// Update proximity rankings for all civs
574+
for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) {
575+
civInfo.updateProximity(otherCiv,
576+
otherCiv.updateProximity(civInfo))
577+
}
578+
for (otherCiv in civInfo.gameInfo.getAliveCityStates()) {
579+
civInfo.updateProximity(otherCiv,
580+
otherCiv.updateProximity(civInfo))
581+
}
559582
}
560583

561584
fun annexCity() = CityInfoConquestFunctions(this).annexCity()

core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ class CityInfoConquestFunctions(val city: CityInfo){
276276

277277
tryUpdateRoadStatus()
278278
cityStats.update()
279+
280+
// Update proximity rankings
281+
civInfo.updateProximity(oldCiv,
282+
oldCiv.updateProximity(civInfo))
279283
}
280284
}
281285

core/src/com/unciv/logic/civilization/CityStateFunctions.kt

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ package com.unciv.logic.civilization
22

33
import com.unciv.Constants
44
import com.unciv.logic.automation.NextTurnAutomation
5-
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
6-
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
7-
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
8-
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
5+
import com.unciv.logic.civilization.diplomacy.*
96
import com.unciv.models.metadata.GameSpeed
107
import com.unciv.models.ruleset.Ruleset
118
import com.unciv.models.stats.Stat
129
import com.unciv.models.translations.getPlaceholderParameters
1310
import com.unciv.models.translations.getPlaceholderText
1411
import com.unciv.ui.victoryscreen.RankingType
12+
import java.util.*
1513
import kotlin.collections.HashMap
14+
import kotlin.collections.HashSet
1615
import kotlin.collections.LinkedHashMap
1716
import kotlin.math.max
1817
import kotlin.math.min
@@ -506,10 +505,58 @@ class CityStateFunctions(val civInfo: CivilizationInfo) {
506505
}
507506
}
508507

509-
/** A city state was attacked. What are its protectors going to do about it??? */
508+
/** A city state was attacked. What are its protectors going to do about it??? Also checks for Wary */
510509
fun cityStateAttacked(attacker: CivilizationInfo) {
511510
if (!civInfo.isCityState()) return // What are we doing here?
512511

512+
// We might become wary!
513+
if (attacker.isMinorCivWarmonger()) { // They've attacked a lot of city-states
514+
civInfo.getDiplomacyManager(attacker).becomeWary()
515+
}
516+
else if (attacker.isMinorCivAggressor()) { // They've attacked a few
517+
if (Random().nextBoolean()) { // 50% chance
518+
civInfo.getDiplomacyManager(attacker).becomeWary()
519+
}
520+
}
521+
// Others might become wary!
522+
if (attacker.isMinorCivAggressor()) {
523+
for (cityState in civInfo.gameInfo.getAliveCityStates()) {
524+
if (cityState == civInfo) // Must be a different minor
525+
continue
526+
if (cityState.getAllyCiv() == attacker.civName) // Must not be allied to the attacker
527+
continue
528+
if (!cityState.knows(attacker)) // Must have met
529+
continue
530+
531+
var probability: Int
532+
if (attacker.isMinorCivWarmonger()) {
533+
// High probability if very aggressive
534+
probability = when (cityState.getProximity(attacker)) {
535+
Proximity.Neighbors -> 100
536+
Proximity.Close -> 75
537+
Proximity.Far -> 50
538+
Proximity.Distant -> 25
539+
else -> 0
540+
}
541+
} else {
542+
// Lower probability if only somewhat aggressive
543+
probability = when (cityState.getProximity(attacker)) {
544+
Proximity.Neighbors -> 50
545+
Proximity.Close -> 20
546+
else -> 0
547+
}
548+
}
549+
550+
// Higher probability if already at war
551+
if (cityState.isAtWarWith(attacker))
552+
probability += 50
553+
554+
if (Random().nextInt(100) <= probability) {
555+
cityState.getDiplomacyManager(attacker).becomeWary()
556+
}
557+
}
558+
}
559+
513560
for (protector in civInfo.getProtectorCivs()) {
514561
if (!protector.knows(attacker)) // Who?
515562
continue

core/src/com/unciv/logic/civilization/CivilizationInfo.kt

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ import com.unciv.logic.civilization.RuinsManager.RuinsManager
1010
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
1111
import com.unciv.logic.civilization.diplomacy.DiplomacyManager
1212
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
13-
import com.unciv.logic.map.MapUnit
14-
import com.unciv.logic.map.TileInfo
15-
import com.unciv.logic.map.UnitMovementAlgorithms
13+
import com.unciv.logic.map.*
1614
import com.unciv.logic.trade.TradeEvaluation
1715
import com.unciv.logic.trade.TradeRequest
1816
import com.unciv.models.Counter
@@ -34,6 +32,14 @@ import kotlin.math.min
3432
import kotlin.math.roundToInt
3533
import kotlin.math.sqrt
3634

35+
enum class Proximity {
36+
None, // ie no cities
37+
Neighbors,
38+
Close,
39+
Far,
40+
Distant
41+
}
42+
3743
class CivilizationInfo {
3844

3945
@Transient
@@ -109,6 +115,7 @@ class CivilizationInfo {
109115
var victoryManager = VictoryManager()
110116
var ruinsManager = RuinsManager()
111117
var diplomacy = HashMap<String, DiplomacyManager>()
118+
var proximity = HashMap<String, Proximity>()
112119
var notifications = ArrayList<Notification>()
113120
val popupAlerts = ArrayList<PopupAlert>()
114121
private var allyCivName: String? = null
@@ -143,6 +150,9 @@ class CivilizationInfo {
143150
// default false once we no longer want legacy save-game compatibility
144151
var hasEverOwnedOriginalCapital: Boolean? = null
145152

153+
// For Aggressor, Warmonger status
154+
private var numMinorCivsAttacked = 0
155+
146156
constructor()
147157

148158
constructor(civName: String) {
@@ -167,6 +177,7 @@ class CivilizationInfo {
167177
toReturn.allyCivName = allyCivName
168178
for (diplomacyManager in diplomacy.values.map { it.clone() })
169179
toReturn.diplomacy[diplomacyManager.otherCivName] = diplomacyManager
180+
toReturn.proximity.putAll(proximity)
170181
toReturn.cities = cities.map { it.clone() }
171182

172183
// This is the only thing that is NOT switched out, which makes it a source of ConcurrentModification errors.
@@ -187,6 +198,7 @@ class CivilizationInfo {
187198
toReturn.boughtConstructionsWithGloballyIncreasingPrice.putAll(boughtConstructionsWithGloballyIncreasingPrice)
188199
//
189200
toReturn.hasEverOwnedOriginalCapital = hasEverOwnedOriginalCapital
201+
toReturn.numMinorCivsAttacked = numMinorCivsAttacked
190202
return toReturn
191203
}
192204

@@ -199,6 +211,9 @@ class CivilizationInfo {
199211
fun getDiplomacyManager(civInfo: CivilizationInfo) = getDiplomacyManager(civInfo.civName)
200212
fun getDiplomacyManager(civName: String) = diplomacy[civName]!!
201213

214+
fun getProximity(civInfo: CivilizationInfo) = getProximity(civInfo.civName)
215+
fun getProximity(civName: String) = proximity[civName] ?: Proximity.None
216+
202217
/** Returns only undefeated civs, aka the ones we care about */
203218
fun getKnownCivs() = diplomacy.values.map { it.otherCiv() }.filter { !it.isDefeated() }
204219
fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName)
@@ -556,6 +571,9 @@ class CivilizationInfo {
556571
fun hasTechOrPolicy(techOrPolicyName: String) =
557572
tech.isResearched(techOrPolicyName) || policies.isAdopted(techOrPolicyName)
558573

574+
fun isMinorCivAggressor() = numMinorCivsAttacked >= 2
575+
fun isMinorCivWarmonger() = numMinorCivsAttacked >= 4
576+
559577
//endregion
560578

561579
//region state-changing functions
@@ -612,6 +630,10 @@ class CivilizationInfo {
612630
updateDetailedCivResources()
613631
}
614632

633+
fun changeMinorCivsAttacked(count: Int) {
634+
numMinorCivsAttacked += count
635+
}
636+
615637
// implementation in a separate class, to not clog up CivInfo
616638
fun initialSetCitiesConnectedToCapitalTransients() = transients().updateCitiesConnectedToCapital(true)
617639
fun updateHasActiveGreatWall() = transients().updateHasActiveGreatWall()
@@ -912,6 +934,81 @@ class CivilizationInfo {
912934
).toInt()
913935
}
914936

937+
fun updateProximity(otherCiv: CivilizationInfo, preCalculated: Proximity? = null): Proximity {
938+
if (otherCiv == this) return Proximity.None
939+
if (preCalculated != null) {
940+
// We usually want to update this for a pair of civs at the same time
941+
// Since this function *should* be symmetrical for both civs, we can just do it once
942+
this.proximity[otherCiv.civName] = preCalculated
943+
return preCalculated
944+
}
945+
if (cities.isEmpty() || otherCiv.cities.isEmpty()) {
946+
proximity[otherCiv.civName] = Proximity.None
947+
return Proximity.None
948+
}
949+
950+
val mapParams = gameInfo.tileMap.mapParameters
951+
var minDistance = 100000 // a long distance
952+
var totalDistance = 0
953+
var connections = 0
954+
955+
var proximity = Proximity.None
956+
957+
for (ourCity in cities) {
958+
for (theirCity in otherCiv.cities) {
959+
val distance = ourCity.getCenterTile().aerialDistanceTo(theirCity.getCenterTile())
960+
totalDistance += distance
961+
connections++
962+
if (minDistance > distance) minDistance = distance
963+
}
964+
}
965+
966+
if (minDistance <= 7) {
967+
proximity = Proximity.Neighbors
968+
} else if (connections > 0) {
969+
val averageDistance = totalDistance / connections
970+
val mapFactor = if (mapParams.shape == MapShape.rectangular)
971+
(mapParams.mapSize.height + mapParams.mapSize.width) / 2
972+
else (mapParams.mapSize.radius * 3) / 2 // slightly less area than equal size rect
973+
974+
val closeDistance = ((mapFactor * 25) / 100).coerceIn(10, 20)
975+
val farDistance = ((mapFactor * 45) / 100).coerceIn(20, 50)
976+
977+
proximity = if (minDistance <= 11 && averageDistance <= closeDistance)
978+
Proximity.Close
979+
else if (averageDistance <= farDistance)
980+
Proximity.Far
981+
else
982+
Proximity.Distant
983+
}
984+
985+
// Check if different continents (unless already max distance, or water map)
986+
if (connections > 0 && proximity != Proximity.Distant
987+
&& !gameInfo.tileMap.isWaterMap()) {
988+
989+
if (getCapital().getCenterTile().getContinent() != otherCiv.getCapital().getCenterTile().getContinent()) {
990+
// Different continents - increase separation by one step
991+
proximity = when (proximity) {
992+
Proximity.Far -> Proximity.Distant
993+
Proximity.Close -> Proximity.Far
994+
Proximity.Neighbors -> Proximity.Close
995+
else -> proximity
996+
}
997+
}
998+
}
999+
1000+
// If there aren't many players (left) we can't be that far
1001+
val numMajors = gameInfo.getAliveMajorCivs().count()
1002+
if (numMajors <= 2 && proximity > Proximity.Close)
1003+
proximity = Proximity.Close
1004+
if (numMajors <= 4 && proximity > Proximity.Far)
1005+
proximity = Proximity.Far
1006+
1007+
this.proximity[otherCiv.civName] = proximity
1008+
1009+
return proximity
1010+
}
1011+
9151012
//////////////////////// City State wrapper functions ////////////////////////
9161013

9171014
fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection<String>)

0 commit comments

Comments
 (0)