summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/api/model.js76
-rw-r--r--frontend/src/api/sevenWondersApi.js76
-rw-r--r--frontend/src/api/websocket.js57
-rw-r--r--frontend/src/sagas.js4
-rw-r--r--frontend/src/sagas/gameBrowser.js12
-rw-r--r--frontend/src/sagas/home.js3
-rw-r--r--frontend/src/sagas/lobby.js5
-rw-r--r--frontend/src/sagas/utils.js10
8 files changed, 185 insertions, 58 deletions
diff --git a/frontend/src/api/model.js b/frontend/src/api/model.js
new file mode 100644
index 00000000..eb7ad9c5
--- /dev/null
+++ b/frontend/src/api/model.js
@@ -0,0 +1,76 @@
+export class ApiError {
+ message: string;
+ details: ApiErrorDetail[];
+}
+
+export class ApiErrorDetail {
+ message: string;
+}
+
+export type ApiGameState = "LOBBY" | "PLAYING";
+
+export class ApiLobby {
+ id: number;
+ name: string;
+ owner: ApiPlayer;
+ players: ApiPlayer[];
+ settings: ApiSettings;
+ state: ApiGameState;
+}
+
+export type ApiWonderSidePickMethod = "EACH_RANDOM" | "ALL_A" | "ALL_B" | "SAME_RANDOM_FOR_ALL";
+
+export class ApiSettings {
+ randomSeedForTests: number;
+ timeLimitInSeconds: number;
+ wonderSidePickMethod: ApiWonderSidePickMethod;
+ initialGold: number;
+ discardedCardGold: number;
+ defaultTradingCost: number;
+ pointsPer3Gold: number;
+ lostPointsPerDefeat: number;
+ wonPointsPerVictoryPerAge: Map<number, number>;
+}
+
+export class ApiPlayer {
+ username: string;
+ displayName: string;
+ index: number;
+ ready: boolean;
+}
+
+export class ApiTable {}
+
+export class ApiHandCard {}
+
+export class ApiCard {}
+
+export class ApiPreparedCard {}
+
+export class ApiPlayerTurnInfo {
+ playerIndex: number;
+ table: ApiTable;
+ currentAge: number;
+ action: Action;
+ hand: ApiHandCard[];
+ neighbourGuildCards: ApiCard[];
+ message: string;
+}
+
+export type ApiMoveType = "PLAY" | "PLAY_FREE" | "UPGRADE_WONDER" | "DISCARD" | "COPY_GUILD";
+export type ApiProvider = "LEFT_NEIGHBOUR" | "RIGHT_NEIGHBOUR";
+export type ApiResourceType = "WOOD" | "STONE" | "ORE" | "CLAY" | "GLASS" | "PAPYRUS" | "LOOM";
+
+export class ApiResources {
+ quantities: Map<ApiResourceType, number>;
+}
+export class ApiBoughtResources {
+ provider: ApiProvider;
+ resources: ApiResources;
+}
+
+export class ApiPlayerMove {
+ type: ApiMoveType;
+ cardName: string;
+ boughtResources: ApiBoughtResources[];
+}
diff --git a/frontend/src/api/sevenWondersApi.js b/frontend/src/api/sevenWondersApi.js
index 4ba0fff3..1004c81e 100644
--- a/frontend/src/api/sevenWondersApi.js
+++ b/frontend/src/api/sevenWondersApi.js
@@ -1,66 +1,82 @@
-import { createJsonSubscriptionChannel, createStompSession } from './websocket';
-import type { Client } from 'webstomp-client';
-import type { Channel } from 'redux-saga';
+import { createJsonStompClient, JsonStompClient, Callback } from './websocket';
+import { ApiError, ApiLobby, ApiPlayer, ApiPlayerMove, ApiPlayerTurnInfo, ApiPreparedCard, ApiTable } from './model';
const wsURL = '/seven-wonders-websocket';
export class SevenWondersSession {
- client: Client;
+ client: JsonStompClient;
- constructor(client: Client) {
+ constructor(client: JsonStompClient) {
this.client = client;
}
- watchErrors(): Channel<ApiError> {
- return createJsonSubscriptionChannel(this.client, '/user/queue/errors');
+ watchErrors(callback: Callback<ApiError>): void {
+ return this.client.subscribe('/user/queue/errors', callback);
}
chooseName(displayName: string): void {
- this.client.send('/app/chooseName', JSON.stringify({ playerName: displayName }));
+ this.client.send('/app/chooseName', { playerName: displayName });
}
- watchNameChoice(): Channel<Object> {
- return createJsonSubscriptionChannel(this.client, '/user/queue/nameChoice');
+ watchNameChoice(callback: Callback<ApiPlayer>): void {
+ return this.client.subscribe('/user/queue/nameChoice', callback);
}
- watchGames(): Channel<Object> {
- return createJsonSubscriptionChannel(this.client, '/topic/games');
+ watchGames(callback: Callback<ApiLobby[]>): void {
+ return this.client.subscribe('/topic/games', callback);
}
- watchLobbyJoined(): Channel<Object> {
- return createJsonSubscriptionChannel(this.client, '/user/queue/lobby/joined');
+ watchLobbyJoined(callback: Callback<Object>): void {
+ return this.client.subscribe('/user/queue/lobby/joined', callback);
}
- watchLobbyUpdated(currentGameId: number): Channel<Object> {
- return createJsonSubscriptionChannel(this.client, `/topic/lobby/${currentGameId}/updated`);
+ watchLobbyUpdated(currentGameId: number, callback: Callback<Object>): void {
+ return this.client.subscribe(`/topic/lobby/${currentGameId}/updated`, callback);
}
- watchGameStarted(currentGameId: number): Channel<Object> {
- return createJsonSubscriptionChannel(this.client, `/topic/lobby/${currentGameId}/started`);
+ watchGameStarted(currentGameId: number, callback: Callback<Object>): void {
+ return this.client.subscribe(`/topic/lobby/${currentGameId}/started`, callback);
}
createGame(gameName: string): void {
- this.client.send('/app/lobby/create', JSON.stringify({ gameName }));
+ this.client.send('/app/lobby/create', { gameName });
}
joinGame(gameId: number): void {
- this.client.send('/app/lobby/join', JSON.stringify({ gameId }));
+ this.client.send('/app/lobby/join', { gameId });
}
startGame(): void {
- this.client.send('/app/lobby/startGame', {});
+ this.client.send('/app/lobby/startGame');
}
-}
-export function createSession(): Promise<SevenWondersSession> {
- return createStompSession(wsURL).then(client => new SevenWondersSession(client));
-}
+ watchPlayerReady(currentGameId: number, callback: Callback<string>): void {
+ return this.client.subscribe(`/topic/game/${currentGameId}/playerReady`, callback);
+ }
+
+ watchTableUpdates(currentGameId: number, callback: Callback<ApiTable>): void {
+ return this.client.subscribe(`/topic/game/${currentGameId}/tableUpdates`, callback);
+ }
+
+ watchPreparedMove(currentGameId: number, callback: Callback<ApiPreparedCard>): void {
+ return this.client.subscribe(`/topic/game/${currentGameId}/prepared`, callback);
+ }
+
+ watchTurnInfo(callback: Callback<ApiPlayerTurnInfo>): void {
+ return this.client.subscribe('/user/queue/game/turnInfo', callback);
+ }
-export class ApiError {
- message: string;
- details: ApiErrorDetail[];
+ sayReady(): void {
+ this.client.send('/app/game/sayReady');
+ }
+
+ prepareMove(move: ApiPlayerMove): void {
+ this.client.send('/app/game/sayReady', { move });
+ }
}
-export class ApiErrorDetail {
- message: string;
+export async function connectToGame(): Promise<SevenWondersSession> {
+ const jsonStompClient: JsonStompClient = createJsonStompClient(wsURL);
+ await jsonStompClient.connect();
+ return new SevenWondersSession(jsonStompClient);
}
diff --git a/frontend/src/api/websocket.js b/frontend/src/api/websocket.js
index 6dc6e1a0..d411587a 100644
--- a/frontend/src/api/websocket.js
+++ b/frontend/src/api/websocket.js
@@ -1,32 +1,49 @@
// @flow
import SockJS from 'sockjs-client';
import Stomp from 'webstomp-client';
-import type { Client, Frame, Subscription } from 'webstomp-client';
+import type { Client, Frame, Options, Subscription } from 'webstomp-client';
-import { eventChannel } from 'redux-saga';
-import type { Channel } from 'redux-saga';
+const DEFAULT_DEBUG_OPTIONS = {
+ debug: process.env.NODE_ENV !== 'production',
+};
-function createStompClient(url: string): Client {
- return Stomp.over(new SockJS(url), {
- debug: process.env.NODE_ENV !== 'production',
- });
-}
+export type Callback<T> = (value: T) => void;
-export function createStompSession(url: string, headers: Object = {}): Promise<Client> {
- return new Promise((resolve, reject) => {
- const client: Client = createStompClient(url);
- const onSuccess = (frame: Frame) => resolve(client);
- client.connect(headers, onSuccess, reject);
- });
-}
+export type Callable = () => void;
+
+export class JsonStompClient {
+ client: Client;
+
+ constructor(client: Client) {
+ this.client = client;
+ }
+
+ connect(headers: Object = {}): Promise<Frame> {
+ return new Promise((resolve, reject) => {
+ this.client.connect(headers, resolve, reject);
+ });
+ }
-export function createJsonSubscriptionChannel(client: Client, path: string): Channel {
- return eventChannel((emitter: (data: any) => void) => {
- const socketSubscription: Subscription = client.subscribe(path, (frame: Frame) => {
+ subscribe<T>(path: string, callback: Callback<T>): Callable {
+ const socketSubscription: Subscription = this.client.subscribe(path, (frame: Frame) => {
// not all frames have a JSON body
const value = frame && frame.body && JSON.parse(frame.body);
- emitter(value);
+ callback(value);
});
return () => socketSubscription.unsubscribe();
- });
+ }
+
+ 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/frontend/src/sagas.js b/frontend/src/sagas.js
index 2b690a02..b7fc4122 100644
--- a/frontend/src/sagas.js
+++ b/frontend/src/sagas.js
@@ -6,12 +6,12 @@ import { makeSagaRoutes } from './routes';
import errorHandlingSaga from './sagas/errors';
import type { History } from 'react-router';
-import { SevenWondersSession, createSession } from './api/sevenWondersApi';
+import { SevenWondersSession, connectToGame } from './api/sevenWondersApi';
export default function* rootSaga(history: History): * {
let sevenWondersSession: SevenWondersSession | void;
try {
- sevenWondersSession = yield call(createSession);
+ sevenWondersSession = yield call(connectToGame);
} catch (error) {
console.error('Could not connect to socket', error);
return;
diff --git a/frontend/src/sagas/gameBrowser.js b/frontend/src/sagas/gameBrowser.js
index 17eb9287..871908f6 100644
--- a/frontend/src/sagas/gameBrowser.js
+++ b/frontend/src/sagas/gameBrowser.js
@@ -8,9 +8,10 @@ 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';
import type { SevenWondersSession } from '../api/sevenWondersApi';
+import { createChannel } from './utils';
function* watchGames(session: SevenWondersSession): * {
- const gamesChannel = yield apply(session, session.watchGames, []);
+ const gamesChannel = yield createChannel(session, session.watchGames);
try {
while (true) {
const gameList = yield take(gamesChannel);
@@ -25,7 +26,7 @@ function* watchGames(session: SevenWondersSession): * {
}
function* watchLobbyJoined(session: SevenWondersSession): * {
- const joinedLobbyChannel = yield apply(session, session.watchLobbyJoined, []);
+ const joinedLobbyChannel = yield createChannel(session, session.watchLobbyJoined);
try {
const joinedLobby = yield take(joinedLobbyChannel);
const normalized = normalize(joinedLobby, gameSchema);
@@ -54,7 +55,12 @@ function* joinGame(session: SevenWondersSession): * {
}
function* gameBrowserSaga(session: SevenWondersSession): * {
- yield [call(watchGames, session), call(watchLobbyJoined, session), call(createGame, session), call(joinGame, session)];
+ yield [
+ call(watchGames, session),
+ call(watchLobbyJoined, session),
+ call(createGame, session),
+ call(joinGame, session),
+ ];
}
export default gameBrowserSaga;
diff --git a/frontend/src/sagas/home.js b/frontend/src/sagas/home.js
index eb65097b..b51bf4dc 100644
--- a/frontend/src/sagas/home.js
+++ b/frontend/src/sagas/home.js
@@ -3,6 +3,7 @@ import { push } from 'react-router-redux';
import { actions, types } from '../redux/players';
import type { SevenWondersSession } from '../api/sevenWondersApi';
+import { createChannel } from './utils';
function* sendUsername(session: SevenWondersSession) {
while (true) {
@@ -12,7 +13,7 @@ function* sendUsername(session: SevenWondersSession) {
}
function* validateUsername(session: SevenWondersSession) {
- const usernameChannel = yield apply(session, session.watchNameChoice, []);
+ const usernameChannel = yield createChannel(session, session.watchNameChoice);
while (true) {
const user = yield take(usernameChannel);
yield put(actions.setCurrentPlayer(user));
diff --git a/frontend/src/sagas/lobby.js b/frontend/src/sagas/lobby.js
index 0c264dde..cc704086 100644
--- a/frontend/src/sagas/lobby.js
+++ b/frontend/src/sagas/lobby.js
@@ -9,6 +9,7 @@ import { game as gameSchema } from '../schemas/games';
import { actions as gameActions, types } from '../redux/games';
import { actions as playerActions } from '../redux/players';
import { SevenWondersSession } from '../api/sevenWondersApi';
+import { createChannel } from './utils';
function getCurrentGameId(): number {
const path = window.location.pathname;
@@ -17,7 +18,7 @@ function getCurrentGameId(): number {
function* watchLobbyUpdates(session: SevenWondersSession) {
const currentGameId: number = getCurrentGameId();
- const lobbyUpdatesChannel: Channel = yield apply(session, session.watchLobbyUpdated, [currentGameId]);
+ const lobbyUpdatesChannel: Channel = yield createChannel(session, session.watchLobbyUpdated, currentGameId);
try {
while (true) {
const lobby = yield take(lobbyUpdatesChannel);
@@ -32,7 +33,7 @@ function* watchLobbyUpdates(session: SevenWondersSession) {
function* watchGameStart(session: SevenWondersSession) {
const currentGameId = getCurrentGameId();
- const gameStartedChannel = yield apply(session, session.watchGameStarted, [currentGameId]);
+ const gameStartedChannel = yield createChannel(session, session.watchGameStarted, currentGameId);
try {
yield take(gameStartedChannel);
yield put(gameActions.enterGame());
diff --git a/frontend/src/sagas/utils.js b/frontend/src/sagas/utils.js
new file mode 100644
index 00000000..28017c87
--- /dev/null
+++ b/frontend/src/sagas/utils.js
@@ -0,0 +1,10 @@
+import { SevenWondersSession } from '../api/sevenWondersApi';
+import { eventChannel } from 'redux-saga';
+
+type Emitter = (value: any) => void;
+
+export function createChannel(session: SevenWondersSession, methodWithCallback: () => void, ...args: Array<any>) {
+ return eventChannel((emitter: Emitter) => {
+ return methodWithCallback.call(session, ...args, emitter);
+ });
+}
bgstack15