mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 07:17:50 +07:00
Counteroffer mechanic, updated trade valuations (#5702)
* counteroffer mechanic * string template * string template * AI always counteroffers if able
This commit is contained in:
@ -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 =
|
||||
|
@ -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<TradeOffer, Int>()
|
||||
val counterofferAsks = HashMap<TradeOffer, Int>()
|
||||
val counterofferGifts = ArrayList<TradeOffer>()
|
||||
|
||||
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<TradeOffer>()
|
||||
for (goldAsk in counterofferAsks.keys
|
||||
.filter { it.type == TradeType.Gold_Per_Turn || it.type == TradeType.Gold }
|
||||
.sortedByDescending { it.type.ordinal }) { // Do GPT first
|
||||
val valueOfOne = evaluation.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
|
||||
|
@ -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) {
|
||||
|
@ -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) &&
|
||||
|
@ -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 /////////////////////////////////////////
|
||||
|
||||
|
Reference in New Issue
Block a user