chore: Split NextTurnAutomation into Religion, Trade, and Diplomacy automation files

This commit is contained in:
Yair Morgenstern 2023-11-05 00:51:33 +02:00
parent 6a7d09b43a
commit fba9048156
6 changed files with 727 additions and 693 deletions

View File

@ -0,0 +1,399 @@
package com.unciv.logic.automation.civilization
import com.unciv.Constants
import com.unciv.logic.automation.Automation
import com.unciv.logic.automation.ThreatLevel
import com.unciv.logic.battle.BattleDamage
import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.map.BFS
import com.unciv.logic.map.tile.Tile
import com.unciv.logic.trade.TradeEvaluation
import com.unciv.logic.trade.TradeLogic
import com.unciv.logic.trade.TradeOffer
import com.unciv.logic.trade.TradeRequest
import com.unciv.logic.trade.TradeType
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.translations.tr
import com.unciv.ui.screens.victoryscreen.RankingType
object DiplomacyAutomation {
internal fun offerDeclarationOfFriendship(civInfo: Civilization) {
val civsThatWeCanDeclareFriendshipWith = civInfo.getKnownCivs()
.filter { civInfo.diplomacyFunctions.canSignDeclarationOfFriendshipWith(it)
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship)}
.sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() }.toList()
for (otherCiv in civsThatWeCanDeclareFriendshipWith) {
// Default setting is 2, this will be changed according to different civ.
if ((1..10).random() <= 2 && wantsToSignDeclarationOfFrienship(civInfo, otherCiv)) {
otherCiv.popupAlerts.add(PopupAlert(AlertType.DeclarationOfFriendship, civInfo.civName))
}
}
}
internal fun wantsToSignDeclarationOfFrienship(civInfo: Civilization, otherCiv: Civilization): Boolean {
val diploManager = civInfo.getDiplomacyManager(otherCiv)
// Shortcut, if it is below favorable then don't consider it
if (diploManager.isRelationshipLevelLT(RelationshipLevel.Favorable)) return false
val numOfFriends = civInfo.diplomacy.count { it.value.hasFlag(DiplomacyFlags.DeclarationOfFriendship) }
val knownCivs = civInfo.getKnownCivs().count { it.isMajorCiv() && it.isAlive() }
val allCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() } - 1 // Don't include us
val deadCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() && !it.isAlive() }
val allAliveCivs = allCivs - deadCivs
// Motivation should be constant as the number of civs changes
var motivation = diploManager.opinionOfOtherCiv().toInt() - 40
// If the other civ is stronger than we are compelled to be nice to them
// If they are too weak, then thier friendship doesn't mean much to us
motivation += when (Automation.threatAssessment(civInfo,otherCiv)) {
ThreatLevel.VeryHigh -> 10
ThreatLevel.High -> 5
ThreatLevel.VeryLow -> -5
else -> 0
}
// Try to ally with a fourth of the civs in play
val civsToAllyWith = 0.25f * allAliveCivs
if (numOfFriends < civsToAllyWith) {
// Goes from 10 to 0 once the civ gets 1/4 of all alive civs as friends
motivation += (10 - 10 * (numOfFriends / civsToAllyWith)).toInt()
} else {
// Goes form 0 to -120 as the civ gets more friends, offset by civsToAllyWith
motivation -= (120f * (numOfFriends - civsToAllyWith) / (knownCivs - civsToAllyWith)).toInt()
}
// Goes from 0 to -50 as more civs die
// this is meant to prevent the game from stalemating when a group of friends
// conquers all oposition
motivation -= deadCivs / allCivs * 50
// Wait to declare frienships until more civs
// Goes from -30 to 0 when we know 75% of allCivs
val civsToKnow = 0.75f * allAliveCivs
motivation -= ((civsToKnow - knownCivs) / civsToKnow * 30f).toInt().coerceAtLeast(0)
motivation -= hasAtLeastMotivationToAttack(civInfo, otherCiv, motivation / 2) * 2
return motivation > 0
}
internal fun offerOpenBorders(civInfo: Civilization) {
if (!civInfo.hasUnique(UniqueType.EnablesOpenBorders)) return
val civsThatWeCanOpenBordersWith = civInfo.getKnownCivs()
.filter { it.isMajorCiv() && !civInfo.isAtWarWith(it)
&& it.hasUnique(UniqueType.EnablesOpenBorders)
&& !civInfo.getDiplomacyManager(it).hasOpenBorders
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedOpenBorders) }
.sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() }.toList()
for (otherCiv in civsThatWeCanOpenBordersWith) {
// Default setting is 3, this will be changed according to different civ.
if ((1..10).random() <= 3 && wantsToOpenBorders(civInfo, otherCiv)) {
val tradeLogic = TradeLogic(civInfo, otherCiv)
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.openBorders, TradeType.Agreement))
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.openBorders, TradeType.Agreement))
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
}
}
}
fun wantsToOpenBorders(civInfo: Civilization, otherCiv: Civilization): Boolean {
if (civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelLT(RelationshipLevel.Favorable)) return false
// Don't accept if they are at war with our friends, they might use our land to attack them
if (civInfo.diplomacy.values.any { it.isRelationshipLevelGE(RelationshipLevel.Friend) && it.otherCiv().isAtWarWith(otherCiv)})
return false
if (hasAtLeastMotivationToAttack(civInfo, otherCiv, (civInfo.getDiplomacyManager(otherCiv).opinionOfOtherCiv()/ 2 - 10).toInt()) >= 0)
return false
return true
}
internal fun offerResearchAgreement(civInfo: Civilization) {
if (!civInfo.diplomacyFunctions.canSignResearchAgreement()) return // don't waste your time
val canSignResearchAgreementCiv = civInfo.getKnownCivs()
.filter {
civInfo.diplomacyFunctions.canSignResearchAgreementsWith(it)
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedResearchAgreement)
}
.sortedByDescending { it.stats.statsForNextTurn.science }
for (otherCiv in canSignResearchAgreementCiv) {
// Default setting is 5, this will be changed according to different civ.
if ((1..10).random() > 5) continue
val tradeLogic = TradeLogic(civInfo, otherCiv)
val cost = civInfo.diplomacyFunctions.getResearchAgreementCost(otherCiv)
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.researchAgreement, TradeType.Treaty, cost))
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.researchAgreement, TradeType.Treaty, cost))
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
}
}
internal fun offerDefensivePact(civInfo: Civilization) {
if (!civInfo.diplomacyFunctions.canSignDefensivePact()) return // don't waste your time
val canSignDefensivePactCiv = civInfo.getKnownCivs()
.filter {
civInfo.diplomacyFunctions.canSignDefensivePactWith(it)
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedDefensivePact)
&& civInfo.getDiplomacyManager(it).relationshipIgnoreAfraid() == RelationshipLevel.Ally
}
for (otherCiv in canSignDefensivePactCiv) {
// Default setting is 3, this will be changed according to different civ.
if ((1..10).random() <= 3 && wantsToSignDefensivePact(civInfo, otherCiv)) {
//todo: Add more in depth evaluation here
val tradeLogic = TradeLogic(civInfo, otherCiv)
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.defensivePact, TradeType.Treaty))
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.defensivePact, TradeType.Treaty))
otherCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
}
}
}
fun wantsToSignDefensivePact(civInfo: Civilization, otherCiv: Civilization): Boolean {
val diploManager = civInfo.getDiplomacyManager(otherCiv)
if (diploManager.isRelationshipLevelLT(RelationshipLevel.Ally)) return false
val commonknownCivs = diploManager.getCommonKnownCivs()
// If they have bad relations with any of our friends, don't consider it
for(thirdCiv in commonknownCivs) {
if (civInfo.getDiplomacyManager(thirdCiv).isRelationshipLevelGE(RelationshipLevel.Friend)
&& thirdCiv.getDiplomacyManager(otherCiv).isRelationshipLevelLT(RelationshipLevel.Favorable))
return false
}
val defensivePacts = civInfo.diplomacy.count { it.value.hasFlag(DiplomacyFlags.DefensivePact) }
val otherCivNonOverlappingDefensivePacts = otherCiv.diplomacy.values.count { it.hasFlag(DiplomacyFlags.DefensivePact)
&& (!it.otherCiv().knows(civInfo) || !it.otherCiv().getDiplomacyManager(civInfo).hasFlag(DiplomacyFlags.DefensivePact)) }
val allCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() } - 1 // Don't include us
val deadCivs = civInfo.gameInfo.civilizations.count { it.isMajorCiv() && !it.isAlive() }
val allAliveCivs = allCivs - deadCivs
// We have to already be at RelationshipLevel.Ally, so we must have 80 oppinion of them
var motivation = diploManager.opinionOfOtherCiv().toInt() - 80
// If they are stronger than us, then we value it a lot more
// If they are weaker than us, then we don't value it
motivation += when (Automation.threatAssessment(civInfo,otherCiv)) {
ThreatLevel.VeryHigh -> 10
ThreatLevel.High -> 5
ThreatLevel.Low -> -15
ThreatLevel.VeryLow -> -30
else -> 0
}
// If they have a defensive pact with another civ then we would get drawn into thier battles as well
motivation -= 10 * otherCivNonOverlappingDefensivePacts
// Try to have a defensive pact with 1/5 of all civs
val civsToAllyWith = 0.20f * allAliveCivs
// Goes form 0 to -50 as the civ gets more allies, offset by civsToAllyWith
motivation -= (50f * (defensivePacts - civsToAllyWith) / (allAliveCivs - civsToAllyWith)).coerceAtMost(0f).toInt()
return motivation > 0
}
internal fun declareWar(civInfo: Civilization) {
if (civInfo.wantsToFocusOn(Victory.Focus.Culture)) return
if (civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
if (civInfo.isAtWar() || civInfo.getHappiness() <= 0) return
val ourMilitaryUnits = civInfo.units.getCivUnits().filter { !it.isCivilian() }.count()
if (ourMilitaryUnits < civInfo.cities.size) return
if (ourMilitaryUnits < 4) return // to stop AI declaring war at the beginning of games when everyone isn't set up well enough\
if (civInfo.cities.size < 3) return // FAR too early for that what are you thinking!
//evaluate war
val enemyCivs = civInfo.getKnownCivs()
.filterNot {
it == civInfo || it.cities.isEmpty() || !civInfo.getDiplomacyManager(it).canDeclareWar()
|| it.cities.none { city -> civInfo.hasExplored(city.getCenterTile()) }
}
// If the AI declares war on a civ without knowing the location of any cities, it'll just keep amassing an army and not sending it anywhere,
// and end up at a massive disadvantage
if (enemyCivs.none()) return
val minMotivationToAttack = 20
val civWithBestMotivationToAttack = enemyCivs
.map { Pair(it, hasAtLeastMotivationToAttack(civInfo, it, minMotivationToAttack)) }
.maxByOrNull { it.second }!!
if (civWithBestMotivationToAttack.second >= minMotivationToAttack)
civInfo.getDiplomacyManager(civWithBestMotivationToAttack.first).declareWar()
}
/** Will return the motivation to attack, but might short circuit if the value is guaranteed to
* be lower than `atLeast`. So any values below `atLeast` should not be used for comparison. */
private fun hasAtLeastMotivationToAttack(civInfo: Civilization, otherCiv: Civilization, atLeast: Int): Int {
val closestCities = NextTurnAutomation.getClosestCities(civInfo, otherCiv) ?: return 0
val baseForce = 30f
var ourCombatStrength = civInfo.getStatForRanking(RankingType.Force).toFloat() + baseForce
if (civInfo.getCapital() != null) ourCombatStrength += CityCombatant(civInfo.getCapital()!!).getCityStrength()
var theirCombatStrength = otherCiv.getStatForRanking(RankingType.Force).toFloat() + baseForce
if(otherCiv.getCapital() != null) theirCombatStrength += CityCombatant(otherCiv.getCapital()!!).getCityStrength()
//for city-states, also consider their protectors
if (otherCiv.isCityState() and otherCiv.cityStateFunctions.getProtectorCivs().isNotEmpty()) {
theirCombatStrength += otherCiv.cityStateFunctions.getProtectorCivs().filterNot { it == civInfo }
.sumOf { it.getStatForRanking(RankingType.Force) }
}
if (theirCombatStrength > ourCombatStrength) return 0
val ourCity = closestCities.city1
val theirCity = closestCities.city2
if (civInfo.units.getCivUnits().filter { it.isMilitary() }.none {
val damageReceivedWhenAttacking =
BattleDamage.calculateDamageToAttacker(
MapUnitCombatant(it),
CityCombatant(theirCity)
)
damageReceivedWhenAttacking < 100
})
return 0 // You don't have any units that can attack this city without dying, don't declare war.
fun isTileCanMoveThrough(tile: Tile): Boolean {
val owner = tile.getOwner()
return !tile.isImpassible()
&& (owner == otherCiv || owner == null || civInfo.diplomacyFunctions.canPassThroughTiles(owner))
}
val modifierMap = HashMap<String, Int>()
val combatStrengthRatio = ourCombatStrength / theirCombatStrength
val combatStrengthModifier = when {
combatStrengthRatio > 3f -> 30
combatStrengthRatio > 2.5f -> 25
combatStrengthRatio > 2f -> 20
combatStrengthRatio > 1.5f -> 10
else -> 0
}
modifierMap["Relative combat strength"] = combatStrengthModifier
if (closestCities.aerialDistance > 7)
modifierMap["Far away cities"] = -10
val diplomacyManager = civInfo.getDiplomacyManager(otherCiv)
if (diplomacyManager.hasFlag(DiplomacyFlags.ResearchAgreement))
modifierMap["Research Agreement"] = -5
if (diplomacyManager.hasFlag(DiplomacyFlags.DeclarationOfFriendship))
modifierMap["Declaration of Friendship"] = -10
val relationshipModifier = when (diplomacyManager.relationshipIgnoreAfraid()) {
RelationshipLevel.Unforgivable -> 10
RelationshipLevel.Enemy -> 5
RelationshipLevel.Ally -> -5 // this is so that ally + DoF is not too unbalanced -
// still possible for AI to declare war for isolated city
else -> 0
}
modifierMap["Relationship"] = relationshipModifier
if (diplomacyManager.resourcesFromTrade().any { it.amount > 0 })
modifierMap["Receiving trade resources"] = -5
if (theirCity.getTiles().none { tile -> tile.neighbors.any { it.getOwner() == theirCity.civ && it.getCity() != theirCity } })
modifierMap["Isolated city"] = 15
if (otherCiv.isCityState()) {
modifierMap["City-state"] = -20
if (otherCiv.getAllyCiv() == civInfo.civName)
modifierMap["Allied City-state"] = -20 // There had better be a DAMN good reason
}
for (city in otherCiv.cities) {
val construction = city.cityConstructions.getCurrentConstruction()
if (construction is Building && construction.hasUnique(UniqueType.TriggersCulturalVictory))
modifierMap["About to win"] = 15
if (construction is BaseUnit && construction.hasUnique(UniqueType.AddInCapital))
modifierMap["About to win"] = 15
}
var motivationSoFar = modifierMap.values.sum()
// We don't need to execute the expensive BFSs below if we're below the threshold here
// anyways, since it won't get better from those, only worse.
if (motivationSoFar < atLeast) {
return motivationSoFar
}
val landPathBFS = BFS(ourCity.getCenterTile()) {
it.isLand && isTileCanMoveThrough(it)
}
landPathBFS.stepUntilDestination(theirCity.getCenterTile())
if (!landPathBFS.hasReachedTile(theirCity.getCenterTile()))
motivationSoFar -= -10
// We don't need to execute the expensive BFSs below if we're below the threshold here
// anyways, since it won't get better from those, only worse.
if (motivationSoFar < atLeast) {
return motivationSoFar
}
val reachableEnemyCitiesBfs = BFS(civInfo.getCapital(true)!!.getCenterTile()) { isTileCanMoveThrough(it) }
reachableEnemyCitiesBfs.stepToEnd()
val reachableEnemyCities = otherCiv.cities.filter { reachableEnemyCitiesBfs.hasReachedTile(it.getCenterTile()) }
if (reachableEnemyCities.isEmpty()) return 0 // Can't even reach the enemy city, no point in war.
return motivationSoFar
}
internal fun offerPeaceTreaty(civInfo: Civilization) {
if (!civInfo.isAtWar() || civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
val enemiesCiv = civInfo.diplomacy.filter { it.value.diplomaticStatus == DiplomaticStatus.War }
.map { it.value.otherCiv() }
.filterNot { it == civInfo || it.isBarbarian() || it.cities.isEmpty() }
.filter { !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedPeace) }
// Don't allow AIs to offer peace to city states allied with their enemies
.filterNot { it.isCityState() && it.getAllyCiv() != null && civInfo.isAtWarWith(civInfo.gameInfo.getCivilization(it.getAllyCiv()!!)) }
// ignore civs that we have already offered peace this turn as a counteroffer to another civ's peace offer
.filter { it.tradeRequests.none { tradeRequest -> tradeRequest.requestingCiv == civInfo.civName && tradeRequest.trade.isPeaceTreaty() } }
for (enemy in enemiesCiv) {
if(hasAtLeastMotivationToAttack(civInfo, enemy, 10) >= 10) {
// We can still fight. Refuse peace.
continue
}
// pay for peace
val tradeLogic = TradeLogic(civInfo, enemy)
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.peaceTreaty, TradeType.Treaty))
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.peaceTreaty, TradeType.Treaty))
var moneyWeNeedToPay = -TradeEvaluation().evaluatePeaceCostForThem(civInfo, enemy)
if (civInfo.gold > 0 && moneyWeNeedToPay > 0) {
if (moneyWeNeedToPay > civInfo.gold) {
moneyWeNeedToPay = civInfo.gold // As much as possible
}
tradeLogic.currentTrade.ourOffers.add(
TradeOffer("Gold".tr(), TradeType.Gold, moneyWeNeedToPay)
)
}
enemy.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
}
}
}

View File

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

View File

@ -5,9 +5,11 @@ import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.managers.ReligionState
import com.unciv.logic.map.tile.Tile
import com.unciv.models.Counter
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.BeliefType
import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import kotlin.math.min
@ -398,5 +400,90 @@ object ReligionAutomation {
return score
}
internal fun chooseReligiousBeliefs(civInfo: Civilization) {
choosePantheon(civInfo)
foundReligion(civInfo)
enhanceReligion(civInfo)
chooseFreeBeliefs(civInfo)
}
private fun choosePantheon(civInfo: Civilization) {
if (!civInfo.religionManager.canFoundOrExpandPantheon()) return
// So looking through the source code of the base game available online,
// the functions for choosing beliefs total in at around 400 lines.
// https://github.com/Gedemon/Civ5-DLL/blob/aa29e80751f541ae04858b6d2a2c7dcca454201e/CvGameCoreDLL_Expansion1/CvReligionClasses.cpp
// line 4426 through 4870.
// This is way too much work for now, so I'll just choose a random pantheon instead.
// Should probably be changed later, but it works for now.
val chosenPantheon = chooseBeliefOfType(civInfo, BeliefType.Pantheon)
?: return // panic!
civInfo.religionManager.chooseBeliefs(
listOf(chosenPantheon),
useFreeBeliefs = civInfo.religionManager.usingFreeBeliefs()
)
}
private fun foundReligion(civInfo: Civilization) {
if (civInfo.religionManager.religionState != ReligionState.FoundingReligion) return
val availableReligionIcons = civInfo.gameInfo.ruleset.religions
.filterNot { civInfo.gameInfo.religions.values.map { religion -> religion.name }.contains(it) }
val favoredReligion = civInfo.nation.favoredReligion
val religionIcon =
if (favoredReligion != null && favoredReligion in availableReligionIcons) favoredReligion
else availableReligionIcons.randomOrNull()
?: return // Wait what? How did we pass the checking when using a great prophet but not this?
civInfo.religionManager.foundReligion(religionIcon, religionIcon)
val chosenBeliefs = chooseBeliefs(civInfo, civInfo.religionManager.getBeliefsToChooseAtFounding()).toList()
civInfo.religionManager.chooseBeliefs(chosenBeliefs)
}
private fun enhanceReligion(civInfo: Civilization) {
if (civInfo.religionManager.religionState != ReligionState.EnhancingReligion) return
civInfo.religionManager.chooseBeliefs(
chooseBeliefs(civInfo, civInfo.religionManager.getBeliefsToChooseAtEnhancing()).toList()
)
}
private fun chooseFreeBeliefs(civInfo: Civilization) {
if (!civInfo.religionManager.hasFreeBeliefs()) return
civInfo.religionManager.chooseBeliefs(
chooseBeliefs(civInfo, civInfo.religionManager.freeBeliefsAsEnums()).toList(),
useFreeBeliefs = true
)
}
private fun chooseBeliefs(civInfo: Civilization, beliefsToChoose: Counter<BeliefType>): HashSet<Belief> {
val chosenBeliefs = hashSetOf<Belief>()
// The `continue`s should never be reached, but just in case I'd rather have the AI have a
// belief less than make the game crash. The `continue`s should only be reached whenever
// there are not enough beliefs to choose, but there should be, as otherwise we could
// not have used a great prophet to found/enhance our religion.
for (belief in BeliefType.values()) {
if (belief == BeliefType.None) continue
repeat(beliefsToChoose[belief]) {
chosenBeliefs.add(
chooseBeliefOfType(civInfo, belief, chosenBeliefs) ?: return@repeat
)
}
}
return chosenBeliefs
}
private fun chooseBeliefOfType(civInfo: Civilization, beliefType: BeliefType, additionalBeliefsToExclude: HashSet<Belief> = hashSetOf()): Belief? {
return civInfo.gameInfo.ruleset.beliefs.values
.filter {
(it.type == beliefType || beliefType == BeliefType.Any)
&& !additionalBeliefsToExclude.contains(it)
&& civInfo.religionManager.getReligionWithBelief(it) == null
&& it.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals)
.none { unique -> !unique.conditionalsApply(civInfo) }
}
.maxByOrNull { ReligionAutomation.rateBelief(civInfo, it) }
}
//endregion
}

View File

@ -0,0 +1,222 @@
package com.unciv.logic.automation.civilization
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.trade.Trade
import com.unciv.logic.trade.TradeEvaluation
import com.unciv.logic.trade.TradeLogic
import com.unciv.logic.trade.TradeOffer
import com.unciv.logic.trade.TradeRequest
import com.unciv.logic.trade.TradeType
import kotlin.math.min
object TradeAutomation {
fun respondToTradeRequests(civInfo: Civilization) {
for (tradeRequest in civInfo.tradeRequests.toList()) {
val otherCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv)
if (!TradeEvaluation().isTradeValid(tradeRequest.trade, civInfo, otherCiv))
continue
val tradeLogic = TradeLogic(civInfo, otherCiv)
tradeLogic.currentTrade.set(tradeRequest.trade)
/** We need to remove this here, so that if the trade is accepted, the updateDetailedCivResources()
* in tradeLogic.acceptTrade() will not consider *both* the trade *and the trade offer as decreasing the
* amount of available resources, since that will lead to "Our proposed trade is no longer valid" if we try to offer
* the same resource to ANOTHER civ in this turn. Complicated!
*/
civInfo.tradeRequests.remove(tradeRequest)
if (TradeEvaluation().isTradeAcceptable(tradeLogic.currentTrade, civInfo, otherCiv)) {
tradeLogic.acceptTrade()
otherCiv.addNotification("[${civInfo.civName}] has accepted your trade request", NotificationCategory.Trade, NotificationIcon.Trade, civInfo.civName)
} else {
val counteroffer = getCounteroffer(civInfo, tradeRequest)
if (counteroffer != null) {
otherCiv.addNotification("[${civInfo.civName}] has made a counteroffer to your trade request", NotificationCategory.Trade, NotificationIcon.Trade, civInfo.civName)
otherCiv.tradeRequests.add(counteroffer)
} else
tradeRequest.decline(civInfo)
}
}
civInfo.tradeRequests.clear()
}
/** @return a TradeRequest with the same ourOffers as [tradeRequest] but with enough theirOffers
* added to make the deal acceptable. Will find a valid counteroffer if any exist, but is not
* guaranteed to find the best or closest one. */
private fun getCounteroffer(civInfo: Civilization, tradeRequest: TradeRequest): TradeRequest? {
val otherCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv)
// AIs counteroffering each other is problematic as they tend to ping-pong back and forth forever
if (otherCiv.playerType == PlayerType.AI)
return null
val evaluation = TradeEvaluation()
var deltaInOurFavor = evaluation.getTradeAcceptability(tradeRequest.trade, civInfo, otherCiv)
if (deltaInOurFavor > 0) deltaInOurFavor = (deltaInOurFavor / 1.1f).toInt() // They seem very interested in this deal, let's push it a bit.
val tradeLogic = TradeLogic(civInfo, otherCiv)
tradeLogic.currentTrade.set(tradeRequest.trade)
// What do they have that we would want?
val potentialAsks = HashMap<TradeOffer, Int>()
val counterofferAsks = HashMap<TradeOffer, Int>()
val counterofferGifts = ArrayList<TradeOffer>()
for (offer in tradeLogic.theirAvailableOffers) {
if ((offer.type == TradeType.Gold || offer.type == TradeType.Gold_Per_Turn)
&& tradeRequest.trade.ourOffers.any { it.type == offer.type })
continue // Don't want to counteroffer straight gold for gold, that's silly
if (!offer.isTradable())
continue // For example resources gained by trade or CS
if (offer.type == TradeType.City)
continue // Players generally don't want to give up their cities, and they might misclick
if (tradeLogic.currentTrade.theirOffers.any { it.type == offer.type && it.name == offer.name })
continue // So you don't get double offers of open borders declarations of war etc.
if (offer.type == TradeType.Treaty)
continue // Don't try to counter with a defensive pact or research pact
val value = evaluation.evaluateBuyCostWithInflation(offer, civInfo, otherCiv)
if (value > 0)
potentialAsks[offer] = value
}
while (potentialAsks.isNotEmpty() && deltaInOurFavor < 0) {
// Keep adding their worst offer until we get above the threshold
val offerToAdd = potentialAsks.minByOrNull { it.value }!!
deltaInOurFavor += offerToAdd.value
counterofferAsks[offerToAdd.key] = offerToAdd.value
potentialAsks.remove(offerToAdd.key)
}
if (deltaInOurFavor < 0)
return null // We couldn't get a good enough deal
// At this point we are sure to find a good counteroffer
while (deltaInOurFavor > 0) {
// Now remove the best offer valued below delta until the deal is barely acceptable
val offerToRemove = counterofferAsks.filter { it.value <= deltaInOurFavor }.maxByOrNull { it.value }
?: break // Nothing more can be removed, at least en bloc
deltaInOurFavor -= offerToRemove.value
counterofferAsks.remove(offerToRemove.key)
}
// Only ask for enough of each resource to get maximum price
for (ask in counterofferAsks.keys.filter { it.type == TradeType.Luxury_Resource || it.type == TradeType.Strategic_Resource }) {
// Remove 1 amount as long as doing so does not change the price
val originalValue = counterofferAsks[ask]!!
while (ask.amount > 1
&& originalValue == evaluation.evaluateBuyCostWithInflation(
TradeOffer(ask.name, ask.type, ask.amount - 1, ask.duration),
civInfo, otherCiv) ) {
ask.amount--
}
}
// Adjust any gold asked for
val toRemove = ArrayList<TradeOffer>()
for (goldAsk in counterofferAsks.keys
.filter { it.type == TradeType.Gold_Per_Turn || it.type == TradeType.Gold }
.sortedByDescending { it.type.ordinal }) { // Do GPT first
val valueOfOne = evaluation.evaluateBuyCostWithInflation(TradeOffer(goldAsk.name, goldAsk.type, 1, goldAsk.duration), civInfo, otherCiv)
val amountCanBeRemoved = deltaInOurFavor / valueOfOne
if (amountCanBeRemoved >= goldAsk.amount) {
deltaInOurFavor -= counterofferAsks[goldAsk]!!
toRemove.add(goldAsk)
} else {
deltaInOurFavor -= valueOfOne * amountCanBeRemoved
goldAsk.amount -= amountCanBeRemoved
}
}
// If the delta is still very in our favor consider sweetening the pot with some gold
if (deltaInOurFavor >= 100) {
deltaInOurFavor = (deltaInOurFavor * 2) / 3 // Only compensate some of it though, they're the ones asking us
// First give some GPT, then lump sum - but only if they're not already offering the same
for (ourGold in tradeLogic.ourAvailableOffers
.filter { it.isTradable() && (it.type == TradeType.Gold || it.type == TradeType.Gold_Per_Turn) }
.sortedByDescending { it.type.ordinal }) {
if (tradeLogic.currentTrade.theirOffers.none { it.type == ourGold.type } &&
counterofferAsks.keys.none { it.type == ourGold.type } ) {
val valueOfOne = evaluation.evaluateSellCostWithInflation(TradeOffer(ourGold.name, ourGold.type, 1, ourGold.duration), civInfo, otherCiv)
val amountToGive = min(deltaInOurFavor / valueOfOne, ourGold.amount)
deltaInOurFavor -= amountToGive * valueOfOne
if (amountToGive > 0) {
counterofferGifts.add(
TradeOffer(
ourGold.name,
ourGold.type,
amountToGive,
ourGold.duration
)
)
}
}
}
}
tradeLogic.currentTrade.theirOffers.addAll(counterofferAsks.keys)
tradeLogic.currentTrade.ourOffers.addAll(counterofferGifts)
// Trades reversed, because when *they* get it then the 'ouroffers' become 'theiroffers'
return TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse())
}
fun exchangeLuxuries(civInfo: Civilization) {
val knownCivs = civInfo.getKnownCivs()
// Player trades are... more complicated.
// When the AI offers a trade, it's not immediately accepted,
// so what if it thinks that it has a spare luxury and offers it to two human players?
// What's to stop the AI "nagging" the player to accept a luxury trade?
// We should A. add some sort of timer (20? 30 turns?) between luxury trade requests if they're denied - see DeclinedLuxExchange
// B. have a way for the AI to keep track of the "pending offers" - see DiplomacyManager.resourcesFromTrade
for (otherCiv in knownCivs.filter {
it.isMajorCiv() && !it.isAtWarWith(civInfo)
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedLuxExchange)
}) {
val isEnemy = civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelLE(RelationshipLevel.Enemy)
if (isEnemy || otherCiv.tradeRequests.any { it.requestingCiv == civInfo.civName })
continue
val trades = potentialLuxuryTrades(civInfo, otherCiv)
for (trade in trades) {
val tradeRequest = TradeRequest(civInfo.civName, trade.reverse())
otherCiv.tradeRequests.add(tradeRequest)
}
}
}
private fun potentialLuxuryTrades(civInfo: Civilization, otherCivInfo: Civilization): ArrayList<Trade> {
val tradeLogic = TradeLogic(civInfo, otherCivInfo)
val ourTradableLuxuryResources = tradeLogic.ourAvailableOffers
.filter { it.type == TradeType.Luxury_Resource && it.amount > 1 }
val theirTradableLuxuryResources = tradeLogic.theirAvailableOffers
.filter { it.type == TradeType.Luxury_Resource && it.amount > 1 }
val weHaveTheyDont = ourTradableLuxuryResources
.filter { resource ->
tradeLogic.theirAvailableOffers
.none { it.name == resource.name && it.type == TradeType.Luxury_Resource }
}
val theyHaveWeDont = theirTradableLuxuryResources
.filter { resource ->
tradeLogic.ourAvailableOffers
.none { it.name == resource.name && it.type == TradeType.Luxury_Resource }
}.sortedBy { civInfo.cities.count { city -> city.demandedResource == it.name } } // Prioritize resources that get WLTKD
val trades = ArrayList<Trade>()
for (i in 0..min(weHaveTheyDont.lastIndex, theyHaveWeDont.lastIndex)) {
val trade = Trade()
trade.ourOffers.add(weHaveTheyDont[i].copy(amount = 1))
trade.theirOffers.add(theyHaveWeDont[i].copy(amount = 1))
trades.add(trade)
}
return trades
}
}

View File

@ -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

View File

@ -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]