From 16e248ea36b8f61e4e94cf1c9c229929340db4d1 Mon Sep 17 00:00:00 2001 From: Joffrey BION Date: Mon, 16 Jul 2018 02:14:44 +0200 Subject: Rework resources representations --- .../luxons/sevenwonders/game/cards/Requirements.kt | 13 +-- .../game/data/serializers/ProductionSerializer.kt | 4 +- .../game/data/serializers/ResourcesSerializer.kt | 9 +- .../org/luxons/sevenwonders/game/moves/Move.kt | 2 +- .../game/resources/BestPriceCalculator.kt | 59 +++++++------- .../sevenwonders/game/resources/Production.kt | 66 +++++---------- .../game/resources/ResourceTransaction.kt | 15 ---- .../game/resources/ResourceTransactions.kt | 39 ++++----- .../sevenwonders/game/resources/Resources.kt | 95 ++++++++++++---------- .../sevenwonders/game/resources/TradingRules.kt | 20 +---- 10 files changed, 136 insertions(+), 186 deletions(-) delete mode 100644 game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransaction.kt (limited to 'game-engine/src/main') diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Requirements.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Requirements.kt index 6a38965f..ad50a1c6 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Requirements.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Requirements.kt @@ -4,11 +4,14 @@ import org.luxons.sevenwonders.game.Player import org.luxons.sevenwonders.game.boards.Board import org.luxons.sevenwonders.game.resources.ResourceTransactions import org.luxons.sevenwonders.game.resources.Resources -import org.luxons.sevenwonders.game.resources.bestPrice +import org.luxons.sevenwonders.game.resources.asResources +import org.luxons.sevenwonders.game.resources.bestSolution +import org.luxons.sevenwonders.game.resources.emptyResources +import org.luxons.sevenwonders.game.resources.execute data class Requirements internal constructor( val gold: Int = 0, - val resources: Resources = Resources() + val resources: Resources = emptyResources() ) { /** * Returns whether the given [board] meets these requirements on its own. @@ -52,8 +55,8 @@ data class Requirements internal constructor( if (producesRequiredResources(board)) { return true } - val bestPrice = bestPrice(resources, player) - return bestPrice != null && bestPrice <= board.gold - gold + val solution = bestSolution(resources, player) + return !solution.possibleTransactions.isEmpty() && solution.price <= board.gold - gold } private fun hasRequiredGold(board: Board): Boolean { @@ -71,7 +74,7 @@ data class Requirements internal constructor( private fun producesRequiredResourcesWithHelp(board: Board, transactions: ResourceTransactions): Boolean { val totalBoughtResources = transactions.asResources() - val remainingResources = this.resources.minus(totalBoughtResources) + val remainingResources = resources - totalBoughtResources return board.production.contains(remainingResources) } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.kt index 06b46bb2..b766dd31 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.kt @@ -15,10 +15,10 @@ import java.lang.reflect.Type internal class ProductionSerializer : JsonSerializer, JsonDeserializer { override fun serialize(production: Production, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { - val fixedResources = production.fixedResources + val fixedResources = production.getFixedResources() val choices = production.getAlternativeResources() return when { - fixedResources.isEmpty -> serializeAsChoice(choices, context) + fixedResources.isEmpty() -> serializeAsChoice(choices, context) choices.isEmpty() -> serializeAsResources(fixedResources, context) else -> throw IllegalArgumentException("Cannot serialize a production with mixed fixed resources and choices") } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.kt index c6c6a962..fcf66d79 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.kt @@ -10,22 +10,19 @@ import com.google.gson.JsonSerializationContext import com.google.gson.JsonSerializer import org.luxons.sevenwonders.game.resources.ResourceType import org.luxons.sevenwonders.game.resources.Resources +import org.luxons.sevenwonders.game.resources.toResources import java.lang.reflect.Type internal class ResourcesSerializer : JsonSerializer, JsonDeserializer { override fun serialize(resources: Resources, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { - val s = resources.asList().map { it.symbol }.joinToString("") + val s = resources.toList().map { it.symbol }.joinToString("") return if (s.isEmpty()) JsonNull.INSTANCE else JsonPrimitive(s) } @Throws(JsonParseException::class) override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Resources { val s = json.asString - val resources = Resources() - for (c in s.toCharArray()) { - resources.add(ResourceType.fromSymbol(c), 1) - } - return resources + return s.toCharArray().map { ResourceType.fromSymbol(it) }.toResources() } } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/Move.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/Move.kt index ce50e3e0..ed982abb 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/Move.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/Move.kt @@ -14,7 +14,7 @@ abstract class Move internal constructor( val type: MoveType = move.type // TODO restore visibility to public - internal val transactions: ResourceTransactions = ResourceTransactions(move.transactions) + internal val transactions: ResourceTransactions = move.transactions internal abstract fun place(discardedCards: MutableList, settings: Settings) diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculator.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculator.kt index 3b86fd97..4159cea3 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculator.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculator.kt @@ -4,20 +4,10 @@ import org.luxons.sevenwonders.game.Player import java.util.ArrayList import java.util.EnumSet -internal fun bestPrice(resources: Resources, player: Player): Int? { - return bestSolution(resources, player)?.price -} - -internal fun bestTransaction(resources: Resources, player: Player): ResourceTransactions? { - return bestSolution(resources, player)?.transactions -} +internal fun bestSolution(resources: Resources, player: Player): TransactionPlan = + BestPriceCalculator(resources, player).computeBestSolution() -internal fun bestSolution(resources: Resources, player: Player): TransactionPlan? { - val calculator = BestPriceCalculator(resources, player) - return calculator.computeBestSolution() -} - -internal data class TransactionPlan(val price: Int, val transactions: ResourceTransactions) +internal data class TransactionPlan(val price: Int, val possibleTransactions: Set) private class ResourcePool( val provider: Provider?, @@ -30,16 +20,16 @@ private class ResourcePool( private class BestPriceCalculator(resourcesToPay: Resources, player: Player) { private val pools: List - private val resourcesLeftToPay: Resources - private val boughtResources: ResourceTransactions = ResourceTransactions() + private val resourcesLeftToPay: MutableResources + private val boughtResources: MutableMap = HashMap() private var pricePaid: Int = 0 - var bestSolution: ResourceTransactions? = null - var bestPrice: Int = Integer.MAX_VALUE + private var bestSolutions: MutableSet = mutableSetOf() + private var bestPrice: Int = Integer.MAX_VALUE init { val board = player.board - this.resourcesLeftToPay = resourcesToPay.minus(board.production.fixedResources) + this.resourcesLeftToPay = resourcesToPay.minus(board.production.getFixedResources()).toMutableResources() this.pools = createResourcePools(player) } @@ -56,24 +46,25 @@ private class BestPriceCalculator(resourcesToPay: Resources, player: Player) { for (provider in providers) { val providerBoard = player.getBoard(provider.boardPosition) - val pool = ResourcePool(provider, rules, providerBoard.publicProduction.asChoices().map { it.toMutableSet() }.toSet()) + val choices = providerBoard.publicProduction.asChoices().map { it.toMutableSet() }.toSet() + val pool = ResourcePool(provider, rules, choices) pools.add(pool) } return pools } - fun computeBestSolution(): TransactionPlan? { + fun computeBestSolution(): TransactionPlan { computePossibilities() - return if (bestSolution == null) null else TransactionPlan(bestPrice, bestSolution!!) + return TransactionPlan(bestPrice, bestSolutions) } private fun computePossibilities() { - if (resourcesLeftToPay.isEmpty) { + if (resourcesLeftToPay.isEmpty()) { updateBestSolutionIfNeeded() return } for (type in ResourceType.values()) { - if (resourcesLeftToPay.getQuantity(type) > 0) { + if (resourcesLeftToPay[type] > 0) { for (pool in pools) { if (pool.provider == null) { computeSelfPossibilities(type, pool) @@ -94,14 +85,22 @@ private class BestPriceCalculator(resourcesToPay: Resources, player: Player) { private fun computeNeighbourPossibilities(pool: ResourcePool, type: ResourceType, provider: Provider) { val cost = pool.getCost(type) resourcesLeftToPay.remove(type, 1) - boughtResources.add(provider, Resources(type)) - pricePaid += cost + buyOne(provider, type, cost) computePossibilitiesWhenUsing(type, pool) - pricePaid -= cost - boughtResources.remove(provider, Resources(type)) + unbuyOne(provider, type, cost) resourcesLeftToPay.add(type, 1) } + fun buyOne(provider: Provider, type: ResourceType, cost: Int) { + boughtResources.getOrPut(provider) { MutableResources() }.add(type, 1) + pricePaid += cost + } + + fun unbuyOne(provider: Provider, type: ResourceType, cost: Int) { + pricePaid -= cost + boughtResources.get(provider)!!.remove(type, 1) + } + private fun computePossibilitiesWhenUsing(type: ResourceType, pool: ResourcePool) { for (choice in pool.choices) { if (choice.contains(type)) { @@ -114,9 +113,13 @@ private class BestPriceCalculator(resourcesToPay: Resources, player: Player) { } private fun updateBestSolutionIfNeeded() { + if (pricePaid > bestPrice) return + if (pricePaid < bestPrice) { bestPrice = pricePaid - bestSolution = ResourceTransactions(boughtResources.asList()) + bestSolutions.clear() } + // avoid mutating the resources from the transactions + bestSolutions.add(boughtResources.mapValues { MutableResources(HashMap(it.value.quantities)) }.toTransactions()) } } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Production.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Production.kt index 2dd6d60f..0b069677 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Production.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Production.kt @@ -3,35 +3,30 @@ package org.luxons.sevenwonders.game.resources import java.util.Arrays import java.util.EnumSet -class Production internal constructor() { - - val fixedResources = Resources() +data class Production internal constructor( + private val fixedResources: MutableResources = mutableResourcesOf(), private val alternativeResources: MutableSet> = mutableSetOf() +) { + fun getFixedResources(): Resources = fixedResources - fun getAlternativeResources(): Set> { - return alternativeResources - } + fun getAlternativeResources(): Set> = alternativeResources - fun addFixedResource(type: ResourceType, quantity: Int) { - fixedResources.add(type, quantity) - } + fun addFixedResource(type: ResourceType, quantity: Int) = fixedResources.add(type, quantity) fun addChoice(vararg options: ResourceType) { val optionSet = EnumSet.copyOf(Arrays.asList(*options)) alternativeResources.add(optionSet) } - fun addAll(resources: Resources) { - fixedResources.addAll(resources) - } + fun addAll(resources: Resources) = fixedResources.add(resources) fun addAll(production: Production) { - fixedResources.addAll(production.fixedResources) + fixedResources.add(production.fixedResources) alternativeResources.addAll(production.getAlternativeResources()) } internal fun asChoices(): Set> { - val fixedAsChoices = fixedResources.asList().map{ EnumSet.of(it) }.toSet() + val fixedAsChoices = fixedResources.toList().map { EnumSet.of(it) }.toSet() return fixedAsChoices + alternativeResources } @@ -42,20 +37,22 @@ class Production internal constructor() { return containedInAlternatives(resources - fixedResources) } - private fun containedInAlternatives(resources: Resources): Boolean { - return containedInAlternatives(resources, alternativeResources) - } + private fun containedInAlternatives(resources: Resources): Boolean = + containedInAlternatives(resources.toMutableResources(), alternativeResources) - private fun containedInAlternatives(resources: Resources, alternatives: MutableSet>): Boolean { - if (resources.isEmpty) { + private fun containedInAlternatives( + resources: MutableResources, + alternatives: MutableSet> + ): Boolean { + if (resources.isEmpty()) { return true } for (type in ResourceType.values()) { - if (resources.getQuantity(type) <= 0) { + if (resources[type] <= 0) { continue } - val candidate = findFirstAlternativeContaining(alternatives, type) - ?: return false // no alternative produces the resource of this entry + // return if no alternative produces the resource of this entry + val candidate = alternatives.firstOrNull { a -> a.contains(type) } ?: return false resources.remove(type, 1) alternatives.remove(candidate) val remainingAreContainedToo = containedInAlternatives(resources, alternatives) @@ -67,29 +64,4 @@ class Production internal constructor() { } return false } - - private fun findFirstAlternativeContaining( - alternatives: Set>, - type: ResourceType - ): Set? { - return alternatives.stream().filter { a -> a.contains(type) }.findAny().orElse(null) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Production - - if (fixedResources != other.fixedResources) return false - if (alternativeResources != other.alternativeResources) return false - - return true - } - - override fun hashCode(): Int { - var result = fixedResources.hashCode() - result = 31 * result + alternativeResources.hashCode() - return result - } } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransaction.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransaction.kt deleted file mode 100644 index 30f4d014..00000000 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransaction.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.luxons.sevenwonders.game.resources - -import org.luxons.sevenwonders.game.Player - -data class ResourceTransaction(val provider: Provider, val resources: Resources) { - - internal fun execute(player: Player) { - val board = player.board - val price = board.tradingRules.computeCost(this) - board.removeGold(price) - val providerPosition = provider.boardPosition - val providerBoard = player.getBoard(providerPosition) - providerBoard.addGold(price) - } -} diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactions.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactions.kt index 52d10064..c7ab3636 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactions.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactions.kt @@ -2,36 +2,25 @@ package org.luxons.sevenwonders.game.resources import org.luxons.sevenwonders.game.Player -internal data class ResourceTransactions(private val resourcesByProvider: MutableMap = mutableMapOf()) { +typealias ResourceTransactions = Collection - constructor(transactions: Collection) : this() { - transactions.forEach { t -> add(t.provider, t.resources) } - } +fun noTransactions(): ResourceTransactions = emptyList() - fun add(provider: Provider, resources: Resources) { - resourcesByProvider.merge(provider, resources) { old, new -> old + new } - } +fun Map.toTransactions() = + filter { (_, res) -> !res.isEmpty() }.map { (p, res) -> ResourceTransaction(p, res) } - fun remove(provider: Provider, resources: Resources) { - resourcesByProvider.compute(provider) { _, prevResources -> - if (prevResources == null) { - throw IllegalStateException("Cannot remove resources from resource transactions") - } - prevResources.minus(resources) - } - } +fun ResourceTransactions.asResources(): Resources = map { it.resources }.merge() - fun execute(player: Player) { - asList().forEach { it.execute(player) } - } +internal fun ResourceTransactions.execute(player: Player) = forEach { it.execute(player) } - fun asList(): List { - return resourcesByProvider - .filter { (_, resources) -> !resources.isEmpty } - .map { (provider, resources) -> ResourceTransaction(provider, resources) } - } +data class ResourceTransaction(val provider: Provider, val resources: Resources) { - fun asResources(): Resources { - return resourcesByProvider.values.fold(Resources(), Resources::plus) + internal fun execute(player: Player) { + val board = player.board + val price = board.tradingRules.computeCost(this) + board.removeGold(price) + val providerPosition = provider.boardPosition + val providerBoard = player.getBoard(providerPosition) + providerBoard.addGold(price) } } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Resources.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Resources.kt index 96ad0b20..15673bd2 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Resources.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Resources.kt @@ -2,71 +2,84 @@ package org.luxons.sevenwonders.game.resources import java.util.NoSuchElementException -class Resources(quantities: Map = emptyMap()) { +fun emptyResources(): Resources = MutableResources() - private val quantities: MutableMap = quantities.toMutableMap() +fun resourcesOf(singleResource: ResourceType): Resources = MutableResources(mutableMapOf(singleResource to 1)) - constructor(singleResource: ResourceType): this(mapOf(singleResource to 1)) +fun resourcesOf(vararg resources: ResourceType): Resources = + resources.fold(MutableResources()) { rs, r -> rs.add(r, 1); rs } - val isEmpty: Boolean - get() = size() == 0 +fun resourcesOf(resources: Iterable): Resources = + resources.fold(MutableResources()) { rs, r -> rs.add(r, 1); rs } - fun add(type: ResourceType, quantity: Int) { - quantities.merge(type, quantity) { x, y -> x + y } - } +fun resourcesOf(vararg resources: Pair): Resources = + resources.fold(MutableResources()) { rs, (type, qty) -> rs.add(type, qty); rs } - fun remove(type: ResourceType, quantity: Int) { - if (getQuantity(type) < quantity) { - throw NoSuchElementException("Can't remove $quantity resources of type $type") - } - quantities.computeIfPresent(type) { _, oldQty -> oldQty - quantity } - } +internal fun mutableResourcesOf() = MutableResources() - fun addAll(resources: Resources) { - resources.quantities.forEach { type, quantity -> this.add(type, quantity) } - } +internal fun mutableResourcesOf(vararg resources: Pair) = + resources.fold(MutableResources()) { rs, (type, qty) -> rs.add(type, qty); rs } + +fun Iterable.toResources(): Resources = resourcesOf(this) + +fun Iterable.merge(): Resources = fold(MutableResources()) { r1, r2 -> r1.add(r2); r1} + +internal fun Resources.toMutableResources(): MutableResources { + val resources = MutableResources() + resources.add(this) + return resources +} - fun getQuantity(type: ResourceType): Int = quantities[type] ?: 0 +interface Resources { - fun asList(): List = quantities.flatMap { e -> List(e.value) { e.key } } + val quantities: Map - fun containsAll(resources: Resources): Boolean = resources.quantities.all { it.value <= this.getQuantity(it.key) } + val size: Int + get() = quantities.map { it.value }.sum() + + fun isEmpty(): Boolean = size == 0 + + operator fun get(key: ResourceType): Int = quantities.getOrDefault(key, 0) + + fun containsAll(resources: Resources): Boolean = resources.quantities.all { it.value <= this[it.key] } operator fun plus(resources: Resources): Resources { - val new = Resources(this.quantities) - new.addAll(resources) + val new = MutableResources() + new.add(this) + new.add(resources) return new } - /** - * Creates a new [Resources] object containing these resources minus the given resources. - * - * @param resources - * the resources to subtract from these resources - * - * @return a new [Resources] object containing these resources minus the given resources. - */ operator fun minus(resources: Resources): Resources { - val diff = Resources() + val diff = MutableResources() quantities.forEach { type, count -> - val remainder = count - resources.getQuantity(type) + val remainder = count - resources[type] diff.quantities[type] = Math.max(0, remainder) } return diff } - fun size(): Int = quantities.values.sum() + fun toList(): List = quantities.flatMap { (type, quantity) -> List(quantity) { type } } +} - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false +data class MutableResources( + override val quantities: MutableMap = mutableMapOf() +) : Resources { - other as Resources + fun add(type: ResourceType, quantity: Int) { + quantities.merge(type, quantity) { x, y -> x + y } + } - if (quantities != other.quantities) return false + fun add(resources: Resources) = resources.quantities.forEach { type, quantity -> this.add(type, quantity) } - return true + fun remove(type: ResourceType, quantity: Int) { + if (this[type] < quantity) { + throw NoSuchElementException("Can't remove $quantity resources of type $type") + } + quantities.computeIfPresent(type) { _, oldQty -> oldQty - quantity } + // to ensure equals() work properly + if (quantities[type] == 0) { + quantities.remove(type) + } } - - override fun hashCode(): Int = quantities.hashCode() } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/TradingRules.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/TradingRules.kt index dc0dc489..3b3c81ea 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/TradingRules.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/TradingRules.kt @@ -15,22 +15,10 @@ class TradingRules internal constructor(private val defaultCost: Int) { costs.computeIfAbsent(type) { mutableMapOf() }[provider] = cost } - internal fun computeCost(transactions: ResourceTransactions): Int { - return transactions.asList().map { this.computeCost(it) }.sum() - } + internal fun computeCost(transactions: ResourceTransactions): Int = transactions.map { computeCost(it) }.sum() - internal fun computeCost(transaction: ResourceTransaction): Int { - val resources = transaction.resources - val provider = transaction.provider - return computeCost(resources, provider) - } + internal fun computeCost(transact: ResourceTransaction) = computeCost(transact.resources, transact.provider) - private fun computeCost(resources: Resources, provider: Provider): Int { - var total = 0 - for (type in ResourceType.values()) { - val count = resources.getQuantity(type) - total += getCost(type, provider) * count - } - return total - } + private fun computeCost(resources: Resources, provider: Provider): Int = + resources.quantities.map { (type, qty) -> getCost(type, provider) * qty }.sum() } -- cgit