From 2382a452456e4bdef4584e1046925e372624cb79 Mon Sep 17 00:00:00 2001 From: Joffrey BION Date: Thu, 16 May 2019 23:48:38 +0200 Subject: Rationalize module names --- sw-ui/src/@types/reflexbox.d.ts | 37 ++++ sw-ui/src/api/model.ts | 187 +++++++++++++++++++++ sw-ui/src/api/sevenWondersApi.ts | 104 ++++++++++++ sw-ui/src/api/websocket.ts | 60 +++++++ sw-ui/src/components/Application.tsx | 16 ++ sw-ui/src/components/game-browser/GameBrowser.tsx | 56 ++++++ sw-ui/src/components/game-browser/GameList.css | 3 + sw-ui/src/components/game-browser/GameList.tsx | 85 ++++++++++ sw-ui/src/components/game-browser/GameStatus.tsx | 17 ++ sw-ui/src/components/game-browser/PlayerCount.css | 3 + sw-ui/src/components/game-browser/PlayerCount.tsx | 12 ++ sw-ui/src/components/game-browser/PlayerInfo.tsx | 27 +++ sw-ui/src/components/game/Board.css | 38 +++++ sw-ui/src/components/game/Board.tsx | 67 ++++++++ sw-ui/src/components/game/CardImage.css | 4 + sw-ui/src/components/game/CardImage.tsx | 26 +++ sw-ui/src/components/game/GameScene.css | 13 ++ sw-ui/src/components/game/GameScene.tsx | 77 +++++++++ sw-ui/src/components/game/Hand.css | 50 ++++++ sw-ui/src/components/game/Hand.tsx | 44 +++++ sw-ui/src/components/game/ProductionBar.css | 50 ++++++ sw-ui/src/components/game/ProductionBar.tsx | 87 ++++++++++ sw-ui/src/components/game/background-papyrus.jpg | Bin 0 -> 100272 bytes sw-ui/src/components/home/ChooseNameForm.tsx | 42 +++++ sw-ui/src/components/home/Home.css | 13 ++ sw-ui/src/components/home/Home.tsx | 12 ++ .../src/components/home/background-zeus-temple.jpg | Bin 0 -> 571089 bytes sw-ui/src/components/home/logo-7-wonders.png | Bin 0 -> 301442 bytes sw-ui/src/components/lobby/Lobby.tsx | 56 ++++++ sw-ui/src/components/lobby/PlayerList.tsx | 41 +++++ sw-ui/src/components/lobby/RadialPlayerList.tsx | 69 ++++++++ .../components/lobby/radial-list/RadialList.css | 23 +++ .../components/lobby/radial-list/RadialList.tsx | 64 +++++++ .../lobby/radial-list/RadialListItem.css | 11 ++ .../lobby/radial-list/RadialListItem.tsx | 18 ++ .../components/lobby/radial-list/radial-math.ts | 48 ++++++ sw-ui/src/components/lobby/round-table.png | Bin 0 -> 18527 bytes sw-ui/src/global-styles.css | 0 sw-ui/src/index.tsx | 21 +++ sw-ui/src/react-app-env.d.ts | 1 + sw-ui/src/reducers.ts | 31 ++++ sw-ui/src/redux/actions/all.ts | 5 + sw-ui/src/redux/actions/game.ts | 32 ++++ sw-ui/src/redux/actions/lobby.ts | 32 ++++ sw-ui/src/redux/actions/user.ts | 17 ++ sw-ui/src/redux/currentGame.ts | 46 +++++ sw-ui/src/redux/games.ts | 56 ++++++ sw-ui/src/redux/user.ts | 43 +++++ sw-ui/src/sagas.ts | 23 +++ sw-ui/src/sagas/errors.ts | 36 ++++ sw-ui/src/sagas/game.ts | 81 +++++++++ sw-ui/src/sagas/gameBrowser.ts | 55 ++++++ sw-ui/src/sagas/home.ts | 28 +++ sw-ui/src/sagas/lobby.ts | 44 +++++ sw-ui/src/setupProxy.js | 8 + sw-ui/src/store.ts | 27 +++ 56 files changed, 2046 insertions(+) create mode 100644 sw-ui/src/@types/reflexbox.d.ts create mode 100644 sw-ui/src/api/model.ts create mode 100644 sw-ui/src/api/sevenWondersApi.ts create mode 100644 sw-ui/src/api/websocket.ts create mode 100644 sw-ui/src/components/Application.tsx create mode 100644 sw-ui/src/components/game-browser/GameBrowser.tsx create mode 100644 sw-ui/src/components/game-browser/GameList.css create mode 100644 sw-ui/src/components/game-browser/GameList.tsx create mode 100644 sw-ui/src/components/game-browser/GameStatus.tsx create mode 100644 sw-ui/src/components/game-browser/PlayerCount.css create mode 100644 sw-ui/src/components/game-browser/PlayerCount.tsx create mode 100644 sw-ui/src/components/game-browser/PlayerInfo.tsx create mode 100644 sw-ui/src/components/game/Board.css create mode 100644 sw-ui/src/components/game/Board.tsx create mode 100644 sw-ui/src/components/game/CardImage.css create mode 100644 sw-ui/src/components/game/CardImage.tsx create mode 100644 sw-ui/src/components/game/GameScene.css create mode 100644 sw-ui/src/components/game/GameScene.tsx create mode 100644 sw-ui/src/components/game/Hand.css create mode 100644 sw-ui/src/components/game/Hand.tsx create mode 100644 sw-ui/src/components/game/ProductionBar.css create mode 100644 sw-ui/src/components/game/ProductionBar.tsx create mode 100644 sw-ui/src/components/game/background-papyrus.jpg create mode 100644 sw-ui/src/components/home/ChooseNameForm.tsx create mode 100644 sw-ui/src/components/home/Home.css create mode 100644 sw-ui/src/components/home/Home.tsx create mode 100644 sw-ui/src/components/home/background-zeus-temple.jpg create mode 100644 sw-ui/src/components/home/logo-7-wonders.png create mode 100644 sw-ui/src/components/lobby/Lobby.tsx create mode 100644 sw-ui/src/components/lobby/PlayerList.tsx create mode 100644 sw-ui/src/components/lobby/RadialPlayerList.tsx create mode 100644 sw-ui/src/components/lobby/radial-list/RadialList.css create mode 100644 sw-ui/src/components/lobby/radial-list/RadialList.tsx create mode 100644 sw-ui/src/components/lobby/radial-list/RadialListItem.css create mode 100644 sw-ui/src/components/lobby/radial-list/RadialListItem.tsx create mode 100644 sw-ui/src/components/lobby/radial-list/radial-math.ts create mode 100644 sw-ui/src/components/lobby/round-table.png create mode 100644 sw-ui/src/global-styles.css create mode 100644 sw-ui/src/index.tsx create mode 100644 sw-ui/src/react-app-env.d.ts create mode 100644 sw-ui/src/reducers.ts create mode 100644 sw-ui/src/redux/actions/all.ts create mode 100644 sw-ui/src/redux/actions/game.ts create mode 100644 sw-ui/src/redux/actions/lobby.ts create mode 100644 sw-ui/src/redux/actions/user.ts create mode 100644 sw-ui/src/redux/currentGame.ts create mode 100644 sw-ui/src/redux/games.ts create mode 100644 sw-ui/src/redux/user.ts create mode 100644 sw-ui/src/sagas.ts create mode 100644 sw-ui/src/sagas/errors.ts create mode 100644 sw-ui/src/sagas/game.ts create mode 100644 sw-ui/src/sagas/gameBrowser.ts create mode 100644 sw-ui/src/sagas/home.ts create mode 100644 sw-ui/src/sagas/lobby.ts create mode 100644 sw-ui/src/setupProxy.js create mode 100644 sw-ui/src/store.ts (limited to 'sw-ui/src') diff --git a/sw-ui/src/@types/reflexbox.d.ts b/sw-ui/src/@types/reflexbox.d.ts new file mode 100644 index 00000000..802bc5f3 --- /dev/null +++ b/sw-ui/src/@types/reflexbox.d.ts @@ -0,0 +1,37 @@ +declare module 'reflexbox' { + + import { HTMLAttributes } from 'react'; + import * as React from 'react' + + export interface BoxProps { + w?: number | string, + h?: number | string, + + flex?: boolean, + wrap?: boolean, + column?: boolean, + auto?: boolean, + order?: number, + align?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline", + justify?: "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly", + + m?: number | string, + mx?: number | string, + my?: number | string, + mt?: number | string, + mb?: number | string, + ml?: number | string, + mr?: number | string, + + p?: number | string, + px?: number | string, + py?: number | string, + pt?: number | string, + pb?: number | string, + pl?: number | string, + pr?: number | string, + } + + export class Flex extends React.Component { } + export class Box extends React.Component { } +} diff --git a/sw-ui/src/api/model.ts b/sw-ui/src/api/model.ts new file mode 100644 index 00000000..2796a6d3 --- /dev/null +++ b/sw-ui/src/api/model.ts @@ -0,0 +1,187 @@ +export type ApiErrorDetail = { + message: string +}; + +export type ApiError = { + message: string, + details: ApiErrorDetail[] +}; + +export type ApiPlayer = { + username: string, + displayName: string, + index: number, + gameOwner: boolean, + user: boolean, +}; + +export type ApiWonderSidePickMethod = "EACH_RANDOM" | "ALL_A" | "ALL_B" | "SAME_RANDOM_FOR_ALL"; + +export type ApiSettings = { + randomSeedForTests: number, + timeLimitInSeconds: number, + wonderSidePickMethod: ApiWonderSidePickMethod, + initialGold: number, + discardedCardGold: number, + defaultTradingCost: number, + pointsPer3Gold: number, + lostPointsPerDefeat: number, + wonPointsPerVictoryPerAge: Map +}; + +export type ApiGameState = "LOBBY" | "PLAYING"; + +export type ApiLobby = { + id: number, + name: string, + owner: string, + players: ApiPlayer[], + settings: ApiSettings, + state: ApiGameState +}; + +export type ApiScience = { + jokers: number, + nbWheels: number, + nbCompasses: number, + nbTablets: number, +} + +export type ApiMilitary = { + nbShields: number, + totalPoints: number, + nbDefeatTokens: number, +} + +export type ApiResourceType = "WOOD" | "STONE" | "ORE" | "CLAY" | "GLASS" | "PAPYRUS" | "LOOM"; + +export type ApiResources = { + quantities: Map, +}; + +export type ApiRequirements = { + gold: number, + resources: ApiResources +} + +export type ApiCardBack = { + image: string, +}; + +export type ApiWonderStage = { + cardBack: ApiCardBack | null, + isBuilt: boolean, + requirements: ApiRequirements, + builtDuringLastMove: boolean, +} + +export type ApiWonderBuildability = { + buildable: boolean +} + +export type ApiWonder = { + name: string, + initialResource: ApiResourceType, + stages: ApiWonderStage[], + image: string, + nbBuiltStages: number, + buildability: ApiWonderBuildability, +} + +export type Color = 'BLUE' | 'GREEN' | 'RED' | 'BROWN' | 'GREY' | 'PURPLE' | 'YELLOW'; + +export type ApiProvider = "LEFT_NEIGHBOUR" | "RIGHT_NEIGHBOUR"; + +export type ApiCountedResource = { + type: ApiResourceType, + count: number, +} + +export type ApiProduction = { + fixedResources: ApiCountedResource[], + alternativeResources: ApiResourceType[][], +} + +export type ApiBoughtResources = { + provider: ApiProvider, + resources: ApiResources, +}; + +export type ApiCard = { + name: string, + color: Color, + requirements: ApiRequirements, + chainParent: String | null, + chainChildren: String[], + image: string, + back: ApiCardBack +}; + +export type ApiTableCard = ApiCard & { + playedDuringLastMove: boolean, +}; + +export type ApiBoard = { + playerIndex: number, + wonder: ApiWonder, + production: ApiProduction, + publicProduction: ApiProduction, + science: ApiScience, + military: ApiMilitary, + playedCards: ApiTableCard[][], + gold: number, +}; + +export type HandRotationDirection = 'LEFT' | 'RIGHT'; + +export type ApiMoveType = "PLAY" | "PLAY_FREE" | "UPGRADE_WONDER" | "DISCARD" | "COPY_GUILD"; + +export type ApiPlayedMove = { + playerIndex: number, + type: ApiMoveType, + card: ApiTableCard, + boughtResources: ApiBoughtResources[], +}; + +export type ApiTable = { + boards: ApiBoard[], + currentAge: number, + handRotationDirection: HandRotationDirection, + lastPlayedMoves: ApiPlayedMove[], + nbPlayers: number, +}; + +export type ApiAction = 'PLAY' | 'PLAY_2' | 'PLAY_LAST' | 'PICK_NEIGHBOR_GUILD' | 'WAIT'; + +export type ApiPlayability = { + playable: boolean, + chainable: boolean, + minPrice: number, +}; + +export type ApiHandCard = ApiCard & { + playability: ApiPlayability, +}; + +export type ApiPreparedCard = { + player: ApiPlayer, + cardBack: ApiCardBack, +}; + +export type ApiPlayerTurnInfo = { + playerIndex: number, + table: ApiTable, + currentAge: number, + action: ApiAction, + hand: ApiHandCard[], + playedMove: ApiPlayedMove | null, + neighbourGuildCards: ApiTableCard[], + message: string, + wonderBuildability: ApiWonderBuildability, +}; + +export type ApiPlayerMove = { + type: ApiMoveType, + cardName: string, + boughtResources: ApiBoughtResources[], +}; diff --git a/sw-ui/src/api/sevenWondersApi.ts b/sw-ui/src/api/sevenWondersApi.ts new file mode 100644 index 00000000..4f76a677 --- /dev/null +++ b/sw-ui/src/api/sevenWondersApi.ts @@ -0,0 +1,104 @@ +import { + ApiError, + ApiLobby, + ApiPlayer, + ApiPlayerMove, + ApiPlayerTurnInfo, + ApiPreparedCard, + ApiSettings, + ApiTable, +} from './model'; +import { JsonStompClient, SubscribeFn } from './websocket'; +import { createJsonStompClient } from './websocket'; + +const WS_URL = '/seven-wonders-websocket'; + +export class SevenWondersSession { + client: JsonStompClient; + + constructor(client: JsonStompClient) { + this.client = client; + } + + watchErrors(): SubscribeFn { + return this.client.subscriber('/user/queue/errors'); + } + + watchNameChoice(): SubscribeFn { + return this.client.subscriber('/user/queue/nameChoice'); + } + + chooseName(displayName: string): void { + this.client.send('/app/chooseName', { playerName: displayName }); + } + + watchGames(): SubscribeFn { + return this.client.subscriber('/topic/games'); + } + + watchLobbyJoined(): SubscribeFn { + return this.client.subscriber('/user/queue/lobby/joined'); + } + + createGame(gameName: string): void { + this.client.send('/app/lobby/create', { gameName }); + } + + joinGame(gameId: number): void { + this.client.send('/app/lobby/join', { gameId }); + } + + watchLobbyUpdated(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/lobby/${currentGameId}/updated`); + } + + watchGameStarted(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/lobby/${currentGameId}/started`); + } + + leave(): void { + this.client.send('/app/lobby/leave'); + } + + reorderPlayers(orderedPlayers: Array): void { + this.client.send('/app/lobby/reorderPlayers', { orderedPlayers }); + } + + updateSettings(settings: ApiSettings): void { + this.client.send('/app/lobby/updateSettings', { settings }); + } + + startGame(): void { + this.client.send('/app/lobby/startGame'); + } + + watchPlayerReady(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/game/${currentGameId}/playerReady`); + } + + watchTableUpdates(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/game/${currentGameId}/tableUpdates`); + } + + watchPreparedCards(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/game/${currentGameId}/prepared`); + } + + watchTurnInfo(): SubscribeFn { + return this.client.subscriber('/user/queue/game/turn'); + } + + sayReady(): void { + this.client.send('/app/game/sayReady'); + } + + prepareMove(move: ApiPlayerMove): void { + this.client.send('/app/game/prepareMove', { move }); + } +} + +export async function connectToGame(): Promise { + const jsonStompClient: JsonStompClient = createJsonStompClient(WS_URL); + await jsonStompClient.connect(); + return new SevenWondersSession(jsonStompClient); +} diff --git a/sw-ui/src/api/websocket.ts b/sw-ui/src/api/websocket.ts new file mode 100644 index 00000000..e9393836 --- /dev/null +++ b/sw-ui/src/api/websocket.ts @@ -0,0 +1,60 @@ +import SockJS from 'sockjs-client'; +import { Client, Frame, Message, Options, Subscription } from 'webstomp-client'; +import * as Stomp from 'webstomp-client'; + +const DEFAULT_DEBUG_OPTIONS = { + debug: process.env.NODE_ENV !== 'production', +}; + +export type Callback = (value: T) => void; +export type UnsubscribeFn = () => void; +export type SubscribeFn = (callback: Callback) => UnsubscribeFn; + +export class JsonStompClient { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + connect(headers: Stomp.ConnectionHeaders = {}): Promise { + return new Promise((resolve, reject) => { + this.client.connect(headers, resolve, reject); + }); + } + + subscribe(path: string, callback: Callback): UnsubscribeFn { + const socketSubscription: Subscription = this.client.subscribe(path, (message: Message) => { + // not all frames have a JSON body + const value: T | void = message && JsonStompClient.parseBody(message); + callback(value || {} as T); + }); + return () => socketSubscription.unsubscribe(); + } + + static parseBody(message: Message): T | void { + try { + return message.body ? JSON.parse(message.body) : undefined; + } catch (jsonParseError) { + throw new Error('Cannot parse websocket message as JSON: ' + jsonParseError.message); + } + } + + subscriber(path: string): SubscribeFn { + return (callback: Callback) => this.subscribe(path, callback); + } + + send(url: string, body?: Object) { + const strBody = body ? JSON.stringify(body) : ''; + this.client.send(url, strBody); + } +} + +function createStompClient(url: string, options: Options = {}): Client { + const optionsWithDebug = Object.assign({}, DEFAULT_DEBUG_OPTIONS, options); + return Stomp.over(new SockJS(url), optionsWithDebug); +} + +export function createJsonStompClient(url: string, options: Options = {}): JsonStompClient { + return new JsonStompClient(createStompClient(url, options)); +} diff --git a/sw-ui/src/components/Application.tsx b/sw-ui/src/components/Application.tsx new file mode 100644 index 00000000..e0ec604d --- /dev/null +++ b/sw-ui/src/components/Application.tsx @@ -0,0 +1,16 @@ +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 = () => ( + + + + + + + +); diff --git a/sw-ui/src/components/game-browser/GameBrowser.tsx b/sw-ui/src/components/game-browser/GameBrowser.tsx new file mode 100644 index 00000000..a6367d5e --- /dev/null +++ b/sw-ui/src/components/game-browser/GameBrowser.tsx @@ -0,0 +1,56 @@ +import { Button, Classes, InputGroup, Intent } from '@blueprintjs/core'; +import React, { ChangeEvent, Component, SyntheticEvent } 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'; + +type GameBrowserProps = { + createGame: (gameName: string) => void, +} + +class GameBrowserPresenter extends Component { + + _gameName: string | void = undefined; + + createGame = (e: SyntheticEvent): void => { + e.preventDefault(); + if (this._gameName !== undefined) { + this.props.createGame(this._gameName); + } + }; + + render() { + return ( +
+ +
+ ) => (this._gameName = e.target.value)} + rightElement={} + /> + + +
+ +
+ ); + } +} + +type CreateGameButtonProps = { + createGame: (e: SyntheticEvent) => void +} + +const CreateGameButton = ({createGame}: CreateGameButtonProps) => ( +