diff --git a/docs/documentation/api/Client.md b/docs/documentation/api/Client.md index 4bcaeb426..4b290a78a 100644 --- a/docs/documentation/api/Client.md +++ b/docs/documentation/api/Client.md @@ -113,8 +113,8 @@ The `Board` component will receive the following as `props`: 11. `playerID`: The player ID associated with the client. -12. `gameMetadata`: An object containing the players that have joined - the game from a [room](/api/Lobby.md). +12. `matchMetadata`: An object containing the players that have joined + the game from a [match](/api/Lobby.md). Example: diff --git a/docs/documentation/api/Lobby.md b/docs/documentation/api/Lobby.md index 665fa3466..13606a3fa 100644 --- a/docs/documentation/api/Lobby.md +++ b/docs/documentation/api/Lobby.md @@ -21,15 +21,15 @@ import { Lobby } from 'boardgame.io/react'; ### Server-side API -The [Server](/api/Server) hosts the Lobby REST API that can be used to create and join rooms. It is particularly useful when you want to +The [Server](/api/Server) hosts the Lobby REST API that can be used to create and join matches. It is particularly useful when you want to authenticate clients to prove that they have the right to send actions on behalf of a player. -Authenticated games are created with server-side tokens for each player. You can create a room with the `create` API call, and join a player to a room with the `join` API call. +Authenticated games are created with server-side tokens for each player. You can create a match with the `create` API call, and join a player to a match with the `join` API call. A game that is authenticated will not accept moves from a client on behalf of a player without the appropriate credential token. -Use the `create` API call to create a room that requires credential tokens. When you call the `join` API, you can retrieve the credential token for a particular player. +Use the `create` API call to create a match that requires credential tokens. When you call the `join` API, you can retrieve the credential token for a particular player. #### Configuration @@ -45,11 +45,11 @@ Options are: - `apiPort`: If specified, it runs the Lobby API in a separate Koa server on this port. Otherwise, it shares the same Koa server runnning on the default boardgame.io `port`. - `apiCallback`: Called when the Koa server is ready. Only applicable if `apiPort` is specified. -#### Creating a room +#### Creating a match ##### POST `/games/{name}/create` -Creates a new authenticated room for a game named `name`. +Creates a new authenticated match for a game named `name`. Accepts three parameters: @@ -57,15 +57,15 @@ Accepts three parameters: `setupData` (optional): custom object that is passed to the game `setup` function. -`unlisted` (optional): if set to `true`, the room will be excluded from the public list of room instances. +`unlisted` (optional): if set to `true`, the match will be excluded from the public list of match instances. -Returns `roomID`, which is the ID of the newly created game instance. +Returns `matchID`, which is the ID of the newly created game instance. #### Joining a game ##### POST `/games/{name}/{id}/join` -Allows a player to join a particular room instance `id` of a game named `name`. +Allows a player to join a particular match instance `id` of a game named `name`. Accepts three JSON body parameters: @@ -81,7 +81,7 @@ Returns `playerCredentials` which is the token this player will require to authe ##### POST `/games/{name}/{id}/update` -Rename and/or update additional information of a user in the room instance `id` of a game named `name` previously joined by the player. +Rename and/or update additional information of a user in the match instance `id` of a game named `name` previously joined by the player. Accepts four parameters, requires at least one of the two optional parameters: @@ -93,11 +93,11 @@ Accepts four parameters, requires at least one of the two optional parameters: `data` (optional): additional information associated to the player. -#### Leaving a room +#### Leaving a match ##### POST `/games/{name}/{id}/leave` -Leave the room instance `id` of a game named `name` previously joined by the player. +Leave the match instance `id` of a game named `name` previously joined by the player. Accepts two parameters, all required: @@ -105,29 +105,29 @@ Accepts two parameters, all required: `credentials`: the authentication token of the player. -#### Listing all room instances of a given game +#### Listing all match instances of a given game ##### GET `/games/{name}` -Returns all room instances of the game named `name`. +Returns all match instances of the game named `name`. -Returns an array of `rooms`. Each instance has fields: +Returns an array of `matches`. Each instance has fields: -`roomID`: the ID of the room instance. +`matchID`: the ID of the match instance. `players`: the list of seats and players that have joined the game, if any. `setupData` (optional): custom object that was passed to the game `setup` function. -#### Getting specific instance of a room by its ID +#### Getting specific instance of a match by its ID ##### GET `/games/{name}/{id}` -Returns a room instance given its roomID. +Returns a match instance given its matchID. -Returns a room instance. Each instance has fields: +Returns a match instance. Each instance has fields: -`roomID`: the ID of the room instance. +`matchID`: the ID of the match instance. `players`: the list of seats and players that have joined the game, if any. @@ -143,9 +143,9 @@ All actions for an authenticated game require an additional payload field `crede `{name}` (required): the name of the game being played again. -`{id}` (required): the ID of the previous finished room. +`{id}` (required): the ID of the previous finished match. -Given a previous room, generates a room ID where users should go if they want to play again. Creates this new room if it didn't exist before. +Given a previous match, generates a match ID where users should go if they want to play again. Creates this new match if it didn't exist before. Accepts these parameters: @@ -153,4 +153,4 @@ Accepts these parameters: `credentials` (required): player's credentials. -Returns `nextRoomID`, which is the ID of the newly created room that the user should go to play again. +Returns `nextMatchID`, which is the ID of the newly created match that the user should go to play again. diff --git a/src/lobby/connection.js b/src/lobby/connection.js index febce4210..b2f8dabb5 100644 --- a/src/lobby/connection.js +++ b/src/lobby/connection.js @@ -12,7 +12,7 @@ class _LobbyConnectionImpl { this.playerName = playerName || 'Visitor'; this.playerCredentials = playerCredentials; this.server = server; - this.rooms = []; + this.matches = []; } _baseUrl() { @@ -21,7 +21,7 @@ class _LobbyConnectionImpl { async refresh() { try { - this.rooms.length = 0; + this.matches.length = 0; const resp = await fetch(this._baseUrl()); if (resp.status !== 200) { throw new Error('HTTP status ' + resp.status); @@ -31,19 +31,19 @@ class _LobbyConnectionImpl { if (!this._getGameComponents(gameName)) continue; const gameResp = await fetch(this._baseUrl() + '/' + gameName); const gameJson = await gameResp.json(); - for (let inst of gameJson.rooms) { + for (let inst of gameJson.matches) { inst.gameName = gameName; } - this.rooms = this.rooms.concat(gameJson.rooms); + this.matches = this.matches.concat(gameJson.matches); } } catch (error) { - throw new Error('failed to retrieve list of games (' + error + ')'); + throw new Error('failed to retrieve list of matches (' + error + ')'); } } - _getGameInstance(gameID) { - for (let inst of this.rooms) { - if (inst['gameID'] === gameID) return inst; + _getMatchInstance(matchID) { + for (let inst of this.matches) { + if (inst['matchID'] === matchID) return inst; } } @@ -54,23 +54,23 @@ class _LobbyConnectionImpl { } _findPlayer(playerName) { - for (let inst of this.rooms) { + for (let inst of this.matches) { if (inst.players.some(player => player.name === playerName)) return inst; } } - async join(gameName, gameID, playerID) { + async join(gameName, matchID, playerID) { try { let inst = this._findPlayer(this.playerName); if (inst) { - throw new Error('player has already joined ' + inst.gameID); + throw new Error('player has already joined ' + inst.matchID); } - inst = this._getGameInstance(gameID); + inst = this._getMatchInstance(matchID); if (!inst) { - throw new Error('game instance ' + gameID + ' not found'); + throw new Error('game instance ' + matchID + ' not found'); } const resp = await fetch( - this._baseUrl() + '/' + gameName + '/' + gameID + '/join', + this._baseUrl() + '/' + gameName + '/' + matchID + '/join', { method: 'POST', body: JSON.stringify({ @@ -85,18 +85,18 @@ class _LobbyConnectionImpl { inst.players[Number.parseInt(playerID)].name = this.playerName; this.playerCredentials = json.playerCredentials; } catch (error) { - throw new Error('failed to join room ' + gameID + ' (' + error + ')'); + throw new Error('failed to join match ' + matchID + ' (' + error + ')'); } } - async leave(gameName, gameID) { + async leave(gameName, matchID) { try { - let inst = this._getGameInstance(gameID); - if (!inst) throw new Error('game instance not found'); + let inst = this._getMatchInstance(matchID); + if (!inst) throw new Error('match instance not found'); for (let player of inst.players) { if (player.name === this.playerName) { const resp = await fetch( - this._baseUrl() + '/' + gameName + '/' + gameID + '/leave', + this._baseUrl() + '/' + gameName + '/' + matchID + '/leave', { method: 'POST', body: JSON.stringify({ @@ -114,18 +114,18 @@ class _LobbyConnectionImpl { return; } } - throw new Error('player not found in room'); + throw new Error('player not found in match'); } catch (error) { - throw new Error('failed to leave room ' + gameID + ' (' + error + ')'); + throw new Error('failed to leave match ' + matchID + ' (' + error + ')'); } } async disconnect() { let inst = this._findPlayer(this.playerName); if (inst) { - await this.leave(inst.gameName, inst.gameID); + await this.leave(inst.gameName, inst.matchID); } - this.rooms = []; + this.matches = []; this.playerName = 'Visitor'; } @@ -148,7 +148,7 @@ class _LobbyConnectionImpl { if (resp.status !== 200) throw new Error('HTTP status ' + resp.status); } catch (error) { throw new Error( - 'failed to create room for ' + gameName + ' (' + error + ')' + 'failed to create match for ' + gameName + ' (' + error + ')' ); } } diff --git a/src/lobby/connection.test.js b/src/lobby/connection.test.js index 649c19057..d52b4882a 100644 --- a/src/lobby/connection.test.js +++ b/src/lobby/connection.test.js @@ -10,21 +10,21 @@ import { LobbyConnection } from './connection.js'; describe('lobby', () => { let lobby; - let room1, room2; + let match1, match2; let jsonResult = []; let nextStatus = 200; beforeEach(async () => { - room1 = { gameID: 'gameID_1', players: [{ id: '0' }] }; - room2 = { gameID: 'gameID_2', players: [{ id: '1' }] }; + match1 = { matchID: 'matchID_1', players: [{ id: '0' }] }; + match2 = { matchID: 'matchID_2', players: [{ id: '1' }] }; // result of connection requests jsonResult = [ () => ['game1', 'game2'], () => { - return { rooms: [room1] }; + return { matches: [match1] }; }, () => { - return { rooms: [room2] }; + return { matches: [match2] }; }, ]; let nextResult = jsonResult.shift.bind(jsonResult); @@ -57,10 +57,10 @@ describe('lobby', () => { await lobby.refresh(); }); - describe('get list of rooms', () => { + describe('get list of matches', () => { test('when the server requests succeed', async () => { expect(fetch).toHaveBeenCalledTimes(3); - expect(lobby.rooms).toEqual([room1, room2]); + expect(lobby.matches).toEqual([match1, match2]); }); test('when the server request fails', async () => { nextStatus = 404; @@ -69,38 +69,38 @@ describe('lobby', () => { } catch (error) { expect(error).toBeInstanceOf(Error); } - expect(lobby.rooms).toEqual([]); + expect(lobby.matches).toEqual([]); }); }); - describe('join a room', () => { + describe('join a match', () => { beforeEach(async () => { // result of request 'join' jsonResult.push(() => { return { playerCredentials: 'SECRET' }; }); }); - test('when the room exists', async () => { - await lobby.join('game1', 'gameID_1', '0'); + test('when the match exists', async () => { + await lobby.join('game1', 'matchID_1', '0'); expect(fetch).toHaveBeenCalledTimes(4); - expect(lobby.rooms[0].players[0]).toEqual({ + expect(lobby.matches[0].players[0]).toEqual({ id: '0', name: 'Bob', }); expect(lobby.playerCredentials).toEqual('SECRET'); }); - test('when the room does not exist', async () => { + test('when the match does not exist', async () => { try { - await lobby.join('game1', 'gameID_3', '0'); + await lobby.join('game1', 'matchID_3', '0'); } catch (error) { expect(error).toBeInstanceOf(Error); } - expect(lobby.rooms).toEqual([room1, room2]); + expect(lobby.matches).toEqual([match1, match2]); }); test('when the seat is not available', async () => { - room1.players[0].name = 'Bob'; + match1.players[0].name = 'Bob'; try { - await lobby.join('game1', 'gameID_3', '0'); + await lobby.join('game1', 'matchID_3', '0'); } catch (error) { expect(error).toBeInstanceOf(Error); } @@ -108,52 +108,52 @@ describe('lobby', () => { test('when the server request fails', async () => { nextStatus = 404; try { - await lobby.join('game1', 'gameID_1', '0'); + await lobby.join('game1', 'matchID_1', '0'); } catch (error) { expect(error).toBeInstanceOf(Error); } }); - test('when the player has already joined another game', async () => { - room2.players[0].name = 'Bob'; + test('when the player has already joined another match', async () => { + match2.players[0].name = 'Bob'; try { - await lobby.join('game1', 'gameID_1', '0'); + await lobby.join('game1', 'matchID_1', '0'); } catch (error) { expect(error).toBeInstanceOf(Error); } }); }); - describe('leave a room', () => { + describe('leave a match', () => { beforeEach(async () => { // result of request 'join' jsonResult.push(() => { return { playerCredentials: 'SECRET' }; }); - await lobby.join('game1', 'gameID_1', '0'); + await lobby.join('game1', 'matchID_1', '0'); // result of request 'leave' jsonResult.push(() => { return {}; }); }); - test('when the room exists', async () => { - await lobby.leave('game1', 'gameID_1'); + test('when the match exists', async () => { + await lobby.leave('game1', 'matchID_1'); expect(fetch).toHaveBeenCalledTimes(5); - expect(lobby.rooms).toEqual([room1, room2]); + expect(lobby.matches).toEqual([match1, match2]); }); - test('when the room does not exist', async () => { + test('when the match does not exist', async () => { try { - await lobby.leave('game1', 'gameID_3'); + await lobby.leave('game1', 'matchID_3'); } catch (error) { expect(error).toBeInstanceOf(Error); } expect(fetch).toHaveBeenCalledTimes(4); - expect(lobby.rooms).toEqual([room1, room2]); + expect(lobby.matches).toEqual([match1, match2]); }); - test('when the player is not in the room', async () => { - await lobby.leave('game1', 'gameID_1'); + test('when the player is not in the match', async () => { + await lobby.leave('game1', 'matchID_1'); expect(fetch).toHaveBeenCalledTimes(5); try { - await lobby.leave('game1', 'gameID_1'); + await lobby.leave('game1', 'matchID_1'); } catch (error) { expect(error).toBeInstanceOf(Error); } @@ -161,7 +161,7 @@ describe('lobby', () => { test('when the server request fails', async () => { nextStatus = 404; try { - await lobby.leave('game1', 'gameID_1'); + await lobby.leave('game1', 'matchID_1'); } catch (error) { expect(error).toBeInstanceOf(Error); } @@ -172,24 +172,24 @@ describe('lobby', () => { beforeEach(async () => {}); test('when the player leaves the lobby', async () => { await lobby.disconnect(); - expect(lobby.rooms).toEqual([]); + expect(lobby.matches).toEqual([]); }); - test('when the player had joined a room', async () => { + test('when the player had joined a match', async () => { // result of request 'join' jsonResult.push(() => { return { playerCredentials: 'SECRET' }; }); - await lobby.join('game1', 'gameID_1', '0'); + await lobby.join('game1', 'matchID_1', '0'); // result of request 'leave' jsonResult.push(() => { return {}; }); await lobby.disconnect(); - expect(lobby.rooms).toEqual([]); + expect(lobby.matches).toEqual([]); }); }); - describe('create a room', () => { + describe('create a match', () => { test('when the server request succeeds', async () => { await lobby.create('game1', 2); expect(fetch).toHaveBeenCalledTimes(4); @@ -230,9 +230,9 @@ describe('lobby', () => { }); await lobby.refresh(); }); - test('get list of rooms for supported games', async () => { + test('get list of matches for supported games', async () => { expect(fetch).toHaveBeenCalledTimes(2); - expect(lobby.rooms).toEqual([room1]); + expect(lobby.matches).toEqual([match1]); }); }); }); diff --git a/src/lobby/create-room-form.js b/src/lobby/create-match-form.js similarity index 84% rename from src/lobby/create-room-form.js rename to src/lobby/create-match-form.js index b8dbaade2..f8e72a185 100644 --- a/src/lobby/create-room-form.js +++ b/src/lobby/create-match-form.js @@ -9,10 +9,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -class LobbyCreateRoomForm extends React.Component { +class LobbyCreateMatchForm extends React.Component { static propTypes = { games: PropTypes.array.isRequired, - createGame: PropTypes.func.isRequired, + createMatch: PropTypes.func.isRequired, }; state = { @@ -24,14 +24,14 @@ class LobbyCreateRoomForm extends React.Component { super(props); /* fix min and max number of players */ for (let game of props.games) { - let game_details = game.game; - if (!game_details.minPlayers) { - game_details.minPlayers = 1; + let matchDetails = game.game; + if (!matchDetails.minPlayers) { + matchDetails.minPlayers = 1; } - if (!game_details.maxPlayers) { - game_details.maxPlayers = 4; + if (!matchDetails.maxPlayers) { + matchDetails.maxPlayers = 4; } - console.assert(game_details.maxPlayers >= game_details.minPlayers); + console.assert(matchDetails.maxPlayers >= matchDetails.minPlayers); } this.state = { selectedGame: 0, @@ -99,11 +99,11 @@ class LobbyCreateRoomForm extends React.Component { }; onClickCreate = () => { - this.props.createGame( + this.props.createMatch( this.props.games[this.state.selectedGame].game.name, this.state.numPlayers ); }; } -export default LobbyCreateRoomForm; +export default LobbyCreateMatchForm; diff --git a/src/lobby/room-instance.js b/src/lobby/match-instance.js similarity index 68% rename from src/lobby/room-instance.js rename to src/lobby/match-instance.js index 2dc4a6380..bec612bc8 100644 --- a/src/lobby/room-instance.js +++ b/src/lobby/match-instance.js @@ -9,11 +9,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -class LobbyRoomInstance extends React.Component { +class LobbyMatchInstance extends React.Component { static propTypes = { - room: PropTypes.shape({ + match: PropTypes.shape({ gameName: PropTypes.string.isRequired, - gameID: PropTypes.string.isRequired, + matchID: PropTypes.string.isRequired, players: PropTypes.array.isRequired, }), playerName: PropTypes.string.isRequired, @@ -28,9 +28,9 @@ class LobbyRoomInstance extends React.Component { _createButtonJoin = (inst, seatId) => ( @@ -48,10 +48,10 @@ class LobbyRoomInstance extends React.Component { _createButtonPlay = (inst, seatId) => ( +
+
diff --git a/src/lobby/react.test.js b/src/lobby/react.test.js index b7cd9e39d..1f545dc65 100644 --- a/src/lobby/react.test.js +++ b/src/lobby/react.test.js @@ -55,7 +55,7 @@ describe('lobby', () => { gameServer="localhost:9000" /> ); - lobby.instance()._startGame('GameName1', { numPlayers: 2 }); + lobby.instance()._startMatch('GameName1', { numPlayers: 2 }); expect(spy).toBeCalledWith( expect.objectContaining({ multiplayer: expect.anything(), @@ -129,9 +129,9 @@ describe('lobby', () => { describe('exiting lobby', () => { beforeEach(async () => { - lobby.instance().connection.rooms = [ + lobby.instance().connection.matches = [ { - gameID: 'gameID1', + matchID: 'matchID1', players: { '0': { id: 0, name: 'Bob' }, '1': { id: 1 }, @@ -171,7 +171,7 @@ describe('lobby', () => { }); }); - describe('rooms list', () => { + describe('matches list', () => { let spyClient = jest.fn(); beforeEach(async () => { // initial state = logged-in as 'Bob' @@ -196,11 +196,11 @@ describe('lobby', () => { spyClient.mockReset(); }); - describe('creating a room', () => { + describe('creating a match', () => { beforeEach(async () => { - lobby.instance().connection.rooms = [ + lobby.instance().connection.matches = [ { - gameID: 'gameID1', + matchID: 'matchID1', players: { '0': { id: 0 } }, gameName: 'GameName1', }, @@ -209,30 +209,30 @@ describe('lobby', () => { lobby.update(); }); - test('room with default number of players', () => { + test('match with default number of players', () => { lobby.instance().connection.create = spy; lobby - .find('LobbyCreateRoomForm') + .find('LobbyCreateMatchForm') .find('button') .simulate('click'); expect(spy).toHaveBeenCalledWith('GameName1', 3); }); - test('room with 2 players', () => { + test('match with 2 players', () => { lobby.instance().connection.create = spy; lobby - .find('LobbyCreateRoomForm') + .find('LobbyCreateMatchForm') .find('select') .first() .props() .onChange({ target: { value: '1' } }); lobby - .find('LobbyCreateRoomForm') + .find('LobbyCreateMatchForm') .find('select') .at(1) .props() .onChange({ target: { value: '2' } }); lobby - .find('LobbyCreateRoomForm') + .find('LobbyCreateMatchForm') .find('button') .simulate('click'); expect(spy).toHaveBeenCalledWith('GameName2', 2); @@ -242,7 +242,7 @@ describe('lobby', () => { throw new Error('fail'); }); await lobby - .find('LobbyCreateRoomForm') + .find('LobbyCreateMatchForm') .find('button') .simulate('click'); expect( @@ -255,14 +255,14 @@ describe('lobby', () => { test('when game has no boundaries on the number of players', async () => { // select 2nd game lobby - .find('LobbyCreateRoomForm') + .find('LobbyCreateMatchForm') .find('select') .first() .props() .onChange({ target: { value: '1' } }); expect( lobby - .find('LobbyCreateRoomForm') + .find('LobbyCreateMatchForm') .find('select') .at(1) .text() @@ -271,7 +271,7 @@ describe('lobby', () => { test('when game has boundaries on the number of players', async () => { expect( lobby - .find('LobbyCreateRoomForm') + .find('LobbyCreateMatchForm') .find('select') .at(1) .text() @@ -279,16 +279,16 @@ describe('lobby', () => { }); }); - describe('joining a room', () => { + describe('joining a match', () => { beforeEach(async () => { - lobby.instance().connection.rooms = [ + lobby.instance().connection.matches = [ { - gameID: 'gameID1', + matchID: 'matchID1', players: { '0': { id: 0 } }, gameName: 'GameName1', }, { - gameID: 'gameID2', + matchID: 'matchID2', players: { '0': { id: 0, name: 'Bob' } }, gameName: 'GameName1', }, @@ -296,21 +296,21 @@ describe('lobby', () => { lobby.instance().forceUpdate(); lobby.update(); }); - test('when room is empty', () => { - // join 1st room + test('when match is empty', () => { + // join 1st match lobby.instance().connection.join = spy; lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .first() .find('button') .simulate('click'); - expect(spy).toHaveBeenCalledWith('GameName1', 'gameID1', '0'); + expect(spy).toHaveBeenCalledWith('GameName1', 'matchID1', '0'); }); - test('when room is full', () => { - // try 2nd room + test('when match is full', () => { + // try 2nd match expect( lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .at(1) .text() ).toContain('RUNNING'); @@ -319,9 +319,9 @@ describe('lobby', () => { lobby.instance().connection.join = spy.mockImplementation(() => { throw new Error('fail'); }); - // join 1st room + // join 1st match await lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .first() .find('button') .simulate('click'); @@ -334,11 +334,11 @@ describe('lobby', () => { }); }); - describe('leaving a room', () => { + describe('leaving a match', () => { beforeEach(async () => { - lobby.instance().connection.rooms = [ + lobby.instance().connection.matches = [ { - gameID: 'gameID1', + matchID: 'matchID1', players: { '0': { id: 0, name: 'Bob' }, '1': { id: 1 }, @@ -350,26 +350,26 @@ describe('lobby', () => { lobby.update(); expect( lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .find('button') .text() ).toBe('Leave'); }); - test('shall leave a room', () => { - // leave room + test('shall leave a match', () => { + // leave match lobby.instance().connection.leave = spy; lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .find('button') .simulate('click'); - expect(spy).toHaveBeenCalledWith('GameName1', 'gameID1'); + expect(spy).toHaveBeenCalledWith('GameName1', 'matchID1'); }); test('when server request fails', async () => { lobby.instance().connection.leave = spy.mockImplementation(() => { throw new Error('fail'); }); await lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .find('button') .simulate('click'); expect( @@ -383,9 +383,9 @@ describe('lobby', () => { describe('starting a game', () => { beforeEach(async () => { - lobby.instance().connection.rooms = [ + lobby.instance().connection.matches = [ { - gameID: 'gameID1', + matchID: 'matchID1', players: { '0': { id: 0, name: 'Bob', credentials: 'SECRET1' }, '1': { id: 1, name: 'Charly', credentials: 'SECRET2' }, @@ -393,17 +393,17 @@ describe('lobby', () => { gameName: 'GameName1', }, { - gameID: 'gameID2', + matchID: 'matchID2', players: { '0': { id: 0, name: 'Alice' } }, gameName: 'GameName2', }, { - gameID: 'gameID3', + matchID: 'matchID3', players: { '0': { id: 0, name: 'Bob' } }, gameName: 'GameName3', }, { - gameID: 'gameID4', + matchID: 'matchID4', players: { '0': { id: 0, name: 'Zoe' } }, gameName: 'GameNameUnknown', }, @@ -415,15 +415,15 @@ describe('lobby', () => { test('if player has joined the game', () => { lobby.instance().connection.playerCredentials = 'SECRET1'; lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .first() .find('button') .first() .simulate('click'); - expect(lobby.instance().state.runningGame).toEqual({ + expect(lobby.instance().state.runningMatch).toEqual({ app: NullComponent, - gameID: 'gameID1', + matchID: 'matchID1', playerID: '0', credentials: 'SECRET1', }); @@ -437,21 +437,21 @@ describe('lobby', () => { test('if player is spectator', () => { lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .at(1) .find('button') .simulate('click'); - expect(lobby.instance().state.runningGame).toEqual({ + expect(lobby.instance().state.runningMatch).toEqual({ app: NullComponent, credentials: undefined, - gameID: 'gameID2', + matchID: 'matchID2', playerID: '0', }); }); test('if game is not supported', () => { lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .at(3) .find('button') .simulate('click'); @@ -466,23 +466,23 @@ describe('lobby', () => { test('if game is monoplayer', () => { lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .at(2) .find('button') .first() .simulate('click'); expect(spy).not.toHaveBeenCalledWith(expect.anything(), { - gameID: 'gameID3', + matchID: 'matchID3', }); }); }); describe('exiting during game', () => { beforeEach(async () => { - lobby.instance().connection.rooms = [ + lobby.instance().connection.matches = [ { - gameID: 'gameID1', + matchID: 'matchID1', players: { '0': { id: 0, name: 'Bob', credentials: 'SECRET1' }, '1': { id: 1, name: 'Charly', credentials: 'SECRET2' }, @@ -497,17 +497,17 @@ describe('lobby', () => { lobby.instance().connection.playerCredentials = 'SECRET1'; // start game lobby - .find('LobbyRoomInstance') + .find('LobbyMatchInstance') .first() .find('button') .first() .simulate('click'); // exit game lobby - .find('#game-exit') + .find('#match-exit') .find('button') .simulate('click'); - expect(lobby.instance().state.runningGame).toEqual(null); + expect(lobby.instance().state.runningMatch).toEqual(null); expect(lobby.instance().state.phase).toEqual('list'); }); }); diff --git a/src/master/master.test.ts b/src/master/master.test.ts index 6affa93a9..a7f5bdfd7 100644 --- a/src/master/master.test.ts +++ b/src/master/master.test.ts @@ -12,7 +12,7 @@ import { Master, redactLog, getPlayerMetadata, - doesGameRequireAuthentication, + doesMatchRequireAuthentication, isActionFromAuthenticPlayer, } from './master'; import { error } from '../core/logger'; @@ -590,7 +590,9 @@ describe('getPlayerMetadata', () => { describe('when metadata does not contain players field', () => { test('then playerMetadata is undefined', () => { - expect(getPlayerMetadata({} as Server.GameMetadata, '0')).toBeUndefined(); + expect( + getPlayerMetadata({} as Server.MatchMetadata, '0') + ).toBeUndefined(); }); }); @@ -617,31 +619,31 @@ describe('getPlayerMetadata', () => { }); }); -describe('doesGameRequireAuthentication', () => { +describe('doesMatchRequireAuthentication', () => { describe('when game metadata is not found', () => { test('then authentication is not required', () => { - const result = doesGameRequireAuthentication(); + const result = doesMatchRequireAuthentication(); expect(result).toBe(false); }); }); - describe('when game has no credentials', () => { + describe('when match has no credentials', () => { test('then authentication is not required', () => { - const gameMetadata = { + const matchMetadata = { gameName: '', setupData: {}, players: { '0': { id: 1 }, }, }; - const result = doesGameRequireAuthentication(gameMetadata); + const result = doesMatchRequireAuthentication(matchMetadata); expect(result).toBe(false); }); }); - describe('when game has credentials', () => { + describe('when match has credentials', () => { test('then authentication is required', () => { - const gameMetadata = { + const matchMetadata = { gameName: '', setupData: {}, players: { @@ -651,7 +653,7 @@ describe('doesGameRequireAuthentication', () => { }, }, }; - const result = doesGameRequireAuthentication(gameMetadata); + const result = doesMatchRequireAuthentication(matchMetadata); expect(result).toBe(true); }); }); @@ -660,7 +662,7 @@ describe('doesGameRequireAuthentication', () => { describe('isActionFromAuthenticPlayer', () => { let action; let playerID; - let gameMetadata; + let matchMetadata; let credentials; let playerMetadata; @@ -671,13 +673,13 @@ describe('isActionFromAuthenticPlayer', () => { payload: { credentials: 'SECRET' }, }; - gameMetadata = { + matchMetadata = { players: { '0': { credentials: 'SECRET' }, }, }; - playerMetadata = gameMetadata.players[playerID]; + playerMetadata = matchMetadata.players[playerID]; ({ credentials } = action.payload || {}); }); diff --git a/src/master/master.ts b/src/master/master.ts index 512f1f044..aef640983 100644 --- a/src/master/master.ts +++ b/src/master/master.ts @@ -26,11 +26,11 @@ import { import * as StorageAPI from '../server/db/base'; export const getPlayerMetadata = ( - gameMetadata: Server.GameMetadata, + matchMetadata: Server.MatchMetadata, playerID: PlayerID ) => { - if (gameMetadata && gameMetadata.players) { - return gameMetadata.players[playerID]; + if (matchMetadata && matchMetadata.players) { + return matchMetadata.players[playerID]; } }; @@ -78,13 +78,13 @@ export function redactLog(log: LogEntry[], playerID: PlayerID) { } /** - * Verifies that the game has metadata and is using credentials. + * Verifies that the match has metadata and is using credentials. */ -export const doesGameRequireAuthentication = ( - gameMetadata?: Server.GameMetadata +export const doesMatchRequireAuthentication = ( + matchMetadata?: Server.MatchMetadata ) => { - if (!gameMetadata) return false; - const { players } = gameMetadata as Server.GameMetadata; + if (!matchMetadata) return false; + const { players } = matchMetadata as Server.MatchMetadata; const hasCredentials = Object.keys(players).some(key => { return !!(players[key] && players[key].credentials); }); @@ -136,7 +136,7 @@ export class Master { transportAPI; subscribeCallback: CallbackFn; auth: null | AuthFn; - shouldAuth: typeof doesGameRequireAuthentication; + shouldAuth: typeof doesMatchRequireAuthentication; constructor( game: Game, @@ -153,7 +153,7 @@ export class Master { if (auth === true) { this.auth = isActionFromAuthenticPlayer; - this.shouldAuth = doesGameRequireAuthentication; + this.shouldAuth = doesMatchRequireAuthentication; } else if (typeof auth === 'function') { this.auth = auth; this.shouldAuth = () => true; @@ -176,7 +176,7 @@ export class Master { playerID: string ) { let isActionAuthentic; - let metadata: Server.GameMetadata | undefined; + let metadata: Server.MatchMetadata | undefined; const credentials = credAction.payload.credentials; if (IsSynchronous(this.storageAPI)) { ({ metadata } = this.storageAPI.fetch(gameID, { metadata: true })); @@ -290,7 +290,7 @@ export class Master { const { deltalog, ...stateWithoutDeltalog } = state; - let newMetadata: Server.GameMetadata | undefined; + let newMetadata: Server.MatchMetadata | undefined; if ( metadata && !('gameover' in metadata) && @@ -326,7 +326,7 @@ export class Master { let state: State; let initialState: State; let log: LogEntry[]; - let gameMetadata: Server.GameMetadata; + let matchMetadata: Server.MatchMetadata; let filteredMetadata: FilteredMetadata; let result: StorageAPI.FetchResult<{ state: true; @@ -354,10 +354,10 @@ export class Master { state = result.state; initialState = result.initialState; log = result.log; - gameMetadata = result.metadata; + matchMetadata = result.metadata; - if (gameMetadata) { - filteredMetadata = Object.values(gameMetadata.players).map(player => { + if (matchMetadata) { + filteredMetadata = Object.values(matchMetadata.players).map(player => { const { credentials, ...filteredData } = player; return filteredData; }); diff --git a/src/server/api.test.ts b/src/server/api.test.ts index 5f8498f3d..bad24e3fc 100644 --- a/src/server/api.test.ts +++ b/src/server/api.test.ts @@ -103,7 +103,7 @@ describe('.createRouter', () => { beforeEach(async () => { delete process.env.API_SECRET; - const uuid = () => 'gameID'; + const uuid = () => 'matchID'; app = createApiServer({ db, games, uuid }); response = await request(app.callback()) @@ -117,7 +117,7 @@ describe('.createRouter', () => { test('creates game state and metadata', () => { expect(db.mocks.createGame).toHaveBeenCalledWith( - 'gameID', + 'matchID', expect.objectContaining({ initialState: expect.objectContaining({ ctx: expect.objectContaining({ @@ -136,8 +136,8 @@ describe('.createRouter', () => { ); }); - test('returns game id', () => { - expect(response.body.gameID).not.toBeNull(); + test('returns match id', () => { + expect(response.body.matchID).not.toBeNull(); }); describe('without numPlayers', () => { @@ -147,7 +147,7 @@ describe('.createRouter', () => { test('uses default numPlayers', () => { expect(db.mocks.createGame).toHaveBeenCalledWith( - 'gameID', + 'matchID', expect.objectContaining({ initialState: expect.objectContaining({ ctx: expect.objectContaining({ @@ -185,7 +185,7 @@ describe('.createRouter', () => { test('includes setupData in metadata', () => { expect(db.mocks.createGame).toHaveBeenCalledWith( - 'gameID', + 'matchID', expect.objectContaining({ metadata: expect.objectContaining({ setupData: expect.objectContaining({ @@ -201,7 +201,7 @@ describe('.createRouter', () => { test('passes setupData to game setup function', () => { expect(db.mocks.createGame).toHaveBeenCalledWith( - 'gameID', + 'matchID', expect.objectContaining({ initialState: expect.objectContaining({ G: expect.objectContaining({ @@ -225,7 +225,7 @@ describe('.createRouter', () => { test('sets unlisted in metadata', () => { expect(db.mocks.createGame).toHaveBeenCalledWith( - 'gameID', + 'matchID', expect.objectContaining({ metadata: expect.objectContaining({ unlisted: true, @@ -319,7 +319,7 @@ describe('.createRouter', () => { const app = createApiServer({ db, games, - uuid: () => 'gameID', + uuid: () => 'matchID', generateCredentials: () => credentials, }); response = await request(app.callback()) @@ -608,7 +608,7 @@ describe('.createRouter', () => { response = await request(app.callback()) .post('/games/foo/1/update') .send('playerID=0&playerName=alice&newName=ali'); - expect(response.text).toEqual('Game 1 not found'); + expect(response.text).toEqual('Match 1 not found'); }); }); @@ -739,7 +739,7 @@ describe('.createRouter', () => { response = await request(app.callback()) .post('/games/foo/1/update') .send({ playerID: 0, data: { subdata: 'text' } }); - expect(response.text).toEqual('Game 1 not found'); + expect(response.text).toEqual('Match 1 not found'); }); }); @@ -1053,7 +1053,7 @@ describe('.createRouter', () => { }), }) ); - expect(response.body.nextRoomID).toBe('newGameID'); + expect(response.body.nextMatchID).toBe('newGameID'); }); test('fetches next id', async () => { @@ -1071,7 +1071,7 @@ describe('.createRouter', () => { credentials: 'SECRET2', }, }, - nextRoomID: '12345', + nextMatchID: '12345', }, }; }, @@ -1080,10 +1080,10 @@ describe('.createRouter', () => { response = await request(app.callback()) .post('/games/foo/1/playAgain') .send('playerID=0&credentials=SECRET1'); - expect(response.body.nextRoomID).toBe('12345'); + expect(response.body.nextMatchID).toBe('12345'); }); - test('when the game does not exist throws a "not found" error', async () => { + test('when the match does not exist throws a "not found" error', async () => { db = new AsyncStorage({ fetch: async () => ({ metadata: null }), }); @@ -1132,7 +1132,7 @@ describe('.createRouter', () => { beforeEach(() => { delete process.env.API_SECRET; db = new AsyncStorage({ - fetch: async gameID => { + fetch: async matchID => { return { metadata: { players: { @@ -1145,8 +1145,8 @@ describe('.createRouter', () => { credentials: 'SECRET2', }, }, - unlisted: gameID === 'bar-4', - gameover: gameID === 'bar-3' ? { winner: 0 } : undefined, + unlisted: matchID === 'bar-4', + gameover: matchID === 'bar-3' ? { winner: 0 } : undefined, }, }; }, @@ -1167,33 +1167,33 @@ describe('.createRouter', () => { }); }); - describe('when given 2 rooms', () => { + describe('when given 2 matches', () => { let response; - let rooms; + let matches; beforeEach(async () => { let games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }]; let app = createApiServer({ db, games }); response = await request(app.callback()).get('/games/bar'); - rooms = JSON.parse(response.text).rooms; + matches = JSON.parse(response.text).matches; }); - test('returns rooms for the selected game', async () => { - expect(rooms).toHaveLength(2); + test('returns matches for the selected game', async () => { + expect(matches).toHaveLength(2); }); - test('returns room ids', async () => { - expect(rooms[0].gameID).toEqual('bar-2'); - expect(rooms[1].gameID).toEqual('bar-3'); + test('returns match ids', async () => { + expect(matches[0].matchID).toEqual('bar-2'); + expect(matches[1].matchID).toEqual('bar-3'); }); test('returns player names', async () => { - expect(rooms[0].players).toEqual([{ id: 0 }, { id: 1 }]); - expect(rooms[1].players).toEqual([{ id: 0 }, { id: 1 }]); + expect(matches[0].players).toEqual([{ id: 0 }, { id: 1 }]); + expect(matches[1].players).toEqual([{ id: 0 }, { id: 1 }]); }); - test('returns gameover data for ended game', async () => { - expect(rooms[0].gameover).toBeUndefined(); - expect(rooms[1].gameover).toEqual({ winner: 0 }); + test('returns gameover data for ended match', async () => { + expect(matches[0].gameover).toBeUndefined(); + expect(matches[1].gameover).toEqual({ winner: 0 }); }); }); }); @@ -1237,7 +1237,7 @@ describe('.createRouter', () => { }); test('returns game ids', async () => { - expect(room.roomID).toEqual('bar-0'); + expect(room.matchID).toEqual('bar-0'); }); test('returns player names', async () => { diff --git a/src/server/api.ts b/src/server/api.ts index fc850da0d..751cfd600 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -17,7 +17,7 @@ import * as StorageAPI from './db/base'; import { Server, Game } from '../types'; /** - * Creates a new game. + * Creates a new match. * * @param {object} db - The storage API. * @param {object} game - The game config object. @@ -25,9 +25,9 @@ import { Server, Game } from '../types'; * @param {object} setupData - User-defined object that's available * during game setup. * @param {object } lobbyConfig - Configuration options for the lobby. - * @param {boolean} unlisted - Whether the game should be excluded from public listing. + * @param {boolean} unlisted - Whether the match should be excluded from public listing. */ -export const CreateGame = async ({ +export const CreateMatch = async ({ db, game, numPlayers, @@ -44,7 +44,7 @@ export const CreateGame = async ({ }) => { if (!numPlayers || typeof numPlayers !== 'number') numPlayers = 2; - const metadata: Server.GameMetadata = { + const metadata: Server.MatchMetadata = { gameName: game.name, unlisted: !!unlisted, players: {}, @@ -54,30 +54,28 @@ export const CreateGame = async ({ metadata.players[playerIndex] = { id: playerIndex }; } - const gameID = uuid(); + const matchID = uuid(); const initialState = InitializeGame({ game, numPlayers, setupData }); - await db.createGame(gameID, { metadata, initialState }); + await db.createGame(matchID, { metadata, initialState }); - return gameID; + return matchID; }; /** * Create a metadata object without secret credentials to return to the client. * - * @param {string} gameID - The identifier of the game the metadata belongs to. - * @param {object} metadata - The game metadata object to strip credentials from. + * @param {string} matchID - The identifier of the match the metadata belongs to. + * @param {object} metadata - The match metadata object to strip credentials from. * @return - A metadata object without player credentials. */ -const createClientGameMetadata = ( - gameID: string, - metadata: Server.GameMetadata +const createClientMatchMetadata = ( + matchID: string, + metadata: Server.MatchMetadata ) => { return { ...metadata, - gameID, - roomID: gameID, - matchID: gameID, + matchID, players: Object.values(metadata.players).map(player => { // strip away credentials const { credentials, ...strippedInfo } = player; @@ -101,10 +99,25 @@ export const createRouter = ({ generateCredentials = generateCredentials || uuid; const router = new Router(); + /** + * List available games. + * + * @return - Array of game names as string. + */ router.get('/games', async ctx => { ctx.body = games.map(game => game.name); }); + /** + * Create a new match of a given game. + * + * @param {string} name - The name of the game of the new match. + * @param {number} numPlayers - The number of players. + * @param {object} setupData - User-defined object that's available + * during game setup. + * @param {boolean} unlisted - Whether the match should be excluded from public listing. + * @return - The ID of the created match. + */ router.post('/games/:name/create', koaBody(), async ctx => { // The name of the game (for example: tic-tac-toe). const gameName = ctx.params.name; @@ -118,7 +131,7 @@ export const createRouter = ({ const game = games.find(g => g.name === gameName); if (!game) ctx.throw(404, 'Game ' + gameName + ' not found'); - const gameID = await CreateGame({ + const matchID = await CreateMatch({ db, game, numPlayers, @@ -128,38 +141,63 @@ export const createRouter = ({ }); ctx.body = { - gameID, + matchID, }; }); + /** + * List matches for a given game. + * + * This does not return matches that are marked as unlisted. + * + * @param {string} name - The name of the game. + * @return - Array of match objects. + */ router.get('/games/:name', async ctx => { const gameName = ctx.params.name; - const gameList = await db.listGames({ gameName }); - let rooms = []; - for (let gameID of gameList) { - const { metadata } = await (db as StorageAPI.Async).fetch(gameID, { + const matchList = await db.listGames({ gameName }); + let matches = []; + for (let matchID of matchList) { + const { metadata } = await (db as StorageAPI.Async).fetch(matchID, { metadata: true, }); if (!metadata.unlisted) { - rooms.push(createClientGameMetadata(gameID, metadata)); + matches.push(createClientMatchMetadata(matchID, metadata)); } } ctx.body = { - rooms: rooms, + matches, }; }); + /** + * Get data about a specific match. + * + * @param {string} name - The name of the game. + * @param {string} id - The ID of the match. + * @return - A match object. + */ router.get('/games/:name/:id', async ctx => { - const gameID = ctx.params.id; - const { metadata } = await (db as StorageAPI.Async).fetch(gameID, { + const matchID = ctx.params.id; + const { metadata } = await (db as StorageAPI.Async).fetch(matchID, { metadata: true, }); if (!metadata) { - ctx.throw(404, 'Room ' + gameID + ' not found'); + ctx.throw(404, 'Match ' + matchID + ' not found'); } - ctx.body = createClientGameMetadata(gameID, metadata); + ctx.body = createClientMatchMetadata(matchID, metadata); }); + /** + * Join a given match. + * + * @param {string} name - The name of the game. + * @param {string} id - The ID of the match. + * @param {string} playerID - The ID of the player who joins. + * @param {string} playerName - The name of the player who joins. + * @param {object} data - The default data of the player in the match. + * @return - Player credentials to use when interacting in the joined match. + */ router.post('/games/:name/:id/join', koaBody(), async ctx => { const playerID = ctx.request.body.playerID; const playerName = ctx.request.body.playerName; @@ -170,12 +208,12 @@ export const createRouter = ({ if (!playerName) { ctx.throw(403, 'playerName is required'); } - const gameID = ctx.params.id; - const { metadata } = await (db as StorageAPI.Async).fetch(gameID, { + const matchID = ctx.params.id; + const { metadata } = await (db as StorageAPI.Async).fetch(matchID, { metadata: true, }); if (!metadata) { - ctx.throw(404, 'Game ' + gameID + ' not found'); + ctx.throw(404, 'Match ' + matchID + ' not found'); } if (!metadata.players[playerID]) { ctx.throw(404, 'Player ' + playerID + ' not found'); @@ -191,18 +229,27 @@ export const createRouter = ({ const playerCredentials = await generateCredentials(ctx); metadata.players[playerID].credentials = playerCredentials; - await db.setMetadata(gameID, metadata); + await db.setMetadata(matchID, metadata); ctx.body = { playerCredentials, }; }); + /** + * Leave a given match. + * + * @param {string} name - The name of the game. + * @param {string} id - The ID of the match. + * @param {string} playerID - The ID of the player who leaves. + * @param {string} credentials - The credentials of the player who leaves. + * @return - Nothing. + */ router.post('/games/:name/:id/leave', koaBody(), async ctx => { - const gameID = ctx.params.id; + const matchID = ctx.params.id; const playerID = ctx.request.body.playerID; const credentials = ctx.request.body.credentials; - const { metadata } = await (db as StorageAPI.Async).fetch(gameID, { + const { metadata } = await (db as StorageAPI.Async).fetch(matchID, { metadata: true, }); if (typeof playerID === 'undefined' || playerID === null) { @@ -210,7 +257,7 @@ export const createRouter = ({ } if (!metadata) { - ctx.throw(404, 'Game ' + gameID + ' not found'); + ctx.throw(404, 'Match ' + matchID + ' not found'); } if (!metadata.players[playerID]) { ctx.throw(404, 'Player ' + playerID + ' not found'); @@ -222,21 +269,31 @@ export const createRouter = ({ delete metadata.players[playerID].name; delete metadata.players[playerID].credentials; if (Object.values(metadata.players).some(player => player.name)) { - await db.setMetadata(gameID, metadata); + await db.setMetadata(matchID, metadata); } else { // remove room - await db.wipe(gameID); + await db.wipe(matchID); } ctx.body = {}; }); + /** + * Start a new match based on another existing match. + * + * @param {string} name - The name of the game. + * @param {string} id - The ID of the match. + * @param {string} playerID - The ID of the player creating the match. + * @param {string} credentials - The credentials of the player creating the match. + * @param {boolean} unlisted - Whether the match should be excluded from public listing. + * @return - The ID of the new match. + */ router.post('/games/:name/:id/playAgain', koaBody(), async ctx => { const gameName = ctx.params.name; - const gameID = ctx.params.id; + const matchID = ctx.params.id; const playerID = ctx.request.body.playerID; const credentials = ctx.request.body.credentials; const unlisted = ctx.request.body.unlisted; - const { metadata } = await (db as StorageAPI.Async).fetch(gameID, { + const { metadata } = await (db as StorageAPI.Async).fetch(matchID, { metadata: true, }); // User-data to pass to the game setup function. @@ -249,7 +306,7 @@ export const createRouter = ({ } if (!metadata) { - ctx.throw(404, 'Game ' + gameID + ' not found'); + ctx.throw(404, 'Match ' + matchID + ' not found'); } if (!metadata.players[playerID]) { ctx.throw(404, 'Player ' + playerID + ' not found'); @@ -258,14 +315,14 @@ export const createRouter = ({ ctx.throw(403, 'Invalid credentials ' + credentials); } - // Check if nextRoom is already set, if so, return that id. - if (metadata.nextRoomID) { - ctx.body = { nextRoomID: metadata.nextRoomID }; + // Check if nextMatch is already set, if so, return that id. + if (metadata.nextMatchID) { + ctx.body = { nextMatchID: metadata.nextMatchID }; return; } const game = games.find(g => g.name === gameName); - const nextRoomID = await CreateGame({ + const nextMatchID = await CreateMatch({ db, game, numPlayers, @@ -273,22 +330,22 @@ export const createRouter = ({ uuid, unlisted, }); - metadata.nextRoomID = nextRoomID; + metadata.nextMatchID = nextMatchID; - await db.setMetadata(gameID, metadata); + await db.setMetadata(matchID, metadata); ctx.body = { - nextRoomID, + nextMatchID, }; }); const updatePlayerMetadata = async (ctx: Koa.Context) => { - const gameID = ctx.params.id; + const matchID = ctx.params.id; const playerID = ctx.request.body.playerID; const credentials = ctx.request.body.credentials; const newName = ctx.request.body.newName; const data = ctx.request.body.data; - const { metadata } = await (db as StorageAPI.Async).fetch(gameID, { + const { metadata } = await (db as StorageAPI.Async).fetch(matchID, { metadata: true, }); if (typeof playerID === 'undefined') { @@ -301,7 +358,7 @@ export const createRouter = ({ ctx.throw(403, `newName must be a string, got ${typeof newName}`); } if (!metadata) { - ctx.throw(404, 'Game ' + gameID + ' not found'); + ctx.throw(404, 'Match ' + matchID + ' not found'); } if (!metadata.players[playerID]) { ctx.throw(404, 'Player ' + playerID + ' not found'); @@ -316,10 +373,20 @@ export const createRouter = ({ if (data) { metadata.players[playerID].data = data; } - await db.setMetadata(gameID, metadata); + await db.setMetadata(matchID, metadata); ctx.body = {}; }; + /** + * Change the name of a player in a given match. + * + * @param {string} name - The name of the game. + * @param {string} id - The ID of the match. + * @param {string} playerID - The ID of the player. + * @param {string} credentials - The credentials of the player. + * @param {object} newName - The new name of the player in the match. + * @return - Nothing. + */ router.post('/games/:name/:id/rename', koaBody(), async ctx => { console.warn( 'This endpoint /rename is deprecated. Please use /update instead.' @@ -327,6 +394,17 @@ export const createRouter = ({ await updatePlayerMetadata(ctx); }); + /** + * Update the player's data for a given match. + * + * @param {string} name - The name of the game. + * @param {string} id - The ID of the match. + * @param {string} playerID - The ID of the player. + * @param {string} credentials - The credentials of the player. + * @param {object} newName - The new name of the player in the match. + * @param {object} data - The new data of the player in the match. + * @return - Nothing. + */ router.post('/games/:name/:id/update', koaBody(), updatePlayerMetadata); return router; diff --git a/src/server/db/base.ts b/src/server/db/base.ts index 7a60cb1ce..26167aa36 100644 --- a/src/server/db/base.ts +++ b/src/server/db/base.ts @@ -22,7 +22,7 @@ export interface FetchOpts { export interface FetchFields { state: State; log: LogEntry[]; - metadata: Server.GameMetadata; + metadata: Server.MatchMetadata; initialState: State; } @@ -43,7 +43,7 @@ export interface ListGamesOpts { */ export interface CreateGameOpts { initialState: State; - metadata: Server.GameMetadata; + metadata: Server.MatchMetadata; } export abstract class Async { @@ -69,7 +69,7 @@ export abstract class Async { * a game is created. For example, it might stow away the * initial game state in a separate field for easier retrieval. */ - abstract createGame(gameID: string, opts: CreateGameOpts): Promise; + abstract createGame(matchID: string, opts: CreateGameOpts): Promise; /** * Update the game state. @@ -78,7 +78,7 @@ export abstract class Async { * existing log for this game. */ abstract setState( - gameID: string, + matchID: string, state: State, deltalog?: LogEntry[] ): Promise; @@ -87,22 +87,22 @@ export abstract class Async { * Update the game metadata. */ abstract setMetadata( - gameID: string, - metadata: Server.GameMetadata + matchID: string, + metadata: Server.MatchMetadata ): Promise; /** * Fetch the game state. */ abstract fetch( - gameID: string, + matchID: string, opts: O ): Promise>; /** * Remove the game state. */ - abstract wipe(gameID: string): Promise; + abstract wipe(matchID: string): Promise; /** * Return all games. @@ -133,7 +133,7 @@ export abstract class Sync { * a game is created. For example, it might stow away the * initial game state in a separate field for easier retrieval. */ - abstract createGame(gameID: string, opts: CreateGameOpts): void; + abstract createGame(matchID: string, opts: CreateGameOpts): void; /** * Update the game state. @@ -141,22 +141,22 @@ export abstract class Sync { * If passed a deltalog array, setState should append its contents to the * existing log for this game. */ - abstract setState(gameID: string, state: State, deltalog?: LogEntry[]): void; + abstract setState(matchID: string, state: State, deltalog?: LogEntry[]): void; /** - * Update the game metadata. + * Update the match metadata. */ - abstract setMetadata(gameID: string, metadata: Server.GameMetadata): void; + abstract setMetadata(matchID: string, metadata: Server.MatchMetadata): void; /** * Fetch the game state. */ - abstract fetch(gameID: string, opts: O): FetchResult; + abstract fetch(matchID: string, opts: O): FetchResult; /** * Remove the game state. */ - abstract wipe(gameID: string): void; + abstract wipe(matchID: string): void; /** * Return all games. diff --git a/src/server/db/flatfile.test.ts b/src/server/db/flatfile.test.ts index 7ef1de34b..aebd5d1c5 100644 --- a/src/server/db/flatfile.test.ts +++ b/src/server/db/flatfile.test.ts @@ -32,7 +32,7 @@ describe('FlatFile', () => { await db.createGame('gameID', { initialState: state as State, - metadata: metadata as Server.GameMetadata, + metadata: metadata as Server.MatchMetadata, }); // Must return created game. diff --git a/src/server/db/flatfile.ts b/src/server/db/flatfile.ts index 49819d09a..95fdb6c55 100644 --- a/src/server/db/flatfile.ts +++ b/src/server/db/flatfile.ts @@ -16,7 +16,7 @@ export class FlatFile extends StorageAPI.Async { private games: { init: (opts: object) => Promise; setItem: (id: string, value: any) => Promise; - getItem: (id: string) => Promise; + getItem: (id: string) => Promise; removeItem: (id: string) => Promise; clear: () => {}; keys: () => Promise; @@ -88,7 +88,7 @@ export class FlatFile extends StorageAPI.Async { const key = MetadataKey(gameID); result.metadata = (await this.request(() => this.games.getItem(key) - )) as Server.GameMetadata; + )) as Server.MatchMetadata; } if (opts.log) { @@ -125,7 +125,7 @@ export class FlatFile extends StorageAPI.Async { return await this.request(() => this.games.setItem(id, state)); } - async setMetadata(id: string, metadata: Server.GameMetadata): Promise { + async setMetadata(id: string, metadata: Server.MatchMetadata): Promise { const key = MetadataKey(id); return await this.request(() => this.games.setItem(key, metadata)); diff --git a/src/server/db/inmemory.test.ts b/src/server/db/inmemory.test.ts index d40d49bb4..4a4695b97 100644 --- a/src/server/db/inmemory.test.ts +++ b/src/server/db/inmemory.test.ts @@ -30,7 +30,7 @@ describe('InMemory', () => { db.createGame('gameID', { metadata: { gameName: 'tic-tac-toe', - } as Server.GameMetadata, + } as Server.MatchMetadata, initialState: stateEntry as State, }); diff --git a/src/server/db/inmemory.ts b/src/server/db/inmemory.ts index d0485f919..cbb744f34 100644 --- a/src/server/db/inmemory.ts +++ b/src/server/db/inmemory.ts @@ -15,7 +15,7 @@ import * as StorageAPI from './base'; export class InMemory extends StorageAPI.Sync { private state: Map; private initial: Map; - private metadata: Map; + private metadata: Map; private log: Map; /** @@ -41,7 +41,7 @@ export class InMemory extends StorageAPI.Sync { /** * Write the game metadata to the in-memory object. */ - setMetadata(gameID: string, metadata: Server.GameMetadata) { + setMetadata(gameID: string, metadata: Server.MatchMetadata) { this.metadata.set(gameID, metadata); } diff --git a/src/types.ts b/src/types.ts index 616dfdf38..1bf3e172e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -276,12 +276,12 @@ export namespace Server { data?: any; }; - export interface GameMetadata { + export interface MatchMetadata { gameName: string; players: { [id: number]: PlayerMetadata }; setupData?: any; gameover?: any; - nextRoomID?: string; + nextMatchID?: string; unlisted?: boolean; } }