Skip to content

Commit

Permalink
Merge 21f2327 into 298ecaf
Browse files Browse the repository at this point in the history
  • Loading branch information
liorp committed Sep 23, 2021
2 parents 298ecaf + 21f2327 commit 8b1b001
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 13 deletions.
3 changes: 2 additions & 1 deletion docs/documentation/api/Lobby.md
Expand Up @@ -147,7 +147,8 @@ Allows a player to join a particular match instance `id` of a game named `name`.

Accepts three JSON body parameters:

- `playerID` (required): the ordinal player in the match that is being joined (`'0'`, `'1'`...).
- `playerID` (optional): the ordinal player in the match that is being joined (`'0'`, `'1'`...).
If not sent, will be automatically assigned to the first available ordinal.

- `playerName` (required): the display name of the player joining the match.

Expand Down
54 changes: 50 additions & 4 deletions src/server/api.test.ts
Expand Up @@ -449,14 +449,60 @@ describe('.configureRouter', () => {

describe('when playerID is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, auth, games });
const app = createApiServer({
db,
auth: new Auth({ generateCredentials: () => credentials }),
games,
uuid: () => 'matchID',
});
response = await request(app.callback())
.post('/games/foo/1/join')
.send('playerName=1');
.send('playerName=alice');
});

test('throws error 403', async () => {
expect(response.status).toEqual(403);
describe('numPlayers is reached in match', () => {
beforeEach(async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': { name: 'alice' },
},
},
};
},
});
const app = createApiServer({ db, auth, games });
response = await request(app.callback())
.post('/games/foo/1/join')
.send('playerName=bob');
});

test('throws error 409', async () => {
expect(response.status).toEqual(409);
});
});

test('is successful', async () => {
expect(response.status).toEqual(200);
});

test('returns the player credentials', async () => {
expect(response.body.playerCredentials).toEqual(credentials);
});

test('updates the player name', async () => {
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
'1',
expect.objectContaining({
players: expect.objectContaining({
'0': expect.objectContaining({
name: 'alice',
}),
}),
})
);
});
});

Expand Down
36 changes: 28 additions & 8 deletions src/server/api.ts
Expand Up @@ -12,7 +12,11 @@ import koaBody from 'koa-body';
import { nanoid } from 'nanoid';
import cors from '@koa/cors';
import type IOTypes from 'socket.io';
import { createMatch } from './util';
import {
createMatch,
getFirstAvailablePlayerIndex,
getNumPlayers,
} from './util';
import type { Auth } from './auth';
import type { Server, LobbyAPI, Game, StorageAPI } from '../types';

Expand Down Expand Up @@ -213,28 +217,44 @@ export const configureRouter = ({
*
* @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} playerID - The ID of the player who joins. If not sent, will be assigned to the first index available.
* @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;
let playerID = ctx.request.body.playerID;
const playerName = ctx.request.body.playerName;
const data = ctx.request.body.data;
if (typeof playerID === 'undefined' || playerID === null) {
ctx.throw(403, 'playerID is required');
}
const matchID = ctx.params.id;
if (!playerName) {
ctx.throw(403, 'playerName is required');
}
const matchID = ctx.params.id;
const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {

// Fetch matchdata early for determining playerID
const { metadata }: { metadata: Server.MatchData } = await (
db as StorageAPI.Async
).fetch(matchID, {
metadata: true,
});
if (!metadata) {
ctx.throw(404, 'Match ' + matchID + ' not found');
}

if (typeof playerID === 'undefined' || playerID === null) {
playerID = getFirstAvailablePlayerIndex(metadata.players);
if (playerID === -1) {
ctx.throw(
409,
'Match ' +
matchID +
' reached maximum number of players (' +
getNumPlayers(metadata.players) +
')'
);
}
}

if (!metadata.players[playerID]) {
ctx.throw(404, 'Player ' + playerID + ' not found');
}
Expand Down
26 changes: 26 additions & 0 deletions src/server/util.ts
Expand Up @@ -61,3 +61,29 @@ export const createMatch = ({

return { metadata, initialState };
};

/**
* Given players, returns the count of players.
*/
export const getNumPlayers = (players: {
[id: number]: Server.PlayerMetadata;
}): number =>
Math.max(...Object.keys(players).map((k) => Number.parseInt(k))) + 1;

/**
* Given players, tries to find the first index of player that can be joined. Returns -1 if there's no available index.
*/
export const getFirstAvailablePlayerIndex = (players: {
[id: number]: Server.PlayerMetadata;
}): number => {
const numPlayers = getNumPlayers(players);
let playerID = -1;
// Try to get the first index available
for (let i = 0; i < numPlayers; i++) {
if (typeof players[i].name === 'undefined' || players[i].name === null) {
playerID = i;
break;
}
}
return playerID;
};

0 comments on commit 8b1b001

Please sign in to comment.