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
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 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 =

View File

@ -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

View File

@ -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) {

View File

@ -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) &&

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),
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 /////////////////////////////////////////