diff options
author | jbion <joffrey.bion@amadeus.com> | 2019-02-20 21:48:18 +0100 |
---|---|---|
committer | jbion <joffrey.bion@amadeus.com> | 2019-02-20 21:48:18 +0100 |
commit | 073e348a23598dc62f8f46bc3057f16c6baf82df (patch) | |
tree | 4bc8bb708c7ab17c7eccf144665b4800a18cd3f2 | |
parent | Separate internal from API stuff in game engine (diff) | |
download | seven-wonders-073e348a23598dc62f8f46bc3057f16c6baf82df.tar.gz seven-wonders-073e348a23598dc62f8f46bc3057f16c6baf82df.tar.bz2 seven-wonders-073e348a23598dc62f8f46bc3057f16c6baf82df.zip |
Improve hand playability calculations and API output
12 files changed, 299 insertions, 136 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 diff --git a/game-engine/src/test/kotlin/org/luxons/sevenwonders/game/GameTest.kt b/game-engine/src/test/kotlin/org/luxons/sevenwonders/game/GameTest.kt index 3736ddc0..5b9d36d0 100644 --- a/game-engine/src/test/kotlin/org/luxons/sevenwonders/game/GameTest.kt +++ b/game-engine/src/test/kotlin/org/luxons/sevenwonders/game/GameTest.kt @@ -2,15 +2,14 @@ package org.luxons.sevenwonders.game import org.junit.Test import org.luxons.sevenwonders.game.api.Action -import org.luxons.sevenwonders.game.api.HandCard +import org.luxons.sevenwonders.game.api.Board +import org.luxons.sevenwonders.game.api.PlayedMove import org.luxons.sevenwonders.game.api.PlayerMove import org.luxons.sevenwonders.game.api.PlayerTurnInfo import org.luxons.sevenwonders.game.api.Table import org.luxons.sevenwonders.game.data.GameDefinition import org.luxons.sevenwonders.game.data.LAST_AGE import org.luxons.sevenwonders.game.moves.MoveType -import org.luxons.sevenwonders.game.resources.ResourceTransactions -import org.luxons.sevenwonders.game.resources.noTransactions import org.luxons.sevenwonders.game.test.testCustomizableSettings import java.util.HashMap import kotlin.test.assertEquals @@ -21,13 +20,25 @@ import kotlin.test.assertTrue class GameTest { @Test - fun testFullGame() { - val nbPlayers = 5 + fun testFullGame3Players() { + playGame(nbPlayers = 3) + } + + @Test + fun testFullGame5Players() { + playGame(nbPlayers = 6) + } + + @Test + fun testFullGame7Players() { + playGame(nbPlayers = 7) + } + + private fun playGame(nbPlayers: Int) { val game = createGame(nbPlayers) - for (age in 1..LAST_AGE) { - playAge(nbPlayers, game, age) - } + (1..LAST_AGE).forEach { playAge(nbPlayers, game, it) } + game.computeScore() } @@ -59,36 +70,24 @@ class GameTest { checkLastPlayedMoves(sentMoves, table) } - private fun getFirstAvailableMove(turnInfo: PlayerTurnInfo): PlayerMove? { - return when (turnInfo.action) { - Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> createPlayCardMove(turnInfo) - Action.PICK_NEIGHBOR_GUILD -> createPickGuildMove(turnInfo) - Action.WAIT -> null - else -> null - } + private fun getFirstAvailableMove(turnInfo: PlayerTurnInfo): PlayerMove? = when (turnInfo.action) { + Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> createPlayCardMove(turnInfo) + Action.PICK_NEIGHBOR_GUILD -> createPickGuildMove(turnInfo) + Action.WAIT -> null } private fun createPlayCardMove(turnInfo: PlayerTurnInfo): PlayerMove { - for (handCard in turnInfo.hand) { - if (handCard.isPlayable) { - val transactions = findResourcesToBuyFor(handCard) - return PlayerMove(MoveType.PLAY, handCard.name, transactions) - } - } - val firstCardInHand = turnInfo.hand[0] - return PlayerMove(MoveType.DISCARD, firstCardInHand.name) - } + val playableCard = turnInfo.hand.firstOrNull { it.isPlayable } - private fun findResourcesToBuyFor(handCard: HandCard): ResourceTransactions { - if (handCard.isFree) { - return noTransactions() + return if (playableCard != null) { + PlayerMove(MoveType.PLAY, playableCard.name, playableCard.cheapestTransactions.first()) + } else { + PlayerMove(MoveType.DISCARD, turnInfo.hand.first().name) } - return handCard.cheapestTransactions.possibleTransactions.first() } private fun createPickGuildMove(turnInfo: PlayerTurnInfo): PlayerMove { val neighbourGuilds = turnInfo.neighbourGuildCards - assertNotNull(neighbourGuilds) assertFalse(neighbourGuilds.isEmpty()) val cardName = neighbourGuilds[0].name return PlayerMove(MoveType.COPY_GUILD, cardName) @@ -100,6 +99,30 @@ class GameTest { assertNotNull(sentMove) assertNotNull(move.card) assertEquals(sentMove.cardName, move.card.name) + assertEquals(sentMove.type, move.type) + assertEquals(sentMove.transactions, move.transactions) + + val board = table.boards[move.playerIndex] + when (sentMove.type) { + MoveType.PLAY, MoveType.PLAY_FREE -> checkLastPlayedCard(move, board) + MoveType.UPGRADE_WONDER -> checkWonderUpgraded(move, board) + else -> Unit + } } } + + private fun checkLastPlayedCard(move: PlayedMove, board: Board) { + val card = board.playedCards.first { it.name == move.card.name } + assertTrue(card.playedDuringLastMove) + } + + private fun checkWonderUpgraded(move: PlayedMove, board: Board) { + val stages = board.wonder.stages + + val lastBuiltStage = stages.last { it.isBuilt } + val otherStages = stages - lastBuiltStage + + assertEquals(move.type == MoveType.UPGRADE_WONDER, lastBuiltStage.builtDuringLastMove) + assertFalse(otherStages.any { it.builtDuringLastMove }) + } } diff --git a/game-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/RequirementsTest.kt b/game-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/RequirementsTest.kt index 5abbba6e..4ed244b6 100644 --- a/game-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/RequirementsTest.kt +++ b/game-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/RequirementsTest.kt @@ -16,9 +16,7 @@ import org.luxons.sevenwonders.game.test.createRequirements import org.luxons.sevenwonders.game.test.createTransactions import org.luxons.sevenwonders.game.test.singleBoardPlayer import org.luxons.sevenwonders.game.test.testBoard -import java.util.Arrays import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertSame import kotlin.test.assertTrue @@ -45,9 +43,18 @@ class RequirementsTest { val board = testBoard(ResourceType.CLAY, boardGold) val player = singleBoardPlayer(board) - assertEquals(boardGold >= requiredGold, requirements.areMetWithoutNeighboursBy(board)) assertEquals(boardGold >= requiredGold, requirements.areMetWithHelpBy(board, noTransactions())) - assertEquals(boardGold >= requiredGold, requirements.areMetBy(player)) + + val satisfaction = requirements.computeSatisfaction(player) + if (boardGold >= requiredGold) { + if (requiredGold == 0) { + assertEquals(RequirementsSatisfaction.noRequirements(), satisfaction) + } else { + assertEquals(RequirementsSatisfaction.enoughGold(requiredGold), satisfaction) + } + } else { + assertEquals(RequirementsSatisfaction.missingRequiredGold(requiredGold), satisfaction) + } } @Theory @@ -57,11 +64,11 @@ class RequirementsTest { val board = testBoard(initialResource, 0) val player = singleBoardPlayer(board) - assertEquals(initialResource == requiredResource, requirements.areMetWithoutNeighboursBy(board)) assertEquals(initialResource == requiredResource, requirements.areMetWithHelpBy(board, noTransactions())) if (initialResource == requiredResource) { - assertTrue(requirements.areMetBy(player)) + val satisfaction = requirements.computeSatisfaction(player) + assertEquals(RequirementsSatisfaction.enoughResources(), satisfaction) } } @@ -79,11 +86,11 @@ class RequirementsTest { board.production.addFixedResource(producedResource, 1) val player = singleBoardPlayer(board) - assertEquals(producedResource == requiredResource, requirements.areMetWithoutNeighboursBy(board)) assertEquals(producedResource == requiredResource, requirements.areMetWithHelpBy(board, noTransactions())) if (producedResource == requiredResource) { - assertTrue(requirements.areMetBy(player)) + val satisfaction = requirements.computeSatisfaction(player) + assertEquals(RequirementsSatisfaction.enoughResources(), satisfaction) } } @@ -100,14 +107,24 @@ class RequirementsTest { val board = testBoard(initialResource, 2) val neighbourBoard = testBoard(initialResource, 0) neighbourBoard.publicProduction.addFixedResource(boughtResource, 1) - val table = Table(Arrays.asList(board, neighbourBoard)) + val table = Table(listOf(board, neighbourBoard)) val player = SimplePlayer(0, table) val resources = createTransactions(Provider.RIGHT_PLAYER, boughtResource) - assertFalse(requirements.areMetWithoutNeighboursBy(board)) - assertEquals(boughtResource == requiredResource, requirements.areMetWithHelpBy(board, resources)) - assertEquals(boughtResource == requiredResource, requirements.areMetBy(player)) + val neighbourHasResource = boughtResource == requiredResource + assertEquals(neighbourHasResource, requirements.areMetWithHelpBy(board, resources)) + + val satisfaction = requirements.computeSatisfaction(player) + if (neighbourHasResource) { + val transactions = setOf( + createTransactions(Provider.LEFT_PLAYER, requiredResource), + createTransactions(Provider.RIGHT_PLAYER, requiredResource) + ) + assertEquals(RequirementsSatisfaction.metWithHelp(2, transactions), satisfaction) + } else { + assertEquals(RequirementsSatisfaction.resourcesUnavailable(), satisfaction) + } } @Theory @@ -118,14 +135,13 @@ class RequirementsTest { val board = testBoard(initialResource, 2) val neighbourBoard = testBoard(requiredResource, 0) - val table = Table(Arrays.asList(board, neighbourBoard)) + val table = Table(listOf(board, neighbourBoard)) val player = SimplePlayer(0, table) val transactions = createTransactions(Provider.RIGHT_PLAYER, requiredResource) - assertFalse(requirements.areMetWithoutNeighboursBy(board)) assertTrue(requirements.areMetWithHelpBy(board, transactions)) - assertTrue(requirements.areMetBy(player)) + assertTrue(requirements.computeSatisfaction(player).satisfied) requirements.pay(player, transactions) |