Counteroffer mechanic, updated trade valuations (#5702)

* counteroffer mechanic

* string template

* string template

* AI always counteroffers if able
This commit is contained in:
SimonCeder 2021-11-23 12:00:30 +01:00 committed by GitHub
parent 694e862944
commit 2dd4415977
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 38 deletions

View File

@ -610,6 +610,7 @@ You and [name] are no longer allies! =
[cityName] has been connected to your capital! = [cityName] has been connected to your capital! =
[cityName] has been disconnected from your capital! = [cityName] has been disconnected from your capital! =
[civName] has accepted your trade request = [civName] has accepted your trade request =
[civName] has made a counteroffer to your trade request =
[civName] has denied your trade request = [civName] has denied your trade request =
[tradeOffer] from [otherCivName] has ended = [tradeOffer] from [otherCivName] has ended =
[tradeOffer] to [otherCivName] has ended = [tradeOffer] to [otherCivName] has ended =

View File

@ -87,12 +87,114 @@ object NextTurnAutomation {
tradeLogic.acceptTrade() tradeLogic.acceptTrade()
otherCiv.addNotification("[${civInfo.civName}] has accepted your trade request", NotificationIcon.Trade, civInfo.civName) otherCiv.addNotification("[${civInfo.civName}] has accepted your trade request", NotificationIcon.Trade, civInfo.civName)
} else { } 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() 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) { private fun respondToPopupAlerts(civInfo: CivilizationInfo) {
for (popupAlert in civInfo.popupAlerts) { for (popupAlert in civInfo.popupAlerts) {
if (popupAlert.type == AlertType.DemandToStopSettlingCitiesNear) { // we're called upon to make a decision if (popupAlert.type == AlertType.DemandToStopSettlingCitiesNear) { // we're called upon to make a decision

View File

@ -262,7 +262,7 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
statMap["Traded Luxuries"] = statMap["Traded Luxuries"] =
luxuriesAllOfWhichAreTradedAway.count() * happinessPerUniqueLuxury * 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 .sumOf { it.params[0].toInt() } / 100f
for (city in civInfo.cities) { for (city in civInfo.cities) {

View File

@ -9,6 +9,8 @@ import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.ResourceType 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 com.unciv.ui.victoryscreen.RankingType
import kotlin.math.min import kotlin.math.min
import kotlin.math.sqrt import kotlin.math.sqrt
@ -54,6 +56,10 @@ class TradeEvaluation {
} }
fun isTradeAcceptable(trade: Trade, evaluator: CivilizationInfo, tradePartner: CivilizationInfo): Boolean { 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() val sumOfTheirOffers = trade.theirOffers.asSequence()
.filter { it.type != TradeType.Treaty } // since treaties should only be evaluated once for 2 sides .filter { it.type != TradeType.Treaty } // since treaties should only be evaluated once for 2 sides
.map { evaluateBuyCost(it, evaluator, tradePartner) }.sum() .map { evaluateBuyCost(it, evaluator, tradePartner) }.sum()
@ -68,10 +74,10 @@ class TradeEvaluation {
else if (relationshipLevel == RelationshipLevel.Unforgivable) sumOfOurOffers *= 2 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) { when (offer.type) {
TradeType.Gold -> return offer.amount TradeType.Gold -> return offer.amount
TradeType.Gold_Per_Turn -> return offer.amount * offer.duration TradeType.Gold_Per_Turn -> return offer.amount * offer.duration
@ -85,40 +91,19 @@ class TradeEvaluation {
} }
TradeType.Luxury_Resource -> { TradeType.Luxury_Resource -> {
val weAreMissingThisLux = !civInfo.hasResource(offer.name) // first off - do we want this for ourselves? 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)
val civsWhoWillTradeUsForTheLux = civInfo.diplomacy.values.map { it.civInfo } // secondly - should we buy this in order to resell it? civInfo.getHappiness() < 0 -> 450
.filter { it != tradePartner } civInfo.getHappiness() < 10 -> 350
.filter { !it.hasResource(offer.name) } //they don't have else -> 300 // Higher than corresponding sell cost since a trade is mutually beneficial!
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)
} }
} } else
var numberOfCivsWhoWouldTradeUsForTheLux = civsWithLuxToTrade.count() 0
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
} }
TradeType.Strategic_Resource -> { TradeType.Strategic_Resource -> {
val resources = civInfo.getCivResourcesByName() 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. if (amountWillingToBuy <= 0) return 0 // we already have enough.
val amountToBuyInOffer = min(amountWillingToBuy, offer.amount) val amountToBuyInOffer = min(amountWillingToBuy, offer.amount)
@ -131,7 +116,7 @@ class TradeEvaluation {
return 50 * amountToBuyInOffer return 50 * amountToBuyInOffer
} }
TradeType.Technology -> TradeType.Technology -> // Currently unused
return (sqrt(civInfo.gameInfo.ruleSet.technologies[offer.name]!!.cost.toDouble()) return (sqrt(civInfo.gameInfo.ruleSet.technologies[offer.name]!!.cost.toDouble())
* civInfo.gameInfo.gameParameters.gameSpeed.modifier).toInt() * 20 * civInfo.gameInfo.gameParameters.gameSpeed.modifier).toInt() * 20
TradeType.Introduction -> return introductionValue(civInfo.gameInfo.ruleSet) TradeType.Introduction -> return introductionValue(civInfo.gameInfo.ruleSet)
@ -173,7 +158,7 @@ class TradeEvaluation {
return 0 return 0
} }
private fun evaluateSellCost(offer: TradeOffer, civInfo: CivilizationInfo, tradePartner: CivilizationInfo): Int { fun evaluateSellCost(offer: TradeOffer, civInfo: CivilizationInfo, tradePartner: CivilizationInfo): Int {
when (offer.type) { when (offer.type) {
TradeType.Gold -> return offer.amount TradeType.Gold -> return offer.amount
TradeType.Gold_Per_Turn -> return offer.amount * offer.duration TradeType.Gold_Per_Turn -> return offer.amount * offer.duration
@ -186,9 +171,12 @@ class TradeEvaluation {
} }
} }
TradeType.Luxury_Resource -> { TradeType.Luxury_Resource -> {
return if (civInfo.getCivResourcesByName()[offer.name]!! > 1) return when {
250 // fair price civInfo.getCivResourcesByName()[offer.name]!! > 1 -> 250 // fair price
else 500 // you want to take away our last lux of this type?! 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 -> { TradeType.Strategic_Resource -> {
if (civInfo.gameInfo.spaceResources.contains(offer.name) && if (civInfo.gameInfo.spaceResources.contains(offer.name) &&

View File

@ -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), 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), 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 ///////////////////////////////////////// ///////////////////////////////////////// CONSTRUCTION UNIQUES /////////////////////////////////////////