diff options
Diffstat (limited to 'sw-ui/src/api')
-rw-r--r-- | sw-ui/src/api/model.ts | 187 | ||||
-rw-r--r-- | sw-ui/src/api/sevenWondersApi.ts | 104 | ||||
-rw-r--r-- | sw-ui/src/api/websocket.ts | 60 |
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)); +} |