diff options
author | Joffrey Bion <joffrey.bion@booking.com> | 2020-05-28 12:47:00 +0200 |
---|---|---|
committer | Joffrey Bion <joffrey.bion@booking.com> | 2020-05-28 13:34:57 +0200 |
commit | a4da60fa4a816e3b8428eaffd2bd605dc0aed031 (patch) | |
tree | 18fcef04d9826fb94854c1a38584460659d8a3dd | |
parent | Add server-side support for PLAY_FREE_DISCARDED special ability (diff) | |
download | seven-wonders-a4da60fa4a816e3b8428eaffd2bd605dc0aed031.tar.gz seven-wonders-a4da60fa4a816e3b8428eaffd2bd605dc0aed031.tar.bz2 seven-wonders-a4da60fa4a816e3b8428eaffd2bd605dc0aed031.zip |
Add UI support for playing discarded cards
Resolves:
https://github.com/joffrey-bion/seven-wonders/issues/25
Resolves:
https://github.com/joffrey-bion/seven-wonders/issues/26
11 files changed, 175 insertions, 124 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 80bad9e4..daa4afe3 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,7 +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.PLAY_FREE_DISCARDED -> prepareMove(createPlayFreeDiscardedCardMove(turn)) Action.PICK_NEIGHBOR_GUILD -> prepareMove(createPickGuildMove(turn)) Action.SAY_READY -> sayReady() Action.WAIT -> Unit @@ -85,8 +85,8 @@ private fun createPlayCardMove(turnInfo: PlayerTurnInfo): PlayerMove { } } -private fun createPlayFreeDiscardedMove(turn: PlayerTurnInfo): PlayerMove { - val card = turn.discardedCards?.random() ?: error("No discarded card to play") +private fun createPlayFreeDiscardedCardMove(turnInfo: PlayerTurnInfo): PlayerMove { + val card = turnInfo.discardedCards?.random() ?: error("No discarded card to play") return PlayerMove(MoveType.PLAY_FREE_DISCARDED, card.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 12084539..54142ad2 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,10 +15,21 @@ 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."), + PLAY_FREE_DISCARDED("Pick a card from the discarded deck, you can play it for free (but you cannot discard for 3 " + + "gold coins or upgrade your wonder with it."), 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!") + WATCH_SCORE("The game is over! Look at the scoreboard to see the final ranking!"); + + fun allowsBuildingWonder(): Boolean = when (this) { + PLAY, PLAY_2, PLAY_LAST -> true + else -> false + } + + fun allowsDiscarding(): Boolean = when (this) { + PLAY, PLAY_2, PLAY_LAST -> true + else -> false + } } @Serializable diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/boards/Boards.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/boards/Boards.kt index 2fdb40b6..68a85898 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/boards/Boards.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/boards/Boards.kt @@ -16,7 +16,8 @@ data class Board( val military: Military, val playedCards: List<List<TableCard>>, val gold: Int, - val bluePoints: Int + val bluePoints: Int, + val canPlayAnyCardForFree: Boolean ) @Serializable 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 5c7616a1..13a9042d 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 @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable import org.luxons.sevenwonders.model.api.PlayerDTO import org.luxons.sevenwonders.model.boards.Requirements import org.luxons.sevenwonders.model.resources.ResourceTransactions +import org.luxons.sevenwonders.model.resources.noTransactions interface Card { val name: String @@ -81,8 +82,18 @@ data class CardPlayability( val isPlayable: Boolean, val isChainable: Boolean = false, val minPrice: Int = Int.MAX_VALUE, - val cheapestTransactions: Set<ResourceTransactions> = emptySet(), + val cheapestTransactions: Set<ResourceTransactions> = setOf(noTransactions()), val playabilityLevel: PlayabilityLevel ) { val isFree: Boolean = minPrice == 0 + + companion object { + val SPECIAL_FREE = CardPlayability( + isPlayable = true, + isChainable = false, + minPrice = 0, + cheapestTransactions = setOf(noTransactions()), + playabilityLevel = PlayabilityLevel.SPECIAL_FREE + ) + } } diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/wonders/Wonders.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/wonders/Wonders.kt index e48a37d0..9e9a5b38 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/wonders/Wonders.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/wonders/Wonders.kt @@ -28,8 +28,8 @@ data class ApiWonderStage( @Serializable data class WonderBuildability( val isBuildable: Boolean, - val minPrice: Int = Int.MAX_VALUE, - val cheapestTransactions: Set<ResourceTransactions> = emptySet(), + val minPrice: Int, + val cheapestTransactions: Set<ResourceTransactions>, val playabilityLevel: PlayabilityLevel ) { val isFree: Boolean = minPrice == 0 diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Boards.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Boards.kt index c974c25f..7ff870cf 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Boards.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Boards.kt @@ -5,6 +5,7 @@ import org.luxons.sevenwonders.engine.boards.ScienceType import org.luxons.sevenwonders.engine.effects.RawPointsIncrease import org.luxons.sevenwonders.engine.moves.Move import org.luxons.sevenwonders.engine.resources.Resources +import org.luxons.sevenwonders.model.Age import org.luxons.sevenwonders.model.MoveType import org.luxons.sevenwonders.model.cards.Color import org.luxons.sevenwonders.model.cards.TableCard @@ -24,7 +25,7 @@ import org.luxons.sevenwonders.model.boards.Science as ApiScience import org.luxons.sevenwonders.model.wonders.ApiWonder as ApiWonder import org.luxons.sevenwonders.model.wonders.ApiWonderStage as ApiWonderStage -internal fun InternalBoard.toApiBoard(player: Player, lastMove: Move?): ApiBoard = +internal fun InternalBoard.toApiBoard(player: Player, lastMove: Move?, currentAge: Age): ApiBoard = ApiBoard( playerIndex = playerIndex, wonder = wonder.toApiWonder(player, lastMove), @@ -37,7 +38,8 @@ internal fun InternalBoard.toApiBoard(player: Player, lastMove: Move?): ApiBoard bluePoints = getPlayedCards() .filter { it.color == Color.BLUE } .flatMap { it.effects.filterIsInstance<RawPointsIncrease>() } - .sumBy { it.points } + .sumBy { it.points }, + canPlayAnyCardForFree = canPlayFreeCard(currentAge) ) internal fun List<TableCard>.toColumns(): List<List<TableCard>> { diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Table.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Table.kt index 290b3dc9..ccb73cc1 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Table.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/converters/Table.kt @@ -7,7 +7,7 @@ import org.luxons.sevenwonders.engine.boards.Table import org.luxons.sevenwonders.model.TableState internal fun Table.toTableState(): TableState = TableState( - boards = boards.mapIndexed { i, b -> b.toApiBoard(SimplePlayer(i, this), lastPlayedMoves.getOrNull(i)) }, + boards = boards.mapIndexed { i, b -> b.toApiBoard(SimplePlayer(i, this), lastPlayedMoves.getOrNull(i), currentAge) }, currentAge = currentAge, handRotationDirection = handRotationDirection, lastPlayedMoves = lastPlayedMoves.map { it.toPlayedMove() } diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/wonders/Wonder.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/wonders/Wonder.kt index 2fd369ee..0e1b37f0 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/wonders/Wonder.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/wonders/Wonder.kt @@ -52,6 +52,8 @@ private object Buildability { fun alreadyBuilt() = WonderBuildability( isBuildable = false, + minPrice = Int.MAX_VALUE, + cheapestTransactions = emptySet(), playabilityLevel = PlayabilityLevel.INCOMPATIBLE_WITH_BOARD ) 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 e1a1b4a7..51a6376a 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 @@ -77,7 +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.PLAY_FREE_DISCARDED -> createPlayFreeDiscardedCardMove(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") @@ -97,7 +97,7 @@ class GameTest { } } - private fun createPlayFreeDiscardedMove(turn: PlayerTurnInfo): MoveExpectation { + private fun createPlayFreeDiscardedCardMove(turn: PlayerTurnInfo): MoveExpectation { val card = turn.discardedCards?.random() ?: error("No discarded card to play") return MoveExpectation( turn.playerIndex, diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt index 64077359..238240ac 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt @@ -68,15 +68,7 @@ private class GameScene(props: GameSceneProps) : RComponent<GameSceneProps, RSta topPlayerBoardsSummaries(topBoards) } handRotationIndicator(turnInfo.table.handRotationDirection) - val hand = turnInfo.hand - if (hand != null) { - handComponent( - cards = hand, - wonderBuildability = turnInfo.wonderBuildability, - preparedMove = props.preparedMove, - prepareMove = props.prepareMove - ) - } + handCards(turnInfo, props.preparedMove, props.prepareMove) val card = props.preparedCard val move = props.preparedMove if (card != null && move != null) { diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt index e2e89a95..dd8595eb 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt @@ -4,137 +4,155 @@ import com.palantir.blueprintjs.* import kotlinx.css.* import kotlinx.css.properties.* import kotlinx.html.DIV -import org.luxons.sevenwonders.model.MoveType -import org.luxons.sevenwonders.model.PlayerMove +import org.luxons.sevenwonders.model.* import org.luxons.sevenwonders.model.cards.CardPlayability import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.model.resources.noTransactions import org.luxons.sevenwonders.model.wonders.WonderBuildability -import react.RBuilder -import react.RElementBuilder +import react.* import styled.StyledDOMBuilder import styled.css import styled.styledDiv import kotlin.math.absoluteValue -fun RBuilder.handComponent( - cards: List<HandCard>, - wonderBuildability: WonderBuildability, - preparedMove: PlayerMove?, - prepareMove: (PlayerMove) -> Unit +private enum class HandAction( + val buttonTitle: String, + val moveType: MoveType, + val icon: IconName ) { - styledDiv { - css { - handStyle() - } - cards.filter { it.name != preparedMove?.cardName }.forEachIndexed { index, c -> - handCard( - card = c, - wonderBuildability = wonderBuildability, - prepareMove = prepareMove - ) { - attrs { - key = index.toString() + PLAY("PLAY", MoveType.PLAY, "play"), + PLAY_FREE("Play as this age's free card", MoveType.PLAY_FREE, "play"), + PLAY_FREE_DISCARDED("Play discarded card", MoveType.PLAY_FREE_DISCARDED, "play"), + COPY_GUILD("Copy this guild card", MoveType.COPY_GUILD, "duplicate") +} + +interface HandProps : RProps { + var turnInfo: PlayerTurnInfo + var preparedMove: PlayerMove? + var prepareMove: (PlayerMove) -> Unit +} + +class HandComponent(props: HandProps) : RComponent<HandProps, RState>(props) { + + override fun RBuilder.render() { + val hand = props.turnInfo.cardsToPlay() ?: return + styledDiv { + css { + handStyle() + } + hand.filter { it.name != props.preparedMove?.cardName }.forEachIndexed { index, c -> + handCard(card = c) { + attrs { + key = index.toString() + } } } } } -} -private fun RBuilder.handCard( - card: HandCard, - wonderBuildability: WonderBuildability, - prepareMove: (PlayerMove) -> Unit, - block: StyledDOMBuilder<DIV>.() -> Unit -) { - styledDiv { - css { - handCardStyle() - } - block() - cardImage(card) { + private fun PlayerTurnInfo.cardsToPlay(): List<HandCard>? = when (action) { + Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> hand + Action.PLAY_FREE_DISCARDED -> discardedCards + Action.PICK_NEIGHBOR_GUILD -> neighbourGuildCards + Action.WAIT, Action.WATCH_SCORE, Action.SAY_READY -> null + } + + private fun RBuilder.handCard( + card: HandCard, + block: StyledDOMBuilder<DIV>.() -> Unit + ) { + styledDiv { css { - handCardImgStyle(card.playability.isPlayable) + handCardStyle() } + block() + cardImage(card) { + css { + handCardImgStyle(card.playability.isPlayable) + } + } + actionButtons(card) } - actionButtons(card, wonderBuildability, prepareMove) } -} -private fun RBuilder.actionButtons(card: HandCard, wonderBuildability: WonderBuildability, prepareMove: (PlayerMove) -> Unit) { - styledDiv { - css { - alignItems = Align.flexEnd - display = Display.none - gridRow = GridRow("1") - gridColumn = GridColumn("1") + private fun RBuilder.actionButtons(card: HandCard) { + styledDiv { + css { + justifyContent = JustifyContent.center + alignItems = Align.flexEnd + display = Display.none + gridRow = GridRow("1") + gridColumn = GridColumn("1") + + ancestorHover(".hand-card") { + display = Display.flex + } + } + bpButtonGroup { + val action = props.turnInfo.action + when (action) { + Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> { + playCardButton(card, HandAction.PLAY) + if (props.turnInfo.getOwnBoard().canPlayAnyCardForFree) { + playCardButton(card.copy(playability = CardPlayability.SPECIAL_FREE), HandAction.PLAY_FREE) + } + } + Action.PLAY_FREE_DISCARDED -> playCardButton(card, HandAction.PLAY_FREE_DISCARDED) + Action.PICK_NEIGHBOR_GUILD -> playCardButton(card, HandAction.COPY_GUILD) + else -> error("unsupported action in hand card: $action") + } - ancestorHover(".hand-card") { - display = Display.flex + if (action.allowsBuildingWonder()) { + upgradeWonderButton(card) + } + if (action.allowsDiscarding()) { + discardButton(card) + } } } - bpButtonGroup { - playCardButton(card, prepareMove) - upgradeWonderButton(wonderBuildability, prepareMove, card) - discardButton(prepareMove, card) - } } -} -private fun RElementBuilder<IButtonGroupProps>.playCardButton( - card: HandCard, - prepareMove: (PlayerMove) -> Unit -) { - bpButton( - title = "PLAY (${cardPlayabilityInfo(card.playability)})", - large = true, - intent = Intent.SUCCESS, - disabled = !card.playability.isPlayable, - onClick = { - val transactions = card.playability.cheapestTransactions.firstOrNull() ?: noTransactions() - prepareMove(PlayerMove(MoveType.PLAY, card.name, transactions)) - } - ) { - bpIcon("play") - if (card.playability.isPlayable && !card.playability.isFree) { - priceInfo(card.playability.minPrice) + private fun RElementBuilder<IButtonGroupProps>.playCardButton(card: HandCard, handAction: HandAction) { + bpButton(title = "${handAction.buttonTitle} (${cardPlayabilityInfo(card.playability)})", + large = true, + intent = Intent.SUCCESS, + disabled = !card.playability.isPlayable, + onClick = { + val transactions = card.playability.cheapestTransactions.first() + props.prepareMove(PlayerMove(handAction.moveType, card.name, transactions)) + }) { + bpIcon(handAction.icon) + if (card.playability.isPlayable && !card.playability.isFree) { + priceInfo(card.playability.minPrice) + } } } -} -private fun RElementBuilder<IButtonGroupProps>.upgradeWonderButton( - wonderBuildability: WonderBuildability, - prepareMove: (PlayerMove) -> Unit, - card: HandCard -) { - bpButton( - title = "UPGRADE WONDER (${wonderBuildabilityInfo(wonderBuildability)})", - large = true, - intent = Intent.PRIMARY, - disabled = !wonderBuildability.isBuildable, - onClick = { - val transactions = wonderBuildability.cheapestTransactions.firstOrNull() ?: noTransactions() - prepareMove(PlayerMove(MoveType.UPGRADE_WONDER, card.name, transactions)) - } - ) { - bpIcon("key-shift") - if (wonderBuildability.isBuildable && !wonderBuildability.isFree) { - priceInfo(wonderBuildability.minPrice) + private fun RElementBuilder<IButtonGroupProps>.upgradeWonderButton(card: HandCard) { + val wonderBuildability = props.turnInfo.wonderBuildability + bpButton(title = "UPGRADE WONDER (${wonderBuildabilityInfo(wonderBuildability)})", + large = true, + intent = Intent.PRIMARY, + disabled = !wonderBuildability.isBuildable, + onClick = { + val transactions = wonderBuildability.cheapestTransactions.first() + props.prepareMove(PlayerMove(MoveType.UPGRADE_WONDER, card.name, transactions)) + }) { + bpIcon("key-shift") + if (wonderBuildability.isBuildable && !wonderBuildability.isFree) { + priceInfo(wonderBuildability.minPrice) + } } } -} -private fun RElementBuilder<IButtonGroupProps>.discardButton( - prepareMove: (PlayerMove) -> Unit, - card: HandCard -) { - bpButton( - title = "DISCARD (+3 coins)", // TODO remove hardcoded value - large = true, - intent = Intent.DANGER, - icon = "cross", - onClick = { prepareMove(PlayerMove(MoveType.DISCARD, card.name)) } - ) + private fun RElementBuilder<IButtonGroupProps>.discardButton(card: HandCard) { + bpButton( + title = "DISCARD (+3 coins)", // TODO remove hardcoded value + large = true, + intent = Intent.DANGER, + icon = "cross", + onClick = { props.prepareMove(PlayerMove(MoveType.DISCARD, card.name)) } + ) + } } private fun cardPlayabilityInfo(playability: CardPlayability) = when (playability.isPlayable) { @@ -224,3 +242,17 @@ private fun CSSBuilder.handCardImgStyle(isPlayable: Boolean) { filter = "grayscale(50%) contrast(50%)" } } + +fun RBuilder.handCards( + turnInfo: PlayerTurnInfo, + preparedMove: PlayerMove?, + prepareMove: (PlayerMove) -> Unit +) { + child(HandComponent::class) { + attrs { + this.turnInfo = turnInfo + this.preparedMove = preparedMove + this.prepareMove = prepareMove + } + } +} |