diff options
author | joffrey-bion <joffrey.bion@gmail.com> | 2020-11-28 00:21:43 +0100 |
---|---|---|
committer | joffrey-bion <joffrey.bion@gmail.com> | 2020-11-28 02:26:32 +0100 |
commit | b8a6afedb14a1d54d77df918dc1d5b53be11c44b (patch) | |
tree | 58143efb9ab9e91dc045dc5ddbb5aeca99ef3596 | |
parent | Add dialog to choose who to buy resources from (diff) | |
download | seven-wonders-b8a6afedb14a1d54d77df918dc1d5b53be11c44b.tar.gz seven-wonders-b8a6afedb14a1d54d77df918dc1d5b53be11c44b.tar.bz2 seven-wonders-b8a6afedb14a1d54d77df918dc1d5b53be11c44b.zip |
Make all transactions available
Sometimes the strategic move can be to spend more money on a different player, rather than less money on the wrong player.
We need to make these strategic moves available through the UI.
To make up for the explosion in combinations, we just have to get rid of the options that result in the same money for each neighbour.
As long as we give the same amounts, we don't care whether it's for wood or clay.
Related:
https://github.com/joffrey-bion/seven-wonders/issues/50
15 files changed, 298 insertions, 263 deletions
diff --git a/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt b/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt index e3d54314..48a96f08 100644 --- a/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt +++ b/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt @@ -70,14 +70,14 @@ private fun createPlayCardMove(turnInfo: PlayerTurnInfo): PlayerMove { val hand = turnInfo.hand ?: error("Cannot choose move, no hand in current turn info!") val wonderBuildability = turnInfo.wonderBuildability if (wonderBuildability.isBuildable) { - val transactions = wonderBuildability.cheapestTransactions.random() + val transactions = wonderBuildability.transactionsOptions.random() return PlayerMove(MoveType.UPGRADE_WONDER, hand.random().name, transactions) } val playableCard = hand .filter { it.playability.isPlayable } .randomOrNull() return if (playableCard != null) { - PlayerMove(MoveType.PLAY, playableCard.name, playableCard.playability.cheapestTransactions.random()) + PlayerMove(MoveType.PLAY, playableCard.name, playableCard.playability.transactionOptions.random()) } else { PlayerMove(MoveType.DISCARD, hand.random().name, noTransactions()) } diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt index 7b228d4d..14e6146a 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt @@ -2,8 +2,8 @@ package org.luxons.sevenwonders.model.cards import kotlinx.serialization.Serializable import org.luxons.sevenwonders.model.boards.Requirements -import org.luxons.sevenwonders.model.resources.PricedResourceTransactions -import org.luxons.sevenwonders.model.resources.noTransactions +import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions +import org.luxons.sevenwonders.model.resources.singleOptionNoTransactionNeeded interface Card { val name: String @@ -82,7 +82,7 @@ data class CardPlayability( val isPlayable: Boolean, val isChainable: Boolean = false, val minPrice: Int = Int.MAX_VALUE, - val cheapestTransactions: Set<PricedResourceTransactions> = setOf(noTransactions()), + val transactionOptions: ResourceTransactionOptions = singleOptionNoTransactionNeeded(), val playabilityLevel: PlayabilityLevel, ) { val isFree: Boolean = minPrice == 0 @@ -92,7 +92,7 @@ data class CardPlayability( isPlayable = true, isChainable = false, minPrice = 0, - cheapestTransactions = setOf(noTransactions()), + transactionOptions = singleOptionNoTransactionNeeded(), playabilityLevel = PlayabilityLevel.SPECIAL_FREE, ) } diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/resources/Resources.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/resources/Resources.kt index 51e7f78d..fc99a41d 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/resources/Resources.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/resources/Resources.kt @@ -57,4 +57,16 @@ data class PricedResourceTransaction( typealias PricedResourceTransactions = Set<PricedResourceTransaction> +typealias ResourceTransactionOptions = List<PricedResourceTransactions> + +val PricedResourceTransactions.totalPrice: Int + get() = sumBy { it.totalPrice } + +val ResourceTransactionOptions.bestPrice: Int + get() = minOfOrNull { it.totalPrice } ?: Int.MAX_VALUE + fun noTransactions(): PricedResourceTransactions = emptySet() + +fun noTransactionOptions(): ResourceTransactionOptions = emptyList() + +fun singleOptionNoTransactionNeeded(): ResourceTransactionOptions = listOf(noTransactions()) diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/wonders/Wonders.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/wonders/Wonders.kt index 56b22e82..4767804a 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/wonders/Wonders.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/wonders/Wonders.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable import org.luxons.sevenwonders.model.boards.Requirements import org.luxons.sevenwonders.model.cards.CardBack import org.luxons.sevenwonders.model.cards.PlayabilityLevel -import org.luxons.sevenwonders.model.resources.PricedResourceTransactions +import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions import org.luxons.sevenwonders.model.resources.ResourceType import kotlin.random.Random @@ -58,7 +58,7 @@ data class ApiWonderStage( data class WonderBuildability( val isBuildable: Boolean, val minPrice: Int, - val cheapestTransactions: Set<PricedResourceTransactions>, + val transactionsOptions: ResourceTransactionOptions, val playabilityLevel: PlayabilityLevel, ) { val isFree: Boolean = minPrice == 0 diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Cards.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Cards.kt index d5468283..b097734a 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Cards.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Cards.kt @@ -8,7 +8,7 @@ import org.luxons.sevenwonders.model.cards.CardPlayability import org.luxons.sevenwonders.model.cards.Color import org.luxons.sevenwonders.model.cards.PlayabilityLevel import org.luxons.sevenwonders.model.resources.ResourceTransactions -import org.luxons.sevenwonders.model.resources.noTransactions +import org.luxons.sevenwonders.model.resources.singleOptionNoTransactionNeeded internal data class Card( val name: String, @@ -55,7 +55,7 @@ private object Playability { isPlayable = true, isChainable = true, minPrice = 0, - cheapestTransactions = setOf(noTransactions()), + transactionOptions = singleOptionNoTransactionNeeded(), playabilityLevel = PlayabilityLevel.CHAINABLE, ) @@ -63,7 +63,7 @@ private object Playability { isPlayable = satisfaction.satisfied, isChainable = false, minPrice = satisfaction.minPrice, - cheapestTransactions = satisfaction.cheapestTransactions, + transactionOptions = satisfaction.transactionOptions, playabilityLevel = satisfaction.level, ) @@ -71,7 +71,7 @@ private object Playability { isPlayable = true, isChainable = false, minPrice = 0, - cheapestTransactions = setOf(noTransactions()), + transactionOptions = singleOptionNoTransactionNeeded(), playabilityLevel = PlayabilityLevel.SPECIAL_FREE, ) } diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Requirements.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Requirements.kt index 945f4463..b78a5057 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Requirements.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Requirements.kt @@ -2,12 +2,9 @@ package org.luxons.sevenwonders.engine.cards import org.luxons.sevenwonders.engine.Player import org.luxons.sevenwonders.engine.boards.Board -import org.luxons.sevenwonders.engine.resources.Resources -import org.luxons.sevenwonders.engine.resources.asResources -import org.luxons.sevenwonders.engine.resources.bestSolution -import org.luxons.sevenwonders.engine.resources.emptyResources -import org.luxons.sevenwonders.engine.resources.execute +import org.luxons.sevenwonders.engine.resources.* import org.luxons.sevenwonders.model.resources.ResourceTransactions +import org.luxons.sevenwonders.model.resources.bestPrice data class Requirements internal constructor( val gold: Int = 0, @@ -37,15 +34,15 @@ data class Requirements internal constructor( } private fun satisfactionWithPotentialHelp(player: Player): RequirementsSatisfaction { - val (minPriceForResources, possibleTransactions) = bestSolution(resources, player) - val minPrice = minPriceForResources + gold - if (possibleTransactions.isEmpty()) { + val options = transactionOptions(resources, player) + val minPrice = options.bestPrice + gold + if (options.isEmpty()) { return RequirementsSatisfaction.unavailableResources() } if (player.board.gold < minPrice) { - return RequirementsSatisfaction.missingGoldForResources(minPrice, possibleTransactions) + return RequirementsSatisfaction.missingGoldForResources(minPrice, options) } - return RequirementsSatisfaction.metWithHelp(minPrice, possibleTransactions) + return RequirementsSatisfaction.metWithHelp(minPrice, options) } /** diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/RequirementsSatisfaction.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/RequirementsSatisfaction.kt index f9566981..55ec1edb 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/RequirementsSatisfaction.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/RequirementsSatisfaction.kt @@ -1,39 +1,40 @@ package org.luxons.sevenwonders.engine.cards import org.luxons.sevenwonders.model.cards.PlayabilityLevel -import org.luxons.sevenwonders.model.resources.PricedResourceTransactions -import org.luxons.sevenwonders.model.resources.noTransactions +import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions +import org.luxons.sevenwonders.model.resources.noTransactionOptions +import org.luxons.sevenwonders.model.resources.singleOptionNoTransactionNeeded internal data class RequirementsSatisfaction( val satisfied: Boolean, val level: PlayabilityLevel, val minPrice: Int, - val cheapestTransactions: Set<PricedResourceTransactions>, + val transactionOptions: ResourceTransactionOptions, ) { companion object { internal fun noRequirements() = - RequirementsSatisfaction(true, PlayabilityLevel.NO_REQUIREMENTS, 0, setOf(noTransactions())) + RequirementsSatisfaction(true, PlayabilityLevel.NO_REQUIREMENTS, 0, singleOptionNoTransactionNeeded()) internal fun enoughResources() = - RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_RESOURCES, 0, setOf(noTransactions())) + RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_RESOURCES, 0, singleOptionNoTransactionNeeded()) internal fun enoughGold(minPrice: Int) = - RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_GOLD, minPrice, setOf(noTransactions())) + RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_GOLD, minPrice, singleOptionNoTransactionNeeded()) internal fun enoughResourcesAndGold(minPrice: Int) = - RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_GOLD_AND_RES, minPrice, setOf(noTransactions())) + RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_GOLD_AND_RES, minPrice, singleOptionNoTransactionNeeded()) - internal fun metWithHelp(minPrice: Int, cheapestTransactions: Set<PricedResourceTransactions>) = - RequirementsSatisfaction(true, PlayabilityLevel.REQUIRES_HELP, minPrice, cheapestTransactions) + internal fun metWithHelp(minPrice: Int, transactionOptions: ResourceTransactionOptions) = + RequirementsSatisfaction(true, PlayabilityLevel.REQUIRES_HELP, minPrice, transactionOptions) internal fun missingRequiredGold(minPrice: Int) = - RequirementsSatisfaction(false, PlayabilityLevel.MISSING_REQUIRED_GOLD, minPrice, emptySet()) + RequirementsSatisfaction(false, PlayabilityLevel.MISSING_REQUIRED_GOLD, minPrice, noTransactionOptions()) - internal fun missingGoldForResources(minPrice: Int, cheapestTransactions: Set<PricedResourceTransactions>) = - RequirementsSatisfaction(false, PlayabilityLevel.MISSING_GOLD_FOR_RES, minPrice, cheapestTransactions) + internal fun missingGoldForResources(minPrice: Int, transactionOptions: ResourceTransactionOptions) = + RequirementsSatisfaction(false, PlayabilityLevel.MISSING_GOLD_FOR_RES, minPrice, transactionOptions) internal fun unavailableResources() = - RequirementsSatisfaction(false, PlayabilityLevel.UNAVAILABLE_RESOURCES, Int.MAX_VALUE, emptySet()) + RequirementsSatisfaction(false, PlayabilityLevel.UNAVAILABLE_RESOURCES, Int.MAX_VALUE, noTransactionOptions()) } } diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/resources/BestPriceCalculator.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/resources/TransactionOptionsCalculator.kt index 967bee2c..fbbcaa6e 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/resources/BestPriceCalculator.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/resources/TransactionOptionsCalculator.kt @@ -1,15 +1,11 @@ package org.luxons.sevenwonders.engine.resources import org.luxons.sevenwonders.engine.Player -import org.luxons.sevenwonders.model.resources.PricedResourceTransactions -import org.luxons.sevenwonders.model.resources.Provider -import org.luxons.sevenwonders.model.resources.ResourceType +import org.luxons.sevenwonders.model.resources.* import java.util.* -internal fun bestSolution(resources: Resources, player: Player): TransactionPlan = - BestPriceCalculator(resources, player).computeBestSolution() - -data class TransactionPlan(val price: Int, val possibleTransactions: Set<PricedResourceTransactions>) +internal fun transactionOptions(resources: Resources, player: Player): ResourceTransactionOptions = + TransactionOptionsCalculator(resources, player).computeOptions() private class ResourcePool( val provider: Provider?, @@ -21,16 +17,14 @@ private class ResourcePool( fun getCost(type: ResourceType): Int = if (provider == null) 0 else rules.getCost(type, provider) } -private class BestPriceCalculator(resourcesToPay: Resources, player: Player) { +private class TransactionOptionsCalculator(resourcesToPay: Resources, player: Player) { private val pools: List<ResourcePool> private val resourcesLeftToPay: MutableResources private val boughtResources: MutableMap<Provider, MutableResources> = EnumMap(Provider::class.java) - private val pricePaid: MutableMap<Provider, Int> = EnumMap(Provider::class.java) - private var totalPricePaid: Int = 0 + private val pricePaidPerProvider: MutableMap<Provider, Int> = EnumMap(Provider::class.java) - private var bestSolutions: MutableSet<PricedResourceTransactions> = mutableSetOf() - private var bestPrice: Int = Integer.MAX_VALUE + private var optionsSoFar: MutableSet<PricedResourceTransactions> = mutableSetOf() init { val board = player.board @@ -53,14 +47,17 @@ private class BestPriceCalculator(resourcesToPay: Resources, player: Player) { return ResourcePool(this, player.board.tradingRules, choices) } - fun computeBestSolution(): TransactionPlan { + fun computeOptions(): ResourceTransactionOptions { computePossibilities() - return TransactionPlan(bestPrice, bestSolutions) + return optionsSoFar.distinctBy { it.costByProvider }.sortedBy { it.totalPrice } } + private val PricedResourceTransactions.costByProvider: Map<Provider, Int> + get() = associate { it.provider to it.totalPrice } + private fun computePossibilities() { if (resourcesLeftToPay.isEmpty()) { - updateBestSolutionIfNeeded() + addCurrentOption() return } for (type in ResourceType.values()) { @@ -93,13 +90,11 @@ private class BestPriceCalculator(resourcesToPay: Resources, player: Player) { fun buyOne(provider: Provider, type: ResourceType, cost: Int) { boughtResources.getOrPut(provider) { MutableResources() }.add(type, 1) - pricePaid.merge(provider, cost) { old, new -> old + new } - totalPricePaid += cost + pricePaidPerProvider.merge(provider, cost) { old, new -> old + new } } fun unbuyOne(provider: Provider, type: ResourceType, cost: Int) { - totalPricePaid -= cost - pricePaid.merge(provider, -cost) { old, new -> old + new } + pricePaidPerProvider.merge(provider, -cost) { old, new -> old + new } boughtResources[provider]!!.remove(type, 1) } @@ -114,15 +109,27 @@ private class BestPriceCalculator(resourcesToPay: Resources, player: Player) { } } - private fun updateBestSolutionIfNeeded() { - if (totalPricePaid > bestPrice) return - - if (totalPricePaid < bestPrice) { - bestPrice = totalPricePaid - bestSolutions.clear() + private fun addCurrentOption() { + if (optionsSoFar.any { it < pricePaidPerProvider }) { + return } // avoid mutating the resources from the transactions - val transactionSet = boughtResources.mapValues { (_, res) -> res.copy() }.toTransactions(pricePaid) - bestSolutions.add(transactionSet) + val transactionsOption = boughtResources.mapValues { (_, res) -> res.copy() }.toTransactions(pricePaidPerProvider) + optionsSoFar.add(transactionsOption) + optionsSoFar.removeIf { it > pricePaidPerProvider } } + + private operator fun PricedResourceTransactions.compareTo(prices: Map<Provider, Int>): Int = when { + left == prices.left -> right.compareTo(prices.right) + right == prices.right -> left.compareTo(prices.left) + else -> 0 + } + + private val Map<Provider, Int>.left: Int get() = this[Provider.LEFT_PLAYER] ?: 0 + private val Map<Provider, Int>.right: Int get() = this[Provider.RIGHT_PLAYER] ?: 0 + + private val PricedResourceTransactions.left: Int + get() = firstOrNull { it.provider == Provider.LEFT_PLAYER }?.totalPrice ?: 0 + private val PricedResourceTransactions.right: Int + get() = firstOrNull { it.provider == Provider.RIGHT_PLAYER }?.totalPrice ?: 0 } diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/wonders/Wonder.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/wonders/Wonder.kt index 4a99b9e9..03ed2f8f 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/wonders/Wonder.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/wonders/Wonder.kt @@ -7,6 +7,7 @@ import org.luxons.sevenwonders.model.cards.CardBack import org.luxons.sevenwonders.model.cards.PlayabilityLevel import org.luxons.sevenwonders.model.resources.ResourceTransactions import org.luxons.sevenwonders.model.resources.ResourceType +import org.luxons.sevenwonders.model.resources.noTransactionOptions import org.luxons.sevenwonders.model.wonders.WonderBuildability internal class Wonder( @@ -53,14 +54,14 @@ private object Buildability { fun wonderFullyBuilt() = WonderBuildability( isBuildable = false, minPrice = Int.MAX_VALUE, - cheapestTransactions = emptySet(), + transactionsOptions = noTransactionOptions(), playabilityLevel = PlayabilityLevel.WONDER_FULLY_BUILT, ) fun requirementDependent(satisfaction: RequirementsSatisfaction) = WonderBuildability( isBuildable = satisfaction.satisfied, minPrice = satisfaction.minPrice, - cheapestTransactions = satisfaction.cheapestTransactions, + transactionsOptions = satisfaction.transactionOptions, playabilityLevel = satisfaction.level, ) } diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt index 783d3eab..62267c7a 100644 --- a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt +++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt @@ -4,23 +4,14 @@ import org.luxons.sevenwonders.engine.data.GameDefinition import org.luxons.sevenwonders.engine.data.LAST_AGE import org.luxons.sevenwonders.engine.test.SEED import org.luxons.sevenwonders.engine.test.testSettings -import org.luxons.sevenwonders.model.Action -import org.luxons.sevenwonders.model.MoveType -import org.luxons.sevenwonders.model.PlayedMove -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.PlayerTurnInfo +import org.luxons.sevenwonders.model.* import org.luxons.sevenwonders.model.cards.HandCard import org.luxons.sevenwonders.model.cards.TableCard import org.luxons.sevenwonders.model.resources.ResourceTransactions import org.luxons.sevenwonders.model.resources.noTransactions import org.luxons.sevenwonders.model.wonders.deal import kotlin.random.Random -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue -import kotlin.test.fail +import kotlin.test.* class GameTest { @@ -91,12 +82,12 @@ class GameTest { private fun createPlayCardMove(turnInfo: PlayerTurnInfo): MoveExpectation { val wonderBuildability = turnInfo.wonderBuildability if (wonderBuildability.isBuildable) { - val transactions = wonderBuildability.cheapestTransactions.first() + val transactions = wonderBuildability.transactionsOptions.first() return planMove(turnInfo, MoveType.UPGRADE_WONDER, turnInfo.hand!!.first(), transactions) } val playableCard = turnInfo.hand!!.firstOrNull { it.playability.isPlayable } return if (playableCard != null) { - planMove(turnInfo, MoveType.PLAY, playableCard, playableCard.playability.cheapestTransactions.first()) + planMove(turnInfo, MoveType.PLAY, playableCard, playableCard.playability.transactionOptions.first()) } else { planMove(turnInfo, MoveType.DISCARD, turnInfo.hand!!.first(), noTransactions()) } diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/cards/RequirementsTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/cards/RequirementsTest.kt index d3fdc94c..36dc8ed1 100644 --- a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/cards/RequirementsTest.kt +++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/cards/RequirementsTest.kt @@ -117,11 +117,11 @@ class RequirementsTest { val satisfaction = requirements.assess(player) if (neighbourHasResource) { - val transactions = setOf( + val transactionOptions = listOf( createPricedTransactions(Provider.LEFT_PLAYER, 2, requiredResource), createPricedTransactions(Provider.RIGHT_PLAYER, 2, requiredResource), ) - assertEquals(RequirementsSatisfaction.metWithHelp(2, transactions), satisfaction) + assertEquals(RequirementsSatisfaction.metWithHelp(2, transactionOptions), satisfaction) } else { assertEquals(RequirementsSatisfaction.unavailableResources(), satisfaction) } diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/resources/BestPriceCalculatorTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/resources/BestPriceCalculatorTest.kt deleted file mode 100644 index 1a56caf4..00000000 --- a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/resources/BestPriceCalculatorTest.kt +++ /dev/null @@ -1,175 +0,0 @@ -package org.luxons.sevenwonders.engine.resources - -import org.junit.Test -import org.luxons.sevenwonders.engine.SimplePlayer -import org.luxons.sevenwonders.engine.boards.Table -import org.luxons.sevenwonders.engine.test.createPricedTransaction -import org.luxons.sevenwonders.engine.test.createPricedTransactions -import org.luxons.sevenwonders.engine.test.testBoard -import org.luxons.sevenwonders.engine.test.testTable -import org.luxons.sevenwonders.model.resources.PricedResourceTransactions -import org.luxons.sevenwonders.model.resources.Provider.LEFT_PLAYER -import org.luxons.sevenwonders.model.resources.Provider.RIGHT_PLAYER -import org.luxons.sevenwonders.model.resources.ResourceType.* -import org.luxons.sevenwonders.model.resources.noTransactions -import kotlin.test.assertEquals - -class BestPriceCalculatorTest { - - private fun solutions(price: Int, vararg resourceTransactions: PricedResourceTransactions) = - TransactionPlan(price, setOf(*resourceTransactions)) - - @Test - fun bestPrice_0forEmptyResources() { - val table = testTable(3) - val player0 = SimplePlayer(0, table) - val emptyResources = emptyResources() - val emptyTransactions = noTransactions() - assertEquals(solutions(0, emptyTransactions), bestSolution(emptyResources, player0)) - } - - @Test - fun bestPrice_fixedResources_defaultCost() { - val left = testBoard(STONE) - val main = testBoard(STONE) - val right = testBoard(WOOD) - val table = Table(listOf(main, right, left)) - - val player0 = SimplePlayer(0, table) - val player1 = SimplePlayer(1, table) - val player2 = SimplePlayer(2, table) - - val resources = resourcesOf(STONE, STONE) - - val stoneLeftSingle = createPricedTransaction(LEFT_PLAYER, 2, STONE) - val stoneRightSingle = createPricedTransaction(RIGHT_PLAYER, 2, STONE) - - val stoneLeft = createPricedTransactions(stoneLeftSingle) - val stoneRight = createPricedTransactions(stoneRightSingle) - val stoneLeftAndRight = createPricedTransactions(stoneLeftSingle, stoneRightSingle) - - assertEquals(solutions(2, stoneLeft), bestSolution(resources, player0)) - assertEquals(solutions(4, stoneLeftAndRight), bestSolution(resources, player1)) - assertEquals(solutions(2, stoneRight), bestSolution(resources, player2)) - } - - @Test - fun bestPrice_fixedResources_overridenCost() { - val main = testBoard(STONE) - main.tradingRules.setCost(WOOD, RIGHT_PLAYER, 1) - - val left = testBoard(WOOD) - val right = testBoard(WOOD) - val opposite = testBoard(GLASS) - val table = Table(listOf(main, right, opposite, left)) - - val player0 = SimplePlayer(0, table) - val player1 = SimplePlayer(1, table) - val player2 = SimplePlayer(2, table) - val player3 = SimplePlayer(3, table) - - val resources = resourcesOf(WOOD) - - val woodLeft = createPricedTransactions(LEFT_PLAYER, 2, WOOD) - val woodRightDiscounted = createPricedTransactions(RIGHT_PLAYER, 1, WOOD) - val woodRight = createPricedTransactions(RIGHT_PLAYER, 2, WOOD) - - assertEquals(solutions(1, woodRightDiscounted), bestSolution(resources, player0)) - assertEquals(solutions(0, noTransactions()), bestSolution(resources, player1)) - assertEquals(solutions(2, woodLeft, woodRight), bestSolution(resources, player2)) - assertEquals(solutions(0, noTransactions()), bestSolution(resources, player3)) - } - - @Test - fun bestPrice_mixedResources_overridenCost() { - val left = testBoard(WOOD) - - val main = testBoard(STONE) - main.tradingRules.setCost(WOOD, RIGHT_PLAYER, 1) - - val right = testBoard(ORE) - right.production.addChoice(WOOD, CLAY) - right.publicProduction.addChoice(WOOD, CLAY) - - val table = Table(listOf(main, right, left)) - - val player0 = SimplePlayer(0, table) - val player1 = SimplePlayer(1, table) - val player2 = SimplePlayer(2, table) - - val resources = resourcesOf(WOOD) - val woodRightDiscounted = createPricedTransactions(RIGHT_PLAYER, 1, WOOD) - - assertEquals(solutions(1, woodRightDiscounted), bestSolution(resources, player0)) - assertEquals(solutions(0, noTransactions()), bestSolution(resources, player1)) - assertEquals(solutions(0, noTransactions()), bestSolution(resources, player2)) - } - - @Test - fun bestPrice_mixedResources_overridenCost_sameMultipleTimes() { - val left = testBoard(WOOD) - - val main = testBoard(WOOD) - main.tradingRules.setCost(CLAY, LEFT_PLAYER, 1) - main.tradingRules.setCost(CLAY, RIGHT_PLAYER, 1) - - main.publicProduction.addFixedResource(STONE, 1) - main.publicProduction.addChoice(CLAY, ORE) - main.production.addFixedResource(STONE, 1) - main.production.addChoice(CLAY, ORE) - - val right = testBoard(CLAY) - right.publicProduction.addFixedResource(ORE, 1) - right.publicProduction.addFixedResource(CLAY, 1) - right.publicProduction.addFixedResource(WOOD, 2) - right.production.addFixedResource(ORE, 1) - right.production.addFixedResource(CLAY, 1) - right.production.addFixedResource(WOOD, 2) - - val table = Table(listOf(main, right, left)) - - val player0 = SimplePlayer(0, table) - val player1 = SimplePlayer(1, table) - val player2 = SimplePlayer(2, table) - - val resources = resourcesOf(WOOD, CLAY, CLAY, CLAY) - - val claysRightDiscounted = createPricedTransaction(RIGHT_PLAYER, 2, CLAY, CLAY) - val claysLeft = createPricedTransaction(LEFT_PLAYER, 4, CLAY, CLAY) - val clayLeft = createPricedTransaction(LEFT_PLAYER, 2, CLAY) - val clayRight = createPricedTransaction(RIGHT_PLAYER, 2, CLAY) - - assertEquals(solutions(2, createPricedTransactions(claysRightDiscounted)), bestSolution(resources, player0)) - assertEquals(solutions(2, createPricedTransactions(clayLeft)), bestSolution(resources, player1)) - assertEquals(solutions(6, createPricedTransactions(claysLeft, clayRight)), bestSolution(resources, player2)) - } - - @Test - fun bestPrice_chooseCheapest() { - val left = testBoard(WOOD) - - val main = testBoard(WOOD) - main.production.addChoice(CLAY, ORE) - main.tradingRules.setCost(CLAY, RIGHT_PLAYER, 1) - - val right = testBoard(WOOD) - right.production.addFixedResource(ORE, 1) - right.production.addFixedResource(CLAY, 1) - right.publicProduction.addFixedResource(ORE, 1) - right.publicProduction.addFixedResource(CLAY, 1) - - val table = Table(listOf(main, right, left)) - - val player0 = SimplePlayer(0, table) - val player1 = SimplePlayer(1, table) - val player2 = SimplePlayer(2, table) - - val resources = resourcesOf(ORE, CLAY) - val oreAndClayLeft = createPricedTransactions(LEFT_PLAYER, 4, ORE, CLAY) - val clayRightDiscounted = createPricedTransactions(RIGHT_PLAYER, 1, CLAY) - - assertEquals(solutions(1, clayRightDiscounted), bestSolution(resources, player0)) - assertEquals(solutions(0, noTransactions()), bestSolution(resources, player1)) - assertEquals(solutions(4, oreAndClayLeft), bestSolution(resources, player2)) - } -} diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/resources/TransactionOptionsCalculatorTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/resources/TransactionOptionsCalculatorTest.kt new file mode 100644 index 00000000..da4b608e --- /dev/null +++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/resources/TransactionOptionsCalculatorTest.kt @@ -0,0 +1,201 @@ +package org.luxons.sevenwonders.engine.resources + +import org.junit.Test +import org.luxons.sevenwonders.engine.SimplePlayer +import org.luxons.sevenwonders.engine.boards.Table +import org.luxons.sevenwonders.engine.test.createPricedTransaction +import org.luxons.sevenwonders.engine.test.createPricedTransactions +import org.luxons.sevenwonders.engine.test.testBoard +import org.luxons.sevenwonders.engine.test.testTable +import org.luxons.sevenwonders.model.resources.Provider.LEFT_PLAYER +import org.luxons.sevenwonders.model.resources.Provider.RIGHT_PLAYER +import org.luxons.sevenwonders.model.resources.ResourceType.* +import org.luxons.sevenwonders.model.resources.noTransactions +import org.luxons.sevenwonders.model.resources.singleOptionNoTransactionNeeded +import kotlin.test.assertEquals + +class TransactionOptionsCalculatorTest { + + @Test + fun transactionOptions_noResourcesRequired_nothingToBuy() { + val table = testTable(3) + val player0 = SimplePlayer(0, table) + val emptyResources = emptyResources() + assertEquals(singleOptionNoTransactionNeeded(), transactionOptions(emptyResources, player0)) + } + + @Test + fun transactionOptions_initialResources_defaultCost() { + val board2 = testBoard(STONE) + val board0 = testBoard(STONE) + val board1 = testBoard(WOOD) + val table = Table(listOf(board0, board1, board2)) + + val player0 = SimplePlayer(0, table) + val player1 = SimplePlayer(1, table) + val player2 = SimplePlayer(2, table) + + val resources = resourcesOf(STONE, STONE) + + val stoneLeftSingle = createPricedTransaction(LEFT_PLAYER, 2, STONE) + val stoneRightSingle = createPricedTransaction(RIGHT_PLAYER, 2, STONE) + + val stoneLeft = createPricedTransactions(stoneLeftSingle) + val stoneRight = createPricedTransactions(stoneRightSingle) + val stoneLeftAndRight = createPricedTransactions(stoneLeftSingle, stoneRightSingle) + + assertEquals(listOf(stoneLeft), transactionOptions(resources, player0)) + assertEquals(listOf(stoneLeftAndRight), transactionOptions(resources, player1)) + assertEquals(listOf(stoneRight), transactionOptions(resources, player2)) + } + + @Test + fun transactionOptions_fixedResources_defaultCost() { + val board2 = testBoard(WOOD) + + val board0 = testBoard(STONE) + board0.publicProduction.addFixedResource(CLAY, 1) + board0.production.addFixedResource(CLAY, 1) + + val board1 = testBoard(ORE) + + val table = Table(listOf(board0, board1, board2)) + + val player0 = SimplePlayer(0, table) + val player1 = SimplePlayer(1, table) + val player2 = SimplePlayer(2, table) + + val resources = resourcesOf(STONE, CLAY) + + val stoneAndClayLeft = createPricedTransaction(LEFT_PLAYER, 4, STONE, CLAY) + val stoneAndClayRight = createPricedTransaction(RIGHT_PLAYER, 4, STONE, CLAY) + + assertEquals(singleOptionNoTransactionNeeded(), transactionOptions(resources, player0)) + assertEquals(listOf(createPricedTransactions(stoneAndClayLeft)), transactionOptions(resources, player1)) + assertEquals(listOf(createPricedTransactions(stoneAndClayRight)), transactionOptions(resources, player2)) + } + + @Test + fun transactionOptions_fixedResources_overridenCost() { + val board0 = testBoard(STONE) + board0.tradingRules.setCost(WOOD, RIGHT_PLAYER, 1) + + val board1 = testBoard(WOOD) + val board2 = testBoard(GLASS) + val board3 = testBoard(WOOD) + + val table = Table(listOf(board0, board1, board2, board3)) + + val player0 = SimplePlayer(0, table) + val player1 = SimplePlayer(1, table) + val player2 = SimplePlayer(2, table) + val player3 = SimplePlayer(3, table) + + val resources = resourcesOf(WOOD) + + val woodLeft = createPricedTransactions(LEFT_PLAYER, 2, WOOD) + val woodRightDiscounted = createPricedTransactions(RIGHT_PLAYER, 1, WOOD) + val woodRight = createPricedTransactions(RIGHT_PLAYER, 2, WOOD) + + assertEquals(listOf(woodRightDiscounted, woodLeft), transactionOptions(resources, player0)) + assertEquals(listOf(noTransactions()), transactionOptions(resources, player1)) + assertEquals(listOf(woodLeft, woodRight), transactionOptions(resources, player2)) + assertEquals(listOf(noTransactions()), transactionOptions(resources, player3)) + } + + @Test + fun transactionOptions_mixedResources_overridenCost() { + val board0 = testBoard(STONE) + board0.tradingRules.setCost(WOOD, RIGHT_PLAYER, 1) + + val board1 = testBoard(ORE) + board1.production.addChoice(WOOD, CLAY) + board1.publicProduction.addChoice(WOOD, CLAY) + + val board2 = testBoard(WOOD) + + val table = Table(listOf(board0, board1, board2)) + + val player0 = SimplePlayer(0, table) + val player1 = SimplePlayer(1, table) + val player2 = SimplePlayer(2, table) + + val resources = resourcesOf(WOOD) + + val woodRightDiscounted = createPricedTransactions(RIGHT_PLAYER, 1, WOOD) + val woodLeft = createPricedTransactions(LEFT_PLAYER, 2, WOOD) + + assertEquals(listOf(woodRightDiscounted, woodLeft), transactionOptions(resources, player0)) + assertEquals(listOf(noTransactions()), transactionOptions(resources, player1)) + assertEquals(listOf(noTransactions()), transactionOptions(resources, player2)) + } + + @Test + fun transactionOptions_mixedResources_overridenCost_sameMultipleTimes() { + val board0 = testBoard(WOOD) + board0.tradingRules.setCost(CLAY, LEFT_PLAYER, 1) + board0.tradingRules.setCost(CLAY, RIGHT_PLAYER, 1) + + board0.publicProduction.addFixedResource(STONE, 1) + board0.publicProduction.addChoice(CLAY, ORE) + board0.production.addFixedResource(STONE, 1) + board0.production.addChoice(CLAY, ORE) + + val board1 = testBoard(CLAY) + board1.publicProduction.addFixedResource(ORE, 1) + board1.publicProduction.addFixedResource(CLAY, 1) + board1.publicProduction.addFixedResource(WOOD, 2) + board1.production.addFixedResource(ORE, 1) + board1.production.addFixedResource(CLAY, 1) + board1.production.addFixedResource(WOOD, 2) + + val board2 = testBoard(WOOD) + + val table = Table(listOf(board0, board1, board2)) + + val player0 = SimplePlayer(0, table) + val player1 = SimplePlayer(1, table) + val player2 = SimplePlayer(2, table) + + val resources = resourcesOf(WOOD, CLAY, CLAY, CLAY) + + val claysRightDiscounted = createPricedTransaction(RIGHT_PLAYER, 2, CLAY, CLAY) + val claysLeft = createPricedTransaction(LEFT_PLAYER, 4, CLAY, CLAY) + val clayLeft = createPricedTransaction(LEFT_PLAYER, 2, CLAY) + val clayRight = createPricedTransaction(RIGHT_PLAYER, 2, CLAY) + + assertEquals(listOf(createPricedTransactions(claysRightDiscounted)), transactionOptions(resources, player0)) + assertEquals(listOf(createPricedTransactions(clayLeft)), transactionOptions(resources, player1)) + assertEquals(listOf(createPricedTransactions(claysLeft, clayRight)), transactionOptions(resources, player2)) + } + + @Test + fun transactionOptions_cheapestFirst() { + val board0 = testBoard(WOOD) + board0.production.addChoice(CLAY, ORE) + board0.tradingRules.setCost(CLAY, RIGHT_PLAYER, 1) + + val board1 = testBoard(WOOD) + board1.production.addFixedResource(ORE, 1) + board1.production.addFixedResource(CLAY, 1) + board1.publicProduction.addFixedResource(ORE, 1) + board1.publicProduction.addFixedResource(CLAY, 1) + + val board2 = testBoard(WOOD) + + val table = Table(listOf(board0, board1, board2)) + + val player0 = SimplePlayer(0, table) + val player1 = SimplePlayer(1, table) + val player2 = SimplePlayer(2, table) + + val resources = resourcesOf(ORE, CLAY) + + val clayRightDiscounted = createPricedTransactions(RIGHT_PLAYER, 1, CLAY) + val oreAndClayLeft = createPricedTransactions(LEFT_PLAYER, 4, ORE, CLAY) + + assertEquals(listOf(clayRightDiscounted), transactionOptions(resources, player0)) + assertEquals(listOf(noTransactions()), transactionOptions(resources, player1)) + assertEquals(listOf(oreAndClayLeft), transactionOptions(resources, player2)) + } +} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt index 373ad0f1..a5ce0357 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt @@ -7,7 +7,7 @@ import kotlinx.html.DIV import org.luxons.sevenwonders.model.* import org.luxons.sevenwonders.model.cards.CardPlayability import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.model.resources.PricedResourceTransactions +import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions import org.luxons.sevenwonders.model.wonders.WonderBuildability import org.luxons.sevenwonders.ui.redux.TransactionSelectorState import react.* @@ -120,7 +120,7 @@ class HandComponent(props: HandProps) : RComponent<HandProps, RState>(props) { large = true, intent = Intent.SUCCESS, disabled = !card.playability.isPlayable, - onClick = { prepareMove(handAction.moveType, card, card.playability.cheapestTransactions) }, + onClick = { prepareMove(handAction.moveType, card, card.playability.transactionOptions) }, ) { bpIcon(handAction.icon) if (card.playability.isPlayable && !card.playability.isFree) { @@ -136,7 +136,7 @@ class HandComponent(props: HandProps) : RComponent<HandProps, RState>(props) { large = true, intent = Intent.PRIMARY, disabled = !wonderBuildability.isBuildable, - onClick = { prepareMove(MoveType.UPGRADE_WONDER, card, wonderBuildability.cheapestTransactions) }, + onClick = { prepareMove(MoveType.UPGRADE_WONDER, card, wonderBuildability.transactionsOptions) }, ) { bpIcon("key-shift") if (wonderBuildability.isBuildable && !wonderBuildability.isFree) { @@ -145,10 +145,10 @@ class HandComponent(props: HandProps) : RComponent<HandProps, RState>(props) { } } - private fun prepareMove(moveType: MoveType, card: HandCard, transactions: Set<PricedResourceTransactions>) { - when (transactions.size) { - 1 -> props.prepareMove(PlayerMove(moveType, card.name, transactions.first())) - else -> props.startTransactionsSelection(TransactionSelectorState(moveType, card, transactions)) + private fun prepareMove(moveType: MoveType, card: HandCard, transactionOptions: ResourceTransactionOptions) { + when (transactionOptions.size) { + 1 -> props.prepareMove(PlayerMove(moveType, card.name, transactionOptions.first())) + else -> props.startTransactionsSelection(TransactionSelectorState(moveType, card, transactionOptions)) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt index ef1f0362..3de763b4 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt @@ -9,7 +9,7 @@ import org.luxons.sevenwonders.model.api.PlayerDTO import org.luxons.sevenwonders.model.api.State import org.luxons.sevenwonders.model.cards.CardBack import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.model.resources.PricedResourceTransactions +import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions import redux.RAction data class SwState( @@ -40,7 +40,7 @@ data class GameState( data class TransactionSelectorState( val moveType: MoveType, val card: HandCard, - val transactionsOptions: Set<PricedResourceTransactions>, + val transactionsOptions: ResourceTransactionOptions, ) fun rootReducer(state: SwState, action: RAction): SwState = state.copy( |