mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-25 10:26:05 +07:00
chore: Split NextTurnAutomation into Religion, Trade, and Diplomacy automation files
This commit is contained in:
parent
6a7d09b43a
commit
fba9048156
@ -0,0 +1,399 @@
|
||||
package com.unciv.logic.automation.civilization
|
||||
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.automation.Automation
|
||||
import com.unciv.logic.automation.ThreatLevel
|
||||
import com.unciv.logic.battle.BattleDamage
|
||||
import com.unciv.logic.battle.CityCombatant
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.civilization.AlertType
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.PopupAlert
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
||||
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
|
||||
import com.unciv.logic.map.BFS
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.logic.trade.TradeEvaluation
|
||||
import com.unciv.logic.trade.TradeLogic
|
||||
import com.unciv.logic.trade.TradeOffer
|
||||
import com.unciv.logic.trade.TradeRequest
|
||||
import com.unciv.logic.trade.TradeType
|
||||
import com.unciv.models.ruleset.Building
|
||||
import com.unciv.models.ruleset.Victory
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.ruleset.unit.BaseUnit
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.screens.victoryscreen.RankingType
|
||||
|
||||
object DiplomacyAutomation {
|
||||
|
||||
internal fun offerDeclarationOfFriendship(civInfo: Civilization) {
|
||||
val civsThatWeCanDeclareFriendshipWith = civInfo.getKnownCivs()
|
||||
.filter { civInfo.diplomacyFunctions.canSignDeclarationOfFriendshipWith(it)
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship)}
|
||||
.sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() }.toList()
|
||||
for (otherCiv in civsThatWeCanDeclareFriendshipWith) {
|
||||
// Default setting is 2, this will be changed according to different civ.
|
||||
if ((1..10).random() <= 2 && wantsToSignDeclarationOfFrienship(civInfo, otherCiv)) {
|
||||
otherCiv.popupAlerts.add(PopupAlert(AlertType.DeclarationOfFriendship, civInfo.civName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun wantsToSignDeclarationOfFrienship(civInfo: Civilization, otherCiv: Civilization): Boolean {
|
||||
val diploManager = civInfo.getDiplomacyManager(otherCiv)
|
||||
// Shortcut, if it is below favorable then don't consider it
|
||||
if (diploManager.isRelationshipLevelLT(RelationshipLevel.Favorable)) return false
|
||||
|
||||
val numOfFriends = civInfo.diplomacy.count { it.value.hasFlag(DiplomacyFlags.DeclarationOfFriendship) }
|
||||
val knownCivs = civInfo.getKnownCivs().count { it.isMajorCiv() && it.isAlive() }
|
||||
val allCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() } - 1 // Don't include us
|
||||
val deadCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() && !it.isAlive() }
|
||||
val allAliveCivs = allCivs - deadCivs
|
||||
|
||||
// Motivation should be constant as the number of civs changes
|
||||
var motivation = diploManager.opinionOfOtherCiv().toInt() - 40
|
||||
|
||||
// If the other civ is stronger than we are compelled to be nice to them
|
||||
// If they are too weak, then thier friendship doesn't mean much to us
|
||||
motivation += when (Automation.threatAssessment(civInfo,otherCiv)) {
|
||||
ThreatLevel.VeryHigh -> 10
|
||||
ThreatLevel.High -> 5
|
||||
ThreatLevel.VeryLow -> -5
|
||||
else -> 0
|
||||
}
|
||||
|
||||
// Try to ally with a fourth of the civs in play
|
||||
val civsToAllyWith = 0.25f * allAliveCivs
|
||||
if (numOfFriends < civsToAllyWith) {
|
||||
// Goes from 10 to 0 once the civ gets 1/4 of all alive civs as friends
|
||||
motivation += (10 - 10 * (numOfFriends / civsToAllyWith)).toInt()
|
||||
} else {
|
||||
// Goes form 0 to -120 as the civ gets more friends, offset by civsToAllyWith
|
||||
motivation -= (120f * (numOfFriends - civsToAllyWith) / (knownCivs - civsToAllyWith)).toInt()
|
||||
}
|
||||
|
||||
// Goes from 0 to -50 as more civs die
|
||||
// this is meant to prevent the game from stalemating when a group of friends
|
||||
// conquers all oposition
|
||||
motivation -= deadCivs / allCivs * 50
|
||||
|
||||
// Wait to declare frienships until more civs
|
||||
// Goes from -30 to 0 when we know 75% of allCivs
|
||||
val civsToKnow = 0.75f * allAliveCivs
|
||||
motivation -= ((civsToKnow - knownCivs) / civsToKnow * 30f).toInt().coerceAtLeast(0)
|
||||
|
||||
motivation -= hasAtLeastMotivationToAttack(civInfo, otherCiv, motivation / 2) * 2
|
||||
|
||||
return motivation > 0
|
||||
}
|
||||
|
||||
internal fun offerOpenBorders(civInfo: Civilization) {
|
||||
if (!civInfo.hasUnique(UniqueType.EnablesOpenBorders)) return
|
||||
val civsThatWeCanOpenBordersWith = civInfo.getKnownCivs()
|
||||
.filter { it.isMajorCiv() && !civInfo.isAtWarWith(it)
|
||||
&& it.hasUnique(UniqueType.EnablesOpenBorders)
|
||||
&& !civInfo.getDiplomacyManager(it).hasOpenBorders
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedOpenBorders) }
|
||||
.sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() }.toList()
|
||||
for (otherCiv in civsThatWeCanOpenBordersWith) {
|
||||
// Default setting is 3, this will be changed according to different civ.
|
||||
if ((1..10).random() <= 3 && wantsToOpenBorders(civInfo, otherCiv)) {
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.openBorders, TradeType.Agreement))
|
||||
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.openBorders, TradeType.Agreement))
|
||||
|
||||
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun wantsToOpenBorders(civInfo: Civilization, otherCiv: Civilization): Boolean {
|
||||
if (civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelLT(RelationshipLevel.Favorable)) return false
|
||||
// Don't accept if they are at war with our friends, they might use our land to attack them
|
||||
if (civInfo.diplomacy.values.any { it.isRelationshipLevelGE(RelationshipLevel.Friend) && it.otherCiv().isAtWarWith(otherCiv)})
|
||||
return false
|
||||
if (hasAtLeastMotivationToAttack(civInfo, otherCiv, (civInfo.getDiplomacyManager(otherCiv).opinionOfOtherCiv()/ 2 - 10).toInt()) >= 0)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun offerResearchAgreement(civInfo: Civilization) {
|
||||
if (!civInfo.diplomacyFunctions.canSignResearchAgreement()) return // don't waste your time
|
||||
|
||||
val canSignResearchAgreementCiv = civInfo.getKnownCivs()
|
||||
.filter {
|
||||
civInfo.diplomacyFunctions.canSignResearchAgreementsWith(it)
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedResearchAgreement)
|
||||
}
|
||||
.sortedByDescending { it.stats.statsForNextTurn.science }
|
||||
|
||||
for (otherCiv in canSignResearchAgreementCiv) {
|
||||
// Default setting is 5, this will be changed according to different civ.
|
||||
if ((1..10).random() > 5) continue
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
val cost = civInfo.diplomacyFunctions.getResearchAgreementCost(otherCiv)
|
||||
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.researchAgreement, TradeType.Treaty, cost))
|
||||
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.researchAgreement, TradeType.Treaty, cost))
|
||||
|
||||
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun offerDefensivePact(civInfo: Civilization) {
|
||||
if (!civInfo.diplomacyFunctions.canSignDefensivePact()) return // don't waste your time
|
||||
|
||||
val canSignDefensivePactCiv = civInfo.getKnownCivs()
|
||||
.filter {
|
||||
civInfo.diplomacyFunctions.canSignDefensivePactWith(it)
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedDefensivePact)
|
||||
&& civInfo.getDiplomacyManager(it).relationshipIgnoreAfraid() == RelationshipLevel.Ally
|
||||
}
|
||||
|
||||
for (otherCiv in canSignDefensivePactCiv) {
|
||||
// Default setting is 3, this will be changed according to different civ.
|
||||
if ((1..10).random() <= 3 && wantsToSignDefensivePact(civInfo, otherCiv)) {
|
||||
//todo: Add more in depth evaluation here
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.defensivePact, TradeType.Treaty))
|
||||
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.defensivePact, TradeType.Treaty))
|
||||
|
||||
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun wantsToSignDefensivePact(civInfo: Civilization, otherCiv: Civilization): Boolean {
|
||||
val diploManager = civInfo.getDiplomacyManager(otherCiv)
|
||||
if (diploManager.isRelationshipLevelLT(RelationshipLevel.Ally)) return false
|
||||
val commonknownCivs = diploManager.getCommonKnownCivs()
|
||||
// If they have bad relations with any of our friends, don't consider it
|
||||
for(thirdCiv in commonknownCivs) {
|
||||
if (civInfo.getDiplomacyManager(thirdCiv).isRelationshipLevelGE(RelationshipLevel.Friend)
|
||||
&& thirdCiv.getDiplomacyManager(otherCiv).isRelationshipLevelLT(RelationshipLevel.Favorable))
|
||||
return false
|
||||
}
|
||||
|
||||
val defensivePacts = civInfo.diplomacy.count { it.value.hasFlag(DiplomacyFlags.DefensivePact) }
|
||||
val otherCivNonOverlappingDefensivePacts = otherCiv.diplomacy.values.count { it.hasFlag(DiplomacyFlags.DefensivePact)
|
||||
&& (!it.otherCiv().knows(civInfo) || !it.otherCiv().getDiplomacyManager(civInfo).hasFlag(DiplomacyFlags.DefensivePact)) }
|
||||
val allCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() } - 1 // Don't include us
|
||||
val deadCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() && !it.isAlive() }
|
||||
val allAliveCivs = allCivs - deadCivs
|
||||
|
||||
// We have to already be at RelationshipLevel.Ally, so we must have 80 oppinion of them
|
||||
var motivation = diploManager.opinionOfOtherCiv().toInt() - 80
|
||||
|
||||
// If they are stronger than us, then we value it a lot more
|
||||
// If they are weaker than us, then we don't value it
|
||||
motivation += when (Automation.threatAssessment(civInfo,otherCiv)) {
|
||||
ThreatLevel.VeryHigh -> 10
|
||||
ThreatLevel.High -> 5
|
||||
ThreatLevel.Low -> -15
|
||||
ThreatLevel.VeryLow -> -30
|
||||
else -> 0
|
||||
}
|
||||
|
||||
// If they have a defensive pact with another civ then we would get drawn into thier battles as well
|
||||
motivation -= 10 * otherCivNonOverlappingDefensivePacts
|
||||
|
||||
// Try to have a defensive pact with 1/5 of all civs
|
||||
val civsToAllyWith = 0.20f * allAliveCivs
|
||||
// Goes form 0 to -50 as the civ gets more allies, offset by civsToAllyWith
|
||||
motivation -= (50f * (defensivePacts - civsToAllyWith) / (allAliveCivs - civsToAllyWith)).coerceAtMost(0f).toInt()
|
||||
|
||||
return motivation > 0
|
||||
}
|
||||
|
||||
internal fun declareWar(civInfo: Civilization) {
|
||||
if (civInfo.wantsToFocusOn(Victory.Focus.Culture)) return
|
||||
if (civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
|
||||
if (civInfo.isAtWar() || civInfo.getHappiness() <= 0) return
|
||||
|
||||
val ourMilitaryUnits = civInfo.units.getCivUnits().filter { !it.isCivilian() }.count()
|
||||
if (ourMilitaryUnits < civInfo.cities.size) return
|
||||
if (ourMilitaryUnits < 4) return // to stop AI declaring war at the beginning of games when everyone isn't set up well enough\
|
||||
if (civInfo.cities.size < 3) return // FAR too early for that what are you thinking!
|
||||
|
||||
//evaluate war
|
||||
val enemyCivs = civInfo.getKnownCivs()
|
||||
.filterNot {
|
||||
it == civInfo || it.cities.isEmpty() || !civInfo.getDiplomacyManager(it).canDeclareWar()
|
||||
|| it.cities.none { city -> civInfo.hasExplored(city.getCenterTile()) }
|
||||
}
|
||||
// If the AI declares war on a civ without knowing the location of any cities, it'll just keep amassing an army and not sending it anywhere,
|
||||
// and end up at a massive disadvantage
|
||||
|
||||
if (enemyCivs.none()) return
|
||||
|
||||
val minMotivationToAttack = 20
|
||||
val civWithBestMotivationToAttack = enemyCivs
|
||||
.map { Pair(it, hasAtLeastMotivationToAttack(civInfo, it, minMotivationToAttack)) }
|
||||
.maxByOrNull { it.second }!!
|
||||
|
||||
if (civWithBestMotivationToAttack.second >= minMotivationToAttack)
|
||||
civInfo.getDiplomacyManager(civWithBestMotivationToAttack.first).declareWar()
|
||||
}
|
||||
|
||||
/** Will return the motivation to attack, but might short circuit if the value is guaranteed to
|
||||
* be lower than `atLeast`. So any values below `atLeast` should not be used for comparison. */
|
||||
private fun hasAtLeastMotivationToAttack(civInfo: Civilization, otherCiv: Civilization, atLeast: Int): Int {
|
||||
val closestCities = NextTurnAutomation.getClosestCities(civInfo, otherCiv) ?: return 0
|
||||
val baseForce = 30f
|
||||
|
||||
var ourCombatStrength = civInfo.getStatForRanking(RankingType.Force).toFloat() + baseForce
|
||||
if (civInfo.getCapital() != null) ourCombatStrength += CityCombatant(civInfo.getCapital()!!).getCityStrength()
|
||||
var theirCombatStrength = otherCiv.getStatForRanking(RankingType.Force).toFloat() + baseForce
|
||||
if(otherCiv.getCapital() != null) theirCombatStrength += CityCombatant(otherCiv.getCapital()!!).getCityStrength()
|
||||
|
||||
//for city-states, also consider their protectors
|
||||
if (otherCiv.isCityState() and otherCiv.cityStateFunctions.getProtectorCivs().isNotEmpty()) {
|
||||
theirCombatStrength += otherCiv.cityStateFunctions.getProtectorCivs().filterNot { it == civInfo }
|
||||
.sumOf { it.getStatForRanking(RankingType.Force) }
|
||||
}
|
||||
|
||||
if (theirCombatStrength > ourCombatStrength) return 0
|
||||
|
||||
val ourCity = closestCities.city1
|
||||
val theirCity = closestCities.city2
|
||||
|
||||
if (civInfo.units.getCivUnits().filter { it.isMilitary() }.none {
|
||||
val damageReceivedWhenAttacking =
|
||||
BattleDamage.calculateDamageToAttacker(
|
||||
MapUnitCombatant(it),
|
||||
CityCombatant(theirCity)
|
||||
)
|
||||
damageReceivedWhenAttacking < 100
|
||||
})
|
||||
return 0 // You don't have any units that can attack this city without dying, don't declare war.
|
||||
|
||||
fun isTileCanMoveThrough(tile: Tile): Boolean {
|
||||
val owner = tile.getOwner()
|
||||
return !tile.isImpassible()
|
||||
&& (owner == otherCiv || owner == null || civInfo.diplomacyFunctions.canPassThroughTiles(owner))
|
||||
}
|
||||
|
||||
val modifierMap = HashMap<String, Int>()
|
||||
val combatStrengthRatio = ourCombatStrength / theirCombatStrength
|
||||
val combatStrengthModifier = when {
|
||||
combatStrengthRatio > 3f -> 30
|
||||
combatStrengthRatio > 2.5f -> 25
|
||||
combatStrengthRatio > 2f -> 20
|
||||
combatStrengthRatio > 1.5f -> 10
|
||||
else -> 0
|
||||
}
|
||||
modifierMap["Relative combat strength"] = combatStrengthModifier
|
||||
|
||||
|
||||
if (closestCities.aerialDistance > 7)
|
||||
modifierMap["Far away cities"] = -10
|
||||
|
||||
val diplomacyManager = civInfo.getDiplomacyManager(otherCiv)
|
||||
if (diplomacyManager.hasFlag(DiplomacyFlags.ResearchAgreement))
|
||||
modifierMap["Research Agreement"] = -5
|
||||
|
||||
if (diplomacyManager.hasFlag(DiplomacyFlags.DeclarationOfFriendship))
|
||||
modifierMap["Declaration of Friendship"] = -10
|
||||
|
||||
val relationshipModifier = when (diplomacyManager.relationshipIgnoreAfraid()) {
|
||||
RelationshipLevel.Unforgivable -> 10
|
||||
RelationshipLevel.Enemy -> 5
|
||||
RelationshipLevel.Ally -> -5 // this is so that ally + DoF is not too unbalanced -
|
||||
// still possible for AI to declare war for isolated city
|
||||
else -> 0
|
||||
}
|
||||
modifierMap["Relationship"] = relationshipModifier
|
||||
|
||||
if (diplomacyManager.resourcesFromTrade().any { it.amount > 0 })
|
||||
modifierMap["Receiving trade resources"] = -5
|
||||
|
||||
if (theirCity.getTiles().none { tile -> tile.neighbors.any { it.getOwner() == theirCity.civ && it.getCity() != theirCity } })
|
||||
modifierMap["Isolated city"] = 15
|
||||
|
||||
if (otherCiv.isCityState()) {
|
||||
modifierMap["City-state"] = -20
|
||||
if (otherCiv.getAllyCiv() == civInfo.civName)
|
||||
modifierMap["Allied City-state"] = -20 // There had better be a DAMN good reason
|
||||
}
|
||||
|
||||
for (city in otherCiv.cities) {
|
||||
val construction = city.cityConstructions.getCurrentConstruction()
|
||||
if (construction is Building && construction.hasUnique(UniqueType.TriggersCulturalVictory))
|
||||
modifierMap["About to win"] = 15
|
||||
if (construction is BaseUnit && construction.hasUnique(UniqueType.AddInCapital))
|
||||
modifierMap["About to win"] = 15
|
||||
}
|
||||
|
||||
var motivationSoFar = modifierMap.values.sum()
|
||||
|
||||
// We don't need to execute the expensive BFSs below if we're below the threshold here
|
||||
// anyways, since it won't get better from those, only worse.
|
||||
if (motivationSoFar < atLeast) {
|
||||
return motivationSoFar
|
||||
}
|
||||
|
||||
|
||||
val landPathBFS = BFS(ourCity.getCenterTile()) {
|
||||
it.isLand && isTileCanMoveThrough(it)
|
||||
}
|
||||
|
||||
landPathBFS.stepUntilDestination(theirCity.getCenterTile())
|
||||
if (!landPathBFS.hasReachedTile(theirCity.getCenterTile()))
|
||||
motivationSoFar -= -10
|
||||
|
||||
// We don't need to execute the expensive BFSs below if we're below the threshold here
|
||||
// anyways, since it won't get better from those, only worse.
|
||||
if (motivationSoFar < atLeast) {
|
||||
return motivationSoFar
|
||||
}
|
||||
|
||||
val reachableEnemyCitiesBfs = BFS(civInfo.getCapital(true)!!.getCenterTile()) { isTileCanMoveThrough(it) }
|
||||
reachableEnemyCitiesBfs.stepToEnd()
|
||||
val reachableEnemyCities = otherCiv.cities.filter { reachableEnemyCitiesBfs.hasReachedTile(it.getCenterTile()) }
|
||||
if (reachableEnemyCities.isEmpty()) return 0 // Can't even reach the enemy city, no point in war.
|
||||
|
||||
return motivationSoFar
|
||||
}
|
||||
|
||||
|
||||
internal fun offerPeaceTreaty(civInfo: Civilization) {
|
||||
if (!civInfo.isAtWar() || civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
|
||||
|
||||
val enemiesCiv = civInfo.diplomacy.filter { it.value.diplomaticStatus == DiplomaticStatus.War }
|
||||
.map { it.value.otherCiv() }
|
||||
.filterNot { it == civInfo || it.isBarbarian() || it.cities.isEmpty() }
|
||||
.filter { !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedPeace) }
|
||||
// Don't allow AIs to offer peace to city states allied with their enemies
|
||||
.filterNot { it.isCityState() && it.getAllyCiv() != null && civInfo.isAtWarWith(civInfo.gameInfo.getCivilization(it.getAllyCiv()!!)) }
|
||||
// ignore civs that we have already offered peace this turn as a counteroffer to another civ's peace offer
|
||||
.filter { it.tradeRequests.none { tradeRequest -> tradeRequest.requestingCiv == civInfo.civName && tradeRequest.trade.isPeaceTreaty() } }
|
||||
|
||||
for (enemy in enemiesCiv) {
|
||||
if(hasAtLeastMotivationToAttack(civInfo, enemy, 10) >= 10) {
|
||||
// We can still fight. Refuse peace.
|
||||
continue
|
||||
}
|
||||
|
||||
// pay for peace
|
||||
val tradeLogic = TradeLogic(civInfo, enemy)
|
||||
|
||||
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.peaceTreaty, TradeType.Treaty))
|
||||
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.peaceTreaty, TradeType.Treaty))
|
||||
|
||||
var moneyWeNeedToPay = -TradeEvaluation().evaluatePeaceCostForThem(civInfo, enemy)
|
||||
|
||||
if (civInfo.gold > 0 && moneyWeNeedToPay > 0) {
|
||||
if (moneyWeNeedToPay > civInfo.gold) {
|
||||
moneyWeNeedToPay = civInfo.gold // As much as possible
|
||||
}
|
||||
tradeLogic.currentTrade.ourOffers.add(
|
||||
TradeOffer("Gold".tr(), TradeType.Gold, moneyWeNeedToPay)
|
||||
)
|
||||
}
|
||||
|
||||
enemy.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,37 +1,19 @@
|
||||
package com.unciv.logic.automation.civilization
|
||||
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.automation.Automation
|
||||
import com.unciv.logic.automation.ThreatLevel
|
||||
import com.unciv.logic.automation.unit.UnitAutomation
|
||||
import com.unciv.logic.battle.BattleDamage
|
||||
import com.unciv.logic.battle.CityCombatant
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.AlertType
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.NotificationCategory
|
||||
import com.unciv.logic.civilization.NotificationIcon
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.civilization.PopupAlert
|
||||
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.managers.ReligionState
|
||||
import com.unciv.logic.map.BFS
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.logic.trade.Trade
|
||||
import com.unciv.logic.trade.TradeEvaluation
|
||||
import com.unciv.logic.trade.TradeLogic
|
||||
import com.unciv.logic.trade.TradeOffer
|
||||
import com.unciv.logic.trade.TradeRequest
|
||||
import com.unciv.logic.trade.TradeType
|
||||
import com.unciv.models.Counter
|
||||
import com.unciv.models.ruleset.Belief
|
||||
import com.unciv.models.ruleset.BeliefType
|
||||
import com.unciv.models.ruleset.Building
|
||||
import com.unciv.models.ruleset.MilestoneType
|
||||
import com.unciv.models.ruleset.ModOptionsConstants
|
||||
import com.unciv.models.ruleset.Policy
|
||||
@ -43,9 +25,7 @@ import com.unciv.models.ruleset.unique.StateForConditionals
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.ruleset.unit.BaseUnit
|
||||
import com.unciv.models.stats.Stat
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.screens.victoryscreen.RankingType
|
||||
import kotlin.math.min
|
||||
import kotlin.random.Random
|
||||
|
||||
object NextTurnAutomation {
|
||||
@ -55,21 +35,21 @@ object NextTurnAutomation {
|
||||
if (civInfo.isBarbarian()) return BarbarianAutomation(civInfo).automate()
|
||||
|
||||
respondToPopupAlerts(civInfo)
|
||||
respondToTradeRequests(civInfo)
|
||||
TradeAutomation.respondToTradeRequests(civInfo)
|
||||
|
||||
if (civInfo.isMajorCiv()) {
|
||||
if (!civInfo.gameInfo.ruleset.modOptions.hasUnique(ModOptionsConstants.diplomaticRelationshipsCannotChange)) {
|
||||
declareWar(civInfo)
|
||||
offerPeaceTreaty(civInfo)
|
||||
offerDeclarationOfFriendship(civInfo)
|
||||
DiplomacyAutomation.declareWar(civInfo)
|
||||
DiplomacyAutomation.offerPeaceTreaty(civInfo)
|
||||
DiplomacyAutomation.offerDeclarationOfFriendship(civInfo)
|
||||
}
|
||||
if (civInfo.gameInfo.isReligionEnabled()) {
|
||||
ReligionAutomation.spendFaithOnReligion(civInfo)
|
||||
}
|
||||
offerOpenBorders(civInfo)
|
||||
offerResearchAgreement(civInfo)
|
||||
offerDefensivePact(civInfo)
|
||||
exchangeLuxuries(civInfo)
|
||||
DiplomacyAutomation.offerOpenBorders(civInfo)
|
||||
DiplomacyAutomation.offerResearchAgreement(civInfo)
|
||||
DiplomacyAutomation.offerDefensivePact(civInfo)
|
||||
TradeAutomation.exchangeLuxuries(civInfo)
|
||||
issueRequests(civInfo)
|
||||
adoptPolicy(civInfo) // todo can take a second - why?
|
||||
freeUpSpaceResources(civInfo)
|
||||
@ -87,9 +67,9 @@ object NextTurnAutomation {
|
||||
}
|
||||
automateUnits(civInfo) // this is the most expensive part
|
||||
|
||||
if (civInfo.isMajorCiv()) {
|
||||
if (civInfo.isMajorCiv() && civInfo.gameInfo.isReligionEnabled()) {
|
||||
// Can only be done now, as the prophet first has to decide to found/enhance a religion
|
||||
chooseReligiousBeliefs(civInfo)
|
||||
ReligionAutomation.chooseReligiousBeliefs(civInfo)
|
||||
}
|
||||
|
||||
automateCities(civInfo) // second most expensive
|
||||
@ -111,155 +91,6 @@ object NextTurnAutomation {
|
||||
else -> ((projectedGold - pissPoor) * maxPercent / stinkingRich).coerceAtMost(maxPercent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun respondToTradeRequests(civInfo: Civilization) {
|
||||
for (tradeRequest in civInfo.tradeRequests.toList()) {
|
||||
val otherCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv)
|
||||
if (!TradeEvaluation().isTradeValid(tradeRequest.trade, civInfo, otherCiv))
|
||||
continue
|
||||
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
tradeLogic.currentTrade.set(tradeRequest.trade)
|
||||
/** We need to remove this here, so that if the trade is accepted, the updateDetailedCivResources()
|
||||
* in tradeLogic.acceptTrade() will not consider *both* the trade *and the trade offer as decreasing the
|
||||
* amount of available resources, since that will lead to "Our proposed trade is no longer valid" if we try to offer
|
||||
* the same resource to ANOTHER civ in this turn. Complicated!
|
||||
*/
|
||||
civInfo.tradeRequests.remove(tradeRequest)
|
||||
if (TradeEvaluation().isTradeAcceptable(tradeLogic.currentTrade, civInfo, otherCiv)) {
|
||||
tradeLogic.acceptTrade()
|
||||
otherCiv.addNotification("[${civInfo.civName}] has accepted your trade request", NotificationCategory.Trade, NotificationIcon.Trade, civInfo.civName)
|
||||
} else {
|
||||
val counteroffer = getCounteroffer(civInfo, tradeRequest)
|
||||
if (counteroffer != null) {
|
||||
otherCiv.addNotification("[${civInfo.civName}] has made a counteroffer to your trade request", NotificationCategory.Trade, NotificationIcon.Trade, civInfo.civName)
|
||||
otherCiv.tradeRequests.add(counteroffer)
|
||||
} else
|
||||
tradeRequest.decline(civInfo)
|
||||
}
|
||||
}
|
||||
civInfo.tradeRequests.clear()
|
||||
}
|
||||
|
||||
/** @return a TradeRequest with the same ourOffers as [tradeRequest] but with enough theirOffers
|
||||
* added to make the deal acceptable. Will find a valid counteroffer if any exist, but is not
|
||||
* guaranteed to find the best or closest one. */
|
||||
private fun getCounteroffer(civInfo: Civilization, tradeRequest: TradeRequest): TradeRequest? {
|
||||
val otherCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv)
|
||||
// AIs counteroffering each other is problematic as they tend to ping-pong back and forth forever
|
||||
if (otherCiv.playerType == PlayerType.AI)
|
||||
return null
|
||||
val evaluation = TradeEvaluation()
|
||||
var deltaInOurFavor = evaluation.getTradeAcceptability(tradeRequest.trade, civInfo, otherCiv)
|
||||
if (deltaInOurFavor > 0) deltaInOurFavor = (deltaInOurFavor / 1.1f).toInt() // They seem very interested in this deal, let's push it a bit.
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
|
||||
tradeLogic.currentTrade.set(tradeRequest.trade)
|
||||
|
||||
// What do they have that we would want?
|
||||
val potentialAsks = HashMap<TradeOffer, Int>()
|
||||
val counterofferAsks = HashMap<TradeOffer, Int>()
|
||||
val counterofferGifts = ArrayList<TradeOffer>()
|
||||
|
||||
for (offer in tradeLogic.theirAvailableOffers) {
|
||||
if ((offer.type == TradeType.Gold || offer.type == TradeType.Gold_Per_Turn)
|
||||
&& tradeRequest.trade.ourOffers.any { it.type == offer.type })
|
||||
continue // Don't want to counteroffer straight gold for gold, that's silly
|
||||
if (!offer.isTradable())
|
||||
continue // For example resources gained by trade or CS
|
||||
if (offer.type == TradeType.City)
|
||||
continue // Players generally don't want to give up their cities, and they might misclick
|
||||
|
||||
if (tradeLogic.currentTrade.theirOffers.any { it.type == offer.type && it.name == offer.name })
|
||||
continue // So you don't get double offers of open borders declarations of war etc.
|
||||
if (offer.type == TradeType.Treaty)
|
||||
continue // Don't try to counter with a defensive pact or research pact
|
||||
|
||||
val value = evaluation.evaluateBuyCostWithInflation(offer, civInfo, otherCiv)
|
||||
if (value > 0)
|
||||
potentialAsks[offer] = value
|
||||
}
|
||||
|
||||
while (potentialAsks.isNotEmpty() && deltaInOurFavor < 0) {
|
||||
// Keep adding their worst offer until we get above the threshold
|
||||
val offerToAdd = potentialAsks.minByOrNull { it.value }!!
|
||||
deltaInOurFavor += offerToAdd.value
|
||||
counterofferAsks[offerToAdd.key] = offerToAdd.value
|
||||
potentialAsks.remove(offerToAdd.key)
|
||||
}
|
||||
if (deltaInOurFavor < 0)
|
||||
return null // We couldn't get a good enough deal
|
||||
|
||||
// At this point we are sure to find a good counteroffer
|
||||
while (deltaInOurFavor > 0) {
|
||||
// Now remove the best offer valued below delta until the deal is barely acceptable
|
||||
val offerToRemove = counterofferAsks.filter { it.value <= deltaInOurFavor }.maxByOrNull { it.value }
|
||||
?: break // Nothing more can be removed, at least en bloc
|
||||
deltaInOurFavor -= offerToRemove.value
|
||||
counterofferAsks.remove(offerToRemove.key)
|
||||
}
|
||||
|
||||
// Only ask for enough of each resource to get maximum price
|
||||
for (ask in counterofferAsks.keys.filter { it.type == TradeType.Luxury_Resource || it.type == TradeType.Strategic_Resource }) {
|
||||
// Remove 1 amount as long as doing so does not change the price
|
||||
val originalValue = counterofferAsks[ask]!!
|
||||
while (ask.amount > 1
|
||||
&& originalValue == evaluation.evaluateBuyCostWithInflation(
|
||||
TradeOffer(ask.name, ask.type, ask.amount - 1, ask.duration),
|
||||
civInfo, otherCiv) ) {
|
||||
ask.amount--
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust any gold asked for
|
||||
val toRemove = ArrayList<TradeOffer>()
|
||||
for (goldAsk in counterofferAsks.keys
|
||||
.filter { it.type == TradeType.Gold_Per_Turn || it.type == TradeType.Gold }
|
||||
.sortedByDescending { it.type.ordinal }) { // Do GPT first
|
||||
val valueOfOne = evaluation.evaluateBuyCostWithInflation(TradeOffer(goldAsk.name, goldAsk.type, 1, goldAsk.duration), civInfo, otherCiv)
|
||||
val amountCanBeRemoved = deltaInOurFavor / valueOfOne
|
||||
if (amountCanBeRemoved >= goldAsk.amount) {
|
||||
deltaInOurFavor -= counterofferAsks[goldAsk]!!
|
||||
toRemove.add(goldAsk)
|
||||
} else {
|
||||
deltaInOurFavor -= valueOfOne * amountCanBeRemoved
|
||||
goldAsk.amount -= amountCanBeRemoved
|
||||
}
|
||||
}
|
||||
|
||||
// If the delta is still very in our favor consider sweetening the pot with some gold
|
||||
if (deltaInOurFavor >= 100) {
|
||||
deltaInOurFavor = (deltaInOurFavor * 2) / 3 // Only compensate some of it though, they're the ones asking us
|
||||
// First give some GPT, then lump sum - but only if they're not already offering the same
|
||||
for (ourGold in tradeLogic.ourAvailableOffers
|
||||
.filter { it.isTradable() && (it.type == TradeType.Gold || it.type == TradeType.Gold_Per_Turn) }
|
||||
.sortedByDescending { it.type.ordinal }) {
|
||||
if (tradeLogic.currentTrade.theirOffers.none { it.type == ourGold.type } &&
|
||||
counterofferAsks.keys.none { it.type == ourGold.type } ) {
|
||||
val valueOfOne = evaluation.evaluateSellCostWithInflation(TradeOffer(ourGold.name, ourGold.type, 1, ourGold.duration), civInfo, otherCiv)
|
||||
val amountToGive = min(deltaInOurFavor / valueOfOne, ourGold.amount)
|
||||
deltaInOurFavor -= amountToGive * valueOfOne
|
||||
if (amountToGive > 0) {
|
||||
counterofferGifts.add(
|
||||
TradeOffer(
|
||||
ourGold.name,
|
||||
ourGold.type,
|
||||
amountToGive,
|
||||
ourGold.duration
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tradeLogic.currentTrade.theirOffers.addAll(counterofferAsks.keys)
|
||||
tradeLogic.currentTrade.ourOffers.addAll(counterofferGifts)
|
||||
|
||||
// Trades reversed, because when *they* get it then the 'ouroffers' become 'theiroffers'
|
||||
return TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse())
|
||||
}
|
||||
|
||||
private fun respondToPopupAlerts(civInfo: Civilization) {
|
||||
for (popupAlert in civInfo.popupAlerts.toList()) { // toList because this can trigger other things that give alerts, like Golden Age
|
||||
if (popupAlert.type == AlertType.DemandToStopSettlingCitiesNear) { // we're called upon to make a decision
|
||||
@ -273,7 +104,7 @@ object NextTurnAutomation {
|
||||
val requestingCiv = civInfo.gameInfo.getCivilization(popupAlert.value)
|
||||
val diploManager = civInfo.getDiplomacyManager(requestingCiv)
|
||||
if (civInfo.diplomacyFunctions.canSignDeclarationOfFriendshipWith(requestingCiv)
|
||||
&& wantsToSignDeclarationOfFrienship(civInfo,requestingCiv)) {
|
||||
&& DiplomacyAutomation.wantsToSignDeclarationOfFrienship(civInfo,requestingCiv)) {
|
||||
diploManager.signDeclarationOfFriendship()
|
||||
requestingCiv.addNotification("We have signed a Declaration of Friendship with [${civInfo.civName}]!", NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, civInfo.civName)
|
||||
} else {
|
||||
@ -501,510 +332,6 @@ object NextTurnAutomation {
|
||||
}
|
||||
}
|
||||
|
||||
private fun chooseReligiousBeliefs(civInfo: Civilization) {
|
||||
choosePantheon(civInfo)
|
||||
foundReligion(civInfo)
|
||||
enhanceReligion(civInfo)
|
||||
chooseFreeBeliefs(civInfo)
|
||||
}
|
||||
|
||||
private fun choosePantheon(civInfo: Civilization) {
|
||||
if (!civInfo.religionManager.canFoundOrExpandPantheon()) return
|
||||
// So looking through the source code of the base game available online,
|
||||
// the functions for choosing beliefs total in at around 400 lines.
|
||||
// https://github.com/Gedemon/Civ5-DLL/blob/aa29e80751f541ae04858b6d2a2c7dcca454201e/CvGameCoreDLL_Expansion1/CvReligionClasses.cpp
|
||||
// line 4426 through 4870.
|
||||
// This is way too much work for now, so I'll just choose a random pantheon instead.
|
||||
// Should probably be changed later, but it works for now.
|
||||
val chosenPantheon = chooseBeliefOfType(civInfo, BeliefType.Pantheon)
|
||||
?: return // panic!
|
||||
civInfo.religionManager.chooseBeliefs(
|
||||
listOf(chosenPantheon),
|
||||
useFreeBeliefs = civInfo.religionManager.usingFreeBeliefs()
|
||||
)
|
||||
}
|
||||
|
||||
private fun foundReligion(civInfo: Civilization) {
|
||||
if (civInfo.religionManager.religionState != ReligionState.FoundingReligion) return
|
||||
val availableReligionIcons = civInfo.gameInfo.ruleset.religions
|
||||
.filterNot { civInfo.gameInfo.religions.values.map { religion -> religion.name }.contains(it) }
|
||||
val favoredReligion = civInfo.nation.favoredReligion
|
||||
val religionIcon =
|
||||
if (favoredReligion != null && favoredReligion in availableReligionIcons) favoredReligion
|
||||
else availableReligionIcons.randomOrNull()
|
||||
?: return // Wait what? How did we pass the checking when using a great prophet but not this?
|
||||
|
||||
civInfo.religionManager.foundReligion(religionIcon, religionIcon)
|
||||
|
||||
val chosenBeliefs = chooseBeliefs(civInfo, civInfo.religionManager.getBeliefsToChooseAtFounding()).toList()
|
||||
civInfo.religionManager.chooseBeliefs(chosenBeliefs)
|
||||
}
|
||||
|
||||
private fun enhanceReligion(civInfo: Civilization) {
|
||||
if (civInfo.religionManager.religionState != ReligionState.EnhancingReligion) return
|
||||
civInfo.religionManager.chooseBeliefs(
|
||||
chooseBeliefs(civInfo, civInfo.religionManager.getBeliefsToChooseAtEnhancing()).toList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun chooseFreeBeliefs(civInfo: Civilization) {
|
||||
if (!civInfo.religionManager.hasFreeBeliefs()) return
|
||||
civInfo.religionManager.chooseBeliefs(
|
||||
chooseBeliefs(civInfo, civInfo.religionManager.freeBeliefsAsEnums()).toList(),
|
||||
useFreeBeliefs = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun chooseBeliefs(civInfo: Civilization, beliefsToChoose: Counter<BeliefType>): HashSet<Belief> {
|
||||
val chosenBeliefs = hashSetOf<Belief>()
|
||||
// The `continue`s should never be reached, but just in case I'd rather have the AI have a
|
||||
// belief less than make the game crash. The `continue`s should only be reached whenever
|
||||
// there are not enough beliefs to choose, but there should be, as otherwise we could
|
||||
// not have used a great prophet to found/enhance our religion.
|
||||
for (belief in BeliefType.values()) {
|
||||
if (belief == BeliefType.None) continue
|
||||
repeat(beliefsToChoose[belief]) {
|
||||
chosenBeliefs.add(
|
||||
chooseBeliefOfType(civInfo, belief, chosenBeliefs) ?: return@repeat
|
||||
)
|
||||
}
|
||||
}
|
||||
return chosenBeliefs
|
||||
}
|
||||
|
||||
private fun chooseBeliefOfType(civInfo: Civilization, beliefType: BeliefType, additionalBeliefsToExclude: HashSet<Belief> = hashSetOf()): Belief? {
|
||||
return civInfo.gameInfo.ruleset.beliefs.values
|
||||
.filter {
|
||||
(it.type == beliefType || beliefType == BeliefType.Any)
|
||||
&& !additionalBeliefsToExclude.contains(it)
|
||||
&& civInfo.religionManager.getReligionWithBelief(it) == null
|
||||
&& it.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals)
|
||||
.none { unique -> !unique.conditionalsApply(civInfo) }
|
||||
}
|
||||
.maxByOrNull { ReligionAutomation.rateBelief(civInfo, it) }
|
||||
}
|
||||
|
||||
private fun potentialLuxuryTrades(civInfo: Civilization, otherCivInfo: Civilization): ArrayList<Trade> {
|
||||
val tradeLogic = TradeLogic(civInfo, otherCivInfo)
|
||||
val ourTradableLuxuryResources = tradeLogic.ourAvailableOffers
|
||||
.filter { it.type == TradeType.Luxury_Resource && it.amount > 1 }
|
||||
val theirTradableLuxuryResources = tradeLogic.theirAvailableOffers
|
||||
.filter { it.type == TradeType.Luxury_Resource && it.amount > 1 }
|
||||
val weHaveTheyDont = ourTradableLuxuryResources
|
||||
.filter { resource ->
|
||||
tradeLogic.theirAvailableOffers
|
||||
.none { it.name == resource.name && it.type == TradeType.Luxury_Resource }
|
||||
}
|
||||
val theyHaveWeDont = theirTradableLuxuryResources
|
||||
.filter { resource ->
|
||||
tradeLogic.ourAvailableOffers
|
||||
.none { it.name == resource.name && it.type == TradeType.Luxury_Resource }
|
||||
}.sortedBy { civInfo.cities.count { city -> city.demandedResource == it.name } } // Prioritize resources that get WLTKD
|
||||
val trades = ArrayList<Trade>()
|
||||
for (i in 0..min(weHaveTheyDont.lastIndex, theyHaveWeDont.lastIndex)) {
|
||||
val trade = Trade()
|
||||
trade.ourOffers.add(weHaveTheyDont[i].copy(amount = 1))
|
||||
trade.theirOffers.add(theyHaveWeDont[i].copy(amount = 1))
|
||||
trades.add(trade)
|
||||
}
|
||||
return trades
|
||||
}
|
||||
|
||||
private fun exchangeLuxuries(civInfo: Civilization) {
|
||||
val knownCivs = civInfo.getKnownCivs()
|
||||
|
||||
// Player trades are... more complicated.
|
||||
// When the AI offers a trade, it's not immediately accepted,
|
||||
// so what if it thinks that it has a spare luxury and offers it to two human players?
|
||||
// What's to stop the AI "nagging" the player to accept a luxury trade?
|
||||
// We should A. add some sort of timer (20? 30 turns?) between luxury trade requests if they're denied - see DeclinedLuxExchange
|
||||
// B. have a way for the AI to keep track of the "pending offers" - see DiplomacyManager.resourcesFromTrade
|
||||
|
||||
for (otherCiv in knownCivs.filter {
|
||||
it.isMajorCiv() && !it.isAtWarWith(civInfo)
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedLuxExchange)
|
||||
}) {
|
||||
|
||||
val isEnemy = civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelLE(RelationshipLevel.Enemy)
|
||||
if (isEnemy || otherCiv.tradeRequests.any { it.requestingCiv == civInfo.civName })
|
||||
continue
|
||||
|
||||
val trades = potentialLuxuryTrades(civInfo, otherCiv)
|
||||
for (trade in trades) {
|
||||
val tradeRequest = TradeRequest(civInfo.civName, trade.reverse())
|
||||
otherCiv.tradeRequests.add(tradeRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun offerDeclarationOfFriendship(civInfo: Civilization) {
|
||||
val civsThatWeCanDeclareFriendshipWith = civInfo.getKnownCivs()
|
||||
.filter { civInfo.diplomacyFunctions.canSignDeclarationOfFriendshipWith(it)
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship)}
|
||||
.sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() }.toList()
|
||||
for (otherCiv in civsThatWeCanDeclareFriendshipWith) {
|
||||
// Default setting is 2, this will be changed according to different civ.
|
||||
if ((1..10).random() <= 2 && wantsToSignDeclarationOfFrienship(civInfo, otherCiv)) {
|
||||
otherCiv.popupAlerts.add(PopupAlert(AlertType.DeclarationOfFriendship, civInfo.civName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wantsToSignDeclarationOfFrienship(civInfo: Civilization, otherCiv: Civilization): Boolean {
|
||||
val diploManager = civInfo.getDiplomacyManager(otherCiv)
|
||||
// Shortcut, if it is below favorable then don't consider it
|
||||
if (diploManager.isRelationshipLevelLT(RelationshipLevel.Favorable)) return false
|
||||
|
||||
val numOfFriends = civInfo.diplomacy.count { it.value.hasFlag(DiplomacyFlags.DeclarationOfFriendship) }
|
||||
val knownCivs = civInfo.getKnownCivs().count { it.isMajorCiv() && it.isAlive() }
|
||||
val allCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() } - 1 // Don't include us
|
||||
val deadCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() && !it.isAlive() }
|
||||
val allAliveCivs = allCivs - deadCivs
|
||||
|
||||
// Motivation should be constant as the number of civs changes
|
||||
var motivation = diploManager.opinionOfOtherCiv().toInt() - 40
|
||||
|
||||
// If the other civ is stronger than we are compelled to be nice to them
|
||||
// If they are too weak, then thier friendship doesn't mean much to us
|
||||
motivation += when (Automation.threatAssessment(civInfo,otherCiv)) {
|
||||
ThreatLevel.VeryHigh -> 10
|
||||
ThreatLevel.High -> 5
|
||||
ThreatLevel.VeryLow -> -5
|
||||
else -> 0
|
||||
}
|
||||
|
||||
// Try to ally with a fourth of the civs in play
|
||||
val civsToAllyWith = 0.25f * allAliveCivs
|
||||
if (numOfFriends < civsToAllyWith) {
|
||||
// Goes from 10 to 0 once the civ gets 1/4 of all alive civs as friends
|
||||
motivation += (10 - 10 * (numOfFriends / civsToAllyWith)).toInt()
|
||||
} else {
|
||||
// Goes form 0 to -120 as the civ gets more friends, offset by civsToAllyWith
|
||||
motivation -= (120f * (numOfFriends - civsToAllyWith) / (knownCivs - civsToAllyWith)).toInt()
|
||||
}
|
||||
|
||||
// Goes from 0 to -50 as more civs die
|
||||
// this is meant to prevent the game from stalemating when a group of friends
|
||||
// conquers all oposition
|
||||
motivation -= deadCivs / allCivs * 50
|
||||
|
||||
// Wait to declare frienships until more civs
|
||||
// Goes from -30 to 0 when we know 75% of allCivs
|
||||
val civsToKnow = 0.75f * allAliveCivs
|
||||
motivation -= ((civsToKnow - knownCivs) / civsToKnow * 30f).toInt().coerceAtLeast(0)
|
||||
|
||||
motivation -= hasAtLeastMotivationToAttack(civInfo, otherCiv, motivation / 2) * 2
|
||||
|
||||
return motivation > 0
|
||||
}
|
||||
|
||||
private fun offerOpenBorders(civInfo: Civilization) {
|
||||
if (!civInfo.hasUnique(UniqueType.EnablesOpenBorders)) return
|
||||
val civsThatWeCanOpenBordersWith = civInfo.getKnownCivs()
|
||||
.filter { it.isMajorCiv() && !civInfo.isAtWarWith(it)
|
||||
&& it.hasUnique(UniqueType.EnablesOpenBorders)
|
||||
&& !civInfo.getDiplomacyManager(it).hasOpenBorders
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedOpenBorders) }
|
||||
.sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() }.toList()
|
||||
for (otherCiv in civsThatWeCanOpenBordersWith) {
|
||||
// Default setting is 3, this will be changed according to different civ.
|
||||
if ((1..10).random() <= 3 && wantsToOpenBorders(civInfo, otherCiv)) {
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.openBorders, TradeType.Agreement))
|
||||
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.openBorders, TradeType.Agreement))
|
||||
|
||||
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun wantsToOpenBorders(civInfo: Civilization, otherCiv: Civilization): Boolean {
|
||||
if (civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelLT(RelationshipLevel.Favorable)) return false
|
||||
// Don't accept if they are at war with our friends, they might use our land to attack them
|
||||
if (civInfo.diplomacy.values.any { it.isRelationshipLevelGE(RelationshipLevel.Friend) && it.otherCiv().isAtWarWith(otherCiv)})
|
||||
return false
|
||||
if (hasAtLeastMotivationToAttack(civInfo, otherCiv, (civInfo.getDiplomacyManager(otherCiv).opinionOfOtherCiv()/ 2 - 10).toInt()) >= 0)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
private fun offerResearchAgreement(civInfo: Civilization) {
|
||||
if (!civInfo.diplomacyFunctions.canSignResearchAgreement()) return // don't waste your time
|
||||
|
||||
val canSignResearchAgreementCiv = civInfo.getKnownCivs()
|
||||
.filter {
|
||||
civInfo.diplomacyFunctions.canSignResearchAgreementsWith(it)
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedResearchAgreement)
|
||||
}
|
||||
.sortedByDescending { it.stats.statsForNextTurn.science }
|
||||
|
||||
for (otherCiv in canSignResearchAgreementCiv) {
|
||||
// Default setting is 5, this will be changed according to different civ.
|
||||
if ((1..10).random() > 5) continue
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
val cost = civInfo.diplomacyFunctions.getResearchAgreementCost(otherCiv)
|
||||
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.researchAgreement, TradeType.Treaty, cost))
|
||||
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.researchAgreement, TradeType.Treaty, cost))
|
||||
|
||||
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun offerDefensivePact(civInfo: Civilization) {
|
||||
if (!civInfo.diplomacyFunctions.canSignDefensivePact()) return // don't waste your time
|
||||
|
||||
val canSignDefensivePactCiv = civInfo.getKnownCivs()
|
||||
.filter {
|
||||
civInfo.diplomacyFunctions.canSignDefensivePactWith(it)
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedDefensivePact)
|
||||
&& civInfo.getDiplomacyManager(it).relationshipIgnoreAfraid() == RelationshipLevel.Ally
|
||||
}
|
||||
|
||||
for (otherCiv in canSignDefensivePactCiv) {
|
||||
// Default setting is 3, this will be changed according to different civ.
|
||||
if ((1..10).random() <= 3 && wantsToSignDefensivePact(civInfo, otherCiv)) {
|
||||
//todo: Add more in depth evaluation here
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.defensivePact, TradeType.Treaty))
|
||||
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.defensivePact, TradeType.Treaty))
|
||||
|
||||
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun wantsToSignDefensivePact(civInfo: Civilization, otherCiv: Civilization): Boolean {
|
||||
val diploManager = civInfo.getDiplomacyManager(otherCiv)
|
||||
if (diploManager.isRelationshipLevelLT(RelationshipLevel.Ally)) return false
|
||||
val commonknownCivs = diploManager.getCommonKnownCivs()
|
||||
// If they have bad relations with any of our friends, don't consider it
|
||||
for(thirdCiv in commonknownCivs) {
|
||||
if (civInfo.getDiplomacyManager(thirdCiv).isRelationshipLevelGE(RelationshipLevel.Friend)
|
||||
&& thirdCiv.getDiplomacyManager(otherCiv).isRelationshipLevelLT(RelationshipLevel.Favorable))
|
||||
return false
|
||||
}
|
||||
|
||||
val defensivePacts = civInfo.diplomacy.count { it.value.hasFlag(DiplomacyFlags.DefensivePact) }
|
||||
val otherCivNonOverlappingDefensivePacts = otherCiv.diplomacy.values.count { it.hasFlag(DiplomacyFlags.DefensivePact)
|
||||
&& (!it.otherCiv().knows(civInfo) || !it.otherCiv().getDiplomacyManager(civInfo).hasFlag(DiplomacyFlags.DefensivePact)) }
|
||||
val allCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() } - 1 // Don't include us
|
||||
val deadCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() && !it.isAlive() }
|
||||
val allAliveCivs = allCivs - deadCivs
|
||||
|
||||
// We have to already be at RelationshipLevel.Ally, so we must have 80 oppinion of them
|
||||
var motivation = diploManager.opinionOfOtherCiv().toInt() - 80
|
||||
|
||||
// If they are stronger than us, then we value it a lot more
|
||||
// If they are weaker than us, then we don't value it
|
||||
motivation += when (Automation.threatAssessment(civInfo,otherCiv)) {
|
||||
ThreatLevel.VeryHigh -> 10
|
||||
ThreatLevel.High -> 5
|
||||
ThreatLevel.Low -> -15
|
||||
ThreatLevel.VeryLow -> -30
|
||||
else -> 0
|
||||
}
|
||||
|
||||
// If they have a defensive pact with another civ then we would get drawn into thier battles as well
|
||||
motivation -= 10 * otherCivNonOverlappingDefensivePacts
|
||||
|
||||
// Try to have a defensive pact with 1/5 of all civs
|
||||
val civsToAllyWith = 0.20f * allAliveCivs
|
||||
// Goes form 0 to -50 as the civ gets more allies, offset by civsToAllyWith
|
||||
motivation -= (50f * (defensivePacts - civsToAllyWith) / (allAliveCivs - civsToAllyWith)).coerceAtMost(0f).toInt()
|
||||
|
||||
return motivation > 0
|
||||
}
|
||||
|
||||
private fun declareWar(civInfo: Civilization) {
|
||||
if (civInfo.wantsToFocusOn(Victory.Focus.Culture)) return
|
||||
if (civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
|
||||
if (civInfo.isAtWar() || civInfo.getHappiness() <= 0) return
|
||||
|
||||
val ourMilitaryUnits = civInfo.units.getCivUnits().filter { !it.isCivilian() }.count()
|
||||
if (ourMilitaryUnits < civInfo.cities.size) return
|
||||
if (ourMilitaryUnits < 4) return // to stop AI declaring war at the beginning of games when everyone isn't set up well enough\
|
||||
if (civInfo.cities.size < 3) return // FAR too early for that what are you thinking!
|
||||
|
||||
//evaluate war
|
||||
val enemyCivs = civInfo.getKnownCivs()
|
||||
.filterNot {
|
||||
it == civInfo || it.cities.isEmpty() || !civInfo.getDiplomacyManager(it).canDeclareWar()
|
||||
|| it.cities.none { city -> civInfo.hasExplored(city.getCenterTile()) }
|
||||
}
|
||||
// If the AI declares war on a civ without knowing the location of any cities, it'll just keep amassing an army and not sending it anywhere,
|
||||
// and end up at a massive disadvantage
|
||||
|
||||
if (enemyCivs.none()) return
|
||||
|
||||
val minMotivationToAttack = 20
|
||||
val civWithBestMotivationToAttack = enemyCivs
|
||||
.map { Pair(it, hasAtLeastMotivationToAttack(civInfo, it, minMotivationToAttack)) }
|
||||
.maxByOrNull { it.second }!!
|
||||
|
||||
if (civWithBestMotivationToAttack.second >= minMotivationToAttack)
|
||||
civInfo.getDiplomacyManager(civWithBestMotivationToAttack.first).declareWar()
|
||||
}
|
||||
|
||||
/** Will return the motivation to attack, but might short circuit if the value is guaranteed to
|
||||
* be lower than `atLeast`. So any values below `atLeast` should not be used for comparison. */
|
||||
private fun hasAtLeastMotivationToAttack(civInfo: Civilization, otherCiv: Civilization, atLeast: Int): Int {
|
||||
val closestCities = getClosestCities(civInfo, otherCiv) ?: return 0
|
||||
val baseForce = 30f
|
||||
|
||||
var ourCombatStrength = civInfo.getStatForRanking(RankingType.Force).toFloat() + baseForce
|
||||
if (civInfo.getCapital() != null) ourCombatStrength += CityCombatant(civInfo.getCapital()!!).getCityStrength()
|
||||
var theirCombatStrength = otherCiv.getStatForRanking(RankingType.Force).toFloat() + baseForce
|
||||
if(otherCiv.getCapital() != null) theirCombatStrength += CityCombatant(otherCiv.getCapital()!!).getCityStrength()
|
||||
|
||||
//for city-states, also consider their protectors
|
||||
if (otherCiv.isCityState() and otherCiv.cityStateFunctions.getProtectorCivs().isNotEmpty()) {
|
||||
theirCombatStrength += otherCiv.cityStateFunctions.getProtectorCivs().filterNot { it == civInfo }
|
||||
.sumOf { it.getStatForRanking(RankingType.Force) }
|
||||
}
|
||||
|
||||
if (theirCombatStrength > ourCombatStrength) return 0
|
||||
|
||||
val ourCity = closestCities.city1
|
||||
val theirCity = closestCities.city2
|
||||
|
||||
if (civInfo.units.getCivUnits().filter { it.isMilitary() }.none {
|
||||
val damageReceivedWhenAttacking =
|
||||
BattleDamage.calculateDamageToAttacker(
|
||||
MapUnitCombatant(it),
|
||||
CityCombatant(theirCity)
|
||||
)
|
||||
damageReceivedWhenAttacking < 100
|
||||
})
|
||||
return 0 // You don't have any units that can attack this city without dying, don't declare war.
|
||||
|
||||
fun isTileCanMoveThrough(tile: Tile): Boolean {
|
||||
val owner = tile.getOwner()
|
||||
return !tile.isImpassible()
|
||||
&& (owner == otherCiv || owner == null || civInfo.diplomacyFunctions.canPassThroughTiles(owner))
|
||||
}
|
||||
|
||||
val modifierMap = HashMap<String, Int>()
|
||||
val combatStrengthRatio = ourCombatStrength / theirCombatStrength
|
||||
val combatStrengthModifier = when {
|
||||
combatStrengthRatio > 3f -> 30
|
||||
combatStrengthRatio > 2.5f -> 25
|
||||
combatStrengthRatio > 2f -> 20
|
||||
combatStrengthRatio > 1.5f -> 10
|
||||
else -> 0
|
||||
}
|
||||
modifierMap["Relative combat strength"] = combatStrengthModifier
|
||||
|
||||
|
||||
if (closestCities.aerialDistance > 7)
|
||||
modifierMap["Far away cities"] = -10
|
||||
|
||||
val diplomacyManager = civInfo.getDiplomacyManager(otherCiv)
|
||||
if (diplomacyManager.hasFlag(DiplomacyFlags.ResearchAgreement))
|
||||
modifierMap["Research Agreement"] = -5
|
||||
|
||||
if (diplomacyManager.hasFlag(DiplomacyFlags.DeclarationOfFriendship))
|
||||
modifierMap["Declaration of Friendship"] = -10
|
||||
|
||||
val relationshipModifier = when (diplomacyManager.relationshipIgnoreAfraid()) {
|
||||
RelationshipLevel.Unforgivable -> 10
|
||||
RelationshipLevel.Enemy -> 5
|
||||
RelationshipLevel.Ally -> -5 // this is so that ally + DoF is not too unbalanced -
|
||||
// still possible for AI to declare war for isolated city
|
||||
else -> 0
|
||||
}
|
||||
modifierMap["Relationship"] = relationshipModifier
|
||||
|
||||
if (diplomacyManager.resourcesFromTrade().any { it.amount > 0 })
|
||||
modifierMap["Receiving trade resources"] = -5
|
||||
|
||||
if (theirCity.getTiles().none { tile -> tile.neighbors.any { it.getOwner() == theirCity.civ && it.getCity() != theirCity } })
|
||||
modifierMap["Isolated city"] = 15
|
||||
|
||||
if (otherCiv.isCityState()) {
|
||||
modifierMap["City-state"] = -20
|
||||
if (otherCiv.getAllyCiv() == civInfo.civName)
|
||||
modifierMap["Allied City-state"] = -20 // There had better be a DAMN good reason
|
||||
}
|
||||
|
||||
for (city in otherCiv.cities) {
|
||||
val construction = city.cityConstructions.getCurrentConstruction()
|
||||
if (construction is Building && construction.hasUnique(UniqueType.TriggersCulturalVictory))
|
||||
modifierMap["About to win"] = 15
|
||||
if (construction is BaseUnit && construction.hasUnique(UniqueType.AddInCapital))
|
||||
modifierMap["About to win"] = 15
|
||||
}
|
||||
|
||||
var motivationSoFar = modifierMap.values.sum()
|
||||
|
||||
// We don't need to execute the expensive BFSs below if we're below the threshold here
|
||||
// anyways, since it won't get better from those, only worse.
|
||||
if (motivationSoFar < atLeast) {
|
||||
return motivationSoFar
|
||||
}
|
||||
|
||||
|
||||
val landPathBFS = BFS(ourCity.getCenterTile()) {
|
||||
it.isLand && isTileCanMoveThrough(it)
|
||||
}
|
||||
|
||||
landPathBFS.stepUntilDestination(theirCity.getCenterTile())
|
||||
if (!landPathBFS.hasReachedTile(theirCity.getCenterTile()))
|
||||
motivationSoFar -= -10
|
||||
|
||||
// We don't need to execute the expensive BFSs below if we're below the threshold here
|
||||
// anyways, since it won't get better from those, only worse.
|
||||
if (motivationSoFar < atLeast) {
|
||||
return motivationSoFar
|
||||
}
|
||||
|
||||
val reachableEnemyCitiesBfs = BFS(civInfo.getCapital(true)!!.getCenterTile()) { isTileCanMoveThrough(it) }
|
||||
reachableEnemyCitiesBfs.stepToEnd()
|
||||
val reachableEnemyCities = otherCiv.cities.filter { reachableEnemyCitiesBfs.hasReachedTile(it.getCenterTile()) }
|
||||
if (reachableEnemyCities.isEmpty()) return 0 // Can't even reach the enemy city, no point in war.
|
||||
|
||||
return motivationSoFar
|
||||
}
|
||||
|
||||
|
||||
private fun offerPeaceTreaty(civInfo: Civilization) {
|
||||
if (!civInfo.isAtWar() || civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
|
||||
|
||||
val enemiesCiv = civInfo.diplomacy.filter { it.value.diplomaticStatus == DiplomaticStatus.War }
|
||||
.map { it.value.otherCiv() }
|
||||
.filterNot { it == civInfo || it.isBarbarian() || it.cities.isEmpty() }
|
||||
.filter { !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedPeace) }
|
||||
// Don't allow AIs to offer peace to city states allied with their enemies
|
||||
.filterNot { it.isCityState() && it.getAllyCiv() != null && civInfo.isAtWarWith(civInfo.gameInfo.getCivilization(it.getAllyCiv()!!)) }
|
||||
// ignore civs that we have already offered peace this turn as a counteroffer to another civ's peace offer
|
||||
.filter { it.tradeRequests.none { tradeRequest -> tradeRequest.requestingCiv == civInfo.civName && tradeRequest.trade.isPeaceTreaty() } }
|
||||
|
||||
for (enemy in enemiesCiv) {
|
||||
if(hasAtLeastMotivationToAttack(civInfo, enemy, 10) >= 10) {
|
||||
// We can still fight. Refuse peace.
|
||||
continue
|
||||
}
|
||||
|
||||
// pay for peace
|
||||
val tradeLogic = TradeLogic(civInfo, enemy)
|
||||
|
||||
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.peaceTreaty, TradeType.Treaty))
|
||||
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.peaceTreaty, TradeType.Treaty))
|
||||
|
||||
var moneyWeNeedToPay = -TradeEvaluation().evaluatePeaceCostForThem(civInfo, enemy)
|
||||
|
||||
if (civInfo.gold > 0 && moneyWeNeedToPay > 0) {
|
||||
if (moneyWeNeedToPay > civInfo.gold) {
|
||||
moneyWeNeedToPay = civInfo.gold // As much as possible
|
||||
}
|
||||
tradeLogic.currentTrade.ourOffers.add(
|
||||
TradeOffer("Gold".tr(), TradeType.Gold, moneyWeNeedToPay)
|
||||
)
|
||||
}
|
||||
|
||||
enemy.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun automateUnits(civInfo: Civilization) {
|
||||
val isAtWar = civInfo.isAtWar()
|
||||
|
@ -5,9 +5,11 @@ import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.managers.ReligionState
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.Counter
|
||||
import com.unciv.models.ruleset.Belief
|
||||
import com.unciv.models.ruleset.BeliefType
|
||||
import com.unciv.models.ruleset.Victory
|
||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.stats.Stat
|
||||
import kotlin.math.min
|
||||
@ -398,5 +400,90 @@ object ReligionAutomation {
|
||||
return score
|
||||
}
|
||||
|
||||
|
||||
internal fun chooseReligiousBeliefs(civInfo: Civilization) {
|
||||
choosePantheon(civInfo)
|
||||
foundReligion(civInfo)
|
||||
enhanceReligion(civInfo)
|
||||
chooseFreeBeliefs(civInfo)
|
||||
}
|
||||
|
||||
private fun choosePantheon(civInfo: Civilization) {
|
||||
if (!civInfo.religionManager.canFoundOrExpandPantheon()) return
|
||||
// So looking through the source code of the base game available online,
|
||||
// the functions for choosing beliefs total in at around 400 lines.
|
||||
// https://github.com/Gedemon/Civ5-DLL/blob/aa29e80751f541ae04858b6d2a2c7dcca454201e/CvGameCoreDLL_Expansion1/CvReligionClasses.cpp
|
||||
// line 4426 through 4870.
|
||||
// This is way too much work for now, so I'll just choose a random pantheon instead.
|
||||
// Should probably be changed later, but it works for now.
|
||||
val chosenPantheon = chooseBeliefOfType(civInfo, BeliefType.Pantheon)
|
||||
?: return // panic!
|
||||
civInfo.religionManager.chooseBeliefs(
|
||||
listOf(chosenPantheon),
|
||||
useFreeBeliefs = civInfo.religionManager.usingFreeBeliefs()
|
||||
)
|
||||
}
|
||||
|
||||
private fun foundReligion(civInfo: Civilization) {
|
||||
if (civInfo.religionManager.religionState != ReligionState.FoundingReligion) return
|
||||
val availableReligionIcons = civInfo.gameInfo.ruleset.religions
|
||||
.filterNot { civInfo.gameInfo.religions.values.map { religion -> religion.name }.contains(it) }
|
||||
val favoredReligion = civInfo.nation.favoredReligion
|
||||
val religionIcon =
|
||||
if (favoredReligion != null && favoredReligion in availableReligionIcons) favoredReligion
|
||||
else availableReligionIcons.randomOrNull()
|
||||
?: return // Wait what? How did we pass the checking when using a great prophet but not this?
|
||||
|
||||
civInfo.religionManager.foundReligion(religionIcon, religionIcon)
|
||||
|
||||
val chosenBeliefs = chooseBeliefs(civInfo, civInfo.religionManager.getBeliefsToChooseAtFounding()).toList()
|
||||
civInfo.religionManager.chooseBeliefs(chosenBeliefs)
|
||||
}
|
||||
|
||||
private fun enhanceReligion(civInfo: Civilization) {
|
||||
if (civInfo.religionManager.religionState != ReligionState.EnhancingReligion) return
|
||||
civInfo.religionManager.chooseBeliefs(
|
||||
chooseBeliefs(civInfo, civInfo.religionManager.getBeliefsToChooseAtEnhancing()).toList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun chooseFreeBeliefs(civInfo: Civilization) {
|
||||
if (!civInfo.religionManager.hasFreeBeliefs()) return
|
||||
civInfo.religionManager.chooseBeliefs(
|
||||
chooseBeliefs(civInfo, civInfo.religionManager.freeBeliefsAsEnums()).toList(),
|
||||
useFreeBeliefs = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun chooseBeliefs(civInfo: Civilization, beliefsToChoose: Counter<BeliefType>): HashSet<Belief> {
|
||||
val chosenBeliefs = hashSetOf<Belief>()
|
||||
// The `continue`s should never be reached, but just in case I'd rather have the AI have a
|
||||
// belief less than make the game crash. The `continue`s should only be reached whenever
|
||||
// there are not enough beliefs to choose, but there should be, as otherwise we could
|
||||
// not have used a great prophet to found/enhance our religion.
|
||||
for (belief in BeliefType.values()) {
|
||||
if (belief == BeliefType.None) continue
|
||||
repeat(beliefsToChoose[belief]) {
|
||||
chosenBeliefs.add(
|
||||
chooseBeliefOfType(civInfo, belief, chosenBeliefs) ?: return@repeat
|
||||
)
|
||||
}
|
||||
}
|
||||
return chosenBeliefs
|
||||
}
|
||||
|
||||
private fun chooseBeliefOfType(civInfo: Civilization, beliefType: BeliefType, additionalBeliefsToExclude: HashSet<Belief> = hashSetOf()): Belief? {
|
||||
return civInfo.gameInfo.ruleset.beliefs.values
|
||||
.filter {
|
||||
(it.type == beliefType || beliefType == BeliefType.Any)
|
||||
&& !additionalBeliefsToExclude.contains(it)
|
||||
&& civInfo.religionManager.getReligionWithBelief(it) == null
|
||||
&& it.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals)
|
||||
.none { unique -> !unique.conditionalsApply(civInfo) }
|
||||
}
|
||||
.maxByOrNull { ReligionAutomation.rateBelief(civInfo, it) }
|
||||
}
|
||||
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
@ -0,0 +1,222 @@
|
||||
package com.unciv.logic.automation.civilization
|
||||
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.NotificationCategory
|
||||
import com.unciv.logic.civilization.NotificationIcon
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
|
||||
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
|
||||
import com.unciv.logic.trade.Trade
|
||||
import com.unciv.logic.trade.TradeEvaluation
|
||||
import com.unciv.logic.trade.TradeLogic
|
||||
import com.unciv.logic.trade.TradeOffer
|
||||
import com.unciv.logic.trade.TradeRequest
|
||||
import com.unciv.logic.trade.TradeType
|
||||
import kotlin.math.min
|
||||
|
||||
object TradeAutomation {
|
||||
|
||||
|
||||
fun respondToTradeRequests(civInfo: Civilization) {
|
||||
for (tradeRequest in civInfo.tradeRequests.toList()) {
|
||||
val otherCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv)
|
||||
if (!TradeEvaluation().isTradeValid(tradeRequest.trade, civInfo, otherCiv))
|
||||
continue
|
||||
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
tradeLogic.currentTrade.set(tradeRequest.trade)
|
||||
/** We need to remove this here, so that if the trade is accepted, the updateDetailedCivResources()
|
||||
* in tradeLogic.acceptTrade() will not consider *both* the trade *and the trade offer as decreasing the
|
||||
* amount of available resources, since that will lead to "Our proposed trade is no longer valid" if we try to offer
|
||||
* the same resource to ANOTHER civ in this turn. Complicated!
|
||||
*/
|
||||
civInfo.tradeRequests.remove(tradeRequest)
|
||||
if (TradeEvaluation().isTradeAcceptable(tradeLogic.currentTrade, civInfo, otherCiv)) {
|
||||
tradeLogic.acceptTrade()
|
||||
otherCiv.addNotification("[${civInfo.civName}] has accepted your trade request", NotificationCategory.Trade, NotificationIcon.Trade, civInfo.civName)
|
||||
} else {
|
||||
val counteroffer = getCounteroffer(civInfo, tradeRequest)
|
||||
if (counteroffer != null) {
|
||||
otherCiv.addNotification("[${civInfo.civName}] has made a counteroffer to your trade request", NotificationCategory.Trade, NotificationIcon.Trade, civInfo.civName)
|
||||
otherCiv.tradeRequests.add(counteroffer)
|
||||
} else
|
||||
tradeRequest.decline(civInfo)
|
||||
}
|
||||
}
|
||||
civInfo.tradeRequests.clear()
|
||||
}
|
||||
|
||||
/** @return a TradeRequest with the same ourOffers as [tradeRequest] but with enough theirOffers
|
||||
* added to make the deal acceptable. Will find a valid counteroffer if any exist, but is not
|
||||
* guaranteed to find the best or closest one. */
|
||||
private fun getCounteroffer(civInfo: Civilization, tradeRequest: TradeRequest): TradeRequest? {
|
||||
val otherCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv)
|
||||
// AIs counteroffering each other is problematic as they tend to ping-pong back and forth forever
|
||||
if (otherCiv.playerType == PlayerType.AI)
|
||||
return null
|
||||
val evaluation = TradeEvaluation()
|
||||
var deltaInOurFavor = evaluation.getTradeAcceptability(tradeRequest.trade, civInfo, otherCiv)
|
||||
if (deltaInOurFavor > 0) deltaInOurFavor = (deltaInOurFavor / 1.1f).toInt() // They seem very interested in this deal, let's push it a bit.
|
||||
val tradeLogic = TradeLogic(civInfo, otherCiv)
|
||||
|
||||
tradeLogic.currentTrade.set(tradeRequest.trade)
|
||||
|
||||
// What do they have that we would want?
|
||||
val potentialAsks = HashMap<TradeOffer, Int>()
|
||||
val counterofferAsks = HashMap<TradeOffer, Int>()
|
||||
val counterofferGifts = ArrayList<TradeOffer>()
|
||||
|
||||
for (offer in tradeLogic.theirAvailableOffers) {
|
||||
if ((offer.type == TradeType.Gold || offer.type == TradeType.Gold_Per_Turn)
|
||||
&& tradeRequest.trade.ourOffers.any { it.type == offer.type })
|
||||
continue // Don't want to counteroffer straight gold for gold, that's silly
|
||||
if (!offer.isTradable())
|
||||
continue // For example resources gained by trade or CS
|
||||
if (offer.type == TradeType.City)
|
||||
continue // Players generally don't want to give up their cities, and they might misclick
|
||||
|
||||
if (tradeLogic.currentTrade.theirOffers.any { it.type == offer.type && it.name == offer.name })
|
||||
continue // So you don't get double offers of open borders declarations of war etc.
|
||||
if (offer.type == TradeType.Treaty)
|
||||
continue // Don't try to counter with a defensive pact or research pact
|
||||
|
||||
val value = evaluation.evaluateBuyCostWithInflation(offer, civInfo, otherCiv)
|
||||
if (value > 0)
|
||||
potentialAsks[offer] = value
|
||||
}
|
||||
|
||||
while (potentialAsks.isNotEmpty() && deltaInOurFavor < 0) {
|
||||
// Keep adding their worst offer until we get above the threshold
|
||||
val offerToAdd = potentialAsks.minByOrNull { it.value }!!
|
||||
deltaInOurFavor += offerToAdd.value
|
||||
counterofferAsks[offerToAdd.key] = offerToAdd.value
|
||||
potentialAsks.remove(offerToAdd.key)
|
||||
}
|
||||
if (deltaInOurFavor < 0)
|
||||
return null // We couldn't get a good enough deal
|
||||
|
||||
// At this point we are sure to find a good counteroffer
|
||||
while (deltaInOurFavor > 0) {
|
||||
// Now remove the best offer valued below delta until the deal is barely acceptable
|
||||
val offerToRemove = counterofferAsks.filter { it.value <= deltaInOurFavor }.maxByOrNull { it.value }
|
||||
?: break // Nothing more can be removed, at least en bloc
|
||||
deltaInOurFavor -= offerToRemove.value
|
||||
counterofferAsks.remove(offerToRemove.key)
|
||||
}
|
||||
|
||||
// Only ask for enough of each resource to get maximum price
|
||||
for (ask in counterofferAsks.keys.filter { it.type == TradeType.Luxury_Resource || it.type == TradeType.Strategic_Resource }) {
|
||||
// Remove 1 amount as long as doing so does not change the price
|
||||
val originalValue = counterofferAsks[ask]!!
|
||||
while (ask.amount > 1
|
||||
&& originalValue == evaluation.evaluateBuyCostWithInflation(
|
||||
TradeOffer(ask.name, ask.type, ask.amount - 1, ask.duration),
|
||||
civInfo, otherCiv) ) {
|
||||
ask.amount--
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust any gold asked for
|
||||
val toRemove = ArrayList<TradeOffer>()
|
||||
for (goldAsk in counterofferAsks.keys
|
||||
.filter { it.type == TradeType.Gold_Per_Turn || it.type == TradeType.Gold }
|
||||
.sortedByDescending { it.type.ordinal }) { // Do GPT first
|
||||
val valueOfOne = evaluation.evaluateBuyCostWithInflation(TradeOffer(goldAsk.name, goldAsk.type, 1, goldAsk.duration), civInfo, otherCiv)
|
||||
val amountCanBeRemoved = deltaInOurFavor / valueOfOne
|
||||
if (amountCanBeRemoved >= goldAsk.amount) {
|
||||
deltaInOurFavor -= counterofferAsks[goldAsk]!!
|
||||
toRemove.add(goldAsk)
|
||||
} else {
|
||||
deltaInOurFavor -= valueOfOne * amountCanBeRemoved
|
||||
goldAsk.amount -= amountCanBeRemoved
|
||||
}
|
||||
}
|
||||
|
||||
// If the delta is still very in our favor consider sweetening the pot with some gold
|
||||
if (deltaInOurFavor >= 100) {
|
||||
deltaInOurFavor = (deltaInOurFavor * 2) / 3 // Only compensate some of it though, they're the ones asking us
|
||||
// First give some GPT, then lump sum - but only if they're not already offering the same
|
||||
for (ourGold in tradeLogic.ourAvailableOffers
|
||||
.filter { it.isTradable() && (it.type == TradeType.Gold || it.type == TradeType.Gold_Per_Turn) }
|
||||
.sortedByDescending { it.type.ordinal }) {
|
||||
if (tradeLogic.currentTrade.theirOffers.none { it.type == ourGold.type } &&
|
||||
counterofferAsks.keys.none { it.type == ourGold.type } ) {
|
||||
val valueOfOne = evaluation.evaluateSellCostWithInflation(TradeOffer(ourGold.name, ourGold.type, 1, ourGold.duration), civInfo, otherCiv)
|
||||
val amountToGive = min(deltaInOurFavor / valueOfOne, ourGold.amount)
|
||||
deltaInOurFavor -= amountToGive * valueOfOne
|
||||
if (amountToGive > 0) {
|
||||
counterofferGifts.add(
|
||||
TradeOffer(
|
||||
ourGold.name,
|
||||
ourGold.type,
|
||||
amountToGive,
|
||||
ourGold.duration
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tradeLogic.currentTrade.theirOffers.addAll(counterofferAsks.keys)
|
||||
tradeLogic.currentTrade.ourOffers.addAll(counterofferGifts)
|
||||
|
||||
// Trades reversed, because when *they* get it then the 'ouroffers' become 'theiroffers'
|
||||
return TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse())
|
||||
}
|
||||
|
||||
|
||||
fun exchangeLuxuries(civInfo: Civilization) {
|
||||
val knownCivs = civInfo.getKnownCivs()
|
||||
|
||||
// Player trades are... more complicated.
|
||||
// When the AI offers a trade, it's not immediately accepted,
|
||||
// so what if it thinks that it has a spare luxury and offers it to two human players?
|
||||
// What's to stop the AI "nagging" the player to accept a luxury trade?
|
||||
// We should A. add some sort of timer (20? 30 turns?) between luxury trade requests if they're denied - see DeclinedLuxExchange
|
||||
// B. have a way for the AI to keep track of the "pending offers" - see DiplomacyManager.resourcesFromTrade
|
||||
|
||||
for (otherCiv in knownCivs.filter {
|
||||
it.isMajorCiv() && !it.isAtWarWith(civInfo)
|
||||
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedLuxExchange)
|
||||
}) {
|
||||
|
||||
val isEnemy = civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelLE(RelationshipLevel.Enemy)
|
||||
if (isEnemy || otherCiv.tradeRequests.any { it.requestingCiv == civInfo.civName })
|
||||
continue
|
||||
|
||||
val trades = potentialLuxuryTrades(civInfo, otherCiv)
|
||||
for (trade in trades) {
|
||||
val tradeRequest = TradeRequest(civInfo.civName, trade.reverse())
|
||||
otherCiv.tradeRequests.add(tradeRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun potentialLuxuryTrades(civInfo: Civilization, otherCivInfo: Civilization): ArrayList<Trade> {
|
||||
val tradeLogic = TradeLogic(civInfo, otherCivInfo)
|
||||
val ourTradableLuxuryResources = tradeLogic.ourAvailableOffers
|
||||
.filter { it.type == TradeType.Luxury_Resource && it.amount > 1 }
|
||||
val theirTradableLuxuryResources = tradeLogic.theirAvailableOffers
|
||||
.filter { it.type == TradeType.Luxury_Resource && it.amount > 1 }
|
||||
val weHaveTheyDont = ourTradableLuxuryResources
|
||||
.filter { resource ->
|
||||
tradeLogic.theirAvailableOffers
|
||||
.none { it.name == resource.name && it.type == TradeType.Luxury_Resource }
|
||||
}
|
||||
val theyHaveWeDont = theirTradableLuxuryResources
|
||||
.filter { resource ->
|
||||
tradeLogic.ourAvailableOffers
|
||||
.none { it.name == resource.name && it.type == TradeType.Luxury_Resource }
|
||||
}.sortedBy { civInfo.cities.count { city -> city.demandedResource == it.name } } // Prioritize resources that get WLTKD
|
||||
val trades = ArrayList<Trade>()
|
||||
for (i in 0..min(weHaveTheyDont.lastIndex, theyHaveWeDont.lastIndex)) {
|
||||
val trade = Trade()
|
||||
trade.ourOffers.add(weHaveTheyDont[i].copy(amount = 1))
|
||||
trade.theirOffers.add(theyHaveWeDont[i].copy(amount = 1))
|
||||
trades.add(trade)
|
||||
}
|
||||
return trades
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,7 @@ package com.unciv.logic.trade
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.automation.Automation
|
||||
import com.unciv.logic.automation.ThreatLevel
|
||||
import com.unciv.logic.automation.civilization.NextTurnAutomation
|
||||
import com.unciv.logic.automation.civilization.DiplomacyAutomation
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
|
||||
@ -93,7 +93,7 @@ class TradeEvaluation {
|
||||
|
||||
return sumOfTheirOffers - sumOfOurOffers
|
||||
}
|
||||
|
||||
|
||||
fun evaluateBuyCostWithInflation(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int {
|
||||
if (offer.type != TradeType.Gold && offer.type != TradeType.Gold_Per_Turn)
|
||||
return (evaluateBuyCost(offer, civInfo, tradePartner) / getGoldInflation(civInfo)).toInt()
|
||||
@ -133,10 +133,10 @@ class TradeEvaluation {
|
||||
val amountToBuyInOffer = min(amountWillingToBuy, offer.amount)
|
||||
|
||||
val canUseForBuildings = civInfo.cities
|
||||
.any { city -> city.cityConstructions.getBuildableBuildings().any {
|
||||
.any { city -> city.cityConstructions.getBuildableBuildings().any {
|
||||
it.getResourceRequirementsPerTurn(StateForConditionals(civInfo, city)).containsKey(offer.name) } }
|
||||
val canUseForUnits = civInfo.cities
|
||||
.any { city -> city.cityConstructions.getConstructableUnits().any {
|
||||
.any { city -> city.cityConstructions.getConstructableUnits().any {
|
||||
it.getResourceRequirementsPerTurn(StateForConditionals(civInfo)).containsKey(offer.name) } }
|
||||
if (!canUseForBuildings && !canUseForUnits) return 0
|
||||
|
||||
@ -217,7 +217,7 @@ class TradeEvaluation {
|
||||
return when (offer.name) {
|
||||
// Since it will be evaluated twice, once when they evaluate our offer and once when they evaluate theirs
|
||||
Constants.peaceTreaty -> evaluatePeaceCostForThem(civInfo, tradePartner)
|
||||
Constants.defensivePact -> if (NextTurnAutomation.wantsToSignDefensivePact(civInfo, tradePartner)) 0
|
||||
Constants.defensivePact -> if (DiplomacyAutomation.wantsToSignDefensivePact(civInfo, tradePartner)) 0
|
||||
else 100000
|
||||
Constants.researchAgreement -> -offer.amount
|
||||
else -> 1000
|
||||
@ -316,7 +316,7 @@ class TradeEvaluation {
|
||||
// So this does not scale off to infinity
|
||||
return modifier / (goldPerTurn.pow(1.2).coerceAtLeast(0.0) + (1.11f * modifier)) + .1f
|
||||
}
|
||||
|
||||
|
||||
/** This code returns a positive value if the city is significantly far away from the capital
|
||||
* and given how this method is used this ends up making such cities more expensive. That's how
|
||||
* I found it. I'm not sure it makes sense. One might also find arguments why cities closer to
|
||||
|
@ -50,9 +50,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
/********************** **********************/
|
||||
// e.g. json configs complete and parseable
|
||||
// Check for mod or Civ_V_GnK to avoid running the same test twice (~200ms for the builtin assets)
|
||||
if (ruleset.folderLocation != null) {
|
||||
checkTilesetSanity(lines)
|
||||
}
|
||||
if (ruleset.folderLocation != null) checkTilesetSanity(lines)
|
||||
|
||||
return lines
|
||||
}
|
||||
@ -331,6 +329,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
) {
|
||||
if (ruleset.terrains.values.none { it.type == TerrainType.Land && !it.impassable })
|
||||
lines += "No passable land terrains exist!"
|
||||
|
||||
for (terrain in ruleset.terrains.values) {
|
||||
for (baseTerrainName in terrain.occursOn) {
|
||||
val baseTerrain = ruleset.terrains[baseTerrainName]
|
||||
|
Loading…
Reference in New Issue
Block a user