AI diplomacy balancing (#10476)

* Merged Original AIDiplomacyBalancing changes to this branch

* Fixed being over supply resulting in the civ not attacking

* Fixed "War with allies" motivation not being added
This commit is contained in:
Oskar Niesen 2023-11-18 14:34:46 -06:00 committed by GitHub
parent 371ec28a11
commit 6fe9b7ea7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 136 additions and 21 deletions

View File

@ -43,10 +43,12 @@ object DiplomacyAutomation {
internal fun wantsToSignDeclarationOfFrienship(civInfo: Civilization, otherCiv: Civilization): Boolean {
val diploManager = civInfo.getDiplomacyManager(otherCiv)
if (diploManager.hasFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship)) return false
// 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 otherCivNumberOfFriends = otherCiv.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() }
@ -70,15 +72,21 @@ object DiplomacyAutomation {
// 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
// Goes from 0 to -120 as the civ gets more friends, offset by civsToAllyWith
motivation -= (120f * (numOfFriends - civsToAllyWith) / (knownCivs - civsToAllyWith)).toInt()
}
// The more friends they have the less we should want to sign friendship (To promote teams)
motivation -= otherCivNumberOfFriends * 10
// 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
// Become more desperate as we have more wars
motivation += civInfo.diplomacy.values.count { it.otherCiv().isMajorCiv() && it.diplomaticStatus == DiplomaticStatus.War } * 10
// Wait to declare frienships until more civs
// Goes from -30 to 0 when we know 75% of allCivs
val civsToKnow = 0.75f * allAliveCivs
@ -99,22 +107,28 @@ object DiplomacyAutomation {
.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)) {
if ((1..10).random() < 7) continue
if (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()))
} else {
// Remember this for a few turns to save computation power
civInfo.getDiplomacyManager(otherCiv).setFlag(DiplomacyFlags.DeclinedOpenBorders, 5)
}
}
}
fun wantsToOpenBorders(civInfo: Civilization, otherCiv: Civilization): Boolean {
if (civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelLT(RelationshipLevel.Favorable)) return false
val diploManager = civInfo.getDiplomacyManager(otherCiv)
if (diploManager.hasFlag(DiplomacyFlags.DeclinedOpenBorders)) return false
if (diploManager.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)
if (hasAtLeastMotivationToAttack(civInfo, otherCiv, (diploManager.opinionOfOtherCiv()/ 2 - 10).toInt()) >= 0)
return false
return true
}
@ -131,7 +145,7 @@ object DiplomacyAutomation {
for (otherCiv in canSignResearchAgreementCiv) {
// Default setting is 5, this will be changed according to different civ.
if ((1..10).random() > 5) continue
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))
@ -153,27 +167,38 @@ object DiplomacyAutomation {
for (otherCiv in canSignDefensivePactCiv) {
// Default setting is 3, this will be changed according to different civ.
if ((1..10).random() <= 3 && wantsToSignDefensivePact(civInfo, otherCiv)) {
if ((1..10).random() <= 7) continue
if (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()))
} else {
// Remember this for a few turns to save computation power
civInfo.getDiplomacyManager(otherCiv).setFlag(DiplomacyFlags.DeclinedDefensivePact, 5)
}
}
}
fun wantsToSignDefensivePact(civInfo: Civilization, otherCiv: Civilization): Boolean {
val diploManager = civInfo.getDiplomacyManager(otherCiv)
if (diploManager.hasFlag(DiplomacyFlags.DeclinedDefensivePact)) return false
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) {
for (thirdCiv in commonknownCivs) {
if (civInfo.getDiplomacyManager(thirdCiv).isRelationshipLevelGE(RelationshipLevel.Friend)
&& thirdCiv.getDiplomacyManager(otherCiv).isRelationshipLevelLT(RelationshipLevel.Favorable))
return false
}
// If they have bad relations with any of thier friends, don't consider it
for (thirdCiv in commonknownCivs) {
if (otherCiv.getDiplomacyManager(thirdCiv).isRelationshipLevelGE(RelationshipLevel.Friend)
&& thirdCiv.getDiplomacyManager(civInfo).isRelationshipLevelLT(RelationshipLevel.Neutral))
return false
}
val defensivePacts = civInfo.diplomacy.count { it.value.hasFlag(DiplomacyFlags.DefensivePact) }
val otherCivNonOverlappingDefensivePacts = otherCiv.diplomacy.values.count { it.hasFlag(DiplomacyFlags.DefensivePact)
@ -190,17 +215,20 @@ object DiplomacyAutomation {
motivation += when (Automation.threatAssessment(civInfo,otherCiv)) {
ThreatLevel.VeryHigh -> 10
ThreatLevel.High -> 5
ThreatLevel.Low -> -15
ThreatLevel.VeryLow -> -30
ThreatLevel.Low -> -5
ThreatLevel.VeryLow -> -10
else -> 0
}
// If they have a defensive pact with another civ then we would get drawn into thier battles as well
motivation -= 10 * otherCivNonOverlappingDefensivePacts
motivation -= 15 * otherCivNonOverlappingDefensivePacts
// Becomre more desperate as we have more wars
motivation += civInfo.diplomacy.values.count { it.otherCiv().isMajorCiv() && it.diplomaticStatus == DiplomaticStatus.War } * 5
// 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
// Goes from 0 to -50 as the civ gets more allies, offset by civsToAllyWith
motivation -= (50f * (defensivePacts - civsToAllyWith) / (allAliveCivs - civsToAllyWith)).coerceAtMost(0f).toInt()
return motivation > 0
@ -228,12 +256,15 @@ object DiplomacyAutomation {
if (enemyCivs.none()) return
val minMotivationToAttack = 20
// Attack the highest score enemy that we are willing to fight.
// This is to help prevent civs from ganging up on smaller civs
// and directs them to fight their competitors instead.
val civWithBestMotivationToAttack = enemyCivs
.map { Pair(it, hasAtLeastMotivationToAttack(civInfo, it, minMotivationToAttack)) }
.maxByOrNull { it.second }!!
.filter { hasAtLeastMotivationToAttack(civInfo, it, minMotivationToAttack) >= 20 }
.maxByOrNull { it.getStatForRanking(RankingType.Score) }
if (civWithBestMotivationToAttack.second >= minMotivationToAttack)
civInfo.getDiplomacyManager(civWithBestMotivationToAttack.first).declareWar()
if (civWithBestMotivationToAttack != null)
civInfo.getDiplomacyManager(civWithBestMotivationToAttack).declareWar()
}
/** Will return the motivation to attack, but might short circuit if the value is guaranteed to
@ -285,6 +316,68 @@ object DiplomacyAutomation {
}
modifierMap["Relative combat strength"] = combatStrengthModifier
var theirAlliesValue = 0
for (thirdCiv in otherCiv.diplomacy.values.filter { it.hasFlag(DiplomacyFlags.DefensivePact) && it.otherCiv() != civInfo }) {
val thirdCivCombatStrengthRatio = otherCiv.getStatForRanking(RankingType.Force).toFloat() + baseForce / ourCombatStrength
theirAlliesValue += when {
thirdCivCombatStrengthRatio > 5 -> -15
thirdCivCombatStrengthRatio > 2.5 -> -10
thirdCivCombatStrengthRatio > 2 -> -8
thirdCivCombatStrengthRatio > 1.5 -> -5
thirdCivCombatStrengthRatio > .8 -> -2
else -> 0
}
}
modifierMap["Their allies"] = theirAlliesValue
// Civs with more score are more threatening to our victory
// Bias towards attacking civs with a high score and low military
// Bias against attacking civs with a low score and a high military
// Designed to mitigate AIs declaring war on weaker civs instead of their rivals
val scoreRatio = otherCiv.getStatForRanking(RankingType.Score).toFloat() / civInfo.getStatForRanking(RankingType.Score).toFloat()
val scoreRatioModifier = when {
scoreRatio > 2f -> 15
scoreRatio > 1.5f -> 10
scoreRatio > 1.25f -> 5
scoreRatio > 1f -> 0
scoreRatio > .5f -> -2
scoreRatio > .25f -> -5
else -> -10
}
modifierMap["Relative score"] = scoreRatioModifier
if (civInfo.stats.getUnitSupplyDeficit() == 0) {
// If either of our Civs are suffering from a supply deficit, our army must be too large
// There is no easy way to check the raw production if a civ has a supply deficit
// We might try to divide the current production by the getUnitSupplyProductionPenalty()
// but it only is true for our turn and not the previous turn and might result in odd values
if (otherCiv.stats.getUnitSupplyDeficit() == 0) {
val productionRatio = civInfo.getStatForRanking(RankingType.Production).toFloat() / otherCiv.getStatForRanking(RankingType.Production).toFloat()
val productionRatioModifier = when {
productionRatio > 2f -> 15
productionRatio > 1.5f -> 7
productionRatio > 1.2 -> 3
productionRatio > .8f -> 0
productionRatio > .5f -> -5
productionRatio > .25f -> -10
else -> -10
}
modifierMap["Relative production"] = productionRatioModifier
}
} else {
modifierMap["Over unit supply"] = (civInfo.stats.getUnitSupplyDeficit() * 2).coerceAtMost(20)
}
val relativeTech = civInfo.getStatForRanking(RankingType.Technologies) - otherCiv.getStatForRanking(RankingType.Technologies)
val relativeTechModifier = when {
relativeTech > 6 -> 10
relativeTech > 3 -> 5
relativeTech > -3 -> 0
relativeTech > -6 -> -2
relativeTech > -9 -> -5
else -> -10
}
modifierMap["Relative technologies"] = relativeTechModifier
if (closestCities.aerialDistance > 7)
modifierMap["Far away cities"] = -10
@ -296,6 +389,9 @@ object DiplomacyAutomation {
if (diplomacyManager.hasFlag(DiplomacyFlags.DeclarationOfFriendship))
modifierMap["Declaration of Friendship"] = -10
if (diplomacyManager.hasFlag(DiplomacyFlags.DefensivePact))
modifierMap["Defensive Pact"] = -10
val relationshipModifier = when (diplomacyManager.relationshipIgnoreAfraid()) {
RelationshipLevel.Unforgivable -> 10
RelationshipLevel.Enemy -> 5
@ -317,14 +413,33 @@ object DiplomacyAutomation {
modifierMap["Allied City-state"] = -20 // There had better be a DAMN good reason
}
var wonderCount = 0
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
wonderCount += city.cityConstructions.getBuiltBuildings().count { it.isWonder }
}
// The more wonders they have, the more beneficial it is to conquer them
// Civs need an army to protect thier wonders which give the most score
if (wonderCount > 0)
modifierMap["Owned Wonders"] = wonderCount
// If they are at war with our allies, then we should join in
var alliedWarMotivation = 0
for (thirdCiv in civInfo.getDiplomacyManager(otherCiv).getCommonKnownCivs()) {
val thirdCivDiploManager = civInfo.getDiplomacyManager(thirdCiv)
if (thirdCivDiploManager.hasFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship)
&& thirdCiv.isAtWarWith(otherCiv)) {
alliedWarMotivation += if (thirdCivDiploManager.hasFlag(DiplomacyFlags.DefensivePact)) 15 else 5
}
}
modifierMap["War with allies"] = alliedWarMotivation
var motivationSoFar = modifierMap.values.sum()
// We don't need to execute the expensive BFSs below if we're below the threshold here

View File

@ -53,19 +53,19 @@ class Trade : IsPartOfGameInfoSerialization {
class TradeRequest : IsPartOfGameInfoSerialization {
fun decline(decliningCiv:Civilization) {
val requestingCivInfo = decliningCiv.gameInfo.getCivilization(requestingCiv)
val diplomacyManager = requestingCivInfo.getDiplomacyManager(decliningCiv)
val requestingCivDiploManager = requestingCivInfo.getDiplomacyManager(decliningCiv)
// the numbers of the flags (20,5) are the amount of turns to wait until offering again
if (trade.ourOffers.all { it.type == TradeType.Luxury_Resource }
&& trade.theirOffers.all { it.type==TradeType.Luxury_Resource })
diplomacyManager.setFlag(DiplomacyFlags.DeclinedLuxExchange,20)
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedLuxExchange,20)
if (trade.ourOffers.any { it.name == Constants.researchAgreement })
diplomacyManager.setFlag(DiplomacyFlags.DeclinedResearchAgreement,20)
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedResearchAgreement,20)
if (trade.ourOffers.any { it.name == Constants.defensivePact })
diplomacyManager.setFlag(DiplomacyFlags.DeclinedDefensivePact,20)
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedDefensivePact,20)
if (trade.ourOffers.any { it.name == Constants.openBorders })
diplomacyManager.setFlag(DiplomacyFlags.DeclinedOpenBorders, if (decliningCiv.isAI()) 10 else 20)
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedOpenBorders, if (decliningCiv.isAI()) 10 else 20)
if (trade.isPeaceTreaty()) diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace, 5)
if (trade.isPeaceTreaty()) requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedPeace, 5)
requestingCivInfo.addNotification("[${decliningCiv.civName}] has denied your trade request",
NotificationCategory.Trade, decliningCiv.civName, NotificationIcon.Trade)