Reworked AI war evaluation and plans (#11688)

* hasAtLeastMotivationToAttack now takes uses City.neighboringCities

* Changed attack pathing to use Astar

* Made the base force higher

* Declare war checks for total population instead of number of cities

* MotivationToAttackAutomation takes into account denunciation

* Set up DeclareWarTargetAutomation.kt

* Added logic for Civs to try and gang up on other Civs

* Added logic for civs to try and join an ally civ in their war and to declare war directly

* Added declineJoinWarOffer flag

* Reduced the likelyhood of declaring war a little

* Civs don't try to join wars against city-states

* Removed calculating targets with 0 motivation

* Relative production is not calculated against city-states

* Improved getAttackPathsModifier to only calculate the best path per city

* Refactored filter statements

* AI now tries to execute planned wars by default

* Culture Civs can now declare war, AI can declare multiple wars

* AI won't gift gold to city-states that it wants to attack

* Changed motivation from combat

* Changed AI aggression towards city-states

* Civs don't want to sign a DOF with the only nearby major civ as much

* City.neighboringCities filters out cities that are not visible

* Fixed some conditionals in tryJoinWar

* Fixed some war plan types breaking instead of continuing

* Civs are more likely to sign open borders if they haven't seen their cities

* Changed far away cities to have less of a value

* Fixed neighboringCities and getNeighboringCivilizations

* Other fixes

* Reduced motivation to attack from relative strength

* Added more to motivation to attack

* Added extra friendship modifiers

* Moved war evaluation to WarPlanEvaluator

* Added comments and re-named preparingWarPlan

* AI Team wars require neutral relations to send

* Added a team war notification

* Added evaluation of join war trades

* Tweaked MotivationToAttackAutomation

* Improved peace deal offers

* AI peace deals wait until 10 turns after declaring war

* Made the AI declare war a little less

* AI builds more military units

* AI keeps at least 2 great generals to not build citadels with

* AI TeamWar is more specialized for fighting stronger Civs

* Removed extra line

* Added more comments

* Improved unit tryPrepare logic

* Minor respelling and style improvements

* Changed MotivationToAttackAutomation HashMap to be a list

* Added a heuristic for the Astar search

* TeamWarPlan focuses more on relative force and fighting stronger civs

* MotivationToAttackAutomation takes into account planned wars and can target stronger civs

* Added logic for AI's to request other civs to join their war

* Fixed some WarDeclaration TradeEvaluation logic and reduced costs

* Added some extra safety against extreme force values in DeclareWarPlanEvaluator
This commit is contained in:
Oskar Niesen 2024-06-08 13:56:51 -05:00 committed by GitHub
parent 6979ea8e85
commit 9cd531c8cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 761 additions and 193 deletions

View File

@ -155,10 +155,15 @@ Denounce ([numberOfTurns] turns) =
We will remember this. =
[civName] has declared war on [targetCivName]! =
# Join War
[civName] has joined [allyCivName] in the war against us! =
We have joined [allyCivName] in the war against [enemyCivName]! =
[civName] has joined [allyCivName] in the war against [enemyCivName]! =
[civName] has joined us in the war against [enemyCivName]! =
# Team War
You and [allyCivName] have declared war against [enemyCivName]! =
[civName] and [allyCivName] have declared war against us! =
[civName] and [allyCivName] have declared war against [enemyCivName]! =
[civName] cancelled their Defensive Pact with [otherCivName]! =
[civName] cancelled their Defensive Pact with us! =

View File

@ -165,13 +165,13 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
private fun addMilitaryUnitChoice() {
if (!isAtWar && !cityIsOverAverageProduction) return // don't make any military units here. Infrastructure first!
if (!isAtWar && (civInfo.stats.statsForNextTurn.gold < 0 || militaryUnits > max(5, cities * 2))) return
if (!isAtWar && (civInfo.stats.statsForNextTurn.gold < 0 || militaryUnits > max(7, cities * 5))) return
if (civInfo.gold < -50) return
val militaryUnit = Automation.chooseMilitaryUnit(city, units) ?: return
val unitsToCitiesRatio = cities.toFloat() / (militaryUnits + 1)
// most buildings and civ units contribute the the civ's growth, military units are anti-growth
var modifier = sqrt(unitsToCitiesRatio) / 2
var modifier = 1 + sqrt(unitsToCitiesRatio) / 2
if (civInfo.wantsToFocusOn(Victory.Focus.Military) || isAtWar) modifier *= 2
if (Automation.afraidOfBarbarians(civInfo)) modifier = 2f // military units are pro-growth if pressured by barbs

View File

@ -0,0 +1,173 @@
package com.unciv.logic.automation.civilization
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.ui.screens.victoryscreen.RankingType
/**
* Contains the logic for evaluating how we want to declare war on another civ.
*/
object DeclareWarPlanEvaluator {
/**
* How much motivation [civInfo] has to do a team war with [teamCiv] against [target].
*
* This style of declaring war favors fighting stronger civilizations.
* @return The movtivation of the plan. If it is > 0 then we can declare the war.
*/
fun evaluateTeamWarPlan(civInfo: Civilization, target: Civilization, teamCiv: Civilization, givenMotivation: Int?): Int {
if (civInfo.getDiplomacyManager(teamCiv).isRelationshipLevelLT(RelationshipLevel.Neutral)) return -1000
var motivation = givenMotivation
?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0)
if (civInfo.getDiplomacyManager(teamCiv).isRelationshipLevelEQ(RelationshipLevel.Neutral)) motivation -= 5
// Make sure that they can actually help us with the target
if (!teamCiv.threatManager.getNeighboringCivilizations().contains(target)) {
motivation -= 40
}
val civForce = civInfo.getStatForRanking(RankingType.Force)
val targetForce = target.getStatForRanking(RankingType.Force)
val teamCivForce = (teamCiv.getStatForRanking(RankingType.Force) - 0.8f * teamCiv.threatManager.getCombinedForceOfWarringCivs()).coerceAtLeast(100f)
// A higher motivation means that we can be riskier
val multiplier = when {
motivation < 5 -> 1.2f
motivation < 10 -> 1.1f
motivation < 20 -> 1f
else -> 0.8f
}
if (civForce + teamCivForce < targetForce * multiplier) {
// We are weaker then them even with our combined forces
// If they have twice our combined force we will have -30 motivation
motivation -= (30 * ((targetForce * multiplier) / (teamCivForce + civForce) - 1)).toInt()
} else if (civForce + teamCivForce > targetForce * 2) {
// Why gang up on such a weaker enemy when we can declare war ourselves?
// If our combined force is twice their force we will have -20 motivation
motivation -= (20 * ((civForce + teamCivForce) / targetForce * 2) - 1).toInt()
}
val civScore = civInfo.getStatForRanking(RankingType.Score)
val teamCivScore = teamCiv.getStatForRanking(RankingType.Score)
val targetCivScore = target.getStatForRanking(RankingType.Score)
if (teamCivScore > civScore * 1.4f && teamCivScore >= targetCivScore) {
// If teamCiv has more score than us and the target they are likely in a good position already
motivation -= (20 * ((teamCivScore / (civScore * 1.4f)) - 1)).toInt()
}
return motivation - 20
}
/**
* How much motivation [civInfo] has to join [civToJoin] in their war against [target].
*
* Favors protecting allies.
* @return The movtivation of the plan. If it is > 0 then we can declare the war.
*/
fun evaluateJoinWarPlan(civInfo: Civilization, target: Civilization, civToJoin: Civilization, givenMotivation: Int?): Int {
if (civInfo.getDiplomacyManager(civToJoin).isRelationshipLevelLE(RelationshipLevel.Favorable)) return -1000
var motivation = givenMotivation
?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0)
// We need to be able to trust the thirdCiv at least somewhat
val thirdCivDiplo = civInfo.getDiplomacyManager(civToJoin)
if (thirdCivDiplo.diplomaticStatus != DiplomaticStatus.DefensivePact &&
thirdCivDiplo.opinionOfOtherCiv() + motivation * 2 < 80) {
motivation -= 80 - (thirdCivDiplo.opinionOfOtherCiv() + motivation * 2).toInt()
}
if (!civToJoin.threatManager.getNeighboringCivilizations().contains(target)) {
motivation -= 20
}
val targetForce = target.getStatForRanking(RankingType.Force) - 0.8f * target.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }.coerceAtLeast(100)
val civForce = civInfo.getStatForRanking(RankingType.Force)
// They need to be at least half the targets size, and we need to be stronger than the target together
val civToJoinForce = (civToJoin.getStatForRanking(RankingType.Force) - 0.8f * civToJoin.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }).coerceAtLeast(100f)
if (civToJoinForce < targetForce / 2) {
// Make sure that there is no wrap around
motivation -= (10 * (targetForce / civToJoinForce)).toInt().coerceIn(-1000, 1000)
}
// A higher motivation means that we can be riskier
val multiplier = when {
motivation < 10 -> 1.4f
motivation < 15 -> 1.3f
motivation < 20 -> 1.2f
motivation < 25 -> 1f
else -> 0.8f
}
if (civToJoinForce + civForce < targetForce * multiplier) {
motivation -= (20 * (targetForce * multiplier) / (civToJoinForce + civForce)).toInt().coerceIn(-1000, 1000)
}
return motivation - 15
}
/**
* How much motivation [civInfo] has for [civToJoin] to join them in their war against [target].
*
* @return The movtivation of the plan. If it is > 0 then we can declare the war.
*/
fun evaluateJoinOurWarPlan(civInfo: Civilization, target: Civilization, civToJoin: Civilization, givenMotivation: Int?): Int {
if (civInfo.getDiplomacyManager(civToJoin).isRelationshipLevelLT(RelationshipLevel.Favorable)) return -1000
var motivation = givenMotivation ?: 0
if (!civToJoin.threatManager.getNeighboringCivilizations().contains(target)) {
motivation -= 50
}
val targetForce = target.getStatForRanking(RankingType.Force)
val civForce = civInfo.getStatForRanking(RankingType.Force)
// They need to be at least half the targets size
val thirdCivForce = (civToJoin.getStatForRanking(RankingType.Force) - 0.8f * civToJoin.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }).coerceAtLeast(100f)
motivation += (20 * thirdCivForce / targetForce.toFloat()).toInt().coerceAtMost(40)
// If we have less relative force then the target then we have more motivation to accept
motivation += (30 * (1 - (civForce / targetForce.toFloat()))).toInt().coerceIn(-30, 30)
return motivation - 20
}
/**
* How much motivation [civInfo] has to declare war against [target] this turn.
* This can be through a prepared war or a suprise war.
*
* @return The movtivation of the plan. If it is > 0 then we can declare the war.
*/
fun evaluateDeclareWarPlan(civInfo: Civilization, target: Civilization, givenMotivation: Int?): Int {
val motivation = givenMotivation
?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0)
val diploManager = civInfo.getDiplomacyManager(target)
if (diploManager.hasFlag(DiplomacyFlags.WaryOf) && diploManager.getFlag(DiplomacyFlags.WaryOf) < 0) {
val turnsToPlan = (10 - (motivation / 10)).coerceAtLeast(3)
val turnsToWait = turnsToPlan + diploManager.getFlag(DiplomacyFlags.WaryOf)
return motivation - turnsToWait * 3
}
return motivation - 40
}
/**
* How much motivation [civInfo] has to start preparing for a war agaist [target].
*
* @return The motivation of the plan. If it is > 0 then we can start planning the war.
*/
fun evaluateStartPreparingWarPlan(civInfo: Civilization, target: Civilization, givenMotivation: Int?): Int {
val motivation = givenMotivation
?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0)
// TODO: We use negative values in WaryOf for now so that we aren't adding any extra fields to the save file
// This will very likely change in the future and we will want to build upon it
val diploManager = civInfo.getDiplomacyManager(target)
if (diploManager.hasFlag(DiplomacyFlags.WaryOf)) return 0
return motivation - 15
}
}

View File

@ -0,0 +1,128 @@
package com.unciv.logic.automation.civilization
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
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.ui.screens.victoryscreen.RankingType
object DeclareWarTargetAutomation {
/**
* Chooses a target civilization along with a plan of attack.
* Note that this doesn't guarantee that we will declare war on them immediatly, or that we will end up declaring war at all.
*/
fun chooseDeclareWarTarget(civInfo: Civilization, civAttackMotivations: List<Pair<Civilization, Int>>) {
val highestValueTargets = civAttackMotivations.sortedByDescending { it.first.getStatForRanking(RankingType.Score) }
for (target in highestValueTargets) {
if (tryDeclareWarWithPlan(civInfo, target.first, target.second))
return // We have successfully found a plan and started executing it!
}
}
/**
* Determines a war plan against this [target] and executes it if able.
*/
private fun tryDeclareWarWithPlan(civInfo: Civilization, target: Civilization, motivation: Int): Boolean {
if (!target.isCityState()) {
if (motivation > 5 && tryTeamWar(civInfo, target, motivation)) return true
if (motivation >= 15 && tryJoinWar(civInfo, target, motivation)) return true
}
if (motivation >= 20 && declareWar(civInfo, target, motivation)) return true
if (motivation >= 15 && prepareWar(civInfo, target, motivation)) return true
return false
}
/**
* The safest option for war is to invite a new ally to join the war with us.
* Together we are stronger and are more likely to take down bigger threats.
*/
private fun tryTeamWar(civInfo: Civilization, target: Civilization, motivation: Int): Boolean {
val potentialAllies = civInfo.getDiplomacyManager(target).getCommonKnownCivs()
.filter {
it.isMajorCiv()
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedJoinWarOffer)
&& civInfo.getDiplomacyManager(it).isRelationshipLevelGE(RelationshipLevel.Neutral)
&& !it.isAtWarWith(target)
}.sortedByDescending { it.getStatForRanking(RankingType.Force) }
for (thirdCiv in potentialAllies) {
if (DeclareWarPlanEvaluator.evaluateTeamWarPlan(civInfo, target, thirdCiv, motivation) <= 0) continue
// Send them an offer
val tradeLogic = TradeLogic(civInfo, thirdCiv)
tradeLogic.currentTrade.ourOffers.add(TradeOffer(target.civName, TradeType.WarDeclaration))
tradeLogic.currentTrade.theirOffers.add(TradeOffer(target.civName, TradeType.WarDeclaration))
thirdCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
return true
}
return false
}
/**
* The next safest aproach is to join an existing war on the side of an ally that is already at war with [target].
*/
private fun tryJoinWar(civInfo: Civilization, target: Civilization, motivation: Int): Boolean {
val potentialAllies = civInfo.getDiplomacyManager(target).getCommonKnownCivs()
.filter {
it.isMajorCiv()
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedJoinWarOffer)
&& civInfo.getDiplomacyManager(it).isRelationshipLevelGE(RelationshipLevel.Favorable)
&& it.isAtWarWith(target)
} // Must be a civ not already at war with them
.sortedByDescending { it.getStatForRanking(RankingType.Force) }
for (thirdCiv in potentialAllies) {
if (DeclareWarPlanEvaluator.evaluateJoinWarPlan(civInfo, target, thirdCiv, motivation) <= 0) continue
// Send them an offer
val tradeLogic = TradeLogic(civInfo, thirdCiv)
tradeLogic.currentTrade.ourOffers.add(TradeOffer(target.civName, TradeType.WarDeclaration))
// TODO: Maybe add in payment requests in some situations
thirdCiv.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
return true
}
return false
}
/**
* Lastly, if our motivation is high enough and we don't have any better plans then lets just declare war.
*/
private fun declareWar(civInfo: Civilization, target: Civilization, motivation: Int): Boolean {
if (DeclareWarPlanEvaluator.evaluateDeclareWarPlan(civInfo, target, motivation) > 0) {
civInfo.getDiplomacyManager(target).declareWar()
return true
}
return false
}
/**
* Slightly safter is to silently plan an invasion and declare war later.
*/
private fun prepareWar(civInfo: Civilization, target: Civilization, motivation: Int): Boolean {
// TODO: We use negative values in WaryOf for now so that we aren't adding any extra fields to the save file
// This will very likely change in the future and we will want to build upon it
val diploManager = civInfo.getDiplomacyManager(target)
if (DeclareWarPlanEvaluator.evaluateStartPreparingWarPlan(civInfo, target, motivation) > 0) {
diploManager.setFlag(DiplomacyFlags.WaryOf, -1)
return true
}
return false
}
}

View File

@ -15,17 +15,20 @@ 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.Victory
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.screens.victoryscreen.RankingType
import kotlin.math.abs
import kotlin.random.Random
object DiplomacyAutomation {
internal fun offerDeclarationOfFriendship(civInfo: Civilization) {
val civsThatWeCanDeclareFriendshipWith = civInfo.getKnownCivs()
.filter { civInfo.diplomacyFunctions.canSignDeclarationOfFriendshipWith(it)
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship)}
.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.
@ -53,7 +56,7 @@ object DiplomacyAutomation {
// 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)) {
motivation += when (Automation.threatAssessment(civInfo, otherCiv)) {
ThreatLevel.VeryHigh -> 10
ThreatLevel.High -> 5
ThreatLevel.VeryLow -> -5
@ -86,6 +89,13 @@ object DiplomacyAutomation {
val civsToKnow = 0.75f * allAliveCivs
motivation -= ((civsToKnow - knownCivs) / civsToKnow * 30f).toInt().coerceAtLeast(0)
// If they are the only non-friendly civ near us then they are the only civ to attack and expand into
if (civInfo.threatManager.getNeighboringCivilizations().none {
it.isMajorCiv() && it != otherCiv
&& civInfo.getDiplomacyManager(it).isRelationshipLevelLT(RelationshipLevel.Favorable)
})
motivation -= 20
motivation -= hasAtLeastMotivationToAttack(civInfo, otherCiv, motivation / 2) * 2
return motivation > 0
@ -94,14 +104,14 @@ object DiplomacyAutomation {
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)
&& !isTradeBeingOffered(civInfo, it, Constants.openBorders)
}
.filter {
it.isMajorCiv() && !civInfo.isAtWarWith(it)
&& it.hasUnique(UniqueType.EnablesOpenBorders)
&& !civInfo.getDiplomacyManager(it).hasOpenBorders
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedOpenBorders)
&& !isTradeBeingOffered(civInfo, it, Constants.openBorders)
}.sortedByDescending { it.getDiplomacyManager(civInfo).relationshipLevel() }.toList()
.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() < 7) continue
@ -123,9 +133,12 @@ object DiplomacyAutomation {
if (diploManager.hasFlag(DiplomacyFlags.DeclinedOpenBorders)) return false
if (diploManager.isRelationshipLevelLT(RelationshipLevel.Favorable)) return false
// Don't accept if they are at war with our friends, they might use our land to attack them
if (civInfo.diplomacy.values.any { it.isRelationshipLevelGE(RelationshipLevel.Friend) && it.otherCiv().isAtWarWith(otherCiv)})
if (civInfo.diplomacy.values.any { it.isRelationshipLevelGE(RelationshipLevel.Friend) && it.otherCiv().isAtWarWith(otherCiv) })
return false
if (hasAtLeastMotivationToAttack(civInfo, otherCiv, (diploManager.opinionOfOtherCiv()/ 2 - 10).toInt()) >= 0)
// Being able to see their cities can give us an advantage later on, especially with espionage enabled
if (otherCiv.cities.count { !it.getCenterTile().isVisible(civInfo) } < otherCiv.cities.count() * .8f)
return true
if (hasAtLeastMotivationToAttack(civInfo, otherCiv, (diploManager.opinionOfOtherCiv() / 2).toInt()) > 0)
return false
return true
}
@ -200,8 +213,10 @@ object DiplomacyAutomation {
}
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 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
@ -211,7 +226,7 @@ object DiplomacyAutomation {
// 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)) {
motivation += when (Automation.threatAssessment(civInfo, otherCiv)) {
ThreatLevel.VeryHigh -> 10
ThreatLevel.High -> 5
ThreatLevel.Low -> -5
@ -234,84 +249,111 @@ object DiplomacyAutomation {
}
internal fun declareWar(civInfo: Civilization) {
if (civInfo.wantsToFocusOn(Victory.Focus.Culture) &&
civInfo.getPersonality().isNeutralPersonality)
return
if (civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
if (civInfo.isAtWar() || civInfo.getHappiness() <= 0) return
if (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!
if (ourMilitaryUnits < 4) return // to stop AI declaring war at the beginning of games when everyone isn't set up well enough
// For mods we can't check the number of cities, so we will check the population instead.
if (civInfo.cities.sumOf { it.population.population } < 12) return // FAR too early for that what are you thinking!
//evaluate war
val enemyCivs = civInfo.getKnownCivs()
val targetCivs = 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 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
if (targetCivs.none()) return
val minMotivationToAttack = 20
// Attack the highest score enemy that we are willing to fight.
// This is to help prevent civs from ganging up on smaller civs
// and directs them to fight their competitors instead.
val civWithBestMotivationToAttack = enemyCivs
.filter { hasAtLeastMotivationToAttack(civInfo, it, minMotivationToAttack) >= 20 }
.maxByOrNull { it.getStatForRanking(RankingType.Score) }
val targetCivsWithMotivation: List<Pair<Civilization, Int>> = targetCivs
.map { Pair(it, hasAtLeastMotivationToAttack(civInfo, it, 0)) }
.filter { it.second > 0 }.toList()
if (civWithBestMotivationToAttack != null)
civInfo.getDiplomacyManager(civWithBestMotivationToAttack).declareWar()
DeclareWarTargetAutomation.chooseDeclareWarTarget(civInfo, targetCivsWithMotivation)
}
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) }
.filterNot {
it == civInfo || it.isBarbarian() || it.cities.isEmpty()
|| it.getDiplomacyManager(civInfo).hasFlag(DiplomacyFlags.DeclaredWar)
|| civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclaredWar)
}.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) {
if (hasAtLeastMotivationToAttack(civInfo, enemy, 10) >= 10) {
// We can still fight. Refuse peace.
continue
}
if (civInfo.getStatForRanking(RankingType.Force) - 0.8f * civInfo.threatManager.getCombinedForceOfWarringCivs() > 0) {
val randomSeed = civInfo.gameInfo.civilizations.indexOf(enemy) + civInfo.getCivsAtWarWith().count() + 123 * civInfo.gameInfo.turns
if (Random(randomSeed).nextInt(100) > 80) 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 (enemy.isMajorCiv()) {
var moneyWeNeedToPay = -TradeEvaluation().evaluatePeaceCostForThem(civInfo, enemy)
if (civInfo.gold > 0 && moneyWeNeedToPay > 0) {
if (moneyWeNeedToPay > civInfo.gold) {
moneyWeNeedToPay = civInfo.gold // As much as possible
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)
)
} else if (moneyWeNeedToPay < -100) {
val moneyTheyNeedToPay = abs(moneyWeNeedToPay).coerceAtMost(enemy.gold)
if (moneyTheyNeedToPay > 0) {
tradeLogic.currentTrade.theirOffers.add(
TradeOffer("Gold".tr(), TradeType.Gold, moneyTheyNeedToPay)
)
}
}
tradeLogic.currentTrade.ourOffers.add(
TradeOffer("Gold".tr(), TradeType.Gold, moneyWeNeedToPay)
)
}
enemy.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
}
}
private fun isTradeBeingOffered(civInfo: Civilization, otherCiv: Civilization, offerName:String): Boolean {
internal fun askForHelp(civInfo: Civilization) {
if (!civInfo.isAtWar() || civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return
for (enemyCiv in civInfo.getCivsAtWarWith().sortedByDescending { it.getStatForRanking(RankingType.Force) }) {
val potentialAllies = enemyCiv.threatManager.getNeighboringCivilizations()
.filter { !it.isAtWarWith(enemyCiv) && civInfo.getDiplomacyManager(it).isRelationshipLevelGE(RelationshipLevel.Friend)
&& !it.getDiplomacyManager(civInfo).hasFlag(DiplomacyFlags.DeclinedJoinWarOffer) }
.sortedByDescending { it.getStatForRanking(RankingType.Force) }
val civToAsk = potentialAllies.firstOrNull {
DeclareWarPlanEvaluator.evaluateJoinOurWarPlan(civInfo, enemyCiv, it, null) > 0 } ?: continue
val tradeLogic = TradeLogic(civInfo, civToAsk)
// TODO: add gold offer here
tradeLogic.currentTrade.theirOffers.add(TradeOffer(enemyCiv.civName, TradeType.WarDeclaration))
civToAsk.tradeRequests.add(TradeRequest(civInfo.civName, tradeLogic.currentTrade.reverse()))
}
}
private fun isTradeBeingOffered(civInfo: Civilization, otherCiv: Civilization, offerName: String): Boolean {
return civInfo.tradeRequests.filter { request -> request.requestingCiv == otherCiv.civName }
.any { trade -> trade.trade.ourOffers.any { offer -> offer.name == offerName }}
.any { trade -> trade.trade.ourOffers.any { offer -> offer.name == offerName } }
|| civInfo.tradeRequests.filter { request -> request.requestingCiv == otherCiv.civName }
.any { trade -> trade.trade.theirOffers.any { offer -> offer.name == offerName }}
.any { trade -> trade.trade.theirOffers.any { offer -> offer.name == offerName } }
}
}

View File

@ -9,6 +9,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomacyManager
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.map.BFS
import com.unciv.logic.map.MapPathing
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.unique.UniqueType
@ -19,95 +20,110 @@ object MotivationToAttackAutomation {
/** 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. */
fun hasAtLeastMotivationToAttack(civInfo: Civilization, otherCiv: Civilization, atLeast: Int): Int {
val closestCities = NextTurnAutomation.getClosestCities(civInfo, otherCiv) ?: return 0
val baseForce = 30f
fun hasAtLeastMotivationToAttack(civInfo: Civilization, targetCiv: Civilization, atLeast: Int): Int {
val targetCitiesWithOurCity = civInfo.threatManager.getNeighboringCitiesOfOtherCivs().filter { it.second.civ == targetCiv }.toList()
val targetCities = targetCitiesWithOurCity.map { it.second }
val ourCombatStrength = calculateSelfCombatStrength(civInfo, baseForce)
val theirCombatStrength = calculateCombatStrengthWithProtectors(otherCiv, baseForce, civInfo)
if (targetCitiesWithOurCity.isEmpty()) return 0
if (theirCombatStrength > ourCombatStrength) return 0
val ourCity = closestCities.city1
val theirCity = closestCities.city2
if (hasNoUnitsThatCanAttackCityWithoutDying(civInfo, theirCity))
if (targetCities.all { hasNoUnitsThatCanAttackCityWithoutDying(civInfo, it) })
return 0
fun isTileCanMoveThrough(tile: Tile): Boolean {
val owner = tile.getOwner()
return !tile.isImpassible()
&& (owner == otherCiv || owner == null || civInfo.diplomacyFunctions.canPassThroughTiles(owner))
val baseForce = 100f
val ourCombatStrength = calculateSelfCombatStrength(civInfo, baseForce)
val theirCombatStrength = calculateCombatStrengthWithProtectors(targetCiv, baseForce, civInfo)
val modifiers:MutableList<Pair<String, Int>> = mutableListOf()
modifiers.add(Pair("Base motivation", -15))
modifiers.add(Pair("Relative combat strength", getCombatStrengthModifier(ourCombatStrength, theirCombatStrength + 0.8f * civInfo.threatManager.getCombinedForceOfWarringCivs())))
// TODO: For now this will be a very high value because the AI can't handle multiple fronts, this should be changed later though
modifiers.add(Pair("Concurrent wars", -civInfo.getCivsAtWarWith().count { it.isMajorCiv() && it != targetCiv } * 20))
modifiers.add(Pair("Their concurrent wars", targetCiv.getCivsAtWarWith().count { it.isMajorCiv() } * 3))
modifiers.add(Pair("Their allies", getDefensivePactAlliesScore(targetCiv, civInfo, baseForce, ourCombatStrength)))
if (civInfo.threatManager.getNeighboringCivilizations().none { it != targetCiv && it.isMajorCiv()
&& civInfo.getDiplomacyManager(it).isRelationshipLevelLT(RelationshipLevel.Friend) })
modifiers.add(Pair("No other threats", 10))
if (targetCiv.isMajorCiv()) {
val scoreRatioModifier = getScoreRatioModifier(targetCiv, civInfo)
modifiers.add(Pair("Relative score", scoreRatioModifier))
modifiers.add(Pair("Relative technologies", getRelativeTechModifier(civInfo, targetCiv)))
if (civInfo.stats.getUnitSupplyDeficit() != 0) {
modifiers.add(Pair("Over unit supply", (civInfo.stats.getUnitSupplyDeficit() * 2).coerceAtMost(20)))
} else if (targetCiv.stats.getUnitSupplyDeficit() == 0 && !targetCiv.isCityState()) {
modifiers.add(Pair("Relative production", getProductionRatioModifier(civInfo, targetCiv)))
}
}
val modifierMap = HashMap<String, Int>()
modifierMap["Relative combat strength"] = getCombatStrengthModifier(ourCombatStrength, theirCombatStrength)
val minTargetCityDistance = targetCitiesWithOurCity.minOf { it.second.getCenterTile().aerialDistanceTo(it.first.getCenterTile()) }
modifiers.add(Pair("Far away cities", when {
minTargetCityDistance > 20 -> -10
minTargetCityDistance > 14 -> -8
minTargetCityDistance > 10 -> -3
else -> 0
}))
if (minTargetCityDistance < 6) modifiers.add(Pair("Close cities", 5))
modifierMap["Their allies"] = getDefensivePactAlliesScore(otherCiv, civInfo, baseForce, ourCombatStrength)
val diplomacyManager = civInfo.getDiplomacyManager(targetCiv)
val scoreRatioModifier = getScoreRatioModifier(otherCiv, civInfo)
modifierMap["Relative score"] = scoreRatioModifier
if (civInfo.stats.getUnitSupplyDeficit() != 0) {
modifierMap["Over unit supply"] = (civInfo.stats.getUnitSupplyDeficit() * 2).coerceAtMost(20)
} else if (otherCiv.stats.getUnitSupplyDeficit() == 0) {
modifierMap["Relative production"] = getProductionRatioModifier(civInfo, otherCiv)
}
modifierMap["Relative technologies"] = getRelativeTechModifier(civInfo, otherCiv)
if (closestCities.aerialDistance > 7)
modifierMap["Far away cities"] = -10
val diplomacyManager = civInfo.getDiplomacyManager(otherCiv)
if (diplomacyManager.hasFlag(DiplomacyFlags.ResearchAgreement))
modifierMap["Research Agreement"] = -5
modifiers.add(Pair("Research Agreement", -5))
if (diplomacyManager.hasFlag(DiplomacyFlags.DeclarationOfFriendship))
modifierMap["Declaration of Friendship"] = -10
modifiers.add(Pair("Declaration of Friendship", -10))
if (diplomacyManager.hasFlag(DiplomacyFlags.DefensivePact))
modifierMap["Defensive Pact"] = -10
modifiers.add(Pair("Defensive Pact", -15))
modifierMap["Relationship"] = getRelationshipModifier(diplomacyManager)
modifiers.add(Pair("Relationship", getRelationshipModifier(diplomacyManager)))
if (diplomacyManager.hasFlag(DiplomacyFlags.Denunciation)) {
modifiers.add(Pair("Denunciation", 5))
}
if (diplomacyManager.hasFlag(DiplomacyFlags.WaryOf) && diplomacyManager.getFlag(DiplomacyFlags.WaryOf) < 0) {
modifiers.add(Pair("PlanningAttack", -diplomacyManager.getFlag(DiplomacyFlags.WaryOf)))
} else {
val attacksPlanned = civInfo.diplomacy.values.count { it.hasFlag(DiplomacyFlags.WaryOf) && it.getFlag(DiplomacyFlags.WaryOf) < 0 }
modifiers.add(Pair("PlanningAttackAgainstOtherCivs", -attacksPlanned * 5))
}
if (diplomacyManager.resourcesFromTrade().any { it.amount > 0 })
modifierMap["Receiving trade resources"] = -5
modifiers.add(Pair("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
// If their cities don't have any nearby cities that are also targets to us and it doesn't include their capital
// Then there cities are likely isolated and a good target.
if (targetCiv.getCapital(true) !in targetCities
&& targetCities.all { theirCity -> !theirCity.neighboringCities.any { it !in targetCities } }) {
modifiers.add(Pair("Isolated city", 15))
}
addWonderBasedMotivations(otherCiv, modifierMap)
modifierMap["War with allies"] = getAlliedWarMotivation(civInfo, otherCiv)
var motivationSoFar = modifierMap.values.sum()
// Short-circuit to avoid expensive BFS
if (motivationSoFar < atLeast) return motivationSoFar
val landPathBFS = BFS(ourCity.getCenterTile()) {
it.isLand && isTileCanMoveThrough(it)
if (targetCiv.isCityState()) {
modifiers.add(Pair("Protectors", -targetCiv.cityStateFunctions.getProtectorCivs().size * 3))
if (targetCiv.cityStateFunctions.getProtectorCivs().contains(civInfo))
modifiers.add(Pair("Under our protection", -15))
if (targetCiv.getAllyCiv() == civInfo.civName)
modifiers.add(Pair("Allied City-state", -20)) // There had better be a DAMN good reason
}
landPathBFS.stepUntilDestination(theirCity.getCenterTile())
if (!landPathBFS.hasReachedTile(theirCity.getCenterTile()))
motivationSoFar -= -10
addWonderBasedMotivations(targetCiv, modifiers)
// Short-circuit to avoid expensive BFS
modifiers.add(Pair("War with allies", getAlliedWarMotivation(civInfo, targetCiv)))
// Purely for debugging, remove modifiers that don't have an effect
modifiers.removeAll { it.second == 0 }
var motivationSoFar = modifiers.sumOf { it.second }
// Short-circuit to avoid A-star
if (motivationSoFar < atLeast) return motivationSoFar
val reachableEnemyCitiesBfs = BFS(civInfo.getCapital()!!.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.
motivationSoFar += getAttackPathsModifier(civInfo, targetCiv, targetCitiesWithOurCity)
return motivationSoFar
}
@ -129,21 +145,21 @@ object MotivationToAttackAutomation {
return ourCombatStrength
}
private fun addWonderBasedMotivations(otherCiv: Civilization, modifierMap: HashMap<String, Int>) {
private fun addWonderBasedMotivations(otherCiv: Civilization, modifiers: MutableList<Pair<String, Int>>) {
var wonderCount = 0
for (city in otherCiv.cities) {
val construction = city.cityConstructions.getCurrentConstruction()
if (construction is Building && construction.hasUnique(UniqueType.TriggersCulturalVictory))
modifierMap["About to win"] = 15
modifiers.add(Pair("About to win", 15))
if (construction is BaseUnit && construction.hasUnique(UniqueType.AddInCapital))
modifierMap["About to win"] = 15
modifiers.add(Pair("About to win", 15))
wonderCount += city.cityConstructions.getBuiltBuildings().count { it.isWonder }
}
// The more wonders they have, the more beneficial it is to conquer them
// Civs need an army to protect thier wonders which give the most score
if (wonderCount > 0)
modifierMap["Owned Wonders"] = wonderCount
modifiers.add(Pair("Owned Wonders", wonderCount))
}
/** If they are at war with our allies, then we should join in */
@ -151,10 +167,14 @@ object MotivationToAttackAutomation {
var alliedWarMotivation = 0
for (thirdCiv in civInfo.getDiplomacyManager(otherCiv).getCommonKnownCivs()) {
val thirdCivDiploManager = civInfo.getDiplomacyManager(thirdCiv)
if (thirdCivDiploManager.hasFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship)
if (thirdCivDiploManager.isRelationshipLevelGE(RelationshipLevel.Friend)
&& thirdCiv.isAtWarWith(otherCiv)
) {
alliedWarMotivation += if (thirdCivDiploManager.hasFlag(DiplomacyFlags.DefensivePact)) 15 else 5
if (thirdCiv.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.Denunciation))
alliedWarMotivation += 2
alliedWarMotivation += if (thirdCivDiploManager.hasFlag(DiplomacyFlags.DefensivePact)) 15
else if (thirdCivDiploManager.isRelationshipLevelGT(RelationshipLevel.Friend)) 5
else 2
}
}
return alliedWarMotivation
@ -162,9 +182,12 @@ object MotivationToAttackAutomation {
private fun getRelationshipModifier(diplomacyManager: DiplomacyManager): Int {
val relationshipModifier = when (diplomacyManager.relationshipIgnoreAfraid()) {
RelationshipLevel.Unforgivable -> 10
RelationshipLevel.Enemy -> 5
RelationshipLevel.Ally -> -5 // this is so that ally + DoF is not too unbalanced -
RelationshipLevel.Unforgivable -> 15
RelationshipLevel.Enemy -> 10
RelationshipLevel.Competitor -> 5
RelationshipLevel.Favorable -> -2
RelationshipLevel.Friend -> -5
RelationshipLevel.Ally -> -10 // this is so that ally + DoF is not too unbalanced -
// still possible for AI to declare war for isolated city
else -> 0
}
@ -210,10 +233,11 @@ object MotivationToAttackAutomation {
// Designed to mitigate AIs declaring war on weaker civs instead of their rivals
val scoreRatio = otherCiv.getStatForRanking(RankingType.Score).toFloat() / civInfo.getStatForRanking(RankingType.Score).toFloat()
val scoreRatioModifier = when {
scoreRatio > 2f -> 15
scoreRatio > 1.5f -> 10
scoreRatio > 1.25f -> 5
scoreRatio > 1f -> 0
scoreRatio > 2f -> 20
scoreRatio > 1.5f -> 15
scoreRatio > 1.25f -> 10
scoreRatio > 1f -> 2
scoreRatio > .8f -> 0
scoreRatio > .5f -> -2
scoreRatio > .25f -> -5
else -> -10
@ -240,11 +264,16 @@ object MotivationToAttackAutomation {
private fun getCombatStrengthModifier(ourCombatStrength: Float, theirCombatStrength: Float): Int {
val combatStrengthRatio = ourCombatStrength / theirCombatStrength
val combatStrengthModifier = when {
combatStrengthRatio > 3f -> 30
combatStrengthRatio > 2.5f -> 25
combatStrengthRatio > 2f -> 20
combatStrengthRatio > 1.5f -> 10
else -> 0
combatStrengthRatio > 5f -> 30
combatStrengthRatio > 4f -> 20
combatStrengthRatio > 3f -> 15
combatStrengthRatio > 2f -> 10
combatStrengthRatio > 1.5f -> 5
combatStrengthRatio > .8f -> 0
combatStrengthRatio > .6f -> -5
combatStrengthRatio > .4f -> -15
combatStrengthRatio > .2f -> -20
else -> -20
}
return combatStrengthModifier
}
@ -258,4 +287,66 @@ object MotivationToAttackAutomation {
damageReceivedWhenAttacking < 100
}
/**
* Checks the routes of attack against [otherCiv] using [targetCitiesWithOurCity].
*
* The more routes of attack and shorter the path the higher a motivation will be returned.
* Sea attack routes are less valuable
*
* @return The motivation ranging from -30 to around +10
*/
private fun getAttackPathsModifier(civInfo: Civilization, otherCiv: Civilization, targetCitiesWithOurCity: List<Pair<City, City>>): Int {
fun isTileCanMoveThrough(civInfo: Civilization, tile: Tile): Boolean {
val owner = tile.getOwner()
return !tile.isImpassible()
&& (owner == otherCiv || owner == null || civInfo.diplomacyFunctions.canPassThroughTiles(owner))
}
fun isLandTileCanMoveThrough(civInfo: Civilization, tile: Tile): Boolean {
return tile.isLand && isTileCanMoveThrough(civInfo, tile)
}
val attackPaths: MutableList<List<Tile>> = mutableListOf()
var attackPathModifiers: Int = -3
// For each city, we want to calculate if there is an attack path to the enemy
for (attacksGroupedByCity in targetCitiesWithOurCity.groupBy { it.first }) {
val cityToAttackFrom = attacksGroupedByCity.key
var cityAttackValue = 0
// We only want to calculate the best attack path and use it's value
// Land routes are clearly better than sea routes
for ((_, cityToAttack) in attacksGroupedByCity.value) {
val landAttackPath = MapPathing.getConnection(civInfo, cityToAttackFrom.getCenterTile(), cityToAttack.getCenterTile(), ::isLandTileCanMoveThrough)
if (landAttackPath != null && landAttackPath.size < 16) {
attackPaths.add(landAttackPath)
cityAttackValue = 3
break
}
if (cityAttackValue > 0) continue
val landAndSeaAttackPath = MapPathing.getConnection(civInfo, cityToAttackFrom.getCenterTile(), cityToAttack.getCenterTile(), ::isTileCanMoveThrough)
if (landAndSeaAttackPath != null && landAndSeaAttackPath.size < 16) {
attackPaths.add(landAndSeaAttackPath)
cityAttackValue += 1
}
}
attackPathModifiers += cityAttackValue
}
if (attackPaths.isEmpty()) {
// Do an expensive BFS to find any possible attack path
val reachableEnemyCitiesBfs = BFS(civInfo.getCapital(true)!!.getCenterTile()) { isTileCanMoveThrough(civInfo, it) }
reachableEnemyCitiesBfs.stepToEnd()
val reachableEnemyCities = otherCiv.cities.filter { reachableEnemyCitiesBfs.hasReachedTile(it.getCenterTile()) }
if (reachableEnemyCities.isEmpty()) return -50 // Can't even reach the enemy city, no point in war.
val minAttackDistance = reachableEnemyCities.minOf { reachableEnemyCitiesBfs.getPathTo(it.getCenterTile()).count() }
// Longer attack paths are worse, but if the attack path is too far away we shouldn't completely discard the possibility
attackPathModifiers -= (minAttackDistance - 10).coerceIn(0, 30)
}
return attackPathModifiers
}
}

View File

@ -43,6 +43,7 @@ object NextTurnAutomation {
if (!civInfo.gameInfo.ruleset.modOptions.hasUnique(UniqueType.DiplomaticRelationshipsCannotChange)) {
DiplomacyAutomation.declareWar(civInfo)
DiplomacyAutomation.offerPeaceTreaty(civInfo)
DiplomacyAutomation.askForHelp(civInfo)
DiplomacyAutomation.offerDeclarationOfFriendship(civInfo)
}
if (civInfo.gameInfo.isReligionEnabled()) {

View File

@ -80,7 +80,7 @@ object TradeAutomation {
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)
val value = evaluation.evaluateBuyCostWithInflation(offer, civInfo, otherCiv, tradeRequest.trade)
if (value > 0)
potentialAsks[offer] = value
}
@ -111,7 +111,7 @@ object TradeAutomation {
while (ask.amount > 1
&& originalValue == evaluation.evaluateBuyCostWithInflation(
TradeOffer(ask.name, ask.type, ask.amount - 1, ask.duration),
civInfo, otherCiv) ) {
civInfo, otherCiv, tradeRequest.trade) ) {
ask.amount--
}
}
@ -121,7 +121,7 @@ object TradeAutomation {
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 valueOfOne = evaluation.evaluateBuyCostWithInflation(TradeOffer(goldAsk.name, goldAsk.type, 1, goldAsk.duration), civInfo, otherCiv, tradeRequest.trade)
val amountCanBeRemoved = deltaInOurFavor / valueOfOne
if (amountCanBeRemoved >= goldAsk.amount) {
deltaInOurFavor -= counterofferAsks[goldAsk]!!
@ -141,7 +141,7 @@ object TradeAutomation {
.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 valueOfOne = evaluation.evaluateSellCostWithInflation(TradeOffer(ourGold.name, ourGold.type, 1, ourGold.duration), civInfo, otherCiv, tradeRequest.trade)
val amountToGive = min(deltaInOurFavor / valueOfOne, ourGold.amount)
deltaInOurFavor -= amountToGive * valueOfOne
if (amountToGive > 0) {

View File

@ -37,7 +37,7 @@ object UseGoldAutomation {
private fun useGoldForCityStates(civ: Civilization) {
// RARE EDGE CASE: If you ally with a city-state, you may reveal more map that includes ANOTHER civ!
// So if we don't lock this list, we may later discover that there are more known civs, concurrent modification exception!
val knownCityStates = civ.getKnownCivs().filter { it.isCityState() }.toList()
val knownCityStates = civ.getKnownCivs().filter { it.isCityState() && MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civ, it, 0) <= 0 }.toList()
// canBeMarriedBy checks actual cost, but it can't be below 500*speedmodifier, and the later check is expensive
if (civ.gold >= 330 && civ.getHappiness() > 0 && civ.hasUnique(UniqueType.CityStateCanBeBoughtForGold)) {

View File

@ -96,7 +96,7 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, cachedForTurn: Int,
val basePriority = rankRoadCapitalPriority(roadToCapitalStatus)
val roadsToBuild: MutableList<RoadPlan> = mutableListOf()
for (closeCity in city.neighboringCities.filter { it.civ == civInfo }) {
for (closeCity in city.neighboringCities.filter { it.civ == civInfo && it.getCenterTile().aerialDistanceTo(city.getCenterTile()) <= 8 }) {
// Try to find if the other city has planned to build a road to this city
if (roadsToBuildByCitiesCache.containsKey(closeCity)) {

View File

@ -27,6 +27,10 @@ object SpecificUnitAutomation {
}
fun automateCitadelPlacer(unit: MapUnit): Boolean {
// Keep at least 2 generals alive
if (unit.hasUnique(UniqueType.StrengthBonusInRadius)
&& unit.civ.units.getCivUnits().count { it.hasUnique(UniqueType.StrengthBonusInRadius) } < 3)
return false
// try to revenge and capture their tiles
val enemyCities = unit.civ.getKnownCivs()
.filter { unit.civ.getDiplomacyManager(it).hasModifier(DiplomaticModifiers.StealingTerritory) }

View File

@ -3,6 +3,7 @@ package com.unciv.logic.automation.unit
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.automation.Automation
import com.unciv.logic.automation.civilization.NextTurnAutomation
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.BattleDamage
import com.unciv.logic.battle.CityCombatant
@ -10,7 +11,9 @@ import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
@ -247,6 +250,8 @@ object UnitAutomation {
if (unit.health < 100 && tryHealUnit(unit)) return
if (tryPrepare(unit)) return
// else, try to go to unreached tiles
if (tryExplore(unit)) return
@ -493,6 +498,34 @@ object UnitAutomation {
return false
}
/**
* Tries to move the unit to the closest city that is close to a target civilization
*/
private fun tryPrepare(unit: MapUnit): Boolean {
val civInfo = unit.civ
fun hasPreparationFlag(targetCiv: Civilization): Boolean {
val diploManager = civInfo.getDiplomacyManager(targetCiv)
if (diploManager.hasFlag(DiplomacyFlags.Denunciation)
|| diploManager.otherCivDiplomacy().hasFlag(DiplomacyFlags.Denunciation)) return true
if (diploManager.hasFlag(DiplomacyFlags.WaryOf) && diploManager.getFlag(DiplomacyFlags.WaryOf) < 0) return true
return false
}
val hostileCivs = civInfo.getKnownCivs().filter { it.isAtWarWith(civInfo) || hasPreparationFlag(it) }
val citiesToDefend = hostileCivs.mapNotNull { NextTurnAutomation.getClosestCities(civInfo, it) }
.sortedBy { unit.getTile().aerialDistanceTo(it.city1.getCenterTile()) }
// Move to the closest city with a tile we can enter nearby
for ((city, enemyCity) in citiesToDefend) {
if (unit.getTile().aerialDistanceTo(city.getCenterTile()) <= 2) return true
val tileToMoveTo = city.getCenterTile().getTilesInDistance(2).firstOrNull { unit.movement.canMoveTo(it) && unit.movement.canReach(it) } ?: continue
unit.movement.headTowards(tileToMoveTo)
return true
}
return false
}
private fun tryAccompanySettlerOrGreatPerson(unit: MapUnit): Boolean {
val distanceToTiles = unit.movement.getDistanceToTiles()
val settlerOrGreatPersonToAccompany = unit.civ.units.getCivUnits()

View File

@ -88,8 +88,8 @@ class City : IsPartOfGameInfoSerialization, INamed {
var updateCitizens = false // flag so that on startTurn() the Governor reassigns Citizens
@delegate:Transient
val neighboringCities: List<City> by lazy {
civ.gameInfo.getCities().filter { it != this && it.getCenterTile().aerialDistanceTo(getCenterTile()) <= 8 }.toList()
val neighboringCities: List<City> by lazy {
civ.gameInfo.getCities().filter { it != this && it.getCenterTile().isExplored(civ) && it.getCenterTile().aerialDistanceTo(getCenterTile()) <= 12 }.toList()
}
var cityAIFocus: String = CityFocus.NoFocus.name

View File

@ -626,6 +626,8 @@ class Civilization : IsPartOfGameInfoSerialization {
fun isAtWar() = diplomacy.values.any { it.diplomaticStatus == DiplomaticStatus.War && !it.otherCiv().isDefeated() }
fun getCivsAtWarWith() = diplomacy.values.filter { it.diplomaticStatus == DiplomaticStatus.War && !it.otherCiv().isDefeated() }.map { it.otherCiv() }
/**
* Returns a civilization caption suitable for greetings including player type info:

View File

@ -72,11 +72,11 @@ object DeclareWar {
otherCiv.popupAlerts.add(PopupAlert(AlertType.WarDeclaration, civInfo.civName))
otherCiv.addNotification("[${civInfo.civName}] has declared war on us!",
NotificationCategory.Diplomacy, NotificationIcon.War, civInfo.civName)
NotificationCategory.Diplomacy, otherCiv.civName, NotificationIcon.War, civInfo.civName)
diplomacyManager.getCommonKnownCivsWithSpectators().forEach {
it.addNotification("[${civInfo.civName}] has declared war on [${diplomacyManager.otherCivName}]!",
NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.War, otherCiv.civName)
it.addNotification("[${civInfo.civName}] has declared war on [${otherCiv.civName}]!",
NotificationCategory.Diplomacy, otherCiv.civName, NotificationIcon.War, civInfo.civName)
}
}
WarType.DefensivePactWar, WarType.CityStateAllianceWar, WarType.JoinWar -> {
@ -86,18 +86,40 @@ object DeclareWar {
val defender = if (declareWarReason.warType == WarType.DefensivePactWar) civInfo else otherCiv
defender.addNotification("[${agressor.civName}] has joined [${allyCiv.civName}] in the war against us!",
NotificationCategory.Diplomacy, NotificationIcon.War, agressor.civName)
NotificationCategory.Diplomacy, defender.civName, NotificationIcon.War, allyCiv.civName, agressor.civName)
agressor.addNotification("We have joined [${allyCiv.civName}] in the war against [${defender.civName}]!",
NotificationCategory.Diplomacy, NotificationIcon.War, defender.civName)
NotificationCategory.Diplomacy, defender.civName, NotificationIcon.War, allyCiv.civName, agressor.civName)
diplomacyManager.getCommonKnownCivsWithSpectators().filterNot { it == allyCiv }.forEach {
it.addNotification("[${agressor.civName}] has joined [${allyCiv.civName}] in the war against [${defender.civName}]!",
NotificationCategory.Diplomacy, agressor.civName, NotificationIcon.War, defender.civName)
NotificationCategory.Diplomacy, defender.civName, NotificationIcon.War, allyCiv.civName, agressor.civName)
}
allyCiv.addNotification("[${agressor.civName}] has joined us in the war against [${defender.civName}]!",
NotificationCategory.Diplomacy, agressor.civName, NotificationIcon.War, defender.civName)
NotificationCategory.Diplomacy, defender.civName, NotificationIcon.War, allyCiv.civName, agressor.civName)
}
WarType.TeamWar -> {
val allyCiv = declareWarReason.allyCiv!!
// We only want to send these notifications once, it doesn't matter who sends it though
if (civInfo.gameInfo.civilizations.indexOf(civInfo) > civInfo.gameInfo.civilizations.indexOf(allyCiv)) return
otherCiv.popupAlerts.add(PopupAlert(AlertType.WarDeclaration, civInfo.civName))
otherCiv.popupAlerts.add(PopupAlert(AlertType.WarDeclaration, allyCiv.civName))
civInfo.addNotification("You and [${allyCiv.civName}] have declared war against [${otherCiv.civName}]!",
NotificationCategory.Diplomacy, otherCiv.civName, NotificationIcon.War, allyCiv.civName, civInfo.civName)
allyCiv.addNotification("You and [${civInfo.civName}] have declared war against [${otherCiv.civName}]!",
NotificationCategory.Diplomacy, otherCiv.civName, NotificationIcon.War, civInfo.civName, allyCiv.civName)
civInfo.addNotification("[${civInfo.civName}] and [${allyCiv.civName}] have declared war against us!",
NotificationCategory.Diplomacy, otherCiv.civName, NotificationIcon.War, allyCiv.civName, civInfo.civName)
diplomacyManager.getCommonKnownCivsWithSpectators().filterNot { it == allyCiv }.forEach {
it.addNotification("[${civInfo.civName}] and [${allyCiv.civName}] have declared war against [${otherCiv.civName}]!",
NotificationCategory.Diplomacy, otherCiv.civName, NotificationIcon.War, allyCiv.civName, civInfo.civName)
}
}
}
}
@ -135,7 +157,7 @@ object DeclareWar {
diplomacyManager.updateHasOpenBorders()
diplomacyManager.removeModifier(DiplomaticModifiers.YearsOfPeace)
diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace, 10)/// AI won't propose peace for 10 turns
diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace, 3)/// AI won't propose peace for 3 turns
diplomacyManager.setFlag(DiplomacyFlags.DeclaredWar, 10) // AI won't agree to trade for 10 turns
diplomacyManager.removeFlag(DiplomacyFlags.BorderConflict)
}
@ -299,8 +321,10 @@ enum class WarType {
CityStateAllianceWar,
/** A civilization has joined a war through it's defensive pact. */
DefensivePactWar,
/** A civilization has joined a war through a trade. Has the same diplomatic repercussions as direct war.*/
/** A civilization has joined a war through a trade.*/
JoinWar,
/** Two civilizations are starting a war through a trade. */
TeamWar,
}
/**

View File

@ -46,6 +46,7 @@ enum class DiplomacyFlags {
DeclinedDeclarationOfFriendship,
DefensivePact,
DeclinedDefensivePact,
DeclinedJoinWarOffer,
ResearchAgreement,
BorderConflict,
SettledCitiesNearUs,

View File

@ -135,9 +135,8 @@ object DiplomacyTurnManager {
private fun DiplomacyManager.nextTurnFlags() {
loop@ for (flag in flagsCountdown.keys.toList()) {
// No need to decrement negative countdown flags: they do not expire
if (flagsCountdown[flag]!! > 0)
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
// We want negative flags to keep on going negative to keep track of time
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
// If we have uniques that make city states grant military units faster when at war with a common enemy, add higher numbers to this flag
if (flag == DiplomacyFlags.ProvideMilitaryUnit.name && civInfo.isMajorCiv() && otherCiv().isCityState() &&
@ -207,6 +206,10 @@ object DiplomacyTurnManager {
}
}
flagsCountdown.remove(flag)
} else if (flag == DiplomacyFlags.WaryOf.name && flagsCountdown[flag]!! < -10) {
// Used in DeclareWarTargetAutomation.declarePlannedWar to count the number of turns preparing
// If we have been preparing for over 10 turns then cancel our attack plan
flagsCountdown.remove(flag)
}
}

View File

@ -1,8 +1,10 @@
package com.unciv.logic.civilization.managers
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.ui.screens.victoryscreen.RankingType
/**
* Handles optimised operations related to finding threats or allies in an area.
@ -169,6 +171,15 @@ class ThreatManager(val civInfo: Civilization) {
return false
}
/** @return a sequence of pairs of cities, the first city is our city and the second city is a nearby city that is not from our civ. */
fun getNeighboringCitiesOfOtherCivs(): Sequence<Pair<City,City>> = civInfo.cities.flatMap {
ourCity -> ourCity.neighboringCities.filter { it.civ != civInfo }.map { Pair(ourCity, it) }
}.asSequence()
fun getNeighboringCivilizations(): Set<Civilization> = civInfo.cities.flatMap { it.neighboringCities }.filter { it.civ != civInfo && civInfo.knows(it.civ) }.map { it.civ }.toSet()
fun getCombinedForceOfWarringCivs(): Int = civInfo.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }
fun clear() {
distanceToClosestEnemyTiles.clear()
}

View File

@ -1,5 +1,6 @@
package com.unciv.logic.map
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.RoadStatus
import com.unciv.logic.map.tile.Tile
@ -83,7 +84,7 @@ object MapPathing {
while (true) {
if (astar.hasEnded()) {
// We failed to find a path
Log.debug("getRoadPath failed at AStar search size ${astar.size()}")
Log.debug("getPath failed at AStar search size ${astar.size()}")
return null
}
if (!astar.hasReachedTile(endTile)) {
@ -97,4 +98,35 @@ object MapPathing {
}
}
/**
* Gets the connection to the end tile. This does not take into account tile movement costs.
* Takes in a civilization instead of a specific unit.
*/
fun getConnection(civ: Civilization,
startTile: Tile,
endTile: Tile,
predicate: (Civilization, Tile) -> Boolean
): List<Tile>? {
val astar = AStar(
startTile,
{ tile -> predicate(civ, tile) },
{ from, to -> 1f },
{ from, to -> from.aerialDistanceTo(to).toFloat() }
)
while (true) {
if (astar.hasEnded()) {
// We failed to find a path
Log.debug("getConnection failed at AStar search size ${astar.size()}")
return null
}
if (!astar.hasReachedTile(endTile)) {
astar.nextStep()
continue
}
// Found a path.
return astar.getPathTo(endTile)
.toList()
.reversed()
}
}
}

View File

@ -56,7 +56,7 @@ class TradeRequest : IsPartOfGameInfoSerialization {
val requestingCivDiploManager = requestingCivInfo.getDiplomacyManager(decliningCiv)
// the numbers of the flags (20,5) are the amount of turns to wait until offering again
if (trade.ourOffers.all { it.type == TradeType.Luxury_Resource }
&& trade.theirOffers.all { it.type==TradeType.Luxury_Resource })
&& trade.theirOffers.all { it.type == TradeType.Luxury_Resource })
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedLuxExchange,20)
if (trade.ourOffers.any { it.name == Constants.researchAgreement })
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedResearchAgreement,20)
@ -64,6 +64,10 @@ class TradeRequest : IsPartOfGameInfoSerialization {
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedDefensivePact,20)
if (trade.ourOffers.any { it.name == Constants.openBorders })
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedOpenBorders, if (decliningCiv.isAI()) 10 else 20)
if (trade.theirOffers.any { it.type == TradeType.WarDeclaration })
requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedJoinWarOffer, if (decliningCiv.isAI()) 10 else 20)
if (trade.ourOffers.any { it.type == TradeType.WarDeclaration })
requestingCivDiploManager.otherCivDiplomacy().setFlag(DiplomacyFlags.DeclinedJoinWarOffer, if (decliningCiv.isAI()) 10 else 20)
if (trade.isPeaceTreaty()) requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedPeace, 5)

View File

@ -1,9 +1,9 @@
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.DiplomacyAutomation
import com.unciv.logic.automation.civilization.MotivationToAttackAutomation
import com.unciv.logic.automation.civilization.DeclareWarPlanEvaluator
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
@ -54,7 +54,7 @@ class TradeEvaluation {
TradeType.Strategic_Resource -> hasResource(tradeOffer)
TradeType.Technology -> true
TradeType.Introduction -> !tradePartner.knows(tradeOffer.name) // You can't introduce them to someone they already know!
TradeType.WarDeclaration -> true
TradeType.WarDeclaration -> !offerer.isAtWarWith(offerer.gameInfo.getCivilization(tradeOffer.name))
TradeType.City -> offerer.cities.any { it.id == tradeOffer.name }
}
}
@ -72,9 +72,9 @@ class TradeEvaluation {
val sumOfTheirOffers = trade.theirOffers.asSequence()
.filter { it.type != TradeType.Treaty } // since treaties should only be evaluated once for 2 sides
.map { evaluateBuyCostWithInflation(it, evaluator, tradePartner) }.sum()
.map { evaluateBuyCostWithInflation(it, evaluator, tradePartner, trade) }.sum()
var sumOfOurOffers = trade.ourOffers.sumOf { evaluateSellCostWithInflation(it, evaluator, tradePartner) }
var sumOfOurOffers = trade.ourOffers.sumOf { evaluateSellCostWithInflation(it, evaluator, tradePartner, trade) }
val relationshipLevel = evaluator.getDiplomacyManager(tradePartner).relationshipIgnoreAfraid()
// If we're making a peace treaty, don't try to up the bargain for people you don't like.
@ -94,13 +94,16 @@ class TradeEvaluation {
return sumOfTheirOffers - sumOfOurOffers
}
fun evaluateBuyCostWithInflation(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int {
fun evaluateBuyCostWithInflation(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization, trade: Trade): Int {
if (offer.type != TradeType.Gold && offer.type != TradeType.Gold_Per_Turn)
return (evaluateBuyCost(offer, civInfo, tradePartner) / getGoldInflation(civInfo)).toInt()
return evaluateBuyCost(offer, civInfo, tradePartner)
return (evaluateBuyCost(offer, civInfo, tradePartner, trade) / getGoldInflation(civInfo)).toInt()
return evaluateBuyCost(offer, civInfo, tradePartner, trade)
}
private fun evaluateBuyCost(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int {
/**
* How much their offer is worth to us in gold
*/
private fun evaluateBuyCost(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization, trade: Trade): Int {
when (offer.type) {
TradeType.Gold -> return offer.amount
// GPT loses 1% of value for each 'future' turn, meaning: gold now is more valuable than gold in the future
@ -154,15 +157,16 @@ class TradeEvaluation {
TradeType.Introduction -> return introductionValue(civInfo.gameInfo.ruleset)
TradeType.WarDeclaration -> {
val civToDeclareWarOn = civInfo.gameInfo.getCivilization(offer.name)
val threatToThem = Automation.threatAssessment(civInfo, civToDeclareWarOn)
return if (!civInfo.isAtWarWith(civToDeclareWarOn)) 0 // why should we pay you to go fight someone...?
else when (threatToThem) {
ThreatLevel.VeryLow -> 0
ThreatLevel.Low -> 0
ThreatLevel.Medium -> 100
ThreatLevel.High -> 500
ThreatLevel.VeryHigh -> 1000
if (trade.theirOffers.any { it.type == TradeType.WarDeclaration && it.name == offer.name }
&& trade.ourOffers.any {it.type == TradeType.WarDeclaration && it.name == offer.name}) {
// Team war is handled in the selling method
return 0
} else if (civInfo.isAtWarWith(civToDeclareWarOn)) {
// We shouldn't require them to pay us to join our war (no negative values)
return (20 * DeclareWarPlanEvaluator.evaluateJoinOurWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).coerceAtLeast(0)
} else {
// Why should we pay you to go fight someone else?
return 0
}
}
TradeType.City -> {
@ -208,13 +212,13 @@ class TradeEvaluation {
}
fun evaluateSellCostWithInflation(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int {
fun evaluateSellCostWithInflation(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization, trade: Trade): Int {
if (offer.type != TradeType.Gold && offer.type != TradeType.Gold_Per_Turn)
return (evaluateSellCost(offer, civInfo, tradePartner) / getGoldInflation(civInfo)).toInt()
return evaluateSellCost(offer, civInfo, tradePartner)
return (evaluateSellCost(offer, civInfo, tradePartner, trade) / getGoldInflation(civInfo)).toInt()
return evaluateSellCost(offer, civInfo, tradePartner, trade)
}
private fun evaluateSellCost(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int {
private fun evaluateSellCost(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization, trade: Trade): Int {
when (offer.type) {
TradeType.Gold -> return offer.amount
TradeType.Gold_Per_Turn -> return offer.amount * offer.duration
@ -273,13 +277,17 @@ class TradeEvaluation {
TradeType.Introduction -> return introductionValue(civInfo.gameInfo.ruleset)
TradeType.WarDeclaration -> {
val civToDeclareWarOn = civInfo.gameInfo.getCivilization(offer.name)
return when (Automation.threatAssessment(civInfo, civToDeclareWarOn)) {
ThreatLevel.VeryLow -> 100
ThreatLevel.Low -> 250
ThreatLevel.Medium -> 500
ThreatLevel.High -> 1000
ThreatLevel.VeryHigh -> 10000 // no way boyo
if (trade.theirOffers.any { it.type == TradeType.WarDeclaration && it.name == offer.name }
&& trade.ourOffers.any {it.type == TradeType.WarDeclaration && it.name == offer.name}) {
// Only accept if the war will benefit us, or if they pay us enough
// We shouldn't want to pay them for us to declare war (no negative values)
return (-20 * DeclareWarPlanEvaluator.evaluateTeamWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).coerceAtLeast(0)
} else if (tradePartner.isAtWarWith(civToDeclareWarOn)) {
// We might want them to pay us to join them in war (no negative values)
return (-20 * DeclareWarPlanEvaluator.evaluateJoinWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).coerceAtLeast(0)
} else {
// We might want them to pay us to declare war (no negative values)
return (-25 * DeclareWarPlanEvaluator.evaluateDeclareWarPlan(civInfo, civToDeclareWarOn, null)).coerceAtLeast(0)
}
}
@ -319,7 +327,7 @@ class TradeEvaluation {
// Goes from 1 at GPT = 0 to .834 at GPT = 100, .296 at GPT = 1000 and 0.116 at GPT = 10000
// The current value of gold will never go below 10% or the .1f that it is set to
// So this does not scale off to infinity
return modifier / (goldPerTurn.pow(1.2).coerceAtLeast(0.0) + (1.11f * modifier)) + .1f
return modifier / (goldPerTurn.coerceAtLeast(1.0).pow(1.2) + (1.11 * modifier)) + .1
}
/** This code returns a positive value if the city is significantly far away from the capital
@ -333,20 +341,21 @@ class TradeEvaluation {
return (distanceToCapital - 500) * civInfo.getEraNumber()
}
fun evaluatePeaceCostForThem(ourCivilization: Civilization, otherCivilization: Civilization): Int {
val ourCombatStrength = ourCivilization.getStatForRanking(RankingType.Force)
val theirCombatStrength = otherCivilization.getStatForRanking(RankingType.Force)
fun evaluatePeaceCostForThem(ourCiv: Civilization, otherCiv: Civilization): Int {
val ourCombatStrength = ourCiv.getStatForRanking(RankingType.Force)
val theirCombatStrength = otherCiv.getStatForRanking(RankingType.Force)
if (ourCombatStrength * 1.5f >= theirCombatStrength && theirCombatStrength * 1.5f >= ourCombatStrength)
return 0 // we're roughly equal, there's no huge power imbalance
if (ourCombatStrength > theirCombatStrength) {
if (MotivationToAttackAutomation.hasAtLeastMotivationToAttack(ourCiv, otherCiv, 0) <= 0) return 0
val absoluteAdvantage = ourCombatStrength - theirCombatStrength
val percentageAdvantage = absoluteAdvantage / theirCombatStrength.toFloat()
// We don't add the same constraint here. We should not make peace easily if we're
// heavily advantaged.
val totalAdvantage = (absoluteAdvantage * percentageAdvantage).toInt() * 10
val totalAdvantage = (absoluteAdvantage * percentageAdvantage).toInt()
if(totalAdvantage < 0) //May be a negative number if strength disparity is such that it leads to integer overflow
return 10000 //in that rare case, the AI would accept peace against a defeated foe.
return totalAdvantage
return (totalAdvantage / (getGoldInflation(otherCiv) * 2)).toInt()
} else {
// This results in huge values for large power imbalances. However, we should not give
// up everything just because there is a big power imbalance. There's a better chance to
@ -371,7 +380,7 @@ class TradeEvaluation {
// (stats ~30 each)
val absoluteAdvantage = theirCombatStrength - ourCombatStrength
val percentageAdvantage = absoluteAdvantage / ourCombatStrength.toFloat()
return -min((absoluteAdvantage * percentageAdvantage).toInt() * 10, 100000)
return -(absoluteAdvantage * percentageAdvantage / (getGoldInflation(ourCiv) * 2)).coerceAtMost(10000.0).toInt()
}
}

View File

@ -139,7 +139,12 @@ class TradeLogic(val ourCivilization: Civilization, val otherCivilization: Civil
TradeType.Introduction -> to.diplomacyFunctions.makeCivilizationsMeet(to.gameInfo.getCivilization(offer.name))
TradeType.WarDeclaration -> {
val nameOfCivToDeclareWarOn = offer.name
from.getDiplomacyManager(nameOfCivToDeclareWarOn).declareWar(DeclareWarReason(WarType.JoinWar, to))
val warType = if (currentTrade.theirOffers.any { it.type == TradeType.WarDeclaration && it.name == nameOfCivToDeclareWarOn }
&& currentTrade.ourOffers.any {it.type == TradeType.WarDeclaration && it.name == nameOfCivToDeclareWarOn})
WarType.TeamWar
else WarType.JoinWar
from.getDiplomacyManager(nameOfCivToDeclareWarOn).declareWar(DeclareWarReason(warType, to))
}
else -> {}
}