From 24f8ab8118f56429094b890d94af76593eb79c49 Mon Sep 17 00:00:00 2001 From: Simon Forsberg Date: Sun, 11 Dec 2022 13:11:54 +0100 Subject: [PATCH] Change Splendor and SpiceRoad to use ResourceMap #320 #313 --- .../games/components/resources/ResourceMap.kt | 15 +- .../net/zomis/games/impl/DslSplendor.kt | 128 +++++------- .../kotlin/net/zomis/games/impl/SpiceRoad.kt | 184 ++++++------------ .../net/zomis/games/impl/SplendorCards.kt | 6 +- .../net/zomis/games/PlayerEliminationsTest.kt | 3 +- .../games/dsl/actions/InfiniteActionsTest.kt | 4 +- 6 files changed, 131 insertions(+), 209 deletions(-) diff --git a/games-dsl/common/src/main/kotlin/net/zomis/games/components/resources/ResourceMap.kt b/games-dsl/common/src/main/kotlin/net/zomis/games/components/resources/ResourceMap.kt index 9a93eed8..aafb5e3c 100644 --- a/games-dsl/common/src/main/kotlin/net/zomis/games/components/resources/ResourceMap.kt +++ b/games-dsl/common/src/main/kotlin/net/zomis/games/components/resources/ResourceMap.kt @@ -12,6 +12,7 @@ interface ResourceMap: Replayable { fun of(vararg resources: Pair) = resources.fold(empty()) { acc, pair -> acc + pair.first.toResourceMap(pair.second) } + fun fromList(list: List): ResourceMap = list.fold(empty(), ResourceMap::plus) } operator fun get(resource: GameResource): Int? fun getOrDefault(resource: GameResource): Int @@ -20,6 +21,16 @@ interface ResourceMap: Replayable { operator fun plus(other: GameResource): ResourceMap operator fun minus(other: ResourceMap): ResourceMap operator fun times(value: Int): ResourceMap + operator fun div(other: ResourceMap): Int { + var times = 0 + var tmp = this + while (tmp.has(other)) { + times += 1 + tmp -= other + } + return times + } + fun has(resource: GameResource, value: Int): Boolean fun count(): Int = this.entries().sumOf { it.value } @@ -38,6 +49,8 @@ interface ResourceMap: Replayable { fun isEmpty(): Boolean = entries().all { it.value == it.resource.defaultValue() } fun toMutableResourceMap(eventFactory: EventFactory = EmptyEventFactory()): MutableResourceMap = ResourceMapImpl(entries().map { it.resource to it.value }, eventFactory) + + fun toMap(): Map = entries().associate { it.resource to it.value } } interface MutableResourceMap: ResourceMap { @@ -62,7 +75,7 @@ interface MutableResourceMap: ResourceMap { // See Splendor Money and Caravan in Spice Road -class ResourceChange(val resourceMap: ResourceMap, val resource: GameResource, val oldValue: Int, var newValue: Int) +data class ResourceChange(val resourceMap: ResourceMap, val resource: GameResource, val oldValue: Int, var newValue: Int) class ResourceMapImpl( private val resources: MutableMap = mutableMapOf(), diff --git a/games-impl/src/commonMain/kotlin/net/zomis/games/impl/DslSplendor.kt b/games-impl/src/commonMain/kotlin/net/zomis/games/impl/DslSplendor.kt index 9c774014..0610d390 100644 --- a/games-impl/src/commonMain/kotlin/net/zomis/games/impl/DslSplendor.kt +++ b/games-impl/src/commonMain/kotlin/net/zomis/games/impl/DslSplendor.kt @@ -2,60 +2,61 @@ package net.zomis.games.impl import net.zomis.games.PlayerEliminationsWrite import net.zomis.games.cards.CardZone -import net.zomis.games.common.mergeWith import net.zomis.games.common.next +import net.zomis.games.components.resources.GameResource +import net.zomis.games.components.resources.MutableResourceMap +import net.zomis.games.components.resources.ResourceMap import net.zomis.games.dsl.GameCreator import net.zomis.games.dsl.ReplayStateI import net.zomis.games.metrics.MetricBuilder -import kotlin.math.absoluteValue import kotlin.math.max data class SplendorPlayer(val index: Int) { - var chips: Money = Money() + var chips: MutableResourceMap = ResourceMap.empty().toMutableResourceMap() val owned: CardZone = CardZone() val reserved: CardZone = CardZone() val nobles = CardZone() val points: Int get() = owned.cards.sumOf { it.points } + nobles.cards.sumOf { it.points } - fun total(): Money = this.discounts() + this.chips + fun total(): ResourceMap = this.discounts() + this.chips fun canBuy(card: SplendorCard): Boolean { val have = this.total() val diff = have - card.costs val wildcardsNeeded = diff.negativeAmount() - return have.moneys.getOrElse(MoneyType.WILDCARD) { 0 } >= wildcardsNeeded + return have.getOrDefault(MoneyType.WILDCARD) >= wildcardsNeeded } - fun pay(costs: Money): Money { - val chipCosts = (costs - this.discounts()).map { it.first to max(it.second, 0) } + fun pay(costs: ResourceMap): ResourceMap { + val chipCosts = (costs - this.discounts()).map { it.resource to max(it.value, 0) } val remaining = (this.chips - chipCosts) val wildcardsNeeded = remaining.negativeAmount() - val actualCosts = this.chips - remaining.map { it.first to max(it.second, 0) } + MoneyType.WILDCARD.toMoney(wildcardsNeeded) + val actualCosts = this.chips - remaining.map { it.resource to max(it.value, 0) } + MoneyType.WILDCARD.toMoney(wildcardsNeeded) - val tokensExpected = chipCosts.count - val oldMoney = this.chips + val tokensExpected = chipCosts.count() + val oldMoney = this.chips.toMutableResourceMap() this.chips -= actualCosts if (actualCosts.negativeAmount() > 0) { throw IllegalStateException("Actual costs has negative: $oldMoney --> ${this.chips}. Cost was $chipCosts. Actual $actualCosts") } - if (oldMoney.count - this.chips.count != tokensExpected) { + if (oldMoney.count() - this.chips.count() != tokensExpected) { throw IllegalStateException("Wrong amount of tokens were taken: $oldMoney --> ${this.chips}. Cost was $chipCosts") } return actualCosts } - fun discounts(): Money { - return this.owned.map { it.discounts }.fold(Money()) { acc, money -> acc + money } - } + fun discounts(): ResourceMap = this.owned.map { it.discounts }.fold(ResourceMap.empty(), ResourceMap::plus) } -data class SplendorCard(val level: Int, val discounts: Money, val costs: Money, val points: Int) { +private fun ResourceMap.negativeAmount(): Int = this.filter { it.value < 0 }.unaryMinus().count() + +data class SplendorCard(val level: Int, val discounts: ResourceMap, val costs: ResourceMap, val points: Int) { // Possible to support multiple discounts on the same card. Because why not! - constructor(level: Int, discount: MoneyType, costs: Money, points: Int): - this(level, Money(discount to 1), costs, points) + constructor(level: Int, discount: MoneyType, costs: ResourceMap, points: Int): + this(level, ResourceMap.of(discount to 1), costs, points) val id: String get() = toStateString() @@ -64,7 +65,7 @@ data class SplendorCard(val level: Int, val discounts: Money, val costs: Money, } } -data class SplendorNoble(val points: Int, val requirements: Money) { +data class SplendorNoble(val points: Int, val requirements: ResourceMap) { fun requirementsFulfilled(player: SplendorPlayer): Boolean { return player.discounts().has(requirements) } @@ -74,58 +75,19 @@ data class SplendorNoble(val points: Int, val requirements: Money) { } } -enum class MoneyType(val char: Char) { +enum class MoneyType(val char: Char): GameResource { WHITE('W'), BLUE('U'), BLACK('B'), RED('R'), GREEN('G'), WILDCARD('*'); - fun toMoney(count: Int): Money { - return Money(mutableMapOf(this to count)) - } + fun toMoney(count: Int) = this.toResourceMap(count) companion object { fun withoutWildcard(): List = MoneyType.values().toList().minus(WILDCARD) } } -data class Money(val moneys: MutableMap = mutableMapOf()) { - val count: Int = moneys.values.sum() - - constructor(vararg money: Pair) : this(mutableMapOf(*money)) - - operator fun plus(other: Money): Money { - val result = moneys.mergeWith(other.moneys) {a, b -> (a ?: 0) + (b ?: 0)} - return Money(result.toMutableMap()) - } - - - fun map(mapping: (Pair) -> Pair): Money { - var result = Money() - moneys.forEach { pair -> result += Money(mutableMapOf(mapping(pair.key to pair.value))) } - return Money(result.moneys.toMutableMap()) - } - - fun filter(entry: (Map.Entry) -> Boolean): Money { - return Money(moneys.entries.filter(entry).associate { it.key to it.value }.toMutableMap()) - } - - fun negativeAmount(): Int = moneys.values.filter { it < 0 }.sum().absoluteValue - - operator fun minus(other: Money): Money { - val result = moneys.mergeWith(other.moneys) {a, b -> (a ?: 0) - (b ?: 0)} - return Money(result.toMutableMap()) - } - - fun toStateString(): String { - return moneys.entries.sortedBy { it.key.char }.joinToString("") { it.key.char.toString().repeat(it.value) } - } - - fun has(requirements: Money): Boolean { - val diff = this - requirements - return diff.negativeAmount() == 0 - } -} data class MoneyChoice(val moneys: List) { - fun toMoney(): Money = Money(moneys.groupBy { it }.mapValues { it.value.size }.toMutableMap()) + fun toMoney() = ResourceMap.fromList(moneys) } fun startingStockForPlayerCount(playerCount: Int): Int { @@ -162,14 +124,14 @@ class SplendorGame(val config: SplendorConfig, val eliminations: PlayerEliminati throw IllegalStateException("Player has negative amount of chips") } if (this.currentPlayer.discounts().negativeAmount() > 0) throw IllegalStateException("Player has negative amount of discounts") - val totalChipsInGame = this.stock + this.players.fold(Money()) { a, b -> a.plus(b.chips) } - if (totalChipsInGame.moneys[MoneyType.WILDCARD] != 5) throw IllegalStateException("Wrong amount of total wildcards: $totalChipsInGame") - if (totalChipsInGame.moneys.any { it.key != MoneyType.WILDCARD && it.value != startingStockForPlayerCount(players.size) }) { + val totalChipsInGame = this.stock + this.players.fold(ResourceMap.empty()) { a, b -> a.plus(b.chips) } + if (totalChipsInGame[MoneyType.WILDCARD] != 5) throw IllegalStateException("Wrong amount of total wildcards: $totalChipsInGame") + if (totalChipsInGame.entries().any { it.resource != MoneyType.WILDCARD && it.value != startingStockForPlayerCount(players.size) }) { throw IllegalStateException("Wrong amount of total chips: $totalChipsInGame") } // Check money count > maxMoney - if (this.currentPlayer.chips.count > config.maxMoney) return null // Need to discard some money + if (this.currentPlayer.chips.count() > config.maxMoney) return null // Need to discard some money // Check noble conditions val noble = this.nobles.cards.find { it.requirementsFulfilled(currentPlayer) } @@ -200,7 +162,7 @@ class SplendorGame(val config: SplendorConfig, val eliminations: PlayerEliminati val playerCount = eliminations.playerCount val players: List = (0 until playerCount).map { SplendorPlayer(it) } val board: CardZone = CardZone(mutableListOf()) - var stock: Money = MoneyType.withoutWildcard().fold(Money()) { money, type -> money + type.toMoney(startingStockForPlayerCount(playerCount))}.plus(MoneyType.WILDCARD.toMoney(5)) + var stock: ResourceMap = MoneyType.withoutWildcard().fold(ResourceMap.empty()) { money, type -> money + type.toMoney(startingStockForPlayerCount(playerCount))}.plus(MoneyType.WILDCARD.toMoney(5)) var currentPlayerIndex: Int = 0 val currentPlayer: SplendorPlayer @@ -217,8 +179,8 @@ data class SplendorConfig( object DslSplendor { - fun viewMoney(money: Money): Map { - return money.moneys.entries.sortedBy { it.key.name }.associate { it.key.name to it.value } + fun viewMoney(money: ResourceMap): Map { + return money.entries().sortedBy { it.resource.name }.associate { it.resource.name to it.value } } fun viewNoble(game: SplendorGame, noble: SplendorNoble): Map = mapOf( "points" to 3, @@ -302,7 +264,7 @@ object DslSplendor { } action(discardMoney) { - forceWhen { game.currentPlayer.chips.count > game.config.maxMoney } + forceWhen { game.currentPlayer.chips.count() > game.config.maxMoney } options { MoneyType.values().toList() } requires { game.currentPlayer.chips.has(action.parameter.toMoney(1)) } effect { @@ -318,7 +280,7 @@ object DslSplendor { action(reserve).effect { val card = game.board.card(action.parameter) game.currentPlayer.reserved.cards.add(card.card) - val wildcardIfAvailable = if (game.stock.moneys.getOrElse(MoneyType.WILDCARD) { 0 } > 0) MoneyType.WILDCARD.toMoney(1) else Money() + val wildcardIfAvailable = if (game.stock.getOrDefault(MoneyType.WILDCARD) > 0) MoneyType.WILDCARD.toMoney(1) else ResourceMap.empty() game.stock -= wildcardIfAvailable game.currentPlayer.chips += wildcardIfAvailable replaceCard(replayable, game, card.card) @@ -341,7 +303,7 @@ object DslSplendor { } action(takeMoney).requires { val moneyChosen = action.parameter.toMoney() - if (moneyChosen.moneys.getOrElse(MoneyType.WILDCARD) { 0 } >= 1) return@requires false + if (moneyChosen.getOrDefault(MoneyType.WILDCARD) >= 1) return@requires false if (!game.stock.has(moneyChosen)) { return@requires false } @@ -378,7 +340,7 @@ object DslSplendor { }.values.toList() } view("stock") { - MoneyType.values().associateWith { game.stock.moneys[it] } + MoneyType.values().associateWith { game.stock.getOrDefault(it) } } view("nobles") { val allNobles = game.players.flatMap { pl -> pl.nobles.cards } + game.nobles.cards @@ -413,27 +375,27 @@ object DslSplendor { // card desiredness -- points, discount needed, usefulness for nobles val player = ctx.model.players[ctx.playerIndex] ctx.model.board.cards.associateWith { card -> - val discount = card.discounts.moneys.entries.single() + val discount = card.discounts.entries().single() check(discount.value == 1) - val discountType = discount.key - val playerHas = player.owned.cards.sumOf { it.discounts.moneys.getOrElse(discountType) { 0 } } + val discountType = discount.resource + val playerHas = player.owned.cards.sumOf { it.discounts.getOrDefault(discountType) } val otherCardsRequiresDiscountType = ctx.model.board.cards.minus(card).count { val money = player.discounts() + player.chips val remaining = it.costs - money - remaining.moneys.getOrElse(discountType) { 0 } > 0 + remaining.getOrDefault(discountType) > 0 } val nobles = ctx.model.nobles.cards.count { noble -> - val requires = noble.requirements.moneys.getOrElse(discountType) { 0 } + val requires = noble.requirements.getOrDefault(discountType) requires > playerHas } CardDesiredness(ctx.model.roundNumber, card.points, otherCardsRequiresDiscountType, nobles) } } - data class CardObtainability(val remainingCost: Money) { + data class CardObtainability(val remainingCost: ResourceMap) { fun turnsLeftUntilBuy(): Int { - val remainingTotal = remainingCost.count / 3 - val remainingMax = remainingCost.moneys.maxOfOrNull { it.value } ?: 0 + val remainingTotal = remainingCost.count() / 3 + val remainingMax = remainingCost.entries().maxOfOrNull { it.value } ?: 0 return maxOf(remainingTotal, remainingMax) } val value = (1.0 - (turnsLeftUntilBuy() / 10.0)).let { it * it } @@ -476,13 +438,13 @@ object DslSplendor { game.players[playerIndex].points } val moneyTaken = builder.actionMetric(takeMoney) { - action.parameter.toMoney().moneys + action.parameter.toMoney().toMap() } val cardCosts = builder.actionMetric(buy) { - action.parameter.costs.moneys + action.parameter.costs.toMap() } val moneyPaid = builder.actionMetric(buy) { - (action.parameter.costs - game.players[action.playerIndex].discounts()).filter { it.value >= 0 }.moneys + (action.parameter.costs - game.players[action.playerIndex].discounts()).filter { it.value >= 0 }.toMap() } // + actionMetric(DslSplendor.buyReserved)...? val noblesGotten = builder.endGamePlayerMetric { game.players[playerIndex].nobles.size @@ -491,7 +453,7 @@ object DslSplendor { eliminations.eliminationFor(playerIndex)!! } val discarded = builder.actionMetric(discardMoney) { - action.parameter.toMoney(1).moneys + action.parameter.toMoney(1).toMap() } } diff --git a/games-impl/src/commonMain/kotlin/net/zomis/games/impl/SpiceRoad.kt b/games-impl/src/commonMain/kotlin/net/zomis/games/impl/SpiceRoad.kt index 47dd5f7e..be8a3a53 100644 --- a/games-impl/src/commonMain/kotlin/net/zomis/games/impl/SpiceRoad.kt +++ b/games-impl/src/commonMain/kotlin/net/zomis/games/impl/SpiceRoad.kt @@ -1,18 +1,16 @@ package net.zomis.games.impl import net.zomis.games.cards.CardZone -import net.zomis.games.common.mergeWith import net.zomis.games.common.next import net.zomis.games.components.resources.GameResource +import net.zomis.games.components.resources.ResourceMap import net.zomis.games.dsl.* -import kotlin.math.absoluteValue object SpiceRoadDsl { - data class PlayParameter(val card: SpiceRoadGameModel.ActionCard, val remove: SpiceRoadGameModel.Caravan, val add: SpiceRoadGameModel.Caravan) - data class AcquireParameter(val card: SpiceRoadGameModel.ActionCard, val payArray: List) - - private fun List.toCaravan(): SpiceRoadGameModel.Caravan - = this.fold(SpiceRoadGameModel.Caravan()) { acc, spice -> acc + spice.toCaravan() } + data class PlayParameter(val card: SpiceRoadGameModel.ActionCard, val remove: ResourceMap, val add: ResourceMap) + data class AcquireParameter(val card: SpiceRoadGameModel.ActionCard, val payArray: List) { + fun payArrayAsResourceMap(): ResourceMap = ResourceMap.fromList(payArray) + } val factory = GameCreator(SpiceRoadGameModel::class) val play = factory.action("play", PlayParameter::class).serializer { @@ -21,7 +19,7 @@ object SpiceRoadDsl { val claim = factory.action("claim", SpiceRoadGameModel.PointCard::class).serializer { it.toStateString() } val rest = factory.action("rest", Unit::class) val acquire = factory.action("acquire", AcquireParameter::class).serializer { - "Acquire Card " + it.card.toStateString() + " PayArray " + it.payArray.joinToString("") { x -> x.char.toString() } + "Acquire Card " + it.card.toStateString() + " PayArray " + it.payArray.joinToString("") { x -> (x as SpiceRoadGameModel.Spice).char.toString() } } val discard = factory.action("discard", SpiceRoadGameModel.Spice::class).serializer {"Discard " + it.char} val game = factory.game("Spice Road") { @@ -85,25 +83,32 @@ object SpiceRoadDsl { requires { game.currentPlayer.caravan.has(action.parameter.remove) } - fun removeUpgrades(chosen: Pair): SpiceRoadGameModel.Caravan { - return chosen.first.filter { it.value < 0 }.map { it.first to it.second.times(-1) } + data class UpgradeChanges(val upgradesRemaining: Int, val changes: ResourceMap = ResourceMap.empty()) { + fun removes(): ResourceMap = changes.filter { it.value < 0 }.map { it.resource to it.value * -1 } + fun adds(): ResourceMap = changes.filter { it.value > 0 } + fun upgradableSpices(startingResources: ResourceMap): Iterable { + return (startingResources - this.removes()).filter { it.value > 0 }.entries().map { it.resource }.toSet() - SpiceRoadGameModel.Spice.BROWN + } + fun upgradableTimes(spiceToUpgrade: GameResource): IntRange { + return 1..(minOf(SpiceRoadGameModel.Spice.BROWN.ordinal - (spiceToUpgrade as SpiceRoadGameModel.Spice).ordinal, this.upgradesRemaining)) + } } choose { optionsWithIds({ game.currentPlayer.hand.cards.map { it.toStateString() to it } }) { card -> when { - card.gain != null -> parameter(PlayParameter(card, SpiceRoadGameModel.Caravan(), card.gain)) + card.gain != null -> parameter(PlayParameter(card, ResourceMap.empty(), card.gain)) card.upgrade != null -> { val upgrades = card.upgrade - recursive(SpiceRoadGameModel.Caravan() to upgrades) { + recursive(UpgradeChanges(upgrades)) { intermediateParameter { true } - parameter { PlayParameter(card, removeUpgrades(chosen), chosen.first.filter { it.value > 0 }) } - until { chosen.second == 0 } - options({ (game.currentPlayer.caravan - removeUpgrades(chosen)).remainingKeys() - SpiceRoadGameModel.Spice.BROWN }) { spiceToUpgrade -> - options({ 1..(minOf(SpiceRoadGameModel.Spice.BROWN.ordinal - spiceToUpgrade.ordinal, chosen.second)) }) { times -> - val caravan = spiceToUpgrade.toCaravan(-1) + spiceToUpgrade.upgrade(times) - recursion(caravan to times) { previous, e -> - (previous.first + e.first) to (previous.second - e.second) + parameter { PlayParameter(card, chosen.removes(), chosen.adds()) } + until { chosen.upgradesRemaining == 0 } + options({ chosen.upgradableSpices(game.currentPlayer.caravan) }) { spiceToUpgrade -> + options({ chosen.upgradableTimes(spiceToUpgrade) }) { times -> + val caravan = spiceToUpgrade.toResourceMap(-1) + (spiceToUpgrade as SpiceRoadGameModel.Spice).upgrade(times) + recursion(UpgradeChanges(times, caravan)) { previous, e -> + UpgradeChanges(previous.upgradesRemaining - e.upgradesRemaining, previous.changes + e.changes) } } } @@ -118,10 +123,10 @@ object SpiceRoadDsl { } action(acquire) { requires { - game.currentPlayer.caravan.has(action.parameter.payArray.fold(SpiceRoadGameModel.Caravan(), { acc, x -> acc + x.toCaravan() })) + game.currentPlayer.caravan.has(action.parameter.payArrayAsResourceMap()) } effect { - game.currentPlayer.caravan -= action.parameter.payArray.fold(SpiceRoadGameModel.Caravan(), { acc, x -> acc + x.toCaravan() }) + game.currentPlayer.caravan -= action.parameter.payArrayAsResourceMap() game.visibleActionCards.cards.mapIndexed { index, card -> card.addSpice(action.parameter.payArray.getOrNull(index)) } game.currentPlayer.caravan += action.parameter.card.takeAllSpice() @@ -133,12 +138,12 @@ object SpiceRoadDsl { } choose { optionsWithIds({ - game.visibleActionCards.cards.filterIndexed { index, _ -> index <= game.currentPlayer.caravan.count } + game.visibleActionCards.cards.filterIndexed { index, _ -> index <= game.currentPlayer.caravan.count() } .map { it.toStateString() to it } }) { card -> - recursive(emptyList()) { - until { chosen.size == game.visibleActionCards.card(card).index } - options({ (game.currentPlayer.caravan - chosen.toCaravan()).spice.filter { it.value > 0 }.keys }) { + recursive(emptyList()) { + until { chosen.count() == game.visibleActionCards.card(card).index } + options({ (game.currentPlayer.caravan - ResourceMap.fromList(chosen)).entries().filter { it.value > 0 }.map { it.resource } }) { recursion(it) { list, e -> list + e } } parameter { AcquireParameter(card, chosen) } @@ -147,11 +152,11 @@ object SpiceRoadDsl { } } action(discard) { - forceWhen { game.currentPlayer.caravan.count > 10 } - options { game.currentPlayer.caravan.spice.keys.filter { game.currentPlayer.caravan.has(it.toCaravan()) } } - requires { game.currentPlayer.caravan.has(action.parameter.toCaravan()) } + forceWhen { game.currentPlayer.caravan.count() > 10 } + options { game.currentPlayer.caravan.resources().filter { game.currentPlayer.caravan.has(it, 1) }.filterIsInstance() } + requires { game.currentPlayer.caravan.has(action.parameter.toResourceMap()) } effect { - game.currentPlayer.caravan -= this.action.parameter.toCaravan() + game.currentPlayer.caravan -= this.action.parameter.toResourceMap() } } allActions.precondition { game.currentPlayer.index == playerIndex } @@ -165,7 +170,7 @@ object SpiceRoadDsl { } } allActions.after { - if (game.players[playerIndex].caravan.count <= 10) { + if (game.players[playerIndex].caravan.count() <= 10) { game.currentPlayerIndex = game.currentPlayerIndex.next(game.playerCount) if (game.currentPlayerIndex == 0) game.round++ } @@ -185,8 +190,8 @@ class SpiceRoadGameModel(val playerCount: Int) { var currentPlayerIndex = 0 val currentPlayer: Player get() = players[currentPlayerIndex] val players = (0 until playerCount).map { Player(it) } - val pointsDeck = CardZone(pointsCards.split("\n").map { x -> x.split(",") }.map { (x, y) -> PointCard(x.toInt(), y.toCaravan()!!) }.toMutableList()) - val actionDeck = CardZone(actionCards.split("\n").map { x -> x.split(",") } + val pointsDeck: CardZone = CardZone(pointsCards.split("\n").map { x -> x.split(",") }.map { (x, y) -> PointCard(x.toInt(), y.toCaravan()!!) }.toMutableList()) + val actionDeck: CardZone = CardZone(actionCards.split("\n").map { x -> x.split(",") } .map { (x, y, z) -> ActionCard( x.toIntOrNull(), @@ -200,63 +205,63 @@ class SpiceRoadGameModel(val playerCount: Int) { val goldCoins = (0 until playerCount * 2).map { Coin(3) }.toMutableList() val silverCoins = (0 until playerCount * 2).map { Coin(1) }.toMutableList() - class ActionCard(val upgrade: Int?, val gain: Caravan?, val trade: Pair?) { - var spiceOnMe = Caravan() + class ActionCard(val upgrade: Int?, val gain: ResourceMap?, val trade: Pair?) { + var spiceOnMe = ResourceMap.empty().toMutableResourceMap() fun toStateString(): String = "$upgrade $gain ${trade?.first}->${trade?.second}" fun toViewable(): Map = mapOf( "upgrade" to upgrade, - "gain" to gain?.toViewable(), + "gain" to gain?.toView(), "trade" to if (trade == null) null else mapOf( - "give" to trade.first.toViewable(), - "get" to trade.second.toViewable() + "give" to trade.first.toView(), + "get" to trade.second.toView() ), "id" to toStateString(), - "bonusSpice" to if (spiceOnMe.count == 0) null else spiceOnMe.toViewable() + "bonusSpice" to if (spiceOnMe.isEmpty()) null else spiceOnMe.toView() ) - fun addSpice(spice: Spice?) { + fun addSpice(spice: GameResource?) { if (spice != null) { - spiceOnMe += spice.toCaravan() + spiceOnMe += spice.toResourceMap() } } - fun takeAllSpice(): Caravan { - val tmp = spiceOnMe - spiceOnMe = Caravan() + fun takeAllSpice(): ResourceMap { + val tmp = spiceOnMe.toMutableResourceMap() + spiceOnMe = ResourceMap.empty().toMutableResourceMap() return tmp } } - class PointCard(val points: Int, val cost: Caravan) { + class PointCard(val points: Int, val cost: ResourceMap) { fun toStateString(): String = "$points $cost" - fun toViewable(): Map = mapOf("points" to points, "cost" to cost.toViewable(), "id" to toStateString()) + fun toViewable(): Map = mapOf("points" to points, "cost" to cost.toView(), "id" to toStateString()) } class Coin(val points: Int) class Player(val index: Int) { var caravan = when (index) { - 0 -> Caravan(Spice.YELLOW to 3) - 1, 2 -> Caravan(Spice.YELLOW to 4) - 3, 4 -> Caravan(Spice.YELLOW to 3, Spice.RED to 1) + 0 -> ResourceMap.of(Spice.YELLOW to 3) + 1, 2 -> ResourceMap.of(Spice.YELLOW to 4) + 3, 4 -> ResourceMap.of(Spice.YELLOW to 3, Spice.RED to 1) else -> throw Exception("You have created a game with too many players") - } + }.toMutableResourceMap() val discard = CardZone() - val hand = CardZone(mutableListOf( + val hand: CardZone = CardZone(mutableListOf( ActionCard(2, null, null), - ActionCard(null, Spice.YELLOW.toCaravan(2), null)) + ActionCard(null, Spice.YELLOW.toResourceMap(2), null)) ) val pointCards = CardZone() val coins = mutableListOf() - val points: Int get() = pointCards.cards.sumBy { it.points } + - coins.sumBy { it.points } + - caravan.spice.filter { it.key > Spice.YELLOW }.values.sum() + val points: Int get() = pointCards.cards.sumOf { it.points } + + coins.sumOf { it.points } + + caravan.entries().filter { it.resource as Spice > Spice.YELLOW }.sumOf { it.value } fun toViewable(): Map { return mapOf( - "caravan" to caravan.toViewable(), + "caravan" to caravan.toView(), "discard" to discard.cards.map { it.toViewable() }, "hand" to hand.cards.map { it.toViewable() }, "points" to points, @@ -269,74 +274,13 @@ class SpiceRoadGameModel(val playerCount: Int) { enum class Spice(val char: Char): GameResource { YELLOW('Y'), RED('R'), GREEN('G'), BROWN('B'); - fun toCaravan(count: Int = 1): Caravan { - return Caravan(mutableMapOf(this to count)) - } - - fun upgrade(steps: Int): Caravan = Spice.values()[this.ordinal + steps].toCaravan() + fun upgrade(steps: Int): ResourceMap = Spice.values()[this.ordinal + steps].toResourceMap() } - private fun String.toCaravan(): Caravan? { + private fun String.toCaravan(): ResourceMap? { return if (this.isEmpty()) null else - this.groupBy { it }.mapValues { it.value.size }.map { Spice.values().find { x -> it.key == x.char }!!.toCaravan(it.value) }.fold(Caravan(), Caravan::plus) + this.groupBy { it }.mapValues { it.value.size }.map { Spice.values().find { x -> it.key == x.char }!! + .toResourceMap(it.value) }.fold(ResourceMap.empty(), ResourceMap::plus) } - data class Caravan(val spice: MutableMap = mutableMapOf()) { - - val count: Int = spice.values.sum() - - constructor(vararg spices: Pair) : this(mutableMapOf(*spices)) - - operator fun plus(other: Caravan): Caravan { - val result = spice.mergeWith(other.spice) { a, b -> (a ?: 0) + (b ?: 0) } - return Caravan(result.toMutableMap()) - } - - fun has(costs: Caravan): Boolean { - val diff = this - costs - return diff.spice.all { it.value >= 0 } - } - - fun map(mapping: (Pair) -> Pair): Caravan { - var result = Caravan() - spice.forEach { pair -> result += Caravan(mutableMapOf(mapping(pair.key to pair.value))) } - return Caravan(result.spice.toMutableMap()) - } - - operator fun times(times: Int): Caravan { - var tmp = Caravan() - for (i in 1..times) { - tmp += this - } - return tmp - } - - fun negativeAmount(): Int = spice.values.filter { it < 0 }.sum().absoluteValue - - operator fun minus(other: Caravan): Caravan { - val result = spice.mergeWith(other.spice) { a, b -> (a ?: 0) - (b ?: 0) } - return Caravan(result.toMutableMap()) - } - - operator fun div(other: Caravan): Int { - var times = 0 - var tmp = this - while (tmp.has(other)) { - times += 1 - tmp -= other - } - return times - } - - fun toStateString(): String { - return spice.entries.sortedBy { it.key.char }.joinToString("") { it.key.char.toString().repeat(it.value) } - } - - fun toViewable(): Map { - return this.spice.entries.sortedBy { it.key.name }.map { it.key.name to it.value }.toMap() - } - - fun remainingKeys() = spice.filter { it.value > 0 }.keys - fun filter(predicate: (Map.Entry) -> Boolean): Caravan = Caravan(spice.filter(predicate).toMutableMap()) - } } diff --git a/games-impl/src/commonMain/kotlin/net/zomis/games/impl/SplendorCards.kt b/games-impl/src/commonMain/kotlin/net/zomis/games/impl/SplendorCards.kt index 17d2aca5..b979a189 100644 --- a/games-impl/src/commonMain/kotlin/net/zomis/games/impl/SplendorCards.kt +++ b/games-impl/src/commonMain/kotlin/net/zomis/games/impl/SplendorCards.kt @@ -1,5 +1,7 @@ package net.zomis.games.impl +import net.zomis.games.components.resources.ResourceMap + val splendorCards = listOf(""" W,0,UGRB W,0,UGGRB @@ -105,8 +107,8 @@ fun splendorCardsFromMultilineCSV(level: Int, multilineString: String): List acc + MoneyType.values().first { it.char == c }.toMoney(1) } + fun String.toMoney(): ResourceMap { + return this.fold(ResourceMap.empty()) { acc, c -> acc + MoneyType.values().first { it.char == c }.toMoney(1) } } return SplendorCard(level, split[0].toMoney(), split[2].toMoney(), split[1].toInt()) diff --git a/games-mpp/games-server/src/test/kotlin/net/zomis/games/PlayerEliminationsTest.kt b/games-mpp/games-server/src/test/kotlin/net/zomis/games/PlayerEliminationsTest.kt index 6b5cdbd5..6bffa4c1 100644 --- a/games-mpp/games-server/src/test/kotlin/net/zomis/games/PlayerEliminationsTest.kt +++ b/games-mpp/games-server/src/test/kotlin/net/zomis/games/PlayerEliminationsTest.kt @@ -1,6 +1,6 @@ package net.zomis.games -import net.zomis.games.impl.Money +import net.zomis.games.components.resources.ResourceMap import net.zomis.games.impl.SplendorCard import net.zomis.games.impl.SplendorConfig import net.zomis.games.impl.SplendorGame @@ -88,6 +88,7 @@ class PlayerEliminationsTest { @Test fun splendor() { + val Money: () -> ResourceMap = { ResourceMap.empty() } val eliminations = PlayerEliminations(3) val game = SplendorGame(SplendorConfig(false, 10, 1, false), eliminations) game.players[0].owned.cards.add(SplendorCard(1, Money(), Money(), 1)) diff --git a/games-mpp/games-server/src/test/kotlin/net/zomis/games/dsl/actions/InfiniteActionsTest.kt b/games-mpp/games-server/src/test/kotlin/net/zomis/games/dsl/actions/InfiniteActionsTest.kt index 3a65ea95..1f5c4a95 100644 --- a/games-mpp/games-server/src/test/kotlin/net/zomis/games/dsl/actions/InfiniteActionsTest.kt +++ b/games-mpp/games-server/src/test/kotlin/net/zomis/games/dsl/actions/InfiniteActionsTest.kt @@ -120,8 +120,8 @@ class InfiniteActionsTest { .depthFirstActions(null) println(a) println(b.toList()) - Assertions.assertTrue(a.none { it.remove.count == 3 }) - Assertions.assertTrue(a.none { (it.add.spice[SpiceRoadGameModel.Spice.GREEN] ?: 0) > 1 }) + Assertions.assertTrue(a.none { it.remove.count() == 3 }) + Assertions.assertTrue(a.none { it.add.getOrDefault(SpiceRoadGameModel.Spice.GREEN) > 1 }) Assertions.assertTrue(b.any()) Assertions.assertTrue(a.size > 2) game.stop()