From cc1624604ef365fd5221d5adcda309a6ec18d6a1 Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Mon, 18 Sep 2023 01:48:22 -0500 Subject: [PATCH] AI diplomatic actions rework (#10071) * AI now can offer declaration of friendship * AI now offers open borders * Added spectator notifications for DoFs and defensive pacts * AI now wants friendship less as more Civs die * Re-added spectator notifications that weren't added in the merge * Replaced min with coerceAtLeast * Replaced .filter and .count() with .count * Removed some minus DoF motivation modifiers being in a military focus. * Fixed AI offering open borders with City-States * AI now signs defensive pacts * Increased motivationToAttack weight when determining value of a declaration of friendship * Removed double trade processing and notifications from Treaties * Removed commented code * Added wantsToSignDefensivePact * Added defensive pact trade evaluation * Revert "Removed commented code" This reverts commit 6476a08d26c66e0d14d425c16944e6468cb12221. * Revert "Removed double trade processing and notifications from Treaties" This reverts commit 371e8e8a62529e9045bc22b7f69d08a2cf452904. * Changed wantsToSignDefensivePact to use a for loop * Changed chance to consider offering a defensive pact back to 30% * Added DeclinedOpenBordersFlag * Added DeclinedDeclarationOfFriendshipFlag * Civ AI now has a positive modifier when friends with under 1/4 of alive Civs * AI values friendship based also on relative strength * Changed AI valueing of a defensive pact * AIs not use DeclinedDeclarationOfFriendship flag * Fixed otherCivNonOverlappingDefensivePacts causing error with unmet Civs --- .../civilization/NextTurnAutomation.kt | 169 +++++++++++++++--- .../diplomacy/DiplomacyFunctions.kt | 6 + .../diplomacy/DiplomacyManager.kt | 14 +- core/src/com/unciv/logic/trade/Trade.kt | 2 + .../com/unciv/logic/trade/TradeEvaluation.kt | 4 + .../ui/screens/worldscreen/AlertPopup.kt | 5 +- 6 files changed, 170 insertions(+), 30 deletions(-) diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index 2d60860b03..d0c8af544c 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -65,11 +65,12 @@ object NextTurnAutomation { if (!civInfo.gameInfo.ruleset.modOptions.hasUnique(ModOptionsConstants.diplomaticRelationshipsCannotChange)) { declareWar(civInfo) offerPeaceTreaty(civInfo) -// offerDeclarationOfFriendship(civInfo) + offerDeclarationOfFriendship(civInfo) } if (civInfo.gameInfo.isReligionEnabled()) { ReligionAutomation.spendFaithOnReligion(civInfo) } + offerOpenBorders(civInfo) offerResearchAgreement(civInfo) offerDefensivePact(civInfo) exchangeLuxuries(civInfo) @@ -275,11 +276,15 @@ object NextTurnAutomation { if (popupAlert.type == AlertType.DeclarationOfFriendship) { val requestingCiv = civInfo.gameInfo.getCivilization(popupAlert.value) val diploManager = civInfo.getDiplomacyManager(requestingCiv) - if (diploManager.isRelationshipLevelGT(RelationshipLevel.Neutral) - && !diploManager.otherCivDiplomacy().hasFlag(DiplomacyFlags.Denunciation)) { + if (civInfo.diplomacyFunctions.canSignDeclarationOfFriendshipWith(requestingCiv) + && wantsToSignDeclarationOfFrienship(civInfo,requestingCiv)) { diploManager.signDeclarationOfFriendship() requestingCiv.addNotification("We have signed a Declaration of Friendship with [${civInfo.civName}]!", NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, civInfo.civName) - } else requestingCiv.addNotification("[${civInfo.civName}] has denied our Declaration of Friendship!", NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, civInfo.civName) + } else { + diploManager.otherCivDiplomacy().setFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship, 10) + requestingCiv.addNotification("[${civInfo.civName}] has denied our Declaration of Friendship!", NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, civInfo.civName) + } + } } @@ -768,23 +773,95 @@ object NextTurnAutomation { } } - @Suppress("unused") //todo: Work in Progress? private fun offerDeclarationOfFriendship(civInfo: Civilization) { val civsThatWeCanDeclareFriendshipWith = civInfo.getKnownCivs() - .filter { - it.isMajorCiv() && !it.isAtWarWith(civInfo) - && it.getDiplomacyManager(civInfo).isRelationshipLevelGT(RelationshipLevel.Neutral) - && !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclarationOfFriendship) - && !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.Denunciation) - } - .sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() } - for (civ in civsThatWeCanDeclareFriendshipWith) { - // Default setting is 5, this will be changed according to different civ. - if ((1..10).random() <= 5) - civInfo.getDiplomacyManager(civ).signDeclarationOfFriendship() + .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) { + val civsThatWeCanDeclareFriendshipWith = civInfo.getKnownCivs() + .filter { it.isMajorCiv() && !civInfo.isAtWarWith(it) + && !civInfo.getDiplomacyManager(it).hasOpenBorders + && !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedOpenBorders) } + .sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() }.toList() + for (otherCiv in civsThatWeCanDeclareFriendshipWith) { + // 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().toInt()) > 0) + return false + return true + } + private fun offerResearchAgreement(civInfo: Civilization) { if (!civInfo.diplomacyFunctions.canSignResearchAgreement()) return // don't waste your time @@ -816,19 +893,61 @@ object NextTurnAutomation { && !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedDefensivePact) && civInfo.getDiplomacyManager(it).relationshipIgnoreAfraid() == RelationshipLevel.Ally } - .sortedByDescending { it.stats.statsForNextTurn.science } for (otherCiv in canSignDefensivePactCiv) { - // Default setting is 1, this will be changed according to different civ. - if ((1..10).random() > 1) continue - //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())) + // 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 diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt index 52945e518a..acecdbc65f 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt @@ -86,6 +86,12 @@ class DiplomacyFunctions(val civInfo: Civilization){ } } } + + fun canSignDeclarationOfFriendshipWith(otherCiv: Civilization): Boolean { + return otherCiv.isMajorCiv() && !otherCiv.isAtWarWith(civInfo) + && !civInfo.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.Denunciation) + && !civInfo.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.DeclarationOfFriendship) + } fun canSignResearchAgreement(): Boolean { if (!civInfo.isMajorCiv()) return false diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index 015eb983ad..df81ef34c0 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -42,8 +42,10 @@ enum class DiplomacyFlags { DeclinedLuxExchange, DeclinedPeace, DeclinedResearchAgreement, + DeclinedOpenBorders, DeclaredWar, DeclarationOfFriendship, + DeclinedDeclarationOfFriendship, DefensivePact, DeclinedDefensivePact, ResearchAgreement, @@ -782,7 +784,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { diploManager.diplomaticStatus = DiplomaticStatus.Peace diploManager.otherCivDiplomacy().diplomaticStatus = DiplomaticStatus.Peace } - for (civ in getCommonKnownCivs().filter { civ -> civ.isMajorCiv() }) { + for (civ in getCommonKnownCivs().filter { civ -> civ.isMajorCiv() || civ.isSpectator() }) { civ.addNotification("[${civInfo.civName}] canceled their Defensive Pact with [${diploManager.otherCivName}]!", NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy, diploManager.otherCivName) } @@ -1002,10 +1004,12 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { setFlag(DiplomacyFlags.DeclarationOfFriendship, 30) otherCivDiplomacy().setFlag(DiplomacyFlags.DeclarationOfFriendship, 30) - for (thirdCiv in getCommonKnownCivs().filter { it.isMajorCiv() }) { + for (thirdCiv in getCommonKnownCivs().filter { it.isMajorCiv() || it.isSpectator() }) { thirdCiv.addNotification("[${civInfo.civName}] and [$otherCivName] have signed the Declaration of Friendship!", NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy, otherCivName) thirdCiv.getDiplomacyManager(civInfo).setFriendshipBasedModifier() + if (thirdCiv.isSpectator()) return + thirdCiv.getDiplomacyManager(civInfo).setFriendshipBasedModifier() } // Ignore contitionals as triggerCivwideUnique will check again, and that would break @@ -1048,9 +1052,10 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { otherCivDiplomacy().diplomaticStatus = DiplomaticStatus.DefensivePact - for (thirdCiv in getCommonKnownCivs().filter { it.isMajorCiv() }) { + for (thirdCiv in getCommonKnownCivs().filter { it.isMajorCiv() || it.isSpectator() }) { thirdCiv.addNotification("[${civInfo.civName}] and [$otherCivName] have signed the Defensive Pact!", NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy, otherCivName) + if (thirdCiv.isSpectator()) return thirdCiv.getDiplomacyManager(civInfo).setDefensivePactBasedModifier() } @@ -1095,9 +1100,10 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, civInfo.civName) // We, A, are denouncing B. What do other major civs (C,D, etc) think of this? - getCommonKnownCivs().filter { it.isMajorCiv() }.forEach { thirdCiv -> + getCommonKnownCivs().filter { it.isMajorCiv() || it.isSpectator() }.forEach { thirdCiv -> thirdCiv.addNotification("[${civInfo.civName}] has denounced [$otherCivName]!", NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy, otherCivName) + if (thirdCiv.isSpectator()) return@forEach val thirdCivRelationshipWithOtherCiv = thirdCiv.getDiplomacyManager(otherCiv()).relationshipIgnoreAfraid() val thirdCivDiplomacyManager = thirdCiv.getDiplomacyManager(civInfo) when (thirdCivRelationshipWithOtherCiv) { diff --git a/core/src/com/unciv/logic/trade/Trade.kt b/core/src/com/unciv/logic/trade/Trade.kt index 29dfdb35e8..f472de28ab 100644 --- a/core/src/com/unciv/logic/trade/Trade.kt +++ b/core/src/com/unciv/logic/trade/Trade.kt @@ -62,6 +62,8 @@ class TradeRequest : IsPartOfGameInfoSerialization { diplomacyManager.setFlag(DiplomacyFlags.DeclinedResearchAgreement,20) if (trade.ourOffers.any { it.name == Constants.defensivePact }) diplomacyManager.setFlag(DiplomacyFlags.DeclinedDefensivePact,10) + if (trade.ourOffers.any { it.name == Constants.openBorders }) + diplomacyManager.setFlag(DiplomacyFlags.DeclinedOpenBorders, 10) if (trade.isPeaceTreaty()) diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace, 5) diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index 1c0545e5fe..7fe306712e 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -3,6 +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.city.City import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.diplomacy.DiplomacyFlags @@ -102,6 +103,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 -> 0 Constants.researchAgreement -> -offer.amount else -> 1000 } @@ -201,6 +203,8 @@ 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 + else 100000 Constants.researchAgreement -> -offer.amount else -> 1000 //Todo:AddDefensiveTreatyHere diff --git a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt index c52dccfe0e..d285c9cf3a 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt @@ -11,6 +11,7 @@ import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.NotificationCategory 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.RelationshipLevel import com.unciv.models.ruleset.unique.UniqueType @@ -192,7 +193,9 @@ class AlertPopup( val playerDiploManager = viewingCiv.getDiplomacyManager(otherciv) addLeaderName(otherciv) addGoodSizedLabel("My friend, shall we declare our friendship to the world?").row() - addCloseButton("We are not interested.", KeyboardBinding.Cancel).row() + addCloseButton("We are not interested.", KeyboardBinding.Cancel) { + playerDiploManager.otherCivDiplomacy().setFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship, 10) + }.row() addCloseButton("Declare Friendship ([30] turns)", KeyboardBinding.Confirm) { playerDiploManager.signDeclarationOfFriendship() }