diff options
9 files changed, 96 insertions, 58 deletions
diff --git a/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt b/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt index 84176e03..d95c32f5 100644 --- a/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt +++ b/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt @@ -113,9 +113,6 @@ class SevenWondersSession(private val stompSession: StompSessionWithKxSerializat suspend fun watchPlayerReady(gameId: Long): StompSubscription<String> = stompSession.subscribe("/topic/game/$gameId/playerReady", String.serializer()) - suspend fun watchTableUpdates(gameId: Long): StompSubscription<GameState> = - stompSession.subscribe("/topic/game/$gameId/tableUpdates", GameState.serializer()) - suspend fun watchPreparedCards(gameId: Long): StompSubscription<PreparedCard> = stompSession.subscribe("/topic/game/$gameId/prepared", PreparedCard.serializer()) 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 05b95b17..22cbfc8c 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 @@ -8,6 +8,7 @@ import org.luxons.sevenwonders.model.resources.noTransactions import org.luxons.sevenwonders.model.wonders.WonderBuildability enum class Action(val message: String) { + SAY_READY("Say when you're ready to get your next hand"), 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."), @@ -20,7 +21,7 @@ data class PlayerTurnInfo( val playerIndex: Int, val table: GameState, val action: Action, - val hand: List<HandCard>, + val hand: List<HandCard>?, val preparedMove: PlayedMove?, val neighbourGuildCards: List<TableCard> ) { diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/Api.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/Api.kt index aca9b140..d28c8ca3 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/Api.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/Api.kt @@ -31,5 +31,6 @@ data class PlayerDTO( val displayName: String, val index: Int, val isGameOwner: Boolean, - val isMe: Boolean + val isMe: Boolean, + val isReady: Boolean ) diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/api/Converters.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/api/Converters.kt index 5fb73b23..a2390a7d 100644 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/api/Converters.kt +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/api/Converters.kt @@ -13,4 +13,4 @@ fun Lobby.toDTO(currentPlayer: Player): LobbyDTO { } fun Player.toDTO(currentUser: String) = - PlayerDTO(username, displayName, index, isGameOwner, username === currentUser) + PlayerDTO(username, displayName, index, isGameOwner, username === currentUser, isReady) diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameController.kt index e1fae39b..0db7a21c 100644 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameController.kt +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameController.kt @@ -2,7 +2,8 @@ package org.luxons.sevenwonders.server.controllers import org.hildan.livedoc.core.annotations.Api import org.luxons.sevenwonders.engine.Game -import org.luxons.sevenwonders.model.GameState +import org.luxons.sevenwonders.model.Action +import org.luxons.sevenwonders.model.PlayerTurnInfo import org.luxons.sevenwonders.model.api.actions.PrepareMoveAction import org.luxons.sevenwonders.model.cards.PreparedCard import org.luxons.sevenwonders.server.api.toDTO @@ -40,8 +41,7 @@ class GameController @Autowired constructor( val game = player.game logger.info("Game {}: player {} is ready for the next turn", game.id, player) - val lobby = player.lobby - val players = lobby.getPlayers() + val players = player.lobby.getPlayers() sendPlayerReady(game.id, player) @@ -49,20 +49,10 @@ class GameController @Autowired constructor( if (allReady) { logger.info("Game {}: all players ready, sending turn info", game.id) players.forEach { it.isReady = false } - sendTurnInfo(players, game) + sendTurnInfo(players, game, false) } } - private fun sendTurnInfo(players: List<Player>, game: Game) { - for (turnInfo in game.getCurrentTurnInfo()) { - val player = players[turnInfo.playerIndex] - template.convertAndSendToUser(player.username, "/queue/game/turn", turnInfo) - } - } - - private fun sendPlayerReady(gameId: Long, player: Player) = - template.convertAndSend("/topic/game/$gameId/playerReady", "\"${player.username}\"") - /** * Prepares the player's next move. When all players have prepared their moves, all moves are executed. * @@ -82,16 +72,28 @@ class GameController @Autowired constructor( if (game.allPlayersPreparedTheirMove()) { logger.info("Game {}: all players have prepared their move, executing turn...", game.id) - val table = game.playTurn() - sendPlayedMoves(game.id, table) + game.playTurn() + sendTurnInfo(player.lobby.getPlayers(), game, true) } } - private fun sendPlayedMoves(gameId: Long, gameState: GameState) = - template.convertAndSend("/topic/game/$gameId/tableUpdates", gameState) + private fun sendPlayerReady(gameId: Long, player: Player) = + template.convertAndSend("/topic/game/$gameId/playerReady", "\"${player.username}\"") private fun sendPreparedCard(gameId: Long, preparedCard: PreparedCard) = - template.convertAndSend("/topic/game/$gameId/prepared", preparedCard) + template.convertAndSend("/topic/game/$gameId/prepared", preparedCard) + + private fun sendTurnInfo(players: List<Player>, game: Game, hideHands: Boolean) { + val turns = game.getCurrentTurnInfo() + val turnsToSend = if (hideHands) turns.hideHandsAndWaitForReadiness() else turns + for (turnInfo in turnsToSend) { + val player = players[turnInfo.playerIndex] + template.convertAndSendToUser(player.username, "/queue/game/turn", turnInfo) + } + } + + private fun Collection<PlayerTurnInfo>.hideHandsAndWaitForReadiness() = + map { it.copy(action = Action.SAY_READY, hand = null) } companion object { private val logger = LoggerFactory.getLogger(GameController::class.java) diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt index 11ce09c1..d9de85d9 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt @@ -2,6 +2,7 @@ package org.luxons.sevenwonders.ui.components.game import com.palantir.blueprintjs.Intent import com.palantir.blueprintjs.bpButton +import com.palantir.blueprintjs.bpButtonGroup import com.palantir.blueprintjs.bpNonIdealState import com.palantir.blueprintjs.org.luxons.sevenwonders.ui.components.game.handComponent import kotlinx.css.CSSBuilder @@ -12,10 +13,16 @@ import kotlinx.css.backgroundSize import kotlinx.css.bottom import kotlinx.css.left import kotlinx.css.overflow +import kotlinx.css.pct import kotlinx.css.position +import kotlinx.css.properties.transform +import kotlinx.css.properties.translate import kotlinx.css.px +import kotlinx.css.rem import kotlinx.css.right import kotlinx.css.top +import kotlinx.html.DIV +import org.luxons.sevenwonders.model.Action import org.luxons.sevenwonders.model.PlayerMove import org.luxons.sevenwonders.model.PlayerTurnInfo import org.luxons.sevenwonders.model.api.PlayerDTO @@ -28,11 +35,14 @@ import react.RClass import react.RComponent import react.RProps import react.RState +import react.ReactElement import react.dom.* +import styled.StyledDOMBuilder import styled.css import styled.styledDiv interface GameSceneStateProps: RProps { + var playerIsReady: Boolean var players: List<PlayerDTO> var turnInfo: PlayerTurnInfo? } @@ -55,45 +65,76 @@ private class GameScene(props: GameSceneProps) : RComponent<GameSceneProps, RSta } val turnInfo = props.turnInfo if (turnInfo == null) { - gamePreStart(props.sayReady) + gamePreStart() } else { turnInfoScene(turnInfo) } } } - private fun RBuilder.gamePreStart(onReadyClicked: () -> Unit) { + private fun RBuilder.gamePreStart() { bpNonIdealState( description = createElement { p { +"Click 'ready' when you are"} }, action = createElement { + sayReadyButton() + } + ) + } + + private fun RBuilder.sayReadyButton(block: StyledDOMBuilder<DIV>.() -> Unit = {}): ReactElement { + val isReady = props.playerIsReady + val intent = if (isReady) Intent.SUCCESS else Intent.PRIMARY + return styledDiv { + bpButtonGroup { bpButton( large = true, - intent = Intent.PRIMARY, - icon = "play", - onClick = { onReadyClicked() } + disabled = isReady, + intent = intent, + icon = if (isReady) "tick-circle" else "play", + onClick = { props.sayReady() } ) { +"READY" } + bpButton( + large = true, + icon = "people", + disabled = isReady, + intent = intent + ) { + +"${props.players.count { it.isReady }}/${props.players.size}" + } } - ) + block() + } } private fun RBuilder.turnInfoScene(turnInfo: PlayerTurnInfo) { val board = turnInfo.table.boards[turnInfo.playerIndex] div { + // TODO use blueprint's Callout component without header and primary intent p { + turnInfo.message } boardComponent(board = board) - handComponent( - cards = turnInfo.hand, - wonderUpgradable = turnInfo.wonderBuildability.isBuildable, - prepareMove = props.prepareMove - ) - productionBar( - gold = board.gold, - production = board.production - ) + val hand = turnInfo.hand + if (hand != null) { + handComponent( + cards = hand, + wonderUpgradable = turnInfo.wonderBuildability.isBuildable, + prepareMove = props.prepareMove + ) + } + if (turnInfo.action == Action.SAY_READY) { + sayReadyButton { + css { + position = Position.absolute + bottom = 4.rem + left = 50.pct + transform { translate(tx = (-50).pct) } + } + } + } + productionBar(gold = board.gold, production = board.production) } } } @@ -108,6 +149,7 @@ private val gameScene: RClass<GameSceneProps> = connectStateAndDispatch<GameScen sayReady = { dispatch(RequestSayReady()) } }, mapStateToProps = { state, _ -> + playerIsReady = state.currentPlayer?.isReady == true players = state.currentLobby?.players ?: emptyList() turnInfo = state.currentTurnInfo } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt index 223cd5c1..37238e0a 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt @@ -1,6 +1,5 @@ package org.luxons.sevenwonders.ui.redux -import org.luxons.sevenwonders.model.GameState import org.luxons.sevenwonders.model.PlayerTurnInfo import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.model.api.PlayerDTO @@ -24,5 +23,3 @@ data class TurnInfoEvent(val turnInfo: PlayerTurnInfo): RAction data class PreparedCardEvent(val card: PreparedCard): RAction data class PlayerReadyEvent(val username: String): RAction - -data class TableUpdateEvent(val table: GameState): RAction diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt index 28bb7993..507a3941 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt @@ -1,6 +1,5 @@ package org.luxons.sevenwonders.ui.redux -import org.luxons.sevenwonders.model.GameState import org.luxons.sevenwonders.model.PlayerTurnInfo import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.model.api.PlayerDTO @@ -11,8 +10,7 @@ data class SwState( val gamesById: Map<Long, LobbyDTO> = emptyMap(), val currentPlayerUsername: String? = null, val currentLobbyId: Long? = null, - val currentTurnInfo: PlayerTurnInfo? = null, - val currentTable: GameState? = null + val currentTurnInfo: PlayerTurnInfo? = null ) { val games: List<LobbyDTO> = gamesById.values.toList() val currentLobby: LobbyDTO? = currentLobbyId?.let { gamesById[it] } @@ -24,14 +22,15 @@ fun rootReducer(state: SwState, action: RAction): SwState = state.copy( gamesById = gamesReducer(state.gamesById, action), currentPlayerUsername = currentPlayerReducer(state.currentPlayerUsername, action), currentLobbyId = currentLobbyReducer(state.currentLobbyId, action), - currentTurnInfo = currentTurnInfoReducer(state.currentTurnInfo, action), - currentTable = currentTableReducer(state.currentTable, action) + currentTurnInfo = currentTurnInfoReducer(state.currentTurnInfo, action) ) private fun playersReducer(playersByUsername: Map<String, PlayerDTO>, action: RAction): Map<String, PlayerDTO> = when (action) { is UpdatePlayers -> playersByUsername + action.players is UpdateLobbyAction -> playersByUsername + action.lobby.players.associateBy { it.username } is SetCurrentPlayerAction -> playersByUsername + (action.player.username to action.player) + is PlayerReadyEvent -> playersByUsername + (action.username to playersByUsername.getValue(action.username) + .copy(isReady = true)) else -> playersByUsername } @@ -39,6 +38,15 @@ private fun gamesReducer(games: Map<Long, LobbyDTO>, action: RAction): Map<Long, is UpdateGameListAction -> action.games.associateBy { it.id } // replaces because should remove deleted games is EnterLobbyAction -> games + (action.lobby.id to action.lobby) is UpdateLobbyAction -> games + (action.lobby.id to action.lobby) + is PlayerReadyEvent -> games.mapValues { (_, l) -> + if (l.players.any { it.username == action.username }) { + l.copy(players = l.players.map { p -> + if (p.username == action.username) p.copy(isReady = true) else p + }) + } else { + l + } + } else -> games } @@ -54,12 +62,5 @@ private fun currentLobbyReducer(currentLobbyId: Long?, action: RAction): Long? = private fun currentTurnInfoReducer(currentTurnInfo: PlayerTurnInfo?, action: RAction): PlayerTurnInfo? = when (action) { is TurnInfoEvent -> action.turnInfo - is TableUpdateEvent -> null else -> currentTurnInfo } - -private fun currentTableReducer(currentTable: GameState?, action: RAction): GameState? = when (action) { - is TurnInfoEvent -> action.turnInfo.table - is TableUpdateEvent -> action.table - else -> currentTable -} diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt index e3675799..cadf56e9 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt @@ -7,7 +7,6 @@ import org.luxons.sevenwonders.ui.redux.PlayerReadyEvent import org.luxons.sevenwonders.ui.redux.PreparedCardEvent import org.luxons.sevenwonders.ui.redux.RequestPrepareMove import org.luxons.sevenwonders.ui.redux.RequestSayReady -import org.luxons.sevenwonders.ui.redux.TableUpdateEvent import org.luxons.sevenwonders.ui.redux.TurnInfoEvent suspend fun SwSagaContext.gameSaga(session: SevenWondersSession) { @@ -15,13 +14,11 @@ suspend fun SwSagaContext.gameSaga(session: SevenWondersSession) { coroutineScope { val playerReadySub = session.watchPlayerReady(lobby.id) val preparedCardsSub = session.watchPreparedCards(lobby.id) - val tableUpdatesSub = session.watchTableUpdates(lobby.id) val turnInfoSub = session.watchTurns() val sayReadyJob = launch { onEach<RequestSayReady> { session.sayReady() } } val prepareMoveJob = launch { onEach<RequestPrepareMove> { session.prepareMove(it.move) } } launch { dispatchAll(playerReadySub.messages) { PlayerReadyEvent(it) } } launch { dispatchAll(preparedCardsSub.messages) { PreparedCardEvent(it) } } - launch { dispatchAll(tableUpdatesSub.messages) { TableUpdateEvent(it) } } launch { dispatchAll(turnInfoSub.messages) { TurnInfoEvent(it) } } // TODO await game end // TODO unsubscribe all subs, cancel all jobs |