From 6fe9b7ea7fd0c73c82f6b755e8c6568b65607baf Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Sat, 18 Nov 2023 14:34:46 -0600 Subject: [PATCH] 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 --- .../civilization/DiplomacyAutomation.kt | 145 ++++++++++++++++-- core/src/com/unciv/logic/trade/Trade.kt | 12 +- 2 files changed, 136 insertions(+), 21 deletions(-) diff --git a/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt b/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt index f0d05ed8cd..5954ee2a3a 100644 --- a/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt @@ -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 diff --git a/core/src/com/unciv/logic/trade/Trade.kt b/core/src/com/unciv/logic/trade/Trade.kt index 0581179849..7eadba3ec1 100644 --- a/core/src/com/unciv/logic/trade/Trade.kt +++ b/core/src/com/unciv/logic/trade/Trade.kt @@ -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)