summaryrefslogtreecommitdiff
path: root/sw-ui/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'sw-ui/src/api')
-rw-r--r--sw-ui/src/api/model.ts187
-rw-r--r--sw-ui/src/api/sevenWondersApi.ts104
-rw-r--r--sw-ui/src/api/websocket.ts60
3 files changed, 351 insertions, 0 deletions
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<number, number>
+};
+
+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<ApiResourceType, number>,
+};
+
+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<ApiError> {
+ return this.client.subscriber('/user/queue/errors');
+ }
+
+ watchNameChoice(): SubscribeFn<ApiPlayer> {
+ return this.client.subscriber('/user/queue/nameChoice');
+ }
+
+ chooseName(displayName: string): void {
+ this.client.send('/app/chooseName', { playerName: displayName });
+ }
+
+ watchGames(): SubscribeFn<ApiLobby[]> {
+ return this.client.subscriber('/topic/games');
+ }
+
+ watchLobbyJoined(): SubscribeFn<Object> {
+ 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<Object> {
+ return this.client.subscriber(`/topic/lobby/${currentGameId}/updated`);
+ }
+
+ watchGameStarted(currentGameId: number): SubscribeFn<Object> {
+ return this.client.subscriber(`/topic/lobby/${currentGameId}/started`);
+ }
+
+ leave(): void {
+ this.client.send('/app/lobby/leave');
+ }
+
+ reorderPlayers(orderedPlayers: Array<string>): 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<string> {
+ return this.client.subscriber(`/topic/game/${currentGameId}/playerReady`);
+ }
+
+ watchTableUpdates(currentGameId: number): SubscribeFn<ApiTable> {
+ return this.client.subscriber(`/topic/game/${currentGameId}/tableUpdates`);
+ }
+
+ watchPreparedCards(currentGameId: number): SubscribeFn<ApiPreparedCard> {
+ return this.client.subscriber(`/topic/game/${currentGameId}/prepared`);
+ }
+
+ watchTurnInfo(): SubscribeFn<ApiPlayerTurnInfo> {
+ 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<SevenWondersSession> {
+ 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<T> = (value: T) => void;
+export type UnsubscribeFn = () => void;
+export type SubscribeFn<T> = (callback: Callback<T>) => UnsubscribeFn;
+
+export class JsonStompClient {
+ client: Client;
+
+ constructor(client: Client) {
+ this.client = client;
+ }
+
+ connect(headers: Stomp.ConnectionHeaders = {}): Promise<Frame | void> {
+ return new Promise((resolve, reject) => {
+ this.client.connect(headers, resolve, reject);
+ });
+ }
+
+ subscribe<T>(path: string, callback: Callback<T>): 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<T>(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<T>(path: string): SubscribeFn<T> {
+ return (callback: Callback<T>) => 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));
+}
bgstack15