diff options
Diffstat (limited to 'game-engine/src/main')
10 files changed, 216 insertions, 92 deletions
diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/Game.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/Game.kt index b535bb96..f92177c8 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/Game.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/Game.kt @@ -48,7 +48,7 @@ class Game internal constructor( private fun createPlayerTurnInfo(player: Player): PlayerTurnInfo { val hand = hands.createHand(player) val action = determineAction(hand, player.board) - val neighbourGuildCards = table.getNeighbourGuildCards(player.index).map { it.toTableCard() } + val neighbourGuildCards = table.getNeighbourGuildCards(player.index).map { it.toTableCard(null) } return PlayerTurnInfo(player.index, table.toApiTable(), action, hand, neighbourGuildCards) } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Boards.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Boards.kt index 01b840c6..e1b9c4e9 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Boards.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Boards.kt @@ -4,6 +4,8 @@ import org.luxons.sevenwonders.game.boards.Military import org.luxons.sevenwonders.game.boards.Science import org.luxons.sevenwonders.game.boards.ScienceType import org.luxons.sevenwonders.game.cards.CardBack +import org.luxons.sevenwonders.game.moves.Move +import org.luxons.sevenwonders.game.moves.MoveType import org.luxons.sevenwonders.game.resources.Production import org.luxons.sevenwonders.game.resources.ResourceType import org.luxons.sevenwonders.game.resources.Resources @@ -22,14 +24,14 @@ data class Board( val gold: Int ) -internal fun InternalBoard.toApiBoard(): Board = Board( +internal fun InternalBoard.toApiBoard(lastMove: Move?): Board = Board( playerIndex = playerIndex, - wonder = wonder.toApiWonder(), + wonder = wonder.toApiWonder(lastMove), production = production.toApiProduction(), publicProduction = publicProduction.toApiProduction(), science = science.toApiScience(), military = military.toApiMilitary(), - playedCards = getPlayedCards().map { it.toTableCard() }, + playedCards = getPlayedCards().map { it.toTableCard(lastMove) }, gold = gold ) @@ -41,23 +43,26 @@ data class Wonder( val nbBuiltStages: Int ) -internal fun InternalWonder.toApiWonder(): Wonder = Wonder( +internal fun InternalWonder.toApiWonder(lastMove: Move?): Wonder = Wonder( name = name, initialResource = initialResource, - stages = stages.map { it.toApiWonderStage() }, + stages = stages.map { it.toApiWonderStage(lastBuiltStage == it, lastMove) }, image = image, nbBuiltStages = nbBuiltStages ) data class WonderStage( val cardBack: CardBack?, - val isBuilt: Boolean + val isBuilt: Boolean, + val builtDuringLastMove: Boolean ) -internal fun InternalWonderStage.toApiWonderStage(): WonderStage = WonderStage( - cardBack = cardBack, - isBuilt = isBuilt -) +internal fun InternalWonderStage.toApiWonderStage(isLastBuiltStage: Boolean, lastMove: Move?): WonderStage = + WonderStage( + cardBack = cardBack, + isBuilt = isBuilt, + builtDuringLastMove = lastMove?.type == MoveType.UPGRADE_WONDER && isLastBuiltStage + ) data class ApiProduction( val fixedResources: Resources, diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Cards.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Cards.kt index b49c6ab0..d238ab77 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Cards.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Cards.kt @@ -3,9 +3,11 @@ package org.luxons.sevenwonders.game.api import org.luxons.sevenwonders.game.Player import org.luxons.sevenwonders.game.cards.Card import org.luxons.sevenwonders.game.cards.CardBack +import org.luxons.sevenwonders.game.cards.CardPlayability import org.luxons.sevenwonders.game.cards.Color import org.luxons.sevenwonders.game.cards.Requirements -import org.luxons.sevenwonders.game.resources.TransactionPlan +import org.luxons.sevenwonders.game.moves.Move +import org.luxons.sevenwonders.game.resources.ResourceTransactions import org.luxons.sevenwonders.game.resources.bestSolution data class TableCard( @@ -15,17 +17,19 @@ data class TableCard( val chainParent: String?, val chainChildren: List<String>, val image: String, - val back: CardBack + val back: CardBack, + val playedDuringLastMove: Boolean ) -internal fun Card.toTableCard(): TableCard = TableCard( +internal fun Card.toTableCard(lastMove: Move? = null): TableCard = TableCard( name = name, color = color, requirements = requirements, chainParent = chainParent, chainChildren = chainChildren, image = image, - back = back + back = back, + playedDuringLastMove = lastMove != null && this.name == lastMove.card.name ) /** @@ -43,19 +47,24 @@ data class HandCard( val isChainable: Boolean, val isFree: Boolean, val isPlayable: Boolean, - val cheapestTransactions: TransactionPlan + val minPrice: Int, + val cheapestTransactions: Set<ResourceTransactions> ) -internal fun Card.toHandCard(player: Player): HandCard = HandCard( - name = name, - color = color, - requirements = requirements, - chainParent = chainParent, - chainChildren = chainChildren, - image = image, - back = back, - isChainable = isChainableOn(player.board), - isFree = isFreeFor(player.board), - isPlayable = isPlayableBy(player), - cheapestTransactions = bestSolution(requirements.resources, player) -) +internal fun Card.toHandCard(player: Player): HandCard { + val playability: CardPlayability = computePlayabilityBy(player) + return HandCard( + name = name, + color = color, + requirements = requirements, + chainParent = chainParent, + chainChildren = chainChildren, + image = image, + back = back, + isChainable = playability.isChainable, + isFree = playability.isFree, + isPlayable = playability.isPlayable, + minPrice = playability.minPrice, + cheapestTransactions = playability.cheapestTransactions + ) +} diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Table.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Table.kt index 05242f00..4be897db 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Table.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Table.kt @@ -17,7 +17,7 @@ data class Table( } internal fun InternalTable.toApiTable(): Table = Table( - boards = boards.map { it.toApiBoard() }, + boards = boards.mapIndexed { i, b -> b.toApiBoard(lastPlayedMoves.getOrNull(i)) }, currentAge = currentAge, handRotationDirection = handRotationDirection, lastPlayedMoves = lastPlayedMoves.map { it.toPlayedMove() } @@ -33,6 +33,6 @@ data class PlayedMove( internal fun Move.toPlayedMove(): PlayedMove = PlayedMove( playerIndex = playerContext.index, type = type, - card = card.toTableCard(), + card = card.toTableCard(this), transactions = transactions ) diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Cards.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Cards.kt index df0e06cc..94c1a81a 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Cards.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Cards.kt @@ -4,9 +4,18 @@ import org.luxons.sevenwonders.game.Player import org.luxons.sevenwonders.game.boards.Board import org.luxons.sevenwonders.game.effects.Effect import org.luxons.sevenwonders.game.resources.ResourceTransactions +import org.luxons.sevenwonders.game.resources.noTransactions data class CardBack(val image: String) +data class CardPlayability( + val isPlayable: Boolean, + val isFree: Boolean = false, + val isChainable: Boolean = false, + val minPrice: Int = Int.MAX_VALUE, + val cheapestTransactions: Set<ResourceTransactions> = emptySet() +) + internal data class Card( val name: String, val color: Color, @@ -17,21 +26,43 @@ internal data class Card( val image: String, val back: CardBack ) { - private fun isAllowedOnBoard(board: Board): Boolean = !board.isPlayed(name) // cannot play twice the same card - - fun isFreeFor(board: Board): Boolean = isChainableOn(board) || isFreeWithoutChainingOn(board) + fun computePlayabilityBy(player: Player): CardPlayability { + if (!isAllowedOnBoard(player.board)) { + return nonPlayable() + } + if (isParentOnBoard(player.board)) { + return chainablePlayability() + } + return requirementsPlayability(player) + } - fun isChainableOn(board: Board): Boolean = - isAllowedOnBoard(board) && chainParent != null && board.isPlayed(chainParent) + private fun nonPlayable() = CardPlayability(isPlayable = false) - private fun isFreeWithoutChainingOn(board: Board) = - isAllowedOnBoard(board) && requirements.areMetWithoutNeighboursBy(board) && requirements.gold == 0 + private fun chainablePlayability(): CardPlayability = CardPlayability( + isPlayable = true, + isFree = true, + isChainable = true, + minPrice = 0, + cheapestTransactions = setOf(noTransactions()) + ) - fun isPlayableBy(player: Player): Boolean { - val board = player.board - return isAllowedOnBoard(board) && (isChainableOn(board) || requirements.areMetBy(player)) + private fun requirementsPlayability(player: Player): CardPlayability { + val satisfaction = requirements.computeSatisfaction(player) + return CardPlayability( + isPlayable = satisfaction is RequirementsSatisfaction.Acceptable, + isFree = satisfaction.minPrice == 0, + isChainable = false, + minPrice = satisfaction.minPrice, + cheapestTransactions = satisfaction.cheapestTransactions + ) } + fun isChainableOn(board: Board): Boolean = isAllowedOnBoard(board) && isParentOnBoard(board) + + private fun isAllowedOnBoard(board: Board): Boolean = !board.isPlayed(name) // cannot play twice the same card + + private fun isParentOnBoard(board: Board): Boolean = chainParent != null && board.isPlayed(chainParent) + fun applyTo(player: Player, transactions: ResourceTransactions) { if (!isChainableOn(player.board)) { requirements.pay(player, transactions) 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 69fb5d61..30c67683 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 @@ -14,14 +14,39 @@ data class Requirements internal constructor( val resources: Resources = emptyResources() ) { /** - * Returns whether the given [board] meets these requirements on its own. - * - * @param board the board to check - * - * @return true if the given board meets these requirements without any transaction with its neighbours + * Returns information about the extent to which the given [player] meets these requirements, either on its own or + * by buying resources to neighbours. */ - internal fun areMetWithoutNeighboursBy(board: Board): Boolean = - hasRequiredGold(board) && producesRequiredResources(board) + internal fun computeSatisfaction(player: Player): RequirementsSatisfaction { + if (player.board.gold < gold) { + return RequirementsSatisfaction.missingRequiredGold(gold) + } + if (resources.isEmpty()) { + if (gold > 0) { + return RequirementsSatisfaction.enoughGold(gold) + } + return RequirementsSatisfaction.noRequirements() + } + if (producesRequiredResources(player.board)) { + if (gold > 0) { + return RequirementsSatisfaction.enoughResourcesAndGold(gold) + } + return RequirementsSatisfaction.enoughResources() + } + return satisfactionWithPotentialHelp(player) + } + + private fun satisfactionWithPotentialHelp(player: Player): RequirementsSatisfaction { + val (minPriceForResources, possibleTransactions) = bestSolution(resources, player) + val minPrice = minPriceForResources + gold + if (possibleTransactions.isEmpty()) { + return RequirementsSatisfaction.resourcesUnavailable() + } + if (player.board.gold < minPrice) { + return RequirementsSatisfaction.missingGoldForResources(minPrice, possibleTransactions) + } + return RequirementsSatisfaction.metWithHelp(minPrice, possibleTransactions) + } /** * Returns whether the given board meets these requirements, if the specified resources are bought from neighbours. @@ -38,28 +63,6 @@ data class Requirements internal constructor( return producesRequiredResources(board) || producesRequiredResourcesWithHelp(board, boughtResources) } - /** - * Returns whether the given player meets these requirements, either on its own or by buying resources to - * neighbours. - * - * @param player the player to check - * - * @return true if the given player's board could meet these requirements - */ - internal fun areMetBy(player: Player): Boolean { - val board = player.board - if (!hasRequiredGold(board)) { - return false - } - if (producesRequiredResources(board)) { - return true - } - val solution = bestSolution(resources, player) - return !solution.possibleTransactions.isEmpty() && solution.price <= board.gold - gold - } - - private fun hasRequiredGold(board: Board): Boolean = board.gold >= gold - private fun hasRequiredGold(board: Board, resourceTransactions: ResourceTransactions): Boolean { val resourcesPrice = board.tradingRules.computeCost(resourceTransactions) return board.gold >= gold + resourcesPrice diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/RequirementsSatisfaction.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/RequirementsSatisfaction.kt new file mode 100644 index 00000000..de78abad --- /dev/null +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/RequirementsSatisfaction.kt @@ -0,0 +1,84 @@ +package org.luxons.sevenwonders.game.cards + +import org.luxons.sevenwonders.game.resources.ResourceTransactions +import org.luxons.sevenwonders.game.resources.noTransactions + +internal sealed class RequirementsSatisfaction( + val satisfied: Boolean, + val minPrice: Int, + val cheapestTransactions: Set<ResourceTransactions> +) { + sealed class Acceptable(minPrice: Int, cheapestTransactions: Set<ResourceTransactions>) : + RequirementsSatisfaction(true, minPrice, cheapestTransactions) { + + sealed class WithoutHelp(minPrice: Int) : Acceptable(minPrice, setOf(noTransactions())) { + + sealed class Free : WithoutHelp(0) { + + object NoRequirement : Free() + + object EnoughResources : Free() + } + + class EnoughGold(minPrice: Int) : WithoutHelp(minPrice) + + class EnoughResourcesAndGold(minPrice: Int) : WithoutHelp(minPrice) + } + + class WithHelp(minPrice: Int, cheapestTransactions: Set<ResourceTransactions>) : + Acceptable(minPrice, cheapestTransactions) + } + + sealed class Insufficient(minPrice: Int, cheapestTransactions: Set<ResourceTransactions>) : + RequirementsSatisfaction(false, minPrice, cheapestTransactions) { + + class MissingRequiredGold(minPrice: Int) : Insufficient(minPrice, emptySet()) + + class MissingGoldForResources(minPrice: Int, cheapestTransactions: Set<ResourceTransactions>) : + Insufficient(minPrice, cheapestTransactions) + + object UnavailableResources : Insufficient(Int.MAX_VALUE, emptySet()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RequirementsSatisfaction + + if (satisfied != other.satisfied) return false + if (minPrice != other.minPrice) return false + if (cheapestTransactions != other.cheapestTransactions) return false + + return true + } + + override fun hashCode(): Int { + var result = satisfied.hashCode() + result = 31 * result + minPrice + result = 31 * result + cheapestTransactions.hashCode() + return result + } + + companion object { + + internal fun noRequirements() = RequirementsSatisfaction.Acceptable.WithoutHelp.Free.NoRequirement + + internal fun enoughResources() = RequirementsSatisfaction.Acceptable.WithoutHelp.Free.EnoughResources + + internal fun enoughGold(minPrice: Int) = RequirementsSatisfaction.Acceptable.WithoutHelp.EnoughGold(minPrice) + + internal fun enoughResourcesAndGold(minPrice: Int) = + RequirementsSatisfaction.Acceptable.WithoutHelp.EnoughResourcesAndGold(minPrice) + + internal fun metWithHelp(minPrice: Int, cheapestTransactions: Set<ResourceTransactions>) = + RequirementsSatisfaction.Acceptable.WithHelp(minPrice, cheapestTransactions) + + internal fun missingRequiredGold(minPrice: Int) = RequirementsSatisfaction.Insufficient.MissingRequiredGold(minPrice) + + internal fun missingGoldForResources(minPrice: Int, cheapestTransactions: Set<ResourceTransactions>) = + RequirementsSatisfaction.Insufficient.MissingGoldForResources(minPrice, cheapestTransactions) + + internal fun resourcesUnavailable() = RequirementsSatisfaction.Insufficient.UnavailableResources + } +} diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/MoveType.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/MoveType.kt index ae00f23f..8062e212 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/MoveType.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/MoveType.kt @@ -11,7 +11,5 @@ enum class MoveType(private val create: (move: PlayerMove, card: Card, context: DISCARD(::DiscardMove), COPY_GUILD(::CopyGuildMove); - internal fun resolve(move: PlayerMove, card: Card, context: PlayerContext): Move { - return create(move, card, context) - } + internal fun resolve(move: PlayerMove, card: Card, context: PlayerContext): Move = create(move, card, context) } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/Wonder.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/Wonder.kt index 46e09d40..4910c51f 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/Wonder.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/Wonder.kt @@ -13,22 +13,18 @@ internal class Wonder( val image: String ) { val nbBuiltStages: Int - get() = stages.filter { it.isBuilt }.count() + get() = stages.count { it.isBuilt } private val nextStage: WonderStage get() { - val nextLevel = nbBuiltStages - if (nextLevel == stages.size) { + if (nbBuiltStages == stages.size) { throw IllegalStateException("This wonder has already reached its maximum level") } - return stages[nextLevel] + return stages[nbBuiltStages] } - private val lastBuiltStage: WonderStage - get() { - val lastLevel = nbBuiltStages - 1 - return stages[lastLevel] - } + val lastBuiltStage: WonderStage? + get() = stages.getOrNull(nbBuiltStages - 1) fun isNextStageBuildable(board: Board, boughtResources: ResourceTransactions): Boolean = nbBuiltStages < stages.size && nextStage.isBuildable(board, boughtResources) @@ -36,11 +32,8 @@ internal class Wonder( fun placeCard(cardBack: CardBack) = nextStage.placeCard(cardBack) fun activateLastBuiltStage(player: Player, boughtResources: ResourceTransactions) = - lastBuiltStage.activate(player, boughtResources) + lastBuiltStage!!.activate(player, boughtResources) fun computePoints(player: Player): Int = - stages.filter { it.isBuilt } - .flatMap { it.effects } - .map { it.computePoints(player) } - .sum() + stages.filter { it.isBuilt }.flatMap { it.effects }.sumBy { it.computePoints(player) } } diff --git a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/WonderStage.kt b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/WonderStage.kt index 39aca254..311e589e 100644 --- a/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/WonderStage.kt +++ b/game-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/WonderStage.kt @@ -7,17 +7,18 @@ import org.luxons.sevenwonders.game.cards.Requirements import org.luxons.sevenwonders.game.effects.Effect import org.luxons.sevenwonders.game.resources.ResourceTransactions -internal class WonderStage(val requirements: Requirements, val effects: List<Effect>) { - +internal class WonderStage( + val requirements: Requirements, + val effects: List<Effect> +) { var cardBack: CardBack? = null private set val isBuilt: Boolean get() = cardBack != null - fun isBuildable(board: Board, boughtResources: ResourceTransactions): Boolean { - return requirements.areMetWithHelpBy(board, boughtResources) - } + fun isBuildable(board: Board, boughtResources: ResourceTransactions): Boolean = + requirements.areMetWithHelpBy(board, boughtResources) fun placeCard(cardBack: CardBack) { this.cardBack = cardBack |