diff options
25 files changed, 167 insertions, 307 deletions
diff --git a/frontend/src/api/model.js b/frontend/src/api/model.js index 2d1d2dde..cd718ff1 100644 --- a/frontend/src/api/model.js +++ b/frontend/src/api/model.js @@ -10,10 +10,18 @@ export type ApiErrorDetail = { export type ApiGameState = "LOBBY" | "PLAYING"; +export type ApiPlayer = { + username: string, + displayName: string, + index: number, + gameOwner: Boolean, + user: Boolean, +}; + export type ApiLobby = { id: number, name: string, - owner: ApiPlayer, + owner: string, players: ApiPlayer[], settings: ApiSettings, state: ApiGameState @@ -33,14 +41,6 @@ export type ApiSettings = { wonPointsPerVictoryPerAge: Map<number, number> }; -export type ApiPlayer = { - username: string, - displayName: string, - index: number, - gameOwner: Boolean, - user: Boolean, -}; - export type ApiTable = { boards: ApiBoard[], currentAge: number, diff --git a/frontend/src/components/game-browser/GameList.jsx b/frontend/src/components/game-browser/GameList.jsx index 64ced9b0..b46cd14c 100644 --- a/frontend/src/components/game-browser/GameList.jsx +++ b/frontend/src/components/game-browser/GameList.jsx @@ -2,7 +2,8 @@ import type { List } from 'immutable'; import React from 'react'; import { connect } from 'react-redux'; -import type { Game } from '../../models/games'; +import type { ApiLobby } from '../../api/model'; +import type { GlobalState } from '../../reducers'; import { actions } from '../../redux/actions/lobby'; import { getAllGames } from '../../redux/games'; import { IconButton } from '../shared/IconButton'; @@ -11,7 +12,7 @@ import { GameStatus } from './GameStatus'; import { PlayerCount } from './PlayerCount'; type GameListProps = { - games: List<Game>, + games: List<ApiLobby>, joinGame: (gameId: string) => void, }; @@ -21,7 +22,7 @@ const GameListPresenter = ({ games, joinGame }: GameListProps) => ( <GameListHeaderRow /> </thead> <tbody> - {games.map((game: Game) => <GameListItemRow key={game.id} game={game} joinGame={joinGame}/>)} + {games.map((game: ApiLobby) => <GameListItemRow key={game.id} game={game} joinGame={joinGame}/>)} </tbody> </table> ); @@ -56,8 +57,8 @@ const JoinButton = ({game, joinGame}) => { return <IconButton minimal disabled={disabled} icon='arrow-right' title='Join Game' onClick={onClick}/>; }; -const mapStateToProps = state => ({ - games: getAllGames(state.get('games')), +const mapStateToProps = (state: GlobalState) => ({ + games: getAllGames(state), }); const mapDispatchToProps = { diff --git a/frontend/src/components/game-browser/GameStatus.jsx b/frontend/src/components/game-browser/GameStatus.jsx index 749d3cfa..fc14bbf6 100644 --- a/frontend/src/components/game-browser/GameStatus.jsx +++ b/frontend/src/components/game-browser/GameStatus.jsx @@ -1,10 +1,10 @@ //@flow import { Tag } from '@blueprintjs/core'; import * as React from 'react'; -import type { GameState } from '../../models/games'; +import type { ApiGameState } from '../../api/model'; type GameStatusProps = { - state: GameState, + state: ApiGameState, } export const GameStatus = ({state}: GameStatusProps) => ( diff --git a/frontend/src/components/game-browser/PlayerInfo.jsx b/frontend/src/components/game-browser/PlayerInfo.jsx index 2f29ea60..baee67c1 100644 --- a/frontend/src/components/game-browser/PlayerInfo.jsx +++ b/frontend/src/components/game-browser/PlayerInfo.jsx @@ -2,23 +2,24 @@ import { Text } from '@blueprintjs/core'; import React from 'react'; import { connect } from 'react-redux'; -import type { Player } from '../../models/players'; -import { getCurrentPlayer } from '../../redux/players'; +import type { GlobalState } from '../../reducers'; +import type { User } from '../../redux/user'; +import { getCurrentUser } from '../../redux/user'; type PlayerInfoProps = { - player: ?Player, + user: ?User, } -const PlayerInfoPresenter = ({player}: PlayerInfoProps) => ( +const PlayerInfoPresenter = ({user}: PlayerInfoProps) => ( <Text> <b>Username:</b> {' '} - {player && player.displayName} + {user && user.displayName} </Text> ); -const mapStateToProps = state => ({ - player: getCurrentPlayer(state), +const mapStateToProps = (state: GlobalState): PlayerInfoProps => ({ + user: getCurrentUser(state), }); const mapDispatchToProps = { diff --git a/frontend/src/components/game/GameScene.jsx b/frontend/src/components/game/GameScene.jsx index 5221ab8e..70e857f0 100644 --- a/frontend/src/components/game/GameScene.jsx +++ b/frontend/src/components/game/GameScene.jsx @@ -2,22 +2,18 @@ import { Button, Classes, Intent, NonIdealState } from '@blueprintjs/core'; import { List } from 'immutable'; import React, { Component } from 'react'; import { connect } from 'react-redux'; -import type { ApiPlayerMove, ApiPlayerTurnInfo } from '../../api/model'; -import { Game } from '../../models/games'; -import { Player } from '../../models/players'; +import type { ApiPlayer, ApiPlayerMove, ApiPlayerTurnInfo } from '../../api/model'; +import type { GlobalState } from '../../reducers'; import { actions } from '../../redux/actions/game'; import { getCurrentTurnInfo } from '../../redux/currentGame'; import { getCurrentGame } from '../../redux/games'; -import { getCurrentPlayer, getPlayers } from '../../redux/players'; import { Board } from './Board'; -import { Hand } from './Hand'; import './GameScene.css' +import { Hand } from './Hand'; import { ProductionBar } from './ProductionBar'; type GameSceneProps = { - game: Game, - currentPlayer: Player, - players: List<Player>, + players: List<ApiPlayer>, turnInfo: ApiPlayerTurnInfo, sayReady: () => void, prepareMove: (move: ApiPlayerMove) => void, @@ -54,15 +50,13 @@ const GamePreStart = ({onReadyClicked}) => <NonIdealState onClick={onReadyClicked}/>} />; -const mapStateToProps: (state) => GameSceneProps = state => { - const game = getCurrentGame(state.get('games')); +const mapStateToProps: (state: GlobalState) => GameSceneProps = state => { + const game = getCurrentGame(state); console.info(game); return { - game: game, - currentPlayer: getCurrentPlayer(state), - players: game ? getPlayers(state.get('players'), game.players) : new List(), - turnInfo: getCurrentTurnInfo(state.get('currentGame')), + players: game ? new List(game.players) : new List(), + turnInfo: getCurrentTurnInfo(state), }; }; diff --git a/frontend/src/components/home/ChooseNameForm.jsx b/frontend/src/components/home/ChooseNameForm.jsx index 619d967c..13f6034b 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/actions/players'; +import { actions } from '../../redux/actions/user'; import { IconButton } from '../shared/IconButton'; type ChooseNameFormPresenterProps = { diff --git a/frontend/src/components/lobby/Lobby.jsx b/frontend/src/components/lobby/Lobby.jsx index f352ab83..cc979190 100644 --- a/frontend/src/components/lobby/Lobby.jsx +++ b/frontend/src/components/lobby/Lobby.jsx @@ -3,17 +3,17 @@ import { Button, Classes, Intent } from '@blueprintjs/core'; import { List } from 'immutable'; import React, { Component } from 'react'; import { connect } from 'react-redux'; -import type { Game } from '../../models/games'; -import type { Player } from '../../models/players'; +import type { ApiLobby, ApiPlayer } from '../../api/model'; +import type { GlobalState } from '../../reducers'; import { actions } from '../../redux/actions/lobby'; import { getCurrentGame } from '../../redux/games'; -import { getCurrentPlayer, getPlayers } from '../../redux/players'; +import { getCurrentPlayer } from '../../redux/user'; import { RadialPlayerList } from './RadialPlayerList'; export type LobbyProps = { - currentGame: Game, - currentPlayer: Player, - players: List<Player>, + currentGame: ApiLobby, + currentPlayer: ApiPlayer, + players: List<ApiPlayer>, startGame: () => void, } @@ -24,7 +24,7 @@ class LobbyPresenter extends Component<LobbyProps> { return ( <div> <h2>{currentGame.name + ' — Lobby'}</h2> - <RadialPlayerList players={players} owner={currentGame.owner} currentPlayer={currentPlayer}/> + <RadialPlayerList players={players}/> {currentPlayer.gameOwner && <Button text="START" className={Classes.LARGE} intent={Intent.PRIMARY} icon='play' onClick={startGame} disabled={players.size < 3}/>} </div> @@ -32,13 +32,13 @@ class LobbyPresenter extends Component<LobbyProps> { } } -const mapStateToProps = state => { - const game = getCurrentGame(state.get('games')); +const mapStateToProps = (state: GlobalState) => { + const game = getCurrentGame(state); console.info(game); return { currentGame: game, currentPlayer: getCurrentPlayer(state), - players: game ? getPlayers(state.get('players'), game.players) : new List(), + players: game ? new List(game.players) : new List(), }; }; diff --git a/frontend/src/components/lobby/PlayerList.jsx b/frontend/src/components/lobby/PlayerList.jsx index 87887dd0..bd37f40d 100644 --- a/frontend/src/components/lobby/PlayerList.jsx +++ b/frontend/src/components/lobby/PlayerList.jsx @@ -3,12 +3,12 @@ import { Icon } from '@blueprintjs/core' import { List } from 'immutable'; import * as React from 'react'; import { Flex } from 'reflexbox'; -import { Player } from '../../models/players'; +import { ApiPlayer } from '../../api/model'; type PlayerListProps = { - players: List<Player>, + players: List<ApiPlayer>, owner: string, - currentPlayer: Player, + currentPlayer: ApiPlayer, }; const PlayerListItem = ({player, isOwner, isUser}) => ( diff --git a/frontend/src/components/lobby/RadialPlayerList.jsx b/frontend/src/components/lobby/RadialPlayerList.jsx index 8345b48c..0f122910 100644 --- a/frontend/src/components/lobby/RadialPlayerList.jsx +++ b/frontend/src/components/lobby/RadialPlayerList.jsx @@ -3,19 +3,17 @@ import { Icon } from '@blueprintjs/core' import { List } from 'immutable'; import * as React from 'react'; import { Flex } from 'reflexbox'; -import { Player } from '../../models/players'; +import { ApiPlayer } from '../../api/model'; import { RadialList } from './radial-list/RadialList'; import roundTable from './round-table.png'; type RadialPlayerListProps = { - players: List<Player>, - owner: string, - currentPlayer: Player, + players: List<ApiPlayer> }; -const PlayerItem = ({player, isOwner, isUser}) => ( +const PlayerItem = ({player}) => ( <Flex column align='center'> - <UserIcon isOwner={isOwner} isUser={isUser} title={isOwner ? 'Game owner' : false}/> + <UserIcon isOwner={player.gameOwner} isUser={player.user} title={player.gameOwner ? 'Game owner' : false}/> <h5 style={{margin: 0}}>{player.displayName}</h5> </Flex> ); @@ -33,12 +31,9 @@ const UserIcon = ({isUser, isOwner, title}) => { return <Icon icon={icon} iconSize={50} intent={intent} title={title}/>; }; -export const RadialPlayerList = ({players, owner, currentPlayer}: RadialPlayerListProps) => { - const orderedPlayers = placeFirst(players.toArray(), currentPlayer.username); - const playerItems = orderedPlayers.map(player => <PlayerItem key={player.username} - player={player} - isOwner={player.username === owner} - isUser={player.username === currentPlayer.username}/>); +export const RadialPlayerList = ({players}: RadialPlayerListProps) => { + const orderedPlayers = placeUserFirst(players.toArray()); + const playerItems = orderedPlayers.map(player => <PlayerItem key={player.username} player={player}/>); const tableImg = <img src={roundTable} alt='Round table' style={{width: 200, height: 200}}/>; return <RadialList items={completeWithPlaceholders(playerItems)} centerElement={tableImg} @@ -48,8 +43,8 @@ export const RadialPlayerList = ({players, owner, currentPlayer}: RadialPlayerLi itemHeight={100}/>; }; -function placeFirst(players: Array<Player>, targetFirstUsername: string): Array<Player> { - while (players[0].username !== targetFirstUsername) { +function placeUserFirst(players: Array<ApiPlayer>): Array<ApiPlayer> { + while (!players[0].user) { players.push(players.shift()); } return players; diff --git a/frontend/src/models/currentGame.js b/frontend/src/models/currentGame.js deleted file mode 100644 index 398c65e8..00000000 --- a/frontend/src/models/currentGame.js +++ /dev/null @@ -1,8 +0,0 @@ -import { List } from 'immutable'; -import type { ApiPlayerTurnInfo, ApiTable } from '../api/model'; - -export class CurrentGameState { - readyUsernames: List<string> = new List(); - turnInfo: ApiPlayerTurnInfo | null = null; - table: ApiTable | null = null; -} diff --git a/frontend/src/models/games.js b/frontend/src/models/games.js deleted file mode 100644 index 85aab2df..00000000 --- a/frontend/src/models/games.js +++ /dev/null @@ -1,84 +0,0 @@ -import { List, Map, Record } from 'immutable'; - -export type SettingsShape = { - initialGold: number, - lostPointsPerDefeat: number, - timeLimitInSeconds: number, - randomSeedForTests: number, - discardedCardGold: number, - defaultTradingCost: number, - wonPointsPerVictoryPerAge: { - "1": number, - "2": number, - "3": number - }, - wonderSidePickMethod: "EACH_RANDOM" | "TODO", - pointsPer3Gold: number -}; - -export type SettingsType = Record<SettingsShape>; - -const SettingsRecord: SettingsType = Record({ - initialGold: 3, - lostPointsPerDefeat: 1, - timeLimitInSeconds: 45, - randomSeedForTests: -1, - discardedCardGold: 3, - defaultTradingCost: 2, - wonPointsPerVictoryPerAge: { - '1': 1, - '2': 3, - '3': 5, - }, - wonderSidePickMethod: 'EACH_RANDOM', - pointsPer3Gold: 1, -}); - -export class Settings extends SettingsRecord {} - -export type GameState = 'LOBBY' | 'PLAYING'; -export type GameShape = { - id: number, - name: string | void, - owner: string, - players: List<string>, - settings: SettingsType, - state: GameState, -}; - -export type GameType = Record<GameShape>; -export type GameMapType = Map<string, GameShape>; -export type GameNormalMapType = { [string]: GameShape }; - -const GameRecord: GameType = Record({ - id: -1, - name: null, - owner: 'anonymous', - players: new List(), - 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); - return this.mergeDeepIn(['all', game.id], game); - } - addGames(games: GameNormalMapType) { - return this.mergeIn(['all'], games.map((game: GameShape): Game => new Game(game))); - } -} diff --git a/frontend/src/models/players.js b/frontend/src/models/players.js deleted file mode 100644 index 587300f5..00000000 --- a/frontend/src/models/players.js +++ /dev/null @@ -1,47 +0,0 @@ -// @flow -import { Map, Record } from 'immutable'; - -export type PlayerShape = { - username: string, - displayName: string, - index: number, - ready: boolean, - gameOwner: boolean, - user: boolean, -}; -export type PlayerType = Record<PlayerShape>; - -const PlayerRecord: PlayerType = Record({ - username: null, - displayName: null, - index: 0, - ready: false, - gameOwner: false, - user: false, -}); -// $FlowFixMe -export class Player extends PlayerRecord {} - -export type PlayersShape = { - all: Map<string, PlayerType>, - current: string -}; -export type PlayersType = Record<PlayersShape>; - -const PlayersRecord: PlayersType = Record({ - all: Map(), - current: '', -}); -// $FlowFixMe -export class PlayerState extends PlayersRecord { - addPlayer(p: PlayerShape) { - const player: Player = new Player(p); - const playerMap = Map({ [player.username]: player }); - return this.addPlayers(playerMap).set('current', player.username); - } - - addPlayers(p: Map<string, PlayerShape>) { - const players: Map<string, PlayerShape> = Map(p); - return this.mergeIn(['all'], players.map((player: PlayerShape): Player => new Player(player))); - } -} diff --git a/frontend/src/reducers.js b/frontend/src/reducers.js index 5e066d28..612bd0c5 100644 --- a/frontend/src/reducers.js +++ b/frontend/src/reducers.js @@ -1,15 +1,25 @@ // @flow import { routerReducer } from 'react-router-redux'; -import { combineReducers } from 'redux-immutable'; +import { combineReducers } from 'redux'; +import type { ApiPlayer } from './api/model'; +import type { CurrentGameState } from './redux/currentGame'; import { createCurrentGameReducer } from './redux/currentGame'; -import { gamesReducer } from './redux/games'; -import { playersReducer } from './redux/players'; +import type { GamesState } from './redux/games'; +import { createGamesReducer } from './redux/games'; +import { currentUserReducer } from './redux/user'; + +export type GlobalState = { + currentGame: CurrentGameState; + currentUser: ApiPlayer; + games: GamesState; + routing: any; +} export function createReducer() { return combineReducers({ currentGame: createCurrentGameReducer(), - games: gamesReducer, - players: playersReducer, + currentUser: currentUserReducer, + games: createGamesReducer(), routing: routerReducer, }); } diff --git a/frontend/src/redux/actions/all.js b/frontend/src/redux/actions/all.js index 45d3ab7a..12522819 100644 --- a/frontend/src/redux/actions/all.js +++ b/frontend/src/redux/actions/all.js @@ -1,5 +1,5 @@ import type { GameAction } from './game'; import type { LobbyAction } from './lobby'; -import type { PlayerAction } from './players'; +import type { PlayerAction } from './user'; export type Action = PlayerAction | LobbyAction | GameAction diff --git a/frontend/src/redux/actions/lobby.js b/frontend/src/redux/actions/lobby.js index b3151a23..8768ec80 100644 --- a/frontend/src/redux/actions/lobby.js +++ b/frontend/src/redux/actions/lobby.js @@ -1,5 +1,4 @@ -import { fromJS } from 'immutable'; -import type { GameMapType, GameNormalMapType } from '../../models/games'; +import type { ApiLobby } from '../../api/model'; export const types = { UPDATE_GAMES: 'GAMES/UPDATE_GAMES', @@ -10,7 +9,7 @@ export const types = { ENTER_GAME: 'GAMES/ENTER_GAME', }; -export type UpdateGamesAction = { type: 'GAMES/UPDATE_GAMES', games: GameMapType }; +export type UpdateGamesAction = { type: 'GAMES/UPDATE_GAMES', games: ApiLobby[]}; 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' }; @@ -26,7 +25,7 @@ export type LobbyAction = | EnterGameAction; export const actions = { - updateGames: (games: GameNormalMapType): UpdateGamesAction => ({ type: types.UPDATE_GAMES, games: fromJS(games) }), + updateGames: (games: ApiLobby[]): UpdateGamesAction => ({ type: types.UPDATE_GAMES, 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 }), diff --git a/frontend/src/redux/actions/players.js b/frontend/src/redux/actions/user.js index 7df174c4..4406230b 100644 --- a/frontend/src/redux/actions/players.js +++ b/frontend/src/redux/actions/user.js @@ -1,5 +1,5 @@ import { Map } from 'immutable'; -import { PlayerShape } from '../../models/players'; +import type { ApiPlayer } from '../../api/model'; export const types = { REQUEST_CHOOSE_USERNAME: 'USER/REQUEST_CHOOSE_USERNAME', @@ -8,8 +8,8 @@ export const types = { }; 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 SetCurrentPlayerAction = { type: types.SET_CURRENT_PLAYER, player: ApiPlayer }; +export type UpdatePlayersAction = { type: types.UPDATE_PLAYERS, players: Map<string, ApiPlayer> }; export type PlayerAction = RequestChooseUsernameAction | SetCurrentPlayerAction | UpdatePlayersAction; @@ -18,12 +18,8 @@ export const actions = { type: types.REQUEST_CHOOSE_USERNAME, username, }), - setCurrentPlayer: (player: PlayerShape): SetCurrentPlayerAction => ({ + setCurrentPlayer: (player: ApiPlayer): SetCurrentPlayerAction => ({ type: types.SET_CURRENT_PLAYER, - player, - }), - updatePlayers: (players: Map<string, PlayerShape>): UpdatePlayersAction => ({ - type: types.UPDATE_PLAYERS, - players, + player: player, }), }; diff --git a/frontend/src/redux/currentGame.js b/frontend/src/redux/currentGame.js index e5659195..e315a1e8 100644 --- a/frontend/src/redux/currentGame.js +++ b/frontend/src/redux/currentGame.js @@ -1,27 +1,22 @@ // @flow -import { List } from 'immutable'; import { combineReducers } from 'redux'; import type { ApiPlayerTurnInfo, ApiTable } from '../api/model'; -import { CurrentGameState } from '../models/currentGame'; +import type { GlobalState } from '../reducers'; import type { Action } from './actions/all'; import { types } from './actions/game'; +export type CurrentGameState = { + turnInfo: ApiPlayerTurnInfo | null; + table: ApiTable | null; +} + export function createCurrentGameReducer() { return combineReducers({ - readyUsernames: readyUsernamesReducer, turnInfo: turnInfoReducer, table: tableUpdatesReducer, }); } -const readyUsernamesReducer = (state: List<string> = new List(), action: Action) => { - if (action.type === types.PLAYER_READY_RECEIVED) { - return state.push(action.username); - } else { - return state; - } -}; - const turnInfoReducer = (state: ApiPlayerTurnInfo | null = null, action: Action) => { switch (action.type) { case types.TURN_INFO_RECEIVED: @@ -44,4 +39,4 @@ const tableUpdatesReducer = (state: ApiTable | null = null, action: Action) => { } }; -export const getCurrentTurnInfo = (state: CurrentGameState): ApiPlayerTurnInfo => state.turnInfo; +export const getCurrentTurnInfo = (state: GlobalState): ApiPlayerTurnInfo => state.currentGame.turnInfo; diff --git a/frontend/src/redux/games.js b/frontend/src/redux/games.js index 68571981..f5543a76 100644 --- a/frontend/src/redux/games.js +++ b/frontend/src/redux/games.js @@ -1,22 +1,42 @@ // @flow -import type { List, Map } from 'immutable'; -import type { Game } from '../models/games'; -import { GamesState } from '../models/games'; +import { List, Map } from 'immutable'; +import { combineReducers } from 'redux'; +import type { ApiLobby } from '../api/model'; +import type { GlobalState } from '../reducers'; import type { Action } from './actions/all'; import { types } from './actions/lobby'; -export const gamesReducer = (state: GamesState = new GamesState(), action: Action) => { +export type GamesState = { + all: Map<string, ApiLobby>, + current: string | void +}; + +export const createGamesReducer = () => { + return combineReducers({ + all: allGamesReducer, + current: currentGameIdReducer + }) +}; + +export const allGamesReducer = (state: Map<string, ApiLobby> = Map(), action: Action) => { switch (action.type) { case types.UPDATE_GAMES: - return state.addGames(action.games); + let newGames = {}; + action.games.forEach(g => newGames[g.id] = g); + return state.merge(Map(newGames)); + default: + return state; + } +}; + +export const currentGameIdReducer = (state: string | void = null, action: Action) => { + switch (action.type) { case types.ENTER_LOBBY: - return state.set('current', action.gameId); + return `${action.gameId}`; default: return state; } }; -export const getAllGamesById = (games: GamesState): Map<string, Game> => games.all; -export const getAllGames = (games: GamesState): List<Game> => getAllGamesById(games).toList(); -export const getGame = (games: GamesState, id: string | number): Game => getAllGamesById(games).get(`${id}`); -export const getCurrentGame = (games: GamesState): Game => getGame(games, games.current); +export const getAllGames = (state: GlobalState): List<ApiLobby> => state.games.all.toList(); +export const getCurrentGame = (state: GlobalState): ApiLobby | null => state.games.all.get(state.games.current); diff --git a/frontend/src/redux/players.js b/frontend/src/redux/players.js deleted file mode 100644 index ce3c305f..00000000 --- a/frontend/src/redux/players.js +++ /dev/null @@ -1,25 +0,0 @@ -import { List } from 'immutable'; -import { Player, PlayerState } from '../models/players'; -import type { Action } from './actions/all'; -import { types } from './actions/players'; - -export const playersReducer = (state = new PlayerState(), action: Action) => { - switch (action.type) { - case types.SET_CURRENT_PLAYER: - return state.addPlayer(action.player); - case types.UPDATE_PLAYERS: - return state.addPlayers(action.players); - default: - return state; - } -}; - -const ANONYMOUS = new Player({displayName: '[NOT LOGGED]'}); - -export function getCurrentPlayer(state): Player { - const players = state.get('players'); - return getPlayer(players, players.current, ANONYMOUS); -} - -export const getPlayer = (players, username, defaultPlayer): ?Player => players.all.get(username, defaultPlayer); -export const getPlayers = (players, usernames): List<Player> => usernames.map(u => getPlayer(players, u, undefined)); diff --git a/frontend/src/redux/user.js b/frontend/src/redux/user.js new file mode 100644 index 00000000..f2247b38 --- /dev/null +++ b/frontend/src/redux/user.js @@ -0,0 +1,37 @@ +import { ApiPlayer } from '../api/model'; +import type { GlobalState } from '../reducers'; +import type { Action } from './actions/all'; +import { types } from './actions/user'; +import { getCurrentGame } from './games'; + +export type User = { + username: string, + displayName: string, +} + +export const currentUserReducer = (state: ?User = null, action: Action) => { + switch (action.type) { + case types.SET_CURRENT_PLAYER: + return { + username: action.player.username, + displayName: action.player.displayName + }; + default: + return state; + } +}; + +export function getCurrentUser(state: GlobalState): ?User { + return state.currentUser +} + +export function getCurrentPlayer(state: GlobalState): ApiPlayer { + let game = getCurrentGame(state); + for (let i = 0; i < game.players.length; i++) { + let player = game.players[i]; + if (player.username === state.currentUser.username) { + return player; + } + } + return null; +} diff --git a/frontend/src/sagas/gameBrowser.js b/frontend/src/sagas/gameBrowser.js index 062603a3..fec83451 100644 --- a/frontend/src/sagas/gameBrowser.js +++ b/frontend/src/sagas/gameBrowser.js @@ -1,24 +1,18 @@ // @flow -import { normalize } from 'normalizr'; import { push } from 'react-router-redux'; import type { SagaIterator } from 'redux-saga'; import { eventChannel } from 'redux-saga'; import { all, apply, call, put, take } from 'redux-saga/effects'; +import type { ApiLobby } from '../api/model'; import type { SevenWondersSession } from '../api/sevenWondersApi'; -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'; +import { actions as gameActions, types } from '../redux/actions/lobby'; function* watchGames(session: SevenWondersSession): SagaIterator { const gamesChannel = yield eventChannel(session.watchGames()); 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 || {})); + yield put(gameActions.updateGames(gameList)); } } finally { yield apply(gamesChannel, gamesChannel.close); @@ -28,13 +22,10 @@ function* watchGames(session: SevenWondersSession): SagaIterator { function* watchLobbyJoined(session: SevenWondersSession): SagaIterator { const joinedLobbyChannel = yield eventChannel(session.watchLobbyJoined()); try { - 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(gameId)); - yield put(push(`/lobby/${gameId}`)); + const joinedLobby: ApiLobby = yield take(joinedLobbyChannel); + yield put(gameActions.updateGames([joinedLobby])); + yield put(gameActions.enterLobby(joinedLobby.id)); + yield put(push(`/lobby/${joinedLobby.id}`)); } finally { yield apply(joinedLobbyChannel, joinedLobbyChannel.close); } diff --git a/frontend/src/sagas/home.js b/frontend/src/sagas/home.js index 705a7a40..43ab97dc 100644 --- a/frontend/src/sagas/home.js +++ b/frontend/src/sagas/home.js @@ -5,8 +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 } from '../redux/actions/players'; -import { types } from '../redux/actions/players'; +import { actions } from '../redux/actions/user'; +import { types } from '../redux/actions/user'; function* sendUsername(session: SevenWondersSession): SagaIterator { while (true) { diff --git a/frontend/src/sagas/lobby.js b/frontend/src/sagas/lobby.js index b0f52d5c..7ff94f34 100644 --- a/frontend/src/sagas/lobby.js +++ b/frontend/src/sagas/lobby.js @@ -1,23 +1,17 @@ // @flow -import { normalize } from 'normalizr'; import { push } from 'react-router-redux'; 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/actions/lobby'; -import { actions as playerActions } from '../redux/actions/players'; -import { game as gameSchema } from '../schemas/games'; 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); - // 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)); + yield put(gameActions.updateGames([lobby])); } } finally { yield apply(lobbyUpdatesChannel, lobbyUpdatesChannel.close); diff --git a/frontend/src/schemas/games.js b/frontend/src/schemas/games.js deleted file mode 100644 index bcae0235..00000000 --- a/frontend/src/schemas/games.js +++ /dev/null @@ -1,9 +0,0 @@ -import { schema } from 'normalizr'; - -const player = new schema.Entity('players', {}, { idAttribute: 'username' }); - -export const game = new schema.Entity('games', { - players: [player], -}); - -export const gameList = [game]; diff --git a/frontend/src/store.js b/frontend/src/store.js index 5c958afc..9c5a063b 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -1,13 +1,13 @@ // @flow import createHistory from 'history/createBrowserHistory'; -import { fromJS } from 'immutable'; import { routerMiddleware } from 'react-router-redux'; import { applyMiddleware, compose, createStore } from 'redux'; import createSagaMiddleware from 'redux-saga'; +import type { GlobalState } from './reducers'; import { createReducer } from './reducers'; import { rootSaga } from './sagas'; -export function configureStore(initialState: Object = {}) { +export function configureStore(initialState: GlobalState = {}) { const sagaMiddleware = createSagaMiddleware(); const history = createHistory(); @@ -22,7 +22,7 @@ export function configureStore(initialState: Object = {}) { ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; - const store = createStore(createReducer(), fromJS(initialState), composeEnhancers(...enhancers)); + const store = createStore(createReducer(), initialState, composeEnhancers(...enhancers)); sagaMiddleware.run(rootSaga); |