diff options
12 files changed, 153 insertions, 50 deletions
diff --git a/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt b/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt index c58eaea8..80bad9e4 100644 --- a/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt +++ b/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt @@ -59,6 +59,7 @@ class SevenWondersBot( private suspend fun SevenWondersSession.playTurn(turn: PlayerTurnInfo): Boolean { when (turn.action) { Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> prepareMove(createPlayCardMove(turn)) + Action.PLAY_FREE_DISCARDED -> prepareMove(createPlayFreeDiscardedMove(turn)) Action.PICK_NEIGHBOR_GUILD -> prepareMove(createPickGuildMove(turn)) Action.SAY_READY -> sayReady() Action.WAIT -> Unit @@ -84,5 +85,10 @@ private fun createPlayCardMove(turnInfo: PlayerTurnInfo): PlayerMove { } } +private fun createPlayFreeDiscardedMove(turn: PlayerTurnInfo): PlayerMove { + val card = turn.discardedCards?.random() ?: error("No discarded card to play") + return PlayerMove(MoveType.PLAY_FREE_DISCARDED, card.name) +} + private fun createPickGuildMove(turnInfo: PlayerTurnInfo): PlayerMove = PlayerMove(MoveType.COPY_GUILD, turnInfo.neighbourGuildCards.random().name) diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt index d5503bc9..12084539 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt @@ -15,6 +15,7 @@ enum class Action(val message: String) { PLAY("Pick the card you want to play or discard."), PLAY_2("Pick the first card you want to play or discard. Note that you have the ability to play these 2 last cards. You will choose how to play the last one during your next turn."), PLAY_LAST("You have the special ability to play your last card. Choose how you want to play it."), + PLAY_FREE_DISCARDED("Pick a card from the discarded deck, you can play it for free."), PICK_NEIGHBOR_GUILD("Choose a Guild card (purple) that you want to copy from one of your neighbours."), WAIT("Please wait for other players to perform extra actions."), WATCH_SCORE("The game is over! Look at the scoreboard to see the final ranking!") @@ -27,7 +28,8 @@ data class PlayerTurnInfo( val action: Action, val hand: List<HandCard>?, val preparedMove: PlayedMove?, - val neighbourGuildCards: List<TableCard>, + val neighbourGuildCards: List<HandCard>, + val discardedCards: List<HandCard>?, // only present when the player can actually see them val scoreBoard: ScoreBoard? = null ) { val currentAge: Int = table.currentAge @@ -62,6 +64,7 @@ data class PlayerMove( enum class MoveType { PLAY, PLAY_FREE, + PLAY_FREE_DISCARDED, UPGRADE_WONDER, DISCARD, COPY_GUILD; diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt index fdb4d11b..5c7616a1 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt @@ -55,6 +55,7 @@ data class CardBack(val image: String) enum class PlayabilityLevel(val message: String) { CHAINABLE("free because of a card on the board"), NO_REQUIREMENTS("free"), + SPECIAL_FREE("free because of a special ability"), ENOUGH_RESOURCES("free"), ENOUGH_GOLD("enough gold"), ENOUGH_GOLD_AND_RES("enough gold and resources"), diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt index f7c29214..b9daf53f 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt @@ -5,20 +5,17 @@ import org.luxons.sevenwonders.engine.boards.Table import org.luxons.sevenwonders.engine.cards.Card import org.luxons.sevenwonders.engine.cards.Decks import org.luxons.sevenwonders.engine.cards.Hands -import org.luxons.sevenwonders.engine.converters.toTableState +import org.luxons.sevenwonders.engine.converters.toHandCard import org.luxons.sevenwonders.engine.converters.toPlayedMove -import org.luxons.sevenwonders.engine.converters.toTableCard +import org.luxons.sevenwonders.engine.converters.toTableState import org.luxons.sevenwonders.engine.data.LAST_AGE import org.luxons.sevenwonders.engine.effects.SpecialAbility import org.luxons.sevenwonders.engine.moves.Move import org.luxons.sevenwonders.engine.moves.resolve -import org.luxons.sevenwonders.model.score.ScoreBoard -import org.luxons.sevenwonders.model.Action -import org.luxons.sevenwonders.model.TableState -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.PlayerTurnInfo +import org.luxons.sevenwonders.model.* import org.luxons.sevenwonders.model.cards.CardBack import org.luxons.sevenwonders.model.cards.HandCard +import org.luxons.sevenwonders.model.score.ScoreBoard class Game internal constructor( val id: Long, @@ -44,10 +41,25 @@ class Game internal constructor( } private fun startNewTurn() { - currentTurnInfo = players.map { createPlayerTurnInfo(it) } + currentTurnInfo = players.map { + val hand = hands.createHand(it) + val action = determineAction(hand, it.board) + createPlayerTurnInfo(it, action, hand) + } } - private fun startEndGame() { + private fun startPlayDiscardedTurn() { + currentTurnInfo = players.map { + val action = if (it.board.hasSpecial(SpecialAbility.PLAY_DISCARDED)) { + Action.PLAY_FREE_DISCARDED + } else { + Action.WAIT + } + createPlayerTurnInfo(it, action, null) + } + } + + private fun startEndGameTurn() { // some player may need to do additional stuff startNewTurn() val allDone = currentTurnInfo.all { it.action == Action.WAIT } @@ -60,17 +72,20 @@ 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(null) } - + private fun createPlayerTurnInfo(player: Player, action: Action, hand: List<HandCard>?): PlayerTurnInfo { + val neighbourGuildCards = table.getNeighbourGuildCards(player.index).map { it.toHandCard(player, true) } + val exposedDiscardedCards = if (action == Action.PLAY_FREE_DISCARDED) { + discardedCards.map { it.toHandCard(player, true) } + } else { + null + } return PlayerTurnInfo( playerIndex = player.index, table = table.toTableState(), action = action, hand = hand, preparedMove = preparedMoves[player.index]?.toPlayedMove(), + discardedCards = exposedDiscardedCards, neighbourGuildCards = neighbourGuildCards ) } @@ -102,13 +117,18 @@ class Game internal constructor( * @return the back of the card that is prepared on the table */ fun prepareMove(playerIndex: Int, move: PlayerMove): CardBack { - val card = decks.getCard(table.currentAge, move.cardName) + val card = move.findCard() val context = PlayerContext(playerIndex, table, hands[playerIndex]) - val resolvedMove = move.type.resolve(move, card, context) + val resolvedMove = move.type.resolve(move, card, context, discardedCards) preparedMoves[playerIndex] = resolvedMove return card.back } + private fun PlayerMove.findCard() = when (type) { + MoveType.PLAY_FREE_DISCARDED -> discardedCards.first { it.name == cardName } + else -> decks.getCard(table.currentAge, cardName) + } + fun unprepareMove(playerIndex: Int) { preparedMoves.remove(playerIndex) } @@ -132,13 +152,17 @@ class Game internal constructor( if (endOfAgeReached()) { executeEndOfAgeEvents() if (endOfGameReached()) { - startEndGame() + startEndGameTurn() } else { startNewAge() } } else { - rotateHandsIfRelevant() - startNewTurn() + if (shouldStartPlayDiscardedTurn()) { + startPlayDiscardedTurn() + } else { + rotateHandsIfRelevant() + startNewTurn() + } } return table.toTableState() } @@ -173,6 +197,19 @@ class Game internal constructor( fun endOfGameReached(): Boolean = endOfAgeReached() && table.currentAge == LAST_AGE + private fun shouldStartPlayDiscardedTurn(): Boolean { + val boardsWithPlayDiscardedAbility = table.boards.filter { it.hasSpecial(SpecialAbility.PLAY_DISCARDED) } + if (boardsWithPlayDiscardedAbility.isEmpty()) { + return false + } + if (discardedCards.isEmpty()) { + // it was wasted for this turn, no discarded card to play + boardsWithPlayDiscardedAbility.forEach { it.removeSpecial(SpecialAbility.PLAY_DISCARDED) } + return false + } + return true + } + private fun rotateHandsIfRelevant() { // we don't rotate hands if some player can play his last card (with the special ability) if (!hands.maxOneCardRemains()) { diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/boards/Board.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/boards/Board.kt index 4c67be82..3f8f0437 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/boards/Board.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/boards/Board.kt @@ -63,6 +63,10 @@ internal class Board(val wonder: Wonder, val playerIndex: Int, settings: Setting specialAbilities.add(specialAbility) } + fun removeSpecial(specialAbility: SpecialAbility) { + specialAbilities.remove(specialAbility) + } + fun hasSpecial(specialAbility: SpecialAbility): Boolean = specialAbilities.contains(specialAbility) fun canPlayFreeCard(age: Age): Boolean = diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Cards.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Cards.kt index cfa46d27..cc00123e 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Cards.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Cards.kt @@ -20,8 +20,9 @@ internal data class Card( val image: String, val back: CardBack ) { - fun computePlayabilityBy(player: Player): CardPlayability = when { + fun computePlayabilityBy(player: Player, forceSpecialFree: Boolean = false): CardPlayability = when { isAlreadyOnBoard(player.board) -> Playability.incompatibleWithBoard() // cannot play twice the same card + forceSpecialFree -> Playability.specialFree() isParentOnBoard(player.board) -> Playability.chainable() else -> Playability.requirementDependent(requirements.assess(player)) } @@ -45,13 +46,13 @@ internal data class Card( private object Playability { - internal fun incompatibleWithBoard(): CardPlayability = + fun incompatibleWithBoard(): CardPlayability = CardPlayability( isPlayable = false, playabilityLevel = PlayabilityLevel.INCOMPATIBLE_WITH_BOARD ) - internal fun chainable(): CardPlayability = + fun chainable(): CardPlayability = CardPlayability( isPlayable = true, isChainable = true, @@ -60,7 +61,7 @@ private object Playability { playabilityLevel = PlayabilityLevel.CHAINABLE ) - internal fun requirementDependent(satisfaction: RequirementsSatisfaction): CardPlayability = + fun requirementDependent(satisfaction: RequirementsSatisfaction): CardPlayability = CardPlayability( isPlayable = satisfaction.satisfied, isChainable = false, @@ -68,4 +69,13 @@ private object Playability { cheapestTransactions = satisfaction.cheapestTransactions, playabilityLevel = satisfaction.level ) + + fun specialFree(): CardPlayability = + CardPlayability( + isPlayable = true, + isChainable = false, + minPrice = 0, + cheapestTransactions = setOf(noTransactions()), + playabilityLevel = PlayabilityLevel.SPECIAL_FREE + ) } diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Hands.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Hands.kt index f1f49cd8..b024848d 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Hands.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/cards/Hands.kt @@ -25,7 +25,7 @@ internal class Hands(private val hands: List<List<Card>>) { return Hands(mutatedHands) } - fun createHand(player: Player): List<HandCard> = hands[player.index].map { c -> c.toHandCard(player) } + fun createHand(player: Player): List<HandCard> = hands[player.index].map { c -> c.toHandCard(player, false) } fun rotate(direction: HandRotationDirection): Hands { val newHands = when (direction) { diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Cards.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Cards.kt index 3d3471bf..f0e17515 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Cards.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Cards.kt @@ -18,7 +18,7 @@ internal fun Card.toTableCard(lastMove: Move? = null): TableCard = playedDuringLastMove = lastMove != null && this.name == lastMove.card.name ) -internal fun Card.toHandCard(player: Player): HandCard = +internal fun Card.toHandCard(player: Player, forceSpecialFree: Boolean): HandCard = HandCard( name = name, color = color, @@ -27,5 +27,5 @@ internal fun Card.toHandCard(player: Player): HandCard = chainChildren = chainChildren, image = image, back = back, - playability = computePlayabilityBy(player) + playability = computePlayabilityBy(player, forceSpecialFree) ) diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/moves/Move.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/moves/Move.kt index 6ae92b9a..1035c1b8 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/moves/Move.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/moves/Move.kt @@ -25,10 +25,12 @@ class InvalidMoveException internal constructor(move: Move, message: String) : I "Player ${move.playerContext.index} cannot perform move ${move.type}: $message" ) -internal fun MoveType.resolve(move: PlayerMove, card: Card, context: PlayerContext): Move = when (this) { - MoveType.PLAY -> PlayCardMove(move, card, context) - MoveType.PLAY_FREE -> PlayFreeCardMove(move, card, context) - MoveType.UPGRADE_WONDER -> BuildWonderMove(move, card, context) - MoveType.DISCARD -> DiscardMove(move, card, context) - MoveType.COPY_GUILD -> CopyGuildMove(move, card, context) -} +internal fun MoveType.resolve(move: PlayerMove, card: Card, context: PlayerContext, discardedCards: List<Card>): Move = + when (this) { + MoveType.PLAY -> PlayCardMove(move, card, context) + MoveType.PLAY_FREE -> PlayFreeCardMove(move, card, context) + MoveType.PLAY_FREE_DISCARDED -> PlayFreeDiscardedCardMove(move, card, context, discardedCards) + MoveType.UPGRADE_WONDER -> BuildWonderMove(move, card, context) + MoveType.DISCARD -> DiscardMove(move, card, context) + MoveType.COPY_GUILD -> CopyGuildMove(move, card, context) + } diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/moves/PlayFreeDiscardedCardMove.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/moves/PlayFreeDiscardedCardMove.kt new file mode 100644 index 00000000..5147ed82 --- /dev/null +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/moves/PlayFreeDiscardedCardMove.kt @@ -0,0 +1,36 @@ +package org.luxons.sevenwonders.engine.moves + +import org.luxons.sevenwonders.engine.PlayerContext +import org.luxons.sevenwonders.engine.Settings +import org.luxons.sevenwonders.engine.cards.Card +import org.luxons.sevenwonders.engine.effects.SpecialAbility +import org.luxons.sevenwonders.model.PlayerMove + +internal class PlayFreeDiscardedCardMove( + move: PlayerMove, + card: Card, + playerContext: PlayerContext, + discardedCards: List<Card> +) : Move(move, card, playerContext) { + + init { + val board = playerContext.board + if (!board.hasSpecial(SpecialAbility.PLAY_DISCARDED)) { + throw InvalidMoveException(this, "no special ability to play a discarded card") + } + if (card !in discardedCards) { + throw InvalidMoveException(this, "Card $card is not among the discard cards") + } + } + + override fun place(discardedCards: MutableList<Card>, settings: Settings) { + discardedCards.remove(card) + playerContext.board.addCard(card) + } + + override fun activate(discardedCards: List<Card>, settings: Settings) { + // only apply effects, without paying the cost + card.effects.forEach { it.applyTo(playerContext) } + playerContext.board.removeSpecial(SpecialAbility.PLAY_DISCARDED) + } +} diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt index 320b3e94..e1a1b4a7 100644 --- a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt +++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt @@ -50,17 +50,18 @@ class GameTest { GameDefinition.load().initGame(0, testCustomizableSettings(), nbPlayers) private fun playAge(nbPlayers: Int, game: Game, age: Int) { - repeat(6) { - playTurn(nbPlayers, game, age, 7 - it) - } + do { + playTurn(nbPlayers, game, age) + } while (!game.getCurrentTurnInfo().first().isStartOfAge(age + 1)) } - private fun playTurn(nbPlayers: Int, game: Game, ageToCheck: Int, handSize: Int) { + private fun PlayerTurnInfo.isStartOfAge(age: Int) = action == Action.WATCH_SCORE || currentAge == age + + private fun playTurn(nbPlayers: Int, game: Game, ageToCheck: Int) { val turnInfos = game.getCurrentTurnInfo() assertEquals(nbPlayers, turnInfos.size) turnInfos.forEach { assertEquals(ageToCheck, it.currentAge) - assertEquals(handSize, it.hand?.size) } val moveExpectations = turnInfos.mapNotNull { it.firstAvailableMove() } @@ -76,6 +77,7 @@ class GameTest { private fun PlayerTurnInfo.firstAvailableMove(): MoveExpectation? = when (action) { Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> createPlayCardMove(this) + Action.PLAY_FREE_DISCARDED -> createPlayFreeDiscardedMove(this) Action.PICK_NEIGHBOR_GUILD -> createPickGuildMove(this) Action.WAIT, Action.SAY_READY -> null Action.WATCH_SCORE -> fail("should not have WATCH_SCORE action before end of game") @@ -95,6 +97,15 @@ class GameTest { } } + private fun createPlayFreeDiscardedMove(turn: PlayerTurnInfo): MoveExpectation { + val card = turn.discardedCards?.random() ?: error("No discarded card to play") + return MoveExpectation( + turn.playerIndex, + PlayerMove(MoveType.PLAY_FREE_DISCARDED, card.name, noTransactions()), + PlayedMove(turn.playerIndex, MoveType.PLAY_FREE_DISCARDED, card.toPlayedCard(), noTransactions()) + ) + } + private fun planMove( turnInfo: PlayerTurnInfo, moveType: MoveType, @@ -111,18 +122,11 @@ class GameTest { // the game should send action WAIT if no guild cards are available around assertFalse(neighbourGuilds.isEmpty()) + val card = neighbourGuilds.first() return MoveExpectation( turnInfo.playerIndex, - PlayerMove( - MoveType.COPY_GUILD, - neighbourGuilds.first().name - ), - PlayedMove( - turnInfo.playerIndex, - MoveType.COPY_GUILD, - neighbourGuilds.first(), - noTransactions() - ) + PlayerMove(MoveType.COPY_GUILD, card.name), + PlayedMove(turnInfo.playerIndex, MoveType.COPY_GUILD, card.toPlayedCard(), noTransactions()) ) } diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/test/TestUtils.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/test/TestUtils.kt index 7c1935ae..cc97dc54 100644 --- a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/test/TestUtils.kt +++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/test/TestUtils.kt @@ -129,7 +129,7 @@ internal fun playCardWithEffect(player: Player, color: Color, effect: Effect) { } internal fun createMove(context: PlayerContext, card: Card, type: MoveType): Move = - type.resolve(PlayerMove(type, card.name), card, context) + type.resolve(PlayerMove(type, card.name), card, context, emptyList()) internal fun singleBoardPlayer(board: Board): Player = object : Player { override val index = 0 |