diff options
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( |