From fba9048156f8a89eb0eeabd5b09ceed893925c93 Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Sun, 5 Nov 2023 00:51:33 +0200 Subject: [PATCH] chore: Split NextTurnAutomation into Religion, Trade, and Diplomacy automation files --- .../civilization/DiplomacyAutomation.kt | 399 ++++++++++ .../civilization/NextTurnAutomation.kt | 695 +----------------- .../civilization/ReligionAutomation.kt | 87 +++ .../civilization/TradeAutomation.kt | 222 ++++++ .../com/unciv/logic/trade/TradeEvaluation.kt | 12 +- .../ruleset/validation/RulesetValidator.kt | 5 +- 6 files changed, 727 insertions(+), 693 deletions(-) create mode 100644 core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt create mode 100644 core/src/com/unciv/logic/automation/civilization/TradeAutomation.kt diff --git a/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt b/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt new file mode 100644 index 0000000000..f0d05ed8cd --- /dev/null +++ b/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt @@ -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() + 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())) + } + } + +} diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index 1c202192ec..2cb5d87478 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -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() - val counterofferAsks = HashMap() - val counterofferGifts = ArrayList() - - 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() - 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): HashSet { - val chosenBeliefs = hashSetOf() - // 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 = 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 { - 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() - 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() - 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() diff --git a/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt b/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt index f37f53877f..cb3065ca13 100644 --- a/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt @@ -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): HashSet { + val chosenBeliefs = hashSetOf() + // 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 = 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 } diff --git a/core/src/com/unciv/logic/automation/civilization/TradeAutomation.kt b/core/src/com/unciv/logic/automation/civilization/TradeAutomation.kt new file mode 100644 index 0000000000..17a186bce8 --- /dev/null +++ b/core/src/com/unciv/logic/automation/civilization/TradeAutomation.kt @@ -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() + val counterofferAsks = HashMap() + val counterofferGifts = ArrayList() + + 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() + 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 { + 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() + 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 + } + +} diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index 5472a8532b..b15094edd8 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -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 diff --git a/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt index 6089ebe89b..f774a68f74 100644 --- a/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt +++ b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt @@ -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]