diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index d52b37fa90..bba0c57ff7 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -610,6 +610,7 @@ You and [name] are no longer allies! = [cityName] has been connected to your capital! = [cityName] has been disconnected from your capital! = [civName] has accepted your trade request = +[civName] has made a counteroffer to your trade request = [civName] has denied your trade request = [tradeOffer] from [otherCivName] has ended = [tradeOffer] to [otherCivName] has ended = diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt index 61a4628ea2..473ecac629 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt @@ -87,12 +87,114 @@ object NextTurnAutomation { tradeLogic.acceptTrade() otherCiv.addNotification("[${civInfo.civName}] has accepted your trade request", NotificationIcon.Trade, civInfo.civName) } else { - otherCiv.addNotification("[${civInfo.civName}] has denied your trade request", NotificationIcon.Trade, civInfo.civName) + val counteroffer = getCounteroffer(civInfo, tradeRequest) + if (counteroffer != null) { + otherCiv.addNotification("[${civInfo.civName}] has made a counteroffer to your trade request", NotificationIcon.Trade, civInfo.civName) + otherCiv.tradeRequests.add(counteroffer) + } else + otherCiv.addNotification("[${civInfo.civName}] has denied your trade request", NotificationIcon.Trade, civInfo.civName) } } 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: CivilizationInfo, tradeRequest: TradeRequest): TradeRequest? { + val otherCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv) + val evaluation = TradeEvaluation() + var delta = evaluation.getTradeAcceptability(tradeRequest.trade, civInfo, otherCiv) + if (delta < 0) delta = (delta * 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.reverse()) + + // What do they have that we would want??? + val potentialAsks = HashMap() + val counterofferAsks = HashMap() + val counterofferGifts = ArrayList() + + for (offer in tradeLogic.theirAvailableOffers) { + if (offer.type == TradeType.Gold && tradeRequest.trade.ourOffers.any { it.type == TradeType.Gold } || + offer.type == TradeType.Gold_Per_Turn && tradeRequest.trade.ourOffers.any { it.type == TradeType.Gold_Per_Turn }) + continue // Don't want to counteroffer straight gold for gold, that's silly + if (offer.amount == 0) + continue // For example resources gained by trade or CS + val value = evaluation.evaluateBuyCost(offer, civInfo, otherCiv) + if (value > 0) + potentialAsks[offer] = value + } + + while (potentialAsks.isNotEmpty() && delta < 0) { + // Keep adding their worst offer until we get above the threshold + val offerToAdd = potentialAsks.minByOrNull { it.value }!! + delta += offerToAdd.value + counterofferAsks[offerToAdd.key] = offerToAdd.value + potentialAsks.remove(offerToAdd.key) + } + if (delta < 0) + return null // We couldn't get a good enough deal + + // At this point we are sure to find a good counteroffer + while (delta > 0) { + // Now remove the best offer valued below delta until the deal is barely acceptable + val offerToRemove = counterofferAsks.filter { it.value <= delta }.maxByOrNull { it.value } + if (offerToRemove == null) + break // Nothing more can be removed, at least en bloc + delta -= 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.evaluateBuyCost( + TradeOffer(ask.name, ask.type, ask.amount - 1, ask.duration), + civInfo, otherCiv) ) { + ask.amount-- + } + } + + // Adjust any gold asked for + val toRemove = ArrayList() + for (goldAsk in counterofferAsks.keys + .filter { it.type == TradeType.Gold_Per_Turn || it.type == TradeType.Gold } + .sortedByDescending { it.type.ordinal }) { // Do GPT first + val valueOfOne = evaluation.evaluateBuyCost(TradeOffer(goldAsk.name, goldAsk.type, 1, goldAsk.duration), civInfo, otherCiv) + val amountCanBeRemoved = delta / valueOfOne + if (amountCanBeRemoved >= goldAsk.amount) { + delta -= counterofferAsks[goldAsk]!! + toRemove.add(goldAsk) + } else { + delta -= valueOfOne * amountCanBeRemoved + goldAsk.amount -= amountCanBeRemoved + } + } + + // If the delta is still very in our favor consider sweetening the pot with some gold + if (delta >= 100) { + delta = (delta * 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.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.evaluateSellCost(TradeOffer(ourGold.name, ourGold.type, 1, ourGold.duration), civInfo, otherCiv) + val amountToGive = min(delta / valueOfOne, ourGold.amount) + delta -= amountToGive * valueOfOne + counterofferGifts.add(TradeOffer(ourGold.name, ourGold.type, amountToGive, ourGold.duration)) + } + } + } + + tradeLogic.currentTrade.ourOffers.addAll(counterofferAsks.keys) + tradeLogic.currentTrade.theirOffers.addAll(counterofferGifts) + return TradeRequest(civInfo.civName, tradeLogic.currentTrade) + } + private fun respondToPopupAlerts(civInfo: CivilizationInfo) { for (popupAlert in civInfo.popupAlerts) { if (popupAlert.type == AlertType.DemandToStopSettlingCitiesNear) { // we're called upon to make a decision diff --git a/core/src/com/unciv/logic/civilization/CivInfoStats.kt b/core/src/com/unciv/logic/civilization/CivInfoStats.kt index 528a63f1a8..7f5b882942 100644 --- a/core/src/com/unciv/logic/civilization/CivInfoStats.kt +++ b/core/src/com/unciv/logic/civilization/CivInfoStats.kt @@ -262,7 +262,7 @@ class CivInfoStats(val civInfo: CivilizationInfo) { statMap["Traded Luxuries"] = luxuriesAllOfWhichAreTradedAway.count() * happinessPerUniqueLuxury * - civInfo.getMatchingUniques("Retain []% of the happiness from a luxury after the last copy has been traded away") + civInfo.getMatchingUniques(UniqueType.RetainHappinessFromLuxury) .sumOf { it.params[0].toInt() } / 100f for (city in civInfo.cities) { diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index 66e7c9fad6..b8b73124aa 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -9,6 +9,8 @@ import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.ui.utils.toPercent import com.unciv.ui.victoryscreen.RankingType import kotlin.math.min import kotlin.math.sqrt @@ -54,6 +56,10 @@ class TradeEvaluation { } fun isTradeAcceptable(trade: Trade, evaluator: CivilizationInfo, tradePartner: CivilizationInfo): Boolean { + return getTradeAcceptability(trade, evaluator, tradePartner) >= 0 + } + + fun getTradeAcceptability(trade: Trade, evaluator: CivilizationInfo, tradePartner: CivilizationInfo): Int { val sumOfTheirOffers = trade.theirOffers.asSequence() .filter { it.type != TradeType.Treaty } // since treaties should only be evaluated once for 2 sides .map { evaluateBuyCost(it, evaluator, tradePartner) }.sum() @@ -68,10 +74,10 @@ class TradeEvaluation { else if (relationshipLevel == RelationshipLevel.Unforgivable) sumOfOurOffers *= 2 } - return sumOfOurOffers <= sumOfTheirOffers + return sumOfTheirOffers - sumOfOurOffers } - private fun evaluateBuyCost(offer: TradeOffer, civInfo: CivilizationInfo, tradePartner: CivilizationInfo): Int { + fun evaluateBuyCost(offer: TradeOffer, civInfo: CivilizationInfo, tradePartner: CivilizationInfo): Int { when (offer.type) { TradeType.Gold -> return offer.amount TradeType.Gold_Per_Turn -> return offer.amount * offer.duration @@ -85,40 +91,19 @@ class TradeEvaluation { } TradeType.Luxury_Resource -> { - val weAreMissingThisLux = !civInfo.hasResource(offer.name) // first off - do we want this for ourselves? - - val civsWhoWillTradeUsForTheLux = civInfo.diplomacy.values.map { it.civInfo } // secondly - should we buy this in order to resell it? - .filter { it != tradePartner } - .filter { !it.hasResource(offer.name) } //they don't have - val ourResourceNames = civInfo.getCivResources().map { it.resource.name } - val civsWithLuxToTrade = civsWhoWillTradeUsForTheLux.filter { - // these are other civs who we could trade this lux away to, in order to get a different lux - it.getCivResources().any { - it.amount > 1 && it.resource.resourceType == ResourceType.Luxury //they have a lux we don't and will be willing to trade it - && !ourResourceNames.contains(it.resource.name) + return if(!civInfo.hasResource(offer.name)) { // we can't trade on resources, so we are only interested in 1 copy for ourselves + when { // We're a lot more interested in luxury if low on happiness (AI is never low on happiness though) + civInfo.getHappiness() < 0 -> 450 + civInfo.getHappiness() < 10 -> 350 + else -> 300 // Higher than corresponding sell cost since a trade is mutually beneficial! } - } - var numberOfCivsWhoWouldTradeUsForTheLux = civsWithLuxToTrade.count() - - var numberOfLuxesWeAreWillingToBuy = 0 - var cost = 0 - if (weAreMissingThisLux) { // for ourselves - numberOfLuxesWeAreWillingToBuy += 1 - cost += 250 - } - - while (numberOfLuxesWeAreWillingToBuy < offer.amount && numberOfCivsWhoWouldTradeUsForTheLux > 0) { - numberOfLuxesWeAreWillingToBuy += 1 // for reselling - cost += 50 - numberOfCivsWhoWouldTradeUsForTheLux -= 1 - } - - return cost + } else + 0 } TradeType.Strategic_Resource -> { val resources = civInfo.getCivResourcesByName() - val amountWillingToBuy = resources[offer.name]!! - 2 + val amountWillingToBuy = 2 - resources[offer.name]!! if (amountWillingToBuy <= 0) return 0 // we already have enough. val amountToBuyInOffer = min(amountWillingToBuy, offer.amount) @@ -131,7 +116,7 @@ class TradeEvaluation { return 50 * amountToBuyInOffer } - TradeType.Technology -> + TradeType.Technology -> // Currently unused return (sqrt(civInfo.gameInfo.ruleSet.technologies[offer.name]!!.cost.toDouble()) * civInfo.gameInfo.gameParameters.gameSpeed.modifier).toInt() * 20 TradeType.Introduction -> return introductionValue(civInfo.gameInfo.ruleSet) @@ -173,7 +158,7 @@ class TradeEvaluation { return 0 } - private fun evaluateSellCost(offer: TradeOffer, civInfo: CivilizationInfo, tradePartner: CivilizationInfo): Int { + fun evaluateSellCost(offer: TradeOffer, civInfo: CivilizationInfo, tradePartner: CivilizationInfo): Int { when (offer.type) { TradeType.Gold -> return offer.amount TradeType.Gold_Per_Turn -> return offer.amount * offer.duration @@ -186,9 +171,12 @@ class TradeEvaluation { } } TradeType.Luxury_Resource -> { - return if (civInfo.getCivResourcesByName()[offer.name]!! > 1) - 250 // fair price - else 500 // you want to take away our last lux of this type?! + return when { + civInfo.getCivResourcesByName()[offer.name]!! > 1 -> 250 // fair price + civInfo.hasUnique(UniqueType.RetainHappinessFromLuxury) -> // If we retain 50% happiness, value at 375 + 750 - (civInfo.getMatchingUniques(UniqueType.RetainHappinessFromLuxury).first().params[0].toPercent() * 250).toInt() + else -> 500 // you want to take away our last lux of this type?! + } } TradeType.Strategic_Resource -> { if (civInfo.gameInfo.spaceResources.contains(offer.name) && diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 481a09f72d..9ecaf05455 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -193,6 +193,8 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget, val flags: MayanGainGreatPerson("Receive a free Great Person at the end of every [comment] (every 394 years), after researching [tech]. Each bonus person can only be chosen once.", UniqueTarget.Nation), MayanCalendarDisplay("Once The Long Count activates, the year on the world screen displays as the traditional Mayan Long Count.", UniqueTarget.Nation), + RetainHappinessFromLuxury("Retain [amount]% of the happiness from a luxury after the last copy has been traded away", UniqueTarget.Nation), + ///////////////////////////////////////// CONSTRUCTION UNIQUES /////////////////////////////////////////