diff options
26 files changed, 357 insertions, 120 deletions
diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt index e05bf319..0cee6531 100644 --- a/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt @@ -2,10 +2,12 @@ package org.luxons.sevenwonders.controllers import org.hildan.livedoc.core.annotations.Api import org.luxons.sevenwonders.actions.PrepareMoveAction +import org.luxons.sevenwonders.api.PlayerDTO +import org.luxons.sevenwonders.api.toDTO import org.luxons.sevenwonders.game.Game import org.luxons.sevenwonders.game.api.Table +import org.luxons.sevenwonders.game.cards.CardBack import org.luxons.sevenwonders.lobby.Player -import org.luxons.sevenwonders.output.PreparedCard import org.luxons.sevenwonders.repositories.PlayerRepository import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -42,13 +44,13 @@ class GameController @Autowired constructor( val lobby = player.lobby val players = lobby.getPlayers() - val allReady = players.stream().allMatch { it.isReady } + sendPlayerReady(game.id, player) + + val allReady = players.all { it.isReady } if (allReady) { logger.info("Game {}: all players ready, sending turn info", game.id) players.forEach { it.isReady = false } sendTurnInfo(players, game) - } else { - sendPlayerReady(game.id, player) } } @@ -60,7 +62,7 @@ class GameController @Autowired constructor( } private fun sendPlayerReady(gameId: Long, player: Player) = - template.convertAndSend("/topic/game/$gameId/playerReady", player.username) + 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. @@ -75,7 +77,7 @@ class GameController @Autowired constructor( val player = principal.player val game = player.game val preparedCardBack = game.prepareMove(player.index, action.move) - val preparedCard = PreparedCard(player, preparedCardBack) + val preparedCard = PreparedCard(player.toDTO(principal.name), preparedCardBack) logger.info("Game {}: player {} prepared move {}", game.id, principal.name, action.move) if (game.allPlayersPreparedTheirMove()) { @@ -97,3 +99,5 @@ class GameController @Autowired constructor( private val logger = LoggerFactory.getLogger(GameController::class.java) } } + +class PreparedCard(val player: PlayerDTO, val cardBack: CardBack) diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt index a3ccd148..bd672000 100644 --- a/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt @@ -25,10 +25,8 @@ class HomeController @Autowired constructor( /** * Creates/updates the player's name (for the user's session). * - * @param action - * the action to choose the name of the player - * @param principal - * the connected user's information + * @param action the action to choose the name of the player + * @param principal the connected user's information * * @return the created [PlayerDTO] object */ diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/output/PreparedCard.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/output/PreparedCard.kt deleted file mode 100644 index 956b1a2c..00000000 --- a/backend/src/main/kotlin/org/luxons/sevenwonders/output/PreparedCard.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.luxons.sevenwonders.output - -import org.luxons.sevenwonders.game.cards.CardBack -import org.luxons.sevenwonders.lobby.Player - -class PreparedCard(val player: Player, val cardBack: CardBack) diff --git a/frontend/src/api/model.js b/frontend/src/api/model.js index 5d1f92c6..3408d5a2 100644 --- a/frontend/src/api/model.js +++ b/frontend/src/api/model.js @@ -37,18 +37,28 @@ export type ApiPlayer = { username: string, displayName: string, index: number, - ready: boolean + gameOwner: Boolean, + user: Boolean, }; -export type ApiTable = {}; +export type ApiTable = { + +}; export type ApiAction = {}; export type ApiHandCard = {}; -export type ApiCard = {}; +export type ApiTableCard = {}; -export type ApiPreparedCard = {}; +export type ApiCardBack = { + image: string +}; + +export type ApiPreparedCard = { + player: ApiPlayer, + cardBack: ApiCardBack +}; export type ApiPlayerTurnInfo = { playerIndex: number, @@ -56,7 +66,7 @@ export type ApiPlayerTurnInfo = { currentAge: number, action: ApiAction, hand: ApiHandCard[], - neighbourGuildCards: ApiCard[], + neighbourGuildCards: ApiTableCard[], message: string }; diff --git a/frontend/src/api/sevenWondersApi.js b/frontend/src/api/sevenWondersApi.js index b5a7df7e..8c4aedaf 100644 --- a/frontend/src/api/sevenWondersApi.js +++ b/frontend/src/api/sevenWondersApi.js @@ -81,7 +81,7 @@ export class SevenWondersSession { return this.client.subscriber(`/topic/game/${currentGameId}/tableUpdates`); } - watchPreparedMove(currentGameId: number): SubscribeFn<ApiPreparedCard> { + watchPreparedCards(currentGameId: number): SubscribeFn<ApiPreparedCard> { return this.client.subscriber(`/topic/game/${currentGameId}/prepared`); } diff --git a/frontend/src/components/Application.jsx b/frontend/src/components/Application.jsx index d7e1738c..e0ec604d 100644 --- a/frontend/src/components/Application.jsx +++ b/frontend/src/components/Application.jsx @@ -1,11 +1,13 @@ import React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { GameBrowser } from './game-browser/GameBrowser'; +import { GameScene } from './game/GameScene'; import { Lobby } from './lobby/Lobby'; import { Home } from './home/Home'; export const Application = () => ( <Switch> + <Route path="/game" component={GameScene} /> <Route path="/games" component={GameBrowser} /> <Route path="/lobby" component={Lobby} /> <Route path="/" component={Home} /> diff --git a/frontend/src/components/game-browser/GameBrowser.jsx b/frontend/src/components/game-browser/GameBrowser.jsx index 10d823b4..cfa0e45e 100644 --- a/frontend/src/components/game-browser/GameBrowser.jsx +++ b/frontend/src/components/game-browser/GameBrowser.jsx @@ -3,9 +3,9 @@ import { Button, Classes, InputGroup, Intent } from '@blueprintjs/core'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Flex } from 'reflexbox'; +import { actions } from '../../redux/actions/lobby'; import { GameList } from './GameList'; import { PlayerInfo } from './PlayerInfo'; -import { actions } from '../../redux/games'; type GameBrowserProps = { createGame: (gameName: string) => void, @@ -26,12 +26,14 @@ class GameBrowserPresenter extends Component<GameBrowserProps> { return ( <div> <Flex align="center" justify='space-between' p={1}> - <InputGroup - placeholder="Game name" - name="game_name" - onChange={(e: SyntheticInputEvent<*>) => (this._gameName = e.target.value)} - rightElement={<CreateGameButton onClick={this.createGame}/>} - /> + <form onSubmit={this.createGame}> + <InputGroup + placeholder="Game name" + name="game_name" + onChange={(e: SyntheticInputEvent<*>) => (this._gameName = e.target.value)} + rightElement={<CreateGameButton onClick={this.createGame}/>} + /> + </form> <PlayerInfo /> </Flex> <GameList /> diff --git a/frontend/src/components/game-browser/GameList.jsx b/frontend/src/components/game-browser/GameList.jsx index 6363cff2..64ced9b0 100644 --- a/frontend/src/components/game-browser/GameList.jsx +++ b/frontend/src/components/game-browser/GameList.jsx @@ -3,7 +3,8 @@ import type { List } from 'immutable'; import React from 'react'; import { connect } from 'react-redux'; import type { Game } from '../../models/games'; -import { actions, getAllGames } from '../../redux/games'; +import { actions } from '../../redux/actions/lobby'; +import { getAllGames } from '../../redux/games'; import { IconButton } from '../shared/IconButton'; import './GameList.css'; import { GameStatus } from './GameStatus'; diff --git a/frontend/src/components/game/GameScene.jsx b/frontend/src/components/game/GameScene.jsx new file mode 100644 index 00000000..fb5763db --- /dev/null +++ b/frontend/src/components/game/GameScene.jsx @@ -0,0 +1,64 @@ +import { Button, Classes, Intent } from '@blueprintjs/core'; +import { List } from 'immutable'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import type { ApiPlayerTurnInfo } from '../../api/model'; +import { Game } from '../../models/games'; +import { Player } from '../../models/players'; +import { actions } from '../../redux/actions/game'; +import { getCurrentTurnInfo } from '../../redux/currentGame'; +import { getCurrentGame } from '../../redux/games'; + +import { getCurrentPlayer, getPlayers } from '../../redux/players'; +import { PlayerList } from '../lobby/PlayerList'; + +type GameSceneProps = { + game: Game, + currentPlayer: Player, + players: List<Player>, + turnInfo: ApiPlayerTurnInfo, + sayReady: () => void +} + +class GameScenePresenter extends Component<GameSceneProps> { + getTitle() { + if (this.props.game) { + return this.props.game.name + ' — Game'; + } else { + return 'What are you doing here? You haven\'t joined a game yet!'; + } + } + + render() { + return ( + <div> + <h2>{this.getTitle()}</h2> + <PlayerList players={this.props.players} currentPlayer={this.props.currentPlayer} owner={this.props.game.owner}/> + <Button text="READY" className={Classes.LARGE} intent={Intent.PRIMARY} icon='play' onClick={this.props.sayReady} /> + + <h3>Turn Info</h3> + <div> + <pre>{JSON.stringify(this.props.turnInfo, null, 2) }</pre> + </div> + </div> + ); + } +} + +const mapStateToProps: (state) => GameSceneProps = state => { + const game = getCurrentGame(state.get('games')); + console.info(game); + + return { + game: game, + currentPlayer: getCurrentPlayer(state), + players: game ? getPlayers(state.get('players'), game.players) : new List(), + turnInfo: getCurrentTurnInfo(state.get('currentGame')) + }; +}; + +const mapDispatchToProps = { + sayReady: actions.sayReady, +}; + +export const GameScene = connect(mapStateToProps, mapDispatchToProps)(GameScenePresenter); diff --git a/frontend/src/components/home/ChooseNameForm.jsx b/frontend/src/components/home/ChooseNameForm.jsx index 614ad172..619d967c 100644 --- a/frontend/src/components/home/ChooseNameForm.jsx +++ b/frontend/src/components/home/ChooseNameForm.jsx @@ -2,7 +2,7 @@ import { Classes, InputGroup, Intent } from '@blueprintjs/core'; import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { actions } from '../../redux/players'; +import { actions } from '../../redux/actions/players'; import { IconButton } from '../shared/IconButton'; type ChooseNameFormPresenterProps = { diff --git a/frontend/src/components/lobby/Lobby.jsx b/frontend/src/components/lobby/Lobby.jsx index 82c00b20..df6557af 100644 --- a/frontend/src/components/lobby/Lobby.jsx +++ b/frontend/src/components/lobby/Lobby.jsx @@ -5,7 +5,8 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import type { Game } from '../../models/games'; import type { Player } from '../../models/players'; -import { actions, getCurrentGame } from '../../redux/games'; +import { actions } from '../../redux/actions/lobby'; +import { getCurrentGame } from '../../redux/games'; import { getCurrentPlayer, getPlayers } from '../../redux/players'; import { RadialPlayerList } from './RadialPlayerList'; diff --git a/frontend/src/models/currentGame.js b/frontend/src/models/currentGame.js new file mode 100644 index 00000000..fcabe875 --- /dev/null +++ b/frontend/src/models/currentGame.js @@ -0,0 +1,6 @@ +import type { ApiPlayerTurnInfo } from '../api/model'; + +export class CurrentGameState { + playersReadiness: Map<string, boolean> = new Map(); + turnInfo: ApiPlayerTurnInfo | null = null +} diff --git a/frontend/src/models/games.js b/frontend/src/models/games.js index 7a8dfbc7..85aab2df 100644 --- a/frontend/src/models/games.js +++ b/frontend/src/models/games.js @@ -15,6 +15,7 @@ export type SettingsShape = { wonderSidePickMethod: "EACH_RANDOM" | "TODO", pointsPer3Gold: number }; + export type SettingsType = Record<SettingsShape>; const SettingsRecord: SettingsType = Record({ @@ -32,6 +33,7 @@ const SettingsRecord: SettingsType = Record({ wonderSidePickMethod: 'EACH_RANDOM', pointsPer3Gold: 1, }); + export class Settings extends SettingsRecord {} export type GameState = 'LOBBY' | 'PLAYING'; @@ -43,6 +45,7 @@ export type GameShape = { settings: SettingsType, state: GameState, }; + export type GameType = Record<GameShape>; export type GameMapType = Map<string, GameShape>; export type GameNormalMapType = { [string]: GameShape }; @@ -55,18 +58,21 @@ const GameRecord: GameType = Record({ settings: new Settings(), state: 'LOBBY', }); + export class Game extends GameRecord {} export type GamesShape = { all: Map<Game>, current: string }; + export type GamesType = Record<GamesShape>; const GamesRecord: GamesType = Record({ all: new Map(), current: null, }); + export class GamesState extends GamesRecord { addGame(g: GameShape) { const game: Game = new Game(g); diff --git a/frontend/src/reducers.js b/frontend/src/reducers.js index 85f7e3c1..f5e7b18d 100644 --- a/frontend/src/reducers.js +++ b/frontend/src/reducers.js @@ -1,11 +1,13 @@ // @flow import { routerReducer } from 'react-router-redux'; import { combineReducers } from 'redux-immutable'; +import { currentGameReducer } from './redux/currentGame'; import { gamesReducer } from './redux/games'; import { playersReducer } from './redux/players'; export function createReducer() { return combineReducers({ + currentGame: currentGameReducer, games: gamesReducer, players: playersReducer, routing: routerReducer, diff --git a/frontend/src/redux/actions/all.js b/frontend/src/redux/actions/all.js new file mode 100644 index 00000000..45d3ab7a --- /dev/null +++ b/frontend/src/redux/actions/all.js @@ -0,0 +1,5 @@ +import type { GameAction } from './game'; +import type { LobbyAction } from './lobby'; +import type { PlayerAction } from './players'; + +export type Action = PlayerAction | LobbyAction | GameAction diff --git a/frontend/src/redux/actions/game.js b/frontend/src/redux/actions/game.js new file mode 100644 index 00000000..1fe49dfb --- /dev/null +++ b/frontend/src/redux/actions/game.js @@ -0,0 +1,25 @@ +import type { ApiPlayerTurnInfo, ApiPreparedCard, ApiTable } from '../../api/model'; + +export const types = { + REQUEST_SAY_READY: 'GAME/REQUEST_SAY_READY', + PLAYER_READY_RECEIVED: 'GAME/PLAYER_READY_RECEIVED', + TABLE_UPDATE_RECEIVED: 'GAME/TABLE_UPDATE_RECEIVED', + PREPARED_CARD_RECEIVED: 'GAME/PREPARED_CARD_RECEIVED', + TURN_INFO_RECEIVED: 'GAME/TURN_INFO_RECEIVED', +}; + +export type SayReadyAction = { type: 'GAME/REQUEST_SAY_READY' }; +export type PlayerReadyEvent = { type: 'GAME/PLAYER_READY_RECEIVED', username: string }; +export type TableUpdateEvent = { type: 'GAME/TABLE_UPDATE_RECEIVED', table: ApiTable }; +export type PreparedCardEvent = { type: 'GAME/PREPARED_CARD_RECEIVED', card: ApiPreparedCard }; +export type TurnInfoEvent = { type: 'GAME/TURN_INFO_RECEIVED', turnInfo: ApiPlayerTurnInfo }; + +export type GameAction = SayReadyAction | PlayerReadyEvent | TableUpdateEvent | PreparedCardEvent | TurnInfoEvent; + +export const actions = { + sayReady: () => ({ type: types.REQUEST_SAY_READY }), + receivePlayerReady: (username: string) => ({ type: types.PLAYER_READY_RECEIVED, username }), + receiveTableUpdate: (table: ApiTable) => ({ type: types.TABLE_UPDATE_RECEIVED, table }), + receivePreparedCard: (card: ApiPreparedCard) => ({ type: types.PREPARED_CARD_RECEIVED, card }), + receiveTurnInfo: (turnInfo: ApiPlayerTurnInfo) => ({ type: types.TURN_INFO_RECEIVED, turnInfo }), +}; diff --git a/frontend/src/redux/actions/lobby.js b/frontend/src/redux/actions/lobby.js new file mode 100644 index 00000000..b3151a23 --- /dev/null +++ b/frontend/src/redux/actions/lobby.js @@ -0,0 +1,35 @@ +import { fromJS } from 'immutable'; +import type { GameMapType, GameNormalMapType } from '../../models/games'; + +export const types = { + UPDATE_GAMES: 'GAMES/UPDATE_GAMES', + REQUEST_CREATE_GAME: 'GAMES/REQUEST_CREATE_GAME', + REQUEST_JOIN_GAME: 'GAMES/REQUEST_JOIN_GAME', + REQUEST_START_GAME: 'GAMES/REQUEST_START_GAME', + ENTER_LOBBY: 'GAMES/ENTER_LOBBY', + ENTER_GAME: 'GAMES/ENTER_GAME', +}; + +export type UpdateGamesAction = { type: 'GAMES/UPDATE_GAMES', games: GameMapType }; +export type RequestCreateGameAction = { type: 'GAMES/REQUEST_CREATE_GAME', gameName: string }; +export type RequestJoinGameAction = { type: 'GAMES/REQUEST_JOIN_GAME', gameId: number }; +export type RequestStartGameAction = { type: 'GAMES/REQUEST_START_GAME' }; +export type EnterLobbyAction = { type: 'GAMES/ENTER_LOBBY', gameId: number }; +export type EnterGameAction = { type: 'GAMES/ENTER_GAME', gameId: number }; + +export type LobbyAction = + | UpdateGamesAction + | RequestCreateGameAction + | RequestJoinGameAction + | RequestStartGameAction + | EnterLobbyAction + | EnterGameAction; + +export const actions = { + updateGames: (games: GameNormalMapType): UpdateGamesAction => ({ type: types.UPDATE_GAMES, games: fromJS(games) }), + requestJoinGame: (gameId: number): RequestJoinGameAction => ({ type: types.REQUEST_JOIN_GAME, gameId }), + requestCreateGame: (gameName: string): RequestCreateGameAction => ({ type: types.REQUEST_CREATE_GAME, gameName }), + requestStartGame: (): RequestStartGameAction => ({ type: types.REQUEST_START_GAME }), + enterLobby: (gameId: number): EnterLobbyAction => ({ type: types.ENTER_LOBBY, gameId }), + enterGame: (gameId: number): EnterGameAction => ({ type: types.ENTER_GAME, gameId }), +}; diff --git a/frontend/src/redux/actions/players.js b/frontend/src/redux/actions/players.js new file mode 100644 index 00000000..7df174c4 --- /dev/null +++ b/frontend/src/redux/actions/players.js @@ -0,0 +1,29 @@ +import { Map } from 'immutable'; +import { PlayerShape } from '../../models/players'; + +export const types = { + REQUEST_CHOOSE_USERNAME: 'USER/REQUEST_CHOOSE_USERNAME', + SET_CURRENT_PLAYER: 'USER/SET_CURRENT_PLAYER', + UPDATE_PLAYERS: 'USER/UPDATE_PLAYERS', +}; + +export type RequestChooseUsernameAction = { type: types.REQUEST_CHOOSE_USERNAME, username: string }; +export type SetCurrentPlayerAction = { type: types.SET_CURRENT_PLAYER, player: PlayerShape }; +export type UpdatePlayersAction = { type: types.UPDATE_PLAYERS, players: Map<string, PlayerShape> }; + +export type PlayerAction = RequestChooseUsernameAction | SetCurrentPlayerAction | UpdatePlayersAction; + +export const actions = { + chooseUsername: (username: string): RequestChooseUsernameAction => ({ + type: types.REQUEST_CHOOSE_USERNAME, + username, + }), + setCurrentPlayer: (player: PlayerShape): SetCurrentPlayerAction => ({ + type: types.SET_CURRENT_PLAYER, + player, + }), + updatePlayers: (players: Map<string, PlayerShape>): UpdatePlayersAction => ({ + type: types.UPDATE_PLAYERS, + players, + }), +}; diff --git a/frontend/src/redux/currentGame.js b/frontend/src/redux/currentGame.js new file mode 100644 index 00000000..cefabb6f --- /dev/null +++ b/frontend/src/redux/currentGame.js @@ -0,0 +1,33 @@ +// @flow +import type { ApiPlayerTurnInfo } from '../api/model'; +import { CurrentGameState } from '../models/currentGame'; +import type { Action } from './actions/all'; +import { types } from './actions/game'; + +export const currentGameReducer = (state: CurrentGameState = new CurrentGameState(), action: Action) => { + switch (action.type) { + case types.REQUEST_SAY_READY: + // TODO handle end of feedback between say ready and ready event received + return state; + case types.PLAYER_READY_RECEIVED: + // const newReadiness = state.playersReadiness.set(action.username, true); + // return { playersReadiness: newReadiness, ...state }; + return state; + case types.TABLE_UPDATE_RECEIVED: + // TODO + return state; + case types.PREPARED_CARD_RECEIVED: + // TODO + return state; + case types.TURN_INFO_RECEIVED: + // TODO find a better way to just update what's needed + const newState = new CurrentGameState(); + newState.turnInfo = action.turnInfo; + newState.playersReadiness = state.playersReadiness; + return newState; + default: + return state; + } +}; + +export const getCurrentTurnInfo = (state: CurrentGameState): ApiPlayerTurnInfo => state.turnInfo; diff --git a/frontend/src/redux/games.js b/frontend/src/redux/games.js index ee8b13df..68571981 100644 --- a/frontend/src/redux/games.js +++ b/frontend/src/redux/games.js @@ -1,48 +1,16 @@ // @flow import type { List, Map } from 'immutable'; -import { fromJS } from 'immutable'; -import type { Game, GameMapType, GameNormalMapType, GameShape } from '../models/games'; +import type { Game } from '../models/games'; import { GamesState } from '../models/games'; +import type { Action } from './actions/all'; +import { types } from './actions/lobby'; -export const types = { - UPDATE_GAMES: 'GAMES/UPDATE_GAMES', - REQUEST_CREATE_GAME: 'GAMES/REQUEST_CREATE_GAME', - REQUEST_JOIN_GAME: 'GAMES/REQUEST_JOIN_GAME', - REQUEST_START_GAME: 'GAMES/REQUEST_START_GAME', - ENTER_LOBBY: 'GAMES/ENTER_LOBBY', - ENTER_GAME: 'GAMES/ENTER_GAME', -}; - -export type UpdateGamesAction = { type: 'GAMES/UPDATE_GAMES', games: GameMapType }; -export type RequestCreateGameAction = { type: 'GAMES/REQUEST_CREATE_GAME', gameName: string }; -export type RequestJoinGameAction = { type: 'GAMES/REQUEST_JOIN_GAME', gameId: string }; -export type RequestStartGameAction = { type: 'GAMES/REQUEST_START_GAME' }; -export type EnterLobbyAction = { type: 'GAMES/ENTER_LOBBY', lobby: GameShape }; -export type EnterGameAction = { type: 'GAMES/ENTER_GAME' }; - -export type GamesAction = - | UpdateGamesAction - | RequestCreateGameAction - | RequestJoinGameAction - | RequestStartGameAction - | EnterLobbyAction - | EnterGameAction; - -export const actions = { - updateGames: (games: GameNormalMapType): UpdateGamesAction => ({ type: types.UPDATE_GAMES, games: fromJS(games) }), - requestJoinGame: (gameId: string): RequestJoinGameAction => ({ type: types.REQUEST_JOIN_GAME, gameId }), - requestCreateGame: (gameName: string): RequestCreateGameAction => ({ type: types.REQUEST_CREATE_GAME, gameName }), - requestStartGame: (): RequestStartGameAction => ({ type: types.REQUEST_START_GAME }), - enterLobby: (lobby: GameShape): EnterLobbyAction => ({ type: types.ENTER_LOBBY, lobby: fromJS(lobby) }), - enterGame: (): EnterGameAction => ({ type: types.ENTER_GAME }), -}; - -export const gamesReducer = (state: GamesState = new GamesState(), action: GamesAction) => { +export const gamesReducer = (state: GamesState = new GamesState(), action: Action) => { switch (action.type) { case types.UPDATE_GAMES: return state.addGames(action.games); case types.ENTER_LOBBY: - return state.set('current', action.lobby.get('id')); + return state.set('current', action.gameId); default: return state; } diff --git a/frontend/src/redux/players.js b/frontend/src/redux/players.js index b9f37c8c..ce3c305f 100644 --- a/frontend/src/redux/players.js +++ b/frontend/src/redux/players.js @@ -1,34 +1,9 @@ -import { Map } from 'immutable'; -import { Player, PlayerShape, PlayerState } from '../models/players'; +import { List } from 'immutable'; +import { Player, PlayerState } from '../models/players'; +import type { Action } from './actions/all'; +import { types } from './actions/players'; -export const types = { - REQUEST_CHOOSE_USERNAME: 'USER/REQUEST_CHOOSE_USERNAME', - SET_CURRENT_PLAYER: 'USER/SET_CURRENT_PLAYER', - UPDATE_PLAYERS: 'USER/UPDATE_PLAYERS', -}; - -export type RequestChooseUsernameAction = { type: types.REQUEST_CHOOSE_USERNAME, username: string }; -export type SetCurrentPlayerAction = { type: types.SET_CURRENT_PLAYER, player: PlayerShape }; -export type UpdatePlayersAction = { type: types.UPDATE_PLAYERS, players: Map<string, PlayerShape> }; - -export type PlayerAction = RequestChooseUsernameAction | SetCurrentPlayerAction | UpdatePlayersAction; - -export const actions = { - chooseUsername: (username: string): RequestChooseUsernameAction => ({ - type: types.REQUEST_CHOOSE_USERNAME, - username, - }), - setCurrentPlayer: (player: PlayerShape): SetCurrentPlayerAction => ({ - type: types.SET_CURRENT_PLAYER, - player, - }), - updatePlayers: (players: Map<string, PlayerShape>): UpdatePlayersAction => ({ - type: types.UPDATE_PLAYERS, - players, - }), -}; - -export const playersReducer = (state = new PlayerState(), action: PlayerAction) => { +export const playersReducer = (state = new PlayerState(), action: Action) => { switch (action.type) { case types.SET_CURRENT_PLAYER: return state.addPlayer(action.player); diff --git a/frontend/src/sagas.js b/frontend/src/sagas.js index bbc9a6b6..1aa72215 100644 --- a/frontend/src/sagas.js +++ b/frontend/src/sagas.js @@ -3,6 +3,7 @@ import type { SagaIterator } from 'redux-saga'; import { call, fork } from 'redux-saga/effects'; import { connectToGame, SevenWondersSession } from './api/sevenWondersApi'; import { errorHandlingSaga } from './sagas/errors'; +import { gameSaga } from './sagas/game'; import { gameBrowserSaga } from './sagas/gameBrowser'; import { homeSaga } from './sagas/home'; import { lobbySaga } from './sagas/lobby'; @@ -19,4 +20,5 @@ export function* rootSaga(): SagaIterator { yield fork(homeSaga, sevenWondersSession); yield fork(gameBrowserSaga, sevenWondersSession); yield fork(lobbySaga, sevenWondersSession); + yield fork(gameSaga, sevenWondersSession); } diff --git a/frontend/src/sagas/game.js b/frontend/src/sagas/game.js new file mode 100644 index 00000000..04209e91 --- /dev/null +++ b/frontend/src/sagas/game.js @@ -0,0 +1,74 @@ +import { eventChannel } from 'redux-saga'; +import { apply, call, put, take } from 'redux-saga/effects'; +import type { ApiPlayerTurnInfo, ApiPreparedCard, ApiTable } from '../api/model'; +import { SevenWondersSession } from '../api/sevenWondersApi'; +import { actions } from '../redux/actions/game'; +import { types } from '../redux/actions/game'; +import { types as gameTypes } from '../redux/actions/lobby'; + +function* watchPlayerReady(session: SevenWondersSession, gameId: number) { + const channel = yield eventChannel(session.watchPlayerReady(gameId)); + try { + while (true) { + const username = yield take(channel); + yield put(actions.receivePlayerReady(username)); + } + } finally { + yield apply(channel, channel.close); + } +} + +function* watchTableUpdates(session: SevenWondersSession, gameId: number) { + const channel = yield eventChannel(session.watchTableUpdates(gameId)); + try { + while (true) { + const table: ApiTable = yield take(channel); + yield put(actions.receiveTableUpdate(table)); + } + } finally { + yield apply(channel, channel.close); + } +} + +function* watchPreparedCards(session: SevenWondersSession, gameId: number) { + const channel = yield eventChannel(session.watchPreparedCards(gameId)); + try { + while (true) { + const preparedCard: ApiPreparedCard = yield take(channel); + yield put(actions.receivePreparedCard(preparedCard)); + } + } finally { + yield apply(channel, channel.close); + } +} + +function* sayReady(session: SevenWondersSession) { + while (true) { + yield take(types.REQUEST_SAY_READY); + yield apply(session, session.sayReady); + } +} + +function* watchTurnInfo(session: SevenWondersSession) { + const channel = yield eventChannel(session.watchTurnInfo()); + try { + while (true) { + const turnInfo: ApiPlayerTurnInfo = yield take(channel); + yield put(actions.receiveTurnInfo(turnInfo)); + } + } finally { + yield apply(channel, channel.close); + } +} + +export function* gameSaga(session: SevenWondersSession) { + const { gameId } = yield take(gameTypes.ENTER_GAME); + console.log('Entered game!', gameId); + yield [ + call(watchPlayerReady, session, gameId), + call(watchTableUpdates, session, gameId), + call(watchPreparedCards, session, gameId), + call(sayReady, session), + call(watchTurnInfo, session) + ]; +} diff --git a/frontend/src/sagas/gameBrowser.js b/frontend/src/sagas/gameBrowser.js index 7cd45667..062603a3 100644 --- a/frontend/src/sagas/gameBrowser.js +++ b/frontend/src/sagas/gameBrowser.js @@ -5,8 +5,9 @@ import type { SagaIterator } from 'redux-saga'; import { eventChannel } from 'redux-saga'; import { all, apply, call, put, take } from 'redux-saga/effects'; import type { SevenWondersSession } from '../api/sevenWondersApi'; -import { actions as gameActions, types } from '../redux/games'; -import { actions as playerActions } from '../redux/players'; +import { actions as gameActions } from '../redux/actions/lobby'; +import { types } from '../redux/actions/lobby'; +import { actions as playerActions } from '../redux/actions/players'; import { game as gameSchema, gameList as gameListSchema } from '../schemas/games'; function* watchGames(session: SevenWondersSession): SagaIterator { @@ -32,7 +33,7 @@ function* watchLobbyJoined(session: SevenWondersSession): SagaIterator { const gameId = normalized.result; yield put(playerActions.updatePlayers(normalized.entities.players)); yield put(gameActions.updateGames(normalized.entities.games)); - yield put(gameActions.enterLobby(normalized.entities.games[gameId])); + yield put(gameActions.enterLobby(gameId)); yield put(push(`/lobby/${gameId}`)); } finally { yield apply(joinedLobbyChannel, joinedLobbyChannel.close); diff --git a/frontend/src/sagas/home.js b/frontend/src/sagas/home.js index 328102fb..705a7a40 100644 --- a/frontend/src/sagas/home.js +++ b/frontend/src/sagas/home.js @@ -5,7 +5,8 @@ import { eventChannel } from 'redux-saga'; import { all, apply, call, put, take } from 'redux-saga/effects'; import type { ApiPlayer } from '../api/model'; import type { SevenWondersSession } from '../api/sevenWondersApi'; -import { actions, types } from '../redux/players'; +import { actions } from '../redux/actions/players'; +import { types } from '../redux/actions/players'; function* sendUsername(session: SevenWondersSession): SagaIterator { while (true) { diff --git a/frontend/src/sagas/lobby.js b/frontend/src/sagas/lobby.js index c87f6ad5..b0f52d5c 100644 --- a/frontend/src/sagas/lobby.js +++ b/frontend/src/sagas/lobby.js @@ -5,37 +5,31 @@ import type { Channel, SagaIterator } from 'redux-saga'; import { eventChannel } from 'redux-saga'; import { all, apply, call, put, take } from 'redux-saga/effects'; import { SevenWondersSession } from '../api/sevenWondersApi'; -import { actions as gameActions, types } from '../redux/games'; -import { actions as playerActions } from '../redux/players'; +import { actions as gameActions, types } from '../redux/actions/lobby'; +import { actions as playerActions } from '../redux/actions/players'; import { game as gameSchema } from '../schemas/games'; -function getCurrentGameId(): number { - const path = window.location.pathname; - return path.split('lobby/')[1]; -} - -function* watchLobbyUpdates(session: SevenWondersSession): SagaIterator { - const currentGameId: number = getCurrentGameId(); - const lobbyUpdatesChannel: Channel = yield eventChannel(session.watchLobbyUpdated(currentGameId)); +function* watchLobbyUpdates(session: SevenWondersSession, lobbyId: number): SagaIterator { + const lobbyUpdatesChannel: Channel = yield eventChannel(session.watchLobbyUpdated(lobbyId)); try { while (true) { const lobby = yield take(lobbyUpdatesChannel); const normalized = normalize(lobby, gameSchema); - yield put(gameActions.updateGames(normalized.entities.games)); + // players update needs to be first, otherwise the UI cannot find the player in the list yield put(playerActions.updatePlayers(normalized.entities.players)); + yield put(gameActions.updateGames(normalized.entities.games)); } } finally { yield apply(lobbyUpdatesChannel, lobbyUpdatesChannel.close); } } -function* watchGameStart(session: SevenWondersSession): SagaIterator { - const currentGameId = getCurrentGameId(); - const gameStartedChannel = yield eventChannel(session.watchGameStarted(currentGameId)); +function* watchGameStart(session: SevenWondersSession, lobbyId: number): SagaIterator { + const gameStartedChannel = yield eventChannel(session.watchGameStarted(lobbyId)); try { yield take(gameStartedChannel); - yield put(gameActions.enterGame()); - yield put(push('/game')); + yield put(gameActions.enterGame(lobbyId)); + yield put(push(`/game/${lobbyId}`)); } finally { yield apply(gameStartedChannel, gameStartedChannel.close); } @@ -49,5 +43,10 @@ function* startGame(session: SevenWondersSession): SagaIterator { } export function* lobbySaga(session: SevenWondersSession): SagaIterator { - yield all([call(watchLobbyUpdates, session), call(watchGameStart, session), call(startGame, session)]); + const { gameId } = yield take(types.ENTER_LOBBY); + yield all([ + call(watchLobbyUpdates, session, gameId), + call(watchGameStart, session, gameId), + call(startGame, session) + ]); } |