Giving the AI good trades is stored as credit (#11326)

* AI Civs now are happy for good trades

* Each gift point is worth 100 gold without inflation

* Gifts can now be used as credit for future trades

* Fixed giftAmount conversions

* Fixed get inflation returning NAN when gpt is negative

* diplomatic gifts are now rounded when checking trade acceptability

* Changed gold gift scaling to account for relationship level

* Fixed percent based value reduction

* Added gold gifting functions to DiplomacyManager

* Added tests

* Declaring war removes gold gifts

* Reversed trade evaluation

* Added more tests for trading

* Fixed who the gifts are given to

* Added more comments

* Added more tests and fixed stuff

* Gifting does not occur with trade treaties

* Renamed handleGoldGifted to GiftGold

* Added two more tests

* Improved comments

* Liberating a civ no longer gives positive relations from open borders
This commit is contained in:
Oskar Niesen
2024-06-14 09:38:54 -05:00
committed by GitHub
parent 4257f9dbc7
commit 728713dc3e
8 changed files with 226 additions and 19 deletions

View File

@ -55,7 +55,7 @@ object TradeAutomation {
if (otherCiv.playerType == PlayerType.AI)
return null
val evaluation = TradeEvaluation()
var deltaInOurFavor = evaluation.getTradeAcceptability(tradeRequest.trade, civInfo, otherCiv)
var deltaInOurFavor = evaluation.getTradeAcceptability(tradeRequest.trade, civInfo, otherCiv, true)
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)

View File

@ -225,7 +225,7 @@ class CityConquestFunctions(val city: City) {
.addModifier(DiplomaticModifiers.CapturedOurCities, respectForLiberatingOurCity)
val openBordersTrade = TradeLogic(foundingCiv, conqueringCiv)
openBordersTrade.currentTrade.ourOffers.add(TradeOffer(Constants.openBorders, TradeType.Agreement))
openBordersTrade.acceptTrade()
openBordersTrade.acceptTrade(false)
} else {
//Liberating a city state gives a large amount of influence, and peace
foundingCiv.getDiplomacyManager(conqueringCiv).setInfluence(90f)
@ -233,7 +233,7 @@ class CityConquestFunctions(val city: City) {
val tradeLogic = TradeLogic(foundingCiv, conqueringCiv)
tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.peaceTreaty, TradeType.Treaty))
tradeLogic.currentTrade.theirOffers.add(TradeOffer(Constants.peaceTreaty, TradeType.Treaty))
tradeLogic.acceptTrade()
tradeLogic.acceptTrade(false)
}
}

View File

@ -237,6 +237,10 @@ object DeclareWar {
otherCivDiplomacy.totalOfScienceDuringRA = 0
}
otherCivDiplomacy.removeFlag(DiplomacyFlags.ResearchAgreement)
// The other civ should keep any gifts we gave them
// But we should not nesessarily take away their gifts
otherCivDiplomacy.removeModifier(DiplomaticModifiers.GaveUsGifts)
}
/**

View File

@ -7,6 +7,7 @@ import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.trade.Trade
import com.unciv.logic.trade.TradeEvaluation
import com.unciv.logic.trade.TradeOffer
import com.unciv.logic.trade.TradeType
import com.unciv.models.ruleset.tile.ResourceSupplyList
@ -674,5 +675,41 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
NotificationCategory.Diplomacy, civInfo.civName)
}
/**
* Resolves adding gifts with negative gold values.
* Prioritises reducing gifts given to the other civ before increasing our gift value.
* Does not take the gold from either civ's stockpile
* @param gold the amount of gold without inflation, can be negative
*/
fun giftGold(gold: Int) {
val otherGold = otherCivDiplomacy().getGoldGifts()
if (otherGold > gold) {
otherCivDiplomacy().recieveGoldGifts(-gold)
} else {
otherCivDiplomacy().removeModifier(DiplomaticModifiers.GaveUsGifts)
recieveGoldGifts(gold - otherGold)
}
}
/**
* Adds a gift from the other civilization of the value of [gold] that will deteriate over time.
* Does not take into account how much gold we have given to the other civ. Use [giftGold] for that.
* Does not take the gold from either civ's stockpile.
* @param gold the amount of gold without inflation, cannot be negative
*/
fun recieveGoldGifts(gold: Int) {
val diplomaticValueOfTrade = (gold * TradeEvaluation().getGoldInflation(civInfo)) / (civInfo.gameInfo.speed.goldGiftModifier * 100)
addModifier(DiplomaticModifiers.GaveUsGifts, diplomaticValueOfTrade.toFloat())
}
/**
* @return the total value of the gold gifts the other civilization has given us
*/
fun getGoldGifts(): Int {
// The inverse of howe we calculate GaveUsGifts in TradeLogic.acceptTrade gives us how much gold it is worth
val giftAmount = getModifier(DiplomaticModifiers.GaveUsGifts)
return ((giftAmount * civInfo.gameInfo.speed.goldGiftModifier * 100) / TradeEvaluation().getGoldInflation(civInfo)).toInt()
}
//endregion
}

View File

@ -9,6 +9,7 @@ import com.unciv.logic.trade.TradeOffer
import com.unciv.logic.trade.TradeType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.toPercent
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
@ -291,7 +292,25 @@ object DiplomacyTurnManager {
// Positives
revertToZero(DiplomaticModifiers.GaveUsUnits, 1 / 4f)
revertToZero(DiplomaticModifiers.LiberatedCity, 1 / 8f)
revertToZero(DiplomaticModifiers.GaveUsGifts, 1 / 4f)
if (hasModifier(DiplomaticModifiers.GaveUsGifts)) {
val giftLoss = when {
relationshipLevel() == RelationshipLevel.Ally -> .5f
relationshipLevel() == RelationshipLevel.Friend -> 1f
relationshipLevel() == RelationshipLevel.Favorable -> 1.5f
relationshipLevel() == RelationshipLevel.Competitor -> 5f
relationshipLevel() == RelationshipLevel.Enemy -> 7.5f
relationshipLevel() == RelationshipLevel.Unforgivable -> 10f
else -> 2f // Neutral
}
// We should subtract a certain amount from this balanced each turn
// Assuming neutral relations we will subtract the higher of either:
// 2% of the total amount or
// roughly 40 gold per turn (a value of ~.4 without inflation)
// This ensures that the amount can be reduced to zero but scales with larger numbers
val amountLost = (getModifier(DiplomaticModifiers.GaveUsGifts).absoluteValue * giftLoss / 100)
.coerceAtLeast(giftLoss / 5)
revertToZero(DiplomaticModifiers.GaveUsGifts, amountLost) // Roughly worth 20 GPT without inflation
}
setFriendshipBasedModifier()
@ -331,7 +350,8 @@ object DiplomacyTurnManager {
private fun DiplomacyManager.revertToZero(modifier: DiplomaticModifiers, amount: Float) {
if (!hasModifier(modifier)) return
val currentAmount = getModifier(modifier)
if (currentAmount > 0) addModifier(modifier, -amount)
if (amount >= currentAmount.absoluteValue) diplomaticModifiers.remove(modifier.name)
else if (currentAmount > 0) addModifier(modifier, -amount)
else addModifier(modifier, amount)
}

View File

@ -60,10 +60,10 @@ class TradeEvaluation {
}
fun isTradeAcceptable(trade: Trade, evaluator: Civilization, tradePartner: Civilization): Boolean {
return getTradeAcceptability(trade, evaluator, tradePartner) >= 0
return getTradeAcceptability(trade, evaluator, tradePartner, true) >= 0
}
fun getTradeAcceptability(trade: Trade, evaluator: Civilization, tradePartner: Civilization): Int {
fun getTradeAcceptability(trade: Trade, evaluator: Civilization, tradePartner: Civilization, includeDiplomaticGifts:Boolean = false): Int {
val citiesAskedToSurrender = trade.ourOffers.count { it.type == TradeType.City }
val maxCitiesToSurrender = ceil(evaluator.cities.size.toFloat() / 5).toInt()
if (citiesAskedToSurrender > maxCitiesToSurrender) {
@ -90,8 +90,8 @@ class TradeEvaluation {
return Int.MIN_VALUE
}
}
return sumOfTheirOffers - sumOfOurOffers
val diplomaticGifts: Int = if (includeDiplomaticGifts) evaluator.getDiplomacyManager(tradePartner).getGoldGifts() else 0
return sumOfTheirOffers - sumOfOurOffers + diplomaticGifts
}
fun evaluateBuyCostWithInflation(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization, trade: Trade): Int {
@ -320,7 +320,7 @@ class TradeEvaluation {
* This returns how much one gold is worth now in comparison to starting out the game
* Gold is worth less as the civilization has a higher income
*/
private fun getGoldInflation(civInfo: Civilization): Double {
fun getGoldInflation(civInfo: Civilization): Double {
val modifier = 1000.0
val goldPerTurn = civInfo.stats.statsForNextTurn.gold.toDouble()
// To visualise the function, plug this into a 2d graphing calculator \frac{1000}{x^{1.2}+1.11*1000}

View File

@ -4,10 +4,8 @@ import com.unciv.Constants
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.diplomacy.CityStateFunctions
import com.unciv.logic.civilization.diplomacy.DeclareWarReason
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.civilization.diplomacy.WarType
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.UniqueType
@ -81,12 +79,15 @@ class TradeLogic(val ourCivilization: Civilization, val otherCivilization: Civil
return offers
}
fun acceptTrade() {
ourCivilization.getDiplomacyManager(otherCivilization).apply {
fun acceptTrade(applyGifts: Boolean = true) {
val ourDiploManager = ourCivilization.getDiplomacyManager(otherCivilization)
val theirDiploManger = otherCivilization.getDiplomacyManager(ourCivilization)
ourDiploManager.apply {
trades.add(currentTrade)
updateHasOpenBorders()
}
otherCivilization.getDiplomacyManager(ourCivilization).apply {
theirDiploManger.apply {
trades.add(currentTrade.reverse())
updateHasOpenBorders()
}
@ -150,10 +151,17 @@ class TradeLogic(val ourCivilization: Civilization, val otherCivilization: Civil
}
}
if (currentTrade.ourOffers.isEmpty()) { // Must evaluate before moving, or else cities have already moved and we get an exception
val goldValueOfTrade = TradeEvaluation().getTradeAcceptability(currentTrade, ourCivilization, otherCivilization)
val diplomaticValueOfTrade = CityStateFunctions(ourCivilization).influenceGainedByGift(otherCivilization, goldValueOfTrade) / 10
ourCivilization.getDiplomacyManager(otherCivilization).addModifier(DiplomaticModifiers.GaveUsGifts, diplomaticValueOfTrade.toFloat())
// We shouldn't evaluate trades if we are doing a peace treaty
// Their value can be so big it throws the gift system out of wack
if (applyGifts && !currentTrade.ourOffers.any { it.name == Constants.peaceTreaty }) {
// Must evaluate before moving, or else cities have already moved and we get an exception
val ourGoldValueOfTrade = TradeEvaluation().getTradeAcceptability(currentTrade, ourCivilization, otherCivilization, includeDiplomaticGifts = false)
val theirGoldValueOfTrade = TradeEvaluation().getTradeAcceptability(currentTrade.reverse(), otherCivilization, ourCivilization, includeDiplomaticGifts = false)
if (ourGoldValueOfTrade > theirGoldValueOfTrade) {
ourDiploManager.giftGold(ourGoldValueOfTrade - theirGoldValueOfTrade.coerceAtLeast(0))
} else if (theirGoldValueOfTrade > ourGoldValueOfTrade) {
theirDiploManger.giftGold(theirGoldValueOfTrade - ourGoldValueOfTrade.coerceAtLeast(0))
}
}
// Transfer of cities needs to happen before peace treaty, to avoid our units teleporting out of areas that soon will be ours

View File

@ -0,0 +1,138 @@
package com.unciv.logic.civilization.diplomacy
import com.unciv.logic.civilization.diplomacy.DiplomacyTurnManager.nextTurn
import com.unciv.logic.trade.TradeEvaluation
import com.unciv.logic.trade.TradeLogic
import com.unciv.logic.trade.TradeOffer
import com.unciv.logic.trade.TradeType
import com.unciv.testing.GdxTestRunner
import com.unciv.testing.TestGame
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(GdxTestRunner::class)
class GoldGiftingTests {
private val testGame = TestGame()
private val a = testGame.addCiv()
private val b = testGame.addCiv()
lateinit var aDiplomacy: DiplomacyManager
lateinit var bDiplomacy: DiplomacyManager
@Before
fun setUp() {
a.diplomacyFunctions.makeCivilizationsMeet(b)
aDiplomacy = a.getDiplomacyManager(b)
bDiplomacy = b.getDiplomacyManager(a)
}
@Test
fun `Gold Gift Recieve Test` () {
assertEquals(0, aDiplomacy.getGoldGifts())
assertEquals(0, bDiplomacy.getGoldGifts())
aDiplomacy.recieveGoldGifts(10)
assertTrue(aDiplomacy.getGoldGifts() > 0)
assertEquals(0, bDiplomacy.getGoldGifts())
}
@Test
fun `Gold Gift Test` () {
assertEquals(0, aDiplomacy.getGoldGifts())
assertEquals(0, bDiplomacy.getGoldGifts())
aDiplomacy.giftGold(10)
assertTrue(aDiplomacy.getGoldGifts() > 0)
assertEquals(0, bDiplomacy.getGoldGifts())
}
@Test
fun `Gifted Gold Disapears` () {
aDiplomacy.recieveGoldGifts(10)
assertTrue(aDiplomacy.getGoldGifts() > 0)
val gold = aDiplomacy.getGoldGifts()
aDiplomacy.nextTurn()
val gold2 = aDiplomacy.getGoldGifts()
assertTrue(gold > gold2)
assertTrue(gold2 >= 0) // Gold should not be negative
// We don't actually test if the gift has completely run out
// since that may change in the future
}
@Test
fun `Gifted Gold is reduced less than 10 percent` () {
aDiplomacy.recieveGoldGifts(1000)
assertTrue(aDiplomacy.getGoldGifts() > 0)
val gold = aDiplomacy.getGoldGifts()
aDiplomacy.nextTurn()
val gold2 = aDiplomacy.getGoldGifts()
assertTrue(gold > gold2)
assertTrue(gold2 >= gold * .9) // We shoulden't loose more than 10% of the value in one turn
assertTrue(gold2 >= 0)
}
@Test
fun `Gold gifted is lost during war` () {
aDiplomacy.recieveGoldGifts(1000)
bDiplomacy.recieveGoldGifts(1000)
assertTrue(aDiplomacy.getGoldGifts() > 0)
assertTrue(bDiplomacy.getGoldGifts() > 0)
bDiplomacy.declareWar()
assertEquals(0, aDiplomacy.getGoldGifts())
assertTrue(bDiplomacy.getGoldGifts() > 0)
}
@Test
fun `Gifting gold reduces previous gifts taken` () {
aDiplomacy.giftGold(1000)
bDiplomacy.giftGold(500)
assertTrue(aDiplomacy.getGoldGifts() > 0)
assertTrue(aDiplomacy.getGoldGifts() < 1000)
assertTrue(bDiplomacy.getGoldGifts() == 0)
}
@Test
fun `Excess gold from a trade become a gift` () {
a.addGold(1000)
assertEquals(0, bDiplomacy.getGoldGifts())
val tradeOffer = TradeLogic(a,b)
tradeOffer.currentTrade.ourOffers.add(tradeOffer.ourAvailableOffers.first { it.type == TradeType.Gold })
assertTrue(TradeEvaluation().getTradeAcceptability(tradeOffer.currentTrade.reverse(), b,a,false) > 0)
tradeOffer.acceptTrade()
assertEquals(0, aDiplomacy.getGoldGifts())
assertTrue(bDiplomacy.getGoldGifts() > 0)
}
@Test
fun `Can ask for 90 percent of gold gift back again a turn later` () {
// Due to rounding, we aren't going to assume we can get 100% of the gold back
// Therefore we only test for 90%
a.addGold(1000)
val tradeOffer = TradeLogic(a,b)
tradeOffer.currentTrade.ourOffers.add(tradeOffer.ourAvailableOffers.first { it.type == TradeType.Gold })
tradeOffer.acceptTrade()
bDiplomacy.nextTurn()
val tradeOffer2 = TradeLogic(a,b)
tradeOffer2.currentTrade.theirOffers.add(TradeOffer("Gold", TradeType.Gold, 900))
assertTrue(TradeEvaluation().getTradeAcceptability(tradeOffer.currentTrade.reverse(), b,a,false) > 0)
tradeOffer2.acceptTrade()
assertTrue(bDiplomacy.getGoldGifts() >= 0) // Must not be negative
}
@Test
fun `Gold gifted impact trade acceptability`() {
a.addGold(1000)
val tradeOffer = TradeLogic(a,b)
tradeOffer.currentTrade.ourOffers.add(tradeOffer.ourAvailableOffers.first { it.type == TradeType.Gold })
assertTrue(TradeEvaluation().getTradeAcceptability(tradeOffer.currentTrade, b,a,true) < 0)
tradeOffer.acceptTrade()
val tradeOffer2 = TradeLogic(a,b)
assertTrue(TradeEvaluation().getTradeAcceptability(tradeOffer2.currentTrade.reverse(), b,a,true) > 0)
}
}