From b6f54ed56121aa5435dd813c952b292b25b56bfb Mon Sep 17 00:00:00 2001 From: Joffrey BION Date: Sun, 14 May 2017 18:43:58 +0200 Subject: Add lobby saga - Fix lobby's player list updates - Fix lobby's player list order - Add 'start game' button (not restricted to owner yet) Resolves: https://github.com/luxons/seven-wonders/issues/7 --- frontend/src/components/playerList.js | 4 +- frontend/src/containers/lobby.js | 13 +++--- frontend/src/redux/games.js | 4 ++ frontend/src/redux/players.js | 4 +- frontend/src/routes.js | 6 ++- frontend/src/sagas/gameBrowser.js | 77 +++++++++++++++-------------------- frontend/src/sagas/lobby.js | 58 ++++++++++++++++++++++++++ 7 files changed, 112 insertions(+), 54 deletions(-) create mode 100644 frontend/src/sagas/lobby.js (limited to 'frontend') diff --git a/frontend/src/components/playerList.js b/frontend/src/components/playerList.js index 45aa01a2..70e8b1f6 100644 --- a/frontend/src/components/playerList.js +++ b/frontend/src/components/playerList.js @@ -5,8 +5,8 @@ import Immutable from 'seamless-immutable' const PlayerList = (props) => (
- {Immutable.asMutable(props.players).map((player, index) => { - return ( + {Immutable.asMutable(props.players).map(player => { + return ( {player.displayName} ({player.username}) ) diff --git a/frontend/src/containers/lobby.js b/frontend/src/containers/lobby.js index f0df0c44..e326698f 100644 --- a/frontend/src/containers/lobby.js +++ b/frontend/src/containers/lobby.js @@ -1,14 +1,14 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import Immutable from 'seamless-immutable' -import { Text } from 'rebass' +import { Button } from 'rebass' import PlayerList from '../components/playerList' class Lobby extends Component { getTitle() { if (this.props.currentGame) { - return this.props.currentGame.name + return this.props.currentGame.name + ' — Lobby' } else { return 'What are you doing here? You haven\'t joined a game yet!' } @@ -17,15 +17,16 @@ class Lobby extends Component { render() { return (
- {this.getTitle()} +

{this.getTitle()}

+
) } } import { getPlayers } from '../redux/players' -import { getCurrentGame } from '../redux/games' +import { getCurrentGame, actions } from '../redux/games' const mapStateToProps = (state) => { const game = getCurrentGame(state) @@ -35,6 +36,8 @@ const mapStateToProps = (state) => { }) } -const mapDispatchToProps = {} +const mapDispatchToProps = { + startGame: actions.requestStartGame +} export default connect(mapStateToProps, mapDispatchToProps)(Lobby) diff --git a/frontend/src/redux/games.js b/frontend/src/redux/games.js index 2b9a92a4..2986de07 100644 --- a/frontend/src/redux/games.js +++ b/frontend/src/redux/games.js @@ -4,14 +4,18 @@ export const types = { UPDATE_GAMES: 'GAME/UPDATE_GAMES', REQUEST_CREATE_GAME: 'GAME/REQUEST_CREATE_GAME', REQUEST_JOIN_GAME: 'GAME/REQUEST_JOIN_GAME', + REQUEST_START_GAME: 'GAME/REQUEST_JOIN_GAME', ENTER_LOBBY: 'GAME/ENTER_LOBBY', + ENTER_GAME: 'GAME/ENTER_GAME', } export const actions = { updateGames: (games) => ({ type: types.UPDATE_GAMES, games: Immutable(games) }), requestJoinGame: (gameId) => ({ type: types.REQUEST_JOIN_GAME, gameId }), requestCreateGame: (gameName) => ({ type: types.REQUEST_CREATE_GAME, gameName }), + requestStartGame: () => ({ type: types.REQUEST_START_GAME }), enterLobby: (lobby) => ({ type: types.ENTER_LOBBY, lobby: Immutable(lobby) }), + enterGame: () => ({ type: types.ENTER_GAME }), } const initialState = Immutable.from({ diff --git a/frontend/src/redux/players.js b/frontend/src/redux/players.js index 84e24796..2b530ca1 100644 --- a/frontend/src/redux/players.js +++ b/frontend/src/redux/players.js @@ -40,5 +40,5 @@ export default (state = initialState, action) => { } export const getCurrentPlayer = state => state.players.all && state.players.all[state.players.current] -export const getPlayers = (state, usernames) => Object.values(state.players.all) - .filter(p => usernames.indexOf(p.username) !== -1) +export const getPlayer = (state, username) => state.players.all[username] +export const getPlayers = (state, usernames) => usernames.map(u => getPlayer(state, u)) diff --git a/frontend/src/routes.js b/frontend/src/routes.js index 1ce46bb3..6fd60c04 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -1,6 +1,7 @@ import { fork } from 'redux-saga/effects' import homeSaga from './sagas/home' import gameBrowserSaga from './sagas/gameBrowser' +import lobbySaga from './sagas/lobby' export const makeSagaRoutes = wsConnection => ({ *'/'() { @@ -8,6 +9,9 @@ export const makeSagaRoutes = wsConnection => ({ }, *'/games'() { yield fork(gameBrowserSaga, wsConnection) + }, + *'/lobby/*'() { + yield fork(lobbySaga, wsConnection) } }) @@ -28,7 +32,7 @@ export const routes = [ component: LobbyLayout, childRoutes: [ { path: '/games', component: GameBrowser }, - { path: '/lobby', component: Lobby } + { path: '/lobby/*', component: Lobby } ] }, { diff --git a/frontend/src/sagas/gameBrowser.js b/frontend/src/sagas/gameBrowser.js index 4f3309c3..10d6ec1d 100644 --- a/frontend/src/sagas/gameBrowser.js +++ b/frontend/src/sagas/gameBrowser.js @@ -1,73 +1,62 @@ import { call, put, take, apply } from 'redux-saga/effects' -import { eventChannel } from 'redux-saga' +import { createSubscriptionChannel } from '../utils/websocket' import { push } from 'react-router-redux' import { normalize } from 'normalizr' -import { game, gameList } from '../schemas/games' +import { game as gameSchema, gameList as gameListSchema} from '../schemas/games' import { actions as gameActions, types } from '../redux/games' import { actions as playerActions } from '../redux/players' -function gameBrowserChannel(socket) { - return eventChannel(emit => { - - const makeHandler = type => event => { - const response = JSON.parse(event.body) - emit({type, response}) - } - - const newGame = socket.subscribe('/topic/games', makeHandler(types.UPDATE_GAMES)) - const joinGame = socket.subscribe('/user/queue/lobby/joined', makeHandler(types.ENTER_LOBBY)) - - return () => { - newGame.unsubscribe() - joinGame.unsubscribe() +function *watchGames({socket}) { + const gamesChannel = yield call(createSubscriptionChannel, socket, '/topic/games') + try { + while (true) { + const gameList = yield take(gamesChannel) + const normGameList = normalize(gameList, gameListSchema) + // for an empty game array, there is no players/games entity maps + yield put(playerActions.updatePlayers(normGameList.entities.players || {})) + yield put(gameActions.updateGames(normGameList.entities.games || {})) } - }) + } finally { + yield apply(gamesChannel, gamesChannel.close) + } } -export function *watchGames({socket}) { - const socketChannel = gameBrowserChannel(socket) - +function *watchLobbyJoined({socket}) { + const joinedLobbyChannel = yield call(createSubscriptionChannel, socket, '/user/queue/lobby/joined') try { - while (true) { - const {type, response} = yield take(socketChannel) - - switch (type) { - case types.UPDATE_GAMES: - const normGameList = normalize(response, gameList) - yield put(playerActions.updatePlayers(normGameList.entities.players || {})) - yield put(gameActions.updateGames(normGameList.entities.games || {})) - break - case types.ENTER_LOBBY: - const normGame = normalize(response, game) - yield put(gameActions.enterLobby(normGame.entities.games[normGame.result])) - socketChannel.close() - yield put(push('/lobby')) - break - default: - console.error('Unknown type') - } - } + const joinedLobby = yield take(joinedLobbyChannel) + const normalized = normalize(joinedLobby, gameSchema) + 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(push(`/lobby/${gameId}`)) } finally { - console.info('gameBrowserChannel closed') + yield apply(joinedLobbyChannel, joinedLobbyChannel.close) } } -export function *createGame({socket}) { +function *createGame({socket}) { const {gameName} = yield take(types.REQUEST_CREATE_GAME) yield apply(socket, socket.send, ['/app/lobby/create', JSON.stringify({gameName}), {}]) } -export function *joinGame({socket}) { +function *joinGame({socket}) { const {gameId} = yield take(types.REQUEST_JOIN_GAME) yield apply(socket, socket.send, ['/app/lobby/join', JSON.stringify({gameId}), {}]) } -export function *gameBrowserSaga(socketConnection) { - yield [call(watchGames, socketConnection), call(createGame, socketConnection), call(joinGame, socketConnection)] +function *gameBrowserSaga(socketConnection) { + yield [ + call(watchGames, socketConnection), + call(watchLobbyJoined, socketConnection), + call(createGame, socketConnection), + call(joinGame, socketConnection) + ] } export default gameBrowserSaga diff --git a/frontend/src/sagas/lobby.js b/frontend/src/sagas/lobby.js new file mode 100644 index 00000000..f002c897 --- /dev/null +++ b/frontend/src/sagas/lobby.js @@ -0,0 +1,58 @@ +import { call, put, take, apply } from 'redux-saga/effects' +import { createSubscriptionChannel } from '../utils/websocket' +import { push } from 'react-router-redux' + +import { normalize } from 'normalizr' +import { game as gameSchema } from '../schemas/games' + +import { actions as gameActions, types } from '../redux/games' +import { actions as playerActions } from '../redux/players' + +function getCurrentGameId() { + const path = window.location.pathname + return path.split('lobby/')[1] +} + +function *watchLobbyUpdates({ socket }) { + const currentGameId = getCurrentGameId() + const lobbyUpdatesChannel = yield call(createSubscriptionChannel, socket, `/topic/lobby/${currentGameId}/updated`) + try { + while (true) { + const lobby = yield take(lobbyUpdatesChannel) + const normalized = normalize(lobby, gameSchema) + yield put(gameActions.updateGames(normalized.entities.games)) + yield put(playerActions.updatePlayers(normalized.entities.players)) + } + } finally { + yield apply(lobbyUpdatesChannel, lobbyUpdatesChannel.close) + } +} + +function *watchGameStart({ socket }) { + const currentGameId = getCurrentGameId() + const gameStartedChannel = yield call(createSubscriptionChannel, socket, `/topic/lobby/${currentGameId}/started`) + try { + yield take(gameStartedChannel) + yield put(gameActions.enterGame()) + yield put(push('/game')) + } finally { + yield apply(gameStartedChannel, gameStartedChannel.close) + } +} + +function *startGame({ socket }) { + while (true) { + yield take(types.REQUEST_START_GAME) + yield apply(socket, socket.send, ['/app/lobby/startGame', {}]) + } +} + +function *lobbySaga(socketConnection) { + yield [ + call(watchLobbyUpdates, socketConnection), + call(watchGameStart, socketConnection), + call(startGame, socketConnection) + ] +} + +export default lobbySaga -- cgit