mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-28 14:24:43 -04:00
Counteroffer mechanic, updated trade valuations (#5702)
* counteroffer mechanic * string template * string template * AI always counteroffers if able
This commit is contained in:
parent
694e862944
commit
2dd4415977
@ -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 =
|
||||||
|
@ -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 {
|
||||||
|
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)
|
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
|
||||||
|
@ -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) {
|
||||||
|
@ -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) &&
|
||||||
|
@ -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 /////////////////////////////////////////
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user