@mnbroatch/boardgame.io 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/ai/package.json +7 -0
- package/client/package.json +7 -0
- package/core/package.json +7 -0
- package/debug/package.json +7 -0
- package/dist/boardgameio.es.js +14238 -0
- package/dist/boardgameio.js +14277 -0
- package/dist/boardgameio.min.js +16 -0
- package/dist/cjs/Debug-9d141c06.js +9586 -0
- package/dist/cjs/ai-e0e8a768.js +377 -0
- package/dist/cjs/ai.js +20 -0
- package/dist/cjs/client-76dec77b.js +258 -0
- package/dist/cjs/client-a22d7500.js +524 -0
- package/dist/cjs/client.js +26 -0
- package/dist/cjs/core.js +52 -0
- package/dist/cjs/debug.js +18 -0
- package/dist/cjs/filter-player-view-bb02e2f6.js +89 -0
- package/dist/cjs/initialize-267fcd69.js +61 -0
- package/dist/cjs/internal.js +25 -0
- package/dist/cjs/master-2904879d.js +320 -0
- package/dist/cjs/master.js +18 -0
- package/dist/cjs/multiplayer.js +23 -0
- package/dist/cjs/plugin-random-7425844d.js +229 -0
- package/dist/cjs/plugins.js +59 -0
- package/dist/cjs/react-native.js +182 -0
- package/dist/cjs/react.js +727 -0
- package/dist/cjs/reducer-16eec232.js +1203 -0
- package/dist/cjs/server.js +4087 -0
- package/dist/cjs/socketio-7a0837eb.js +478 -0
- package/dist/cjs/testing.js +30 -0
- package/dist/cjs/transport-b1874dfa.js +37 -0
- package/dist/cjs/turn-order-b2ff8740.js +1136 -0
- package/dist/cjs/util-fcfd8fb8.js +140 -0
- package/dist/esm/Debug-0141fe2d.js +9577 -0
- package/dist/esm/ai-5c06e761.js +371 -0
- package/dist/esm/ai.js +8 -0
- package/dist/esm/client-2e653027.js +522 -0
- package/dist/esm/client-5f57c3f2.js +255 -0
- package/dist/esm/client.js +16 -0
- package/dist/esm/core.js +40 -0
- package/dist/esm/debug.js +10 -0
- package/dist/esm/filter-player-view-2c6cc96f.js +87 -0
- package/dist/esm/initialize-11d626ca.js +59 -0
- package/dist/esm/internal.js +10 -0
- package/dist/esm/master-fa8f2e43.js +318 -0
- package/dist/esm/master.js +10 -0
- package/dist/esm/multiplayer.js +14 -0
- package/dist/esm/plugin-random-087f861e.js +226 -0
- package/dist/esm/plugins.js +55 -0
- package/dist/esm/react-native.js +173 -0
- package/dist/esm/react.js +716 -0
- package/dist/esm/reducer-c46da7e5.js +1198 -0
- package/dist/esm/socketio-c22ffa65.js +455 -0
- package/dist/esm/testing.js +26 -0
- package/dist/esm/transport-ce07b771.js +35 -0
- package/dist/esm/turn-order-376d315e.js +1091 -0
- package/dist/esm/util-b6147cef.js +135 -0
- package/dist/types/packages/ai.d.ts +5 -0
- package/dist/types/packages/client.d.ts +3 -0
- package/dist/types/packages/core.d.ts +5 -0
- package/dist/types/packages/debug.d.ts +2 -0
- package/dist/types/packages/internal.d.ts +8 -0
- package/dist/types/packages/master.d.ts +2 -0
- package/dist/types/packages/multiplayer.d.ts +3 -0
- package/dist/types/packages/plugins.d.ts +3 -0
- package/dist/types/packages/react-native.d.ts +2 -0
- package/dist/types/packages/react.d.ts +3 -0
- package/dist/types/packages/server.d.ts +6 -0
- package/dist/types/packages/testing.d.ts +1 -0
- package/dist/types/src/ai/ai.d.ts +53 -0
- package/dist/types/src/ai/ai.test.d.ts +1 -0
- package/dist/types/src/ai/bot.d.ts +40 -0
- package/dist/types/src/ai/mcts-bot.d.ts +60 -0
- package/dist/types/src/ai/random-bot.d.ts +27 -0
- package/dist/types/src/client/client.d.ts +104 -0
- package/dist/types/src/client/client.test.d.ts +1 -0
- package/dist/types/src/client/debug/tests/debug.test.d.ts +1 -0
- package/dist/types/src/client/manager.d.ts +61 -0
- package/dist/types/src/client/react.d.ts +75 -0
- package/dist/types/src/client/react.ssr.test.d.ts +4 -0
- package/dist/types/src/client/react.test.d.ts +1 -0
- package/dist/types/src/client/transport/dummy.d.ts +18 -0
- package/dist/types/src/client/transport/local.d.ts +59 -0
- package/dist/types/src/client/transport/local.test.d.ts +1 -0
- package/dist/types/src/client/transport/socketio.d.ts +45 -0
- package/dist/types/src/client/transport/socketio.test.d.ts +1 -0
- package/dist/types/src/client/transport/transport.d.ts +50 -0
- package/dist/types/src/client/transport/transport.test.d.ts +1 -0
- package/dist/types/src/core/action-creators.d.ts +144 -0
- package/dist/types/src/core/action-types.d.ts +10 -0
- package/dist/types/src/core/backwards-compatibility.d.ts +12 -0
- package/dist/types/src/core/constants.d.ts +6 -0
- package/dist/types/src/core/errors.d.ts +15 -0
- package/dist/types/src/core/flow.d.ts +28 -0
- package/dist/types/src/core/flow.test.d.ts +1 -0
- package/dist/types/src/core/game-methods.d.ts +9 -0
- package/dist/types/src/core/game.d.ts +26 -0
- package/dist/types/src/core/game.test.d.ts +1 -0
- package/dist/types/src/core/initialize.d.ts +9 -0
- package/dist/types/src/core/logger.d.ts +2 -0
- package/dist/types/src/core/player-view.d.ts +7 -0
- package/dist/types/src/core/player-view.test.d.ts +1 -0
- package/dist/types/src/core/reducer.d.ts +155 -0
- package/dist/types/src/core/reducer.test.d.ts +1 -0
- package/dist/types/src/core/turn-order.d.ts +179 -0
- package/dist/types/src/core/turn-order.test.d.ts +8 -0
- package/dist/types/src/lobby/client.d.ts +194 -0
- package/dist/types/src/lobby/client.test.d.ts +1 -0
- package/dist/types/src/lobby/connection.d.ts +44 -0
- package/dist/types/src/lobby/connection.test.d.ts +1 -0
- package/dist/types/src/lobby/create-match-form.d.ts +26 -0
- package/dist/types/src/lobby/login-form.d.ts +23 -0
- package/dist/types/src/lobby/match-instance.d.ts +31 -0
- package/dist/types/src/lobby/react.d.ts +113 -0
- package/dist/types/src/lobby/react.ssr.test.d.ts +4 -0
- package/dist/types/src/lobby/react.test.d.ts +1 -0
- package/dist/types/src/master/filter-player-view.d.ts +96 -0
- package/dist/types/src/master/filter-player-view.test.d.ts +1 -0
- package/dist/types/src/master/master.d.ts +94 -0
- package/dist/types/src/master/master.test.d.ts +1 -0
- package/dist/types/src/plugins/events/events.d.ts +54 -0
- package/dist/types/src/plugins/events/events.test.d.ts +1 -0
- package/dist/types/src/plugins/main.d.ts +75 -0
- package/dist/types/src/plugins/main.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-events.d.ts +5 -0
- package/dist/types/src/plugins/plugin-immer.d.ts +7 -0
- package/dist/types/src/plugins/plugin-immer.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-log.d.ts +14 -0
- package/dist/types/src/plugins/plugin-log.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-player.d.ts +29 -0
- package/dist/types/src/plugins/plugin-player.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-random.d.ts +4 -0
- package/dist/types/src/plugins/plugin-serializable.d.ts +7 -0
- package/dist/types/src/plugins/plugin-serializable.test.d.ts +1 -0
- package/dist/types/src/plugins/random/random.alea.d.ts +19 -0
- package/dist/types/src/plugins/random/random.d.ts +54 -0
- package/dist/types/src/plugins/random/random.test.d.ts +1 -0
- package/dist/types/src/server/api.d.ts +13 -0
- package/dist/types/src/server/api.test.d.ts +1 -0
- package/dist/types/src/server/auth.d.ts +38 -0
- package/dist/types/src/server/auth.test.d.ts +1 -0
- package/dist/types/src/server/cors.d.ts +4 -0
- package/dist/types/src/server/cors.test.d.ts +1 -0
- package/dist/types/src/server/db/base.d.ts +192 -0
- package/dist/types/src/server/db/flatfile.d.ts +44 -0
- package/dist/types/src/server/db/flatfile.test.d.ts +1 -0
- package/dist/types/src/server/db/index.d.ts +4 -0
- package/dist/types/src/server/db/index.test.d.ts +1 -0
- package/dist/types/src/server/db/inmemory.d.ts +43 -0
- package/dist/types/src/server/db/inmemory.test.d.ts +1 -0
- package/dist/types/src/server/db/localstorage.d.ts +7 -0
- package/dist/types/src/server/db/localstorage.test.d.ts +1 -0
- package/dist/types/src/server/index.d.ts +68 -0
- package/dist/types/src/server/index.test.d.ts +1 -0
- package/dist/types/src/server/transport/pubsub/generic-pub-sub.d.ts +6 -0
- package/dist/types/src/server/transport/pubsub/in-memory-pub-sub.d.ts +7 -0
- package/dist/types/src/server/transport/pubsub/in-memory-pub-sub.test.d.ts +1 -0
- package/dist/types/src/server/transport/socketio-simultaneous.test.d.ts +1 -0
- package/dist/types/src/server/transport/socketio.d.ts +65 -0
- package/dist/types/src/server/transport/socketio.test.d.ts +1 -0
- package/dist/types/src/server/util.d.ts +35 -0
- package/dist/types/src/testing/mock-random.d.ts +15 -0
- package/dist/types/src/testing/mock-random.test.d.ts +1 -0
- package/dist/types/src/types.d.ts +387 -0
- package/internal/package.json +7 -0
- package/master/package.json +7 -0
- package/multiplayer/package.json +7 -0
- package/package.json +211 -0
- package/plugins/package.json +7 -0
- package/react/package.json +7 -0
- package/react-native/package.json +7 -0
- package/server/package.json +6 -0
- package/src/ai/ai.test.ts +433 -0
- package/src/ai/ai.ts +84 -0
- package/src/ai/bot.ts +122 -0
- package/src/ai/mcts-bot.ts +331 -0
- package/src/ai/random-bot.ts +20 -0
- package/src/client/client.test.ts +993 -0
- package/src/client/client.ts +588 -0
- package/src/client/debug/Debug.svelte +239 -0
- package/src/client/debug/Menu.svelte +65 -0
- package/src/client/debug/ai/AI.svelte +215 -0
- package/src/client/debug/ai/Options.svelte +48 -0
- package/src/client/debug/info/Info.svelte +22 -0
- package/src/client/debug/info/Item.svelte +24 -0
- package/src/client/debug/log/Log.svelte +157 -0
- package/src/client/debug/log/LogEvent.svelte +149 -0
- package/src/client/debug/log/LogMetadata.svelte +7 -0
- package/src/client/debug/log/PhaseMarker.svelte +27 -0
- package/src/client/debug/log/TurnMarker.svelte +23 -0
- package/src/client/debug/main/ClientSwitcher.svelte +59 -0
- package/src/client/debug/main/Controls.svelte +58 -0
- package/src/client/debug/main/Hotkey.svelte +84 -0
- package/src/client/debug/main/InteractiveFunction.svelte +85 -0
- package/src/client/debug/main/Main.svelte +121 -0
- package/src/client/debug/main/Move.svelte +68 -0
- package/src/client/debug/main/PlayerInfo.svelte +70 -0
- package/src/client/debug/mcts/Action.svelte +22 -0
- package/src/client/debug/mcts/MCTS.svelte +78 -0
- package/src/client/debug/mcts/Table.svelte +98 -0
- package/src/client/debug/tests/JSONTree.mock.svelte +3 -0
- package/src/client/debug/tests/debug.test.ts +183 -0
- package/src/client/debug/utils/shortcuts.js +50 -0
- package/src/client/debug/utils/shortcuts.test.js +49 -0
- package/src/client/manager.ts +177 -0
- package/src/client/react-native.js +136 -0
- package/src/client/react-native.test.js +229 -0
- package/src/client/react.ssr.test.tsx +24 -0
- package/src/client/react.test.tsx +213 -0
- package/src/client/react.tsx +192 -0
- package/src/client/transport/dummy.ts +19 -0
- package/src/client/transport/local.test.ts +353 -0
- package/src/client/transport/local.ts +230 -0
- package/src/client/transport/socketio.test.ts +328 -0
- package/src/client/transport/socketio.ts +210 -0
- package/src/client/transport/transport.test.ts +27 -0
- package/src/client/transport/transport.ts +95 -0
- package/src/core/action-creators.ts +159 -0
- package/src/core/action-types.ts +18 -0
- package/src/core/backwards-compatibility.ts +23 -0
- package/src/core/constants.ts +6 -0
- package/src/core/errors.ts +35 -0
- package/src/core/flow.test.ts +2433 -0
- package/src/core/flow.ts +897 -0
- package/src/core/game-methods.ts +9 -0
- package/src/core/game.test.ts +286 -0
- package/src/core/game.ts +114 -0
- package/src/core/initialize.ts +77 -0
- package/src/core/logger.test.js +90 -0
- package/src/core/logger.ts +18 -0
- package/src/core/player-view.test.ts +50 -0
- package/src/core/player-view.ts +39 -0
- package/src/core/reducer.test.ts +991 -0
- package/src/core/reducer.ts +532 -0
- package/src/core/turn-order.test.ts +1123 -0
- package/src/core/turn-order.ts +473 -0
- package/src/lobby/client.test.ts +385 -0
- package/src/lobby/client.ts +358 -0
- package/src/lobby/connection.test.ts +207 -0
- package/src/lobby/connection.ts +162 -0
- package/src/lobby/create-match-form.tsx +122 -0
- package/src/lobby/login-form.tsx +75 -0
- package/src/lobby/match-instance.tsx +135 -0
- package/src/lobby/react.ssr.test.tsx +22 -0
- package/src/lobby/react.test.tsx +594 -0
- package/src/lobby/react.tsx +402 -0
- package/src/master/filter-player-view.test.ts +381 -0
- package/src/master/filter-player-view.ts +102 -0
- package/src/master/master.test.ts +1068 -0
- package/src/master/master.ts +492 -0
- package/src/plugins/events/events.test.ts +108 -0
- package/src/plugins/events/events.ts +209 -0
- package/src/plugins/main.test.ts +411 -0
- package/src/plugins/main.ts +314 -0
- package/src/plugins/plugin-events.ts +40 -0
- package/src/plugins/plugin-immer.test.ts +86 -0
- package/src/plugins/plugin-immer.ts +37 -0
- package/src/plugins/plugin-log.test.ts +37 -0
- package/src/plugins/plugin-log.ts +40 -0
- package/src/plugins/plugin-player.test.ts +172 -0
- package/src/plugins/plugin-player.ts +100 -0
- package/src/plugins/plugin-random.ts +40 -0
- package/src/plugins/plugin-serializable.test.ts +40 -0
- package/src/plugins/plugin-serializable.ts +55 -0
- package/src/plugins/random/random.alea.ts +109 -0
- package/src/plugins/random/random.test.ts +167 -0
- package/src/plugins/random/random.ts +198 -0
- package/src/server/api.test.ts +1699 -0
- package/src/server/api.ts +527 -0
- package/src/server/auth.test.ts +275 -0
- package/src/server/auth.ts +89 -0
- package/src/server/cors.test.ts +121 -0
- package/src/server/cors.ts +7 -0
- package/src/server/db/base.ts +296 -0
- package/src/server/db/flatfile.test.ts +221 -0
- package/src/server/db/flatfile.ts +228 -0
- package/src/server/db/index.test.ts +8 -0
- package/src/server/db/index.ts +12 -0
- package/src/server/db/inmemory.test.ts +143 -0
- package/src/server/db/inmemory.ts +143 -0
- package/src/server/db/localstorage.test.ts +73 -0
- package/src/server/db/localstorage.ts +44 -0
- package/src/server/index.test.ts +265 -0
- package/src/server/index.ts +175 -0
- package/src/server/transport/pubsub/generic-pub-sub.ts +11 -0
- package/src/server/transport/pubsub/in-memory-pub-sub.test.ts +47 -0
- package/src/server/transport/pubsub/in-memory-pub-sub.ts +28 -0
- package/src/server/transport/socketio-simultaneous.test.ts +603 -0
- package/src/server/transport/socketio.test.ts +303 -0
- package/src/server/transport/socketio.ts +279 -0
- package/src/server/util.ts +85 -0
- package/src/testing/mock-random.test.ts +45 -0
- package/src/testing/mock-random.ts +27 -0
- package/src/types.ts +511 -0
- package/testing/package.json +7 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2018 The boardgame.io Authors
|
|
3
|
+
*
|
|
4
|
+
* Use of this source code is governed by a MIT-style
|
|
5
|
+
* license that can be found in the LICENSE file or at
|
|
6
|
+
* https://opensource.org/licenses/MIT.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CorsOptions } from 'cors';
|
|
10
|
+
import type Koa from 'koa';
|
|
11
|
+
import type Router from '@koa/router';
|
|
12
|
+
import koaBody from 'koa-body';
|
|
13
|
+
import { nanoid } from 'nanoid';
|
|
14
|
+
import cors from '@koa/cors';
|
|
15
|
+
import { createMatch, getFirstAvailablePlayerID, getNumPlayers } from './util';
|
|
16
|
+
import type { Auth } from './auth';
|
|
17
|
+
import type { Server, LobbyAPI, Game, StorageAPI } from '../types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new match.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} db - The storage API.
|
|
23
|
+
* @param {object} game - The game config object.
|
|
24
|
+
* @param {number} numPlayers - The number of players.
|
|
25
|
+
* @param {object} setupData - User-defined object that's available
|
|
26
|
+
* during game setup.
|
|
27
|
+
* @param {object } lobbyConfig - Configuration options for the lobby.
|
|
28
|
+
* @param {boolean} unlisted - Whether the match should be excluded from public listing.
|
|
29
|
+
*/
|
|
30
|
+
const CreateMatch = async ({
|
|
31
|
+
ctx,
|
|
32
|
+
db,
|
|
33
|
+
uuid,
|
|
34
|
+
...opts
|
|
35
|
+
}: {
|
|
36
|
+
db: StorageAPI.Sync | StorageAPI.Async;
|
|
37
|
+
ctx: Koa.BaseContext;
|
|
38
|
+
uuid: () => string;
|
|
39
|
+
} & Parameters<typeof createMatch>[0]): Promise<string> => {
|
|
40
|
+
const matchID = uuid();
|
|
41
|
+
const match = createMatch(opts);
|
|
42
|
+
|
|
43
|
+
if ('setupDataError' in match) {
|
|
44
|
+
ctx.throw(400, match.setupDataError);
|
|
45
|
+
} else {
|
|
46
|
+
await db.createMatch(matchID, match);
|
|
47
|
+
return matchID;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a metadata object without secret credentials to return to the client.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} matchID - The identifier of the match the metadata belongs to.
|
|
55
|
+
* @param {object} metadata - The match metadata object to strip credentials from.
|
|
56
|
+
* @return - A metadata object without player credentials.
|
|
57
|
+
*/
|
|
58
|
+
const createClientMatchData = (
|
|
59
|
+
matchID: string,
|
|
60
|
+
metadata: Server.MatchData
|
|
61
|
+
): LobbyAPI.Match => {
|
|
62
|
+
return {
|
|
63
|
+
...metadata,
|
|
64
|
+
matchID,
|
|
65
|
+
players: Object.values(metadata.players).map((player) => {
|
|
66
|
+
// strip away credentials
|
|
67
|
+
const { credentials, ...strippedInfo } = player;
|
|
68
|
+
return strippedInfo;
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Utility extracting `string` from a query if it is `string[]`. */
|
|
74
|
+
const unwrapQuery = (
|
|
75
|
+
query: undefined | string | string[]
|
|
76
|
+
): string | undefined => (Array.isArray(query) ? query[0] : query);
|
|
77
|
+
|
|
78
|
+
export const configureRouter = ({
|
|
79
|
+
router,
|
|
80
|
+
db,
|
|
81
|
+
auth,
|
|
82
|
+
games,
|
|
83
|
+
uuid = () => nanoid(11),
|
|
84
|
+
}: {
|
|
85
|
+
router: Router<any, Server.AppCtx>;
|
|
86
|
+
auth: Auth;
|
|
87
|
+
games: Game[];
|
|
88
|
+
uuid?: () => string;
|
|
89
|
+
db: StorageAPI.Sync | StorageAPI.Async;
|
|
90
|
+
}) => {
|
|
91
|
+
/**
|
|
92
|
+
* List available games.
|
|
93
|
+
*
|
|
94
|
+
* @return - Array of game names as string.
|
|
95
|
+
*/
|
|
96
|
+
router.get('/games', async (ctx) => {
|
|
97
|
+
const body: LobbyAPI.GameList = games.map((game) => game.name);
|
|
98
|
+
ctx.body = body;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a new match of a given game.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} name - The name of the game of the new match.
|
|
105
|
+
* @param {number} numPlayers - The number of players.
|
|
106
|
+
* @param {object} setupData - User-defined object that's available
|
|
107
|
+
* during game setup.
|
|
108
|
+
* @param {boolean} unlisted - Whether the match should be excluded from public listing.
|
|
109
|
+
* @return - The ID of the created match.
|
|
110
|
+
*/
|
|
111
|
+
router.post('/games/:name/create', koaBody(), async (ctx) => {
|
|
112
|
+
// The name of the game (for example: tic-tac-toe).
|
|
113
|
+
const gameName = ctx.params.name;
|
|
114
|
+
// User-data to pass to the game setup function.
|
|
115
|
+
const setupData = ctx.request.body.setupData;
|
|
116
|
+
// Whether the game should be excluded from public listing.
|
|
117
|
+
const unlisted = ctx.request.body.unlisted;
|
|
118
|
+
// The number of players for this game instance.
|
|
119
|
+
const numPlayers = Number.parseInt(ctx.request.body.numPlayers);
|
|
120
|
+
|
|
121
|
+
const game = games.find((g) => g.name === gameName);
|
|
122
|
+
if (!game) ctx.throw(404, 'Game ' + gameName + ' not found');
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
ctx.request.body.numPlayers !== undefined &&
|
|
126
|
+
(Number.isNaN(numPlayers) ||
|
|
127
|
+
(game.minPlayers && numPlayers < game.minPlayers) ||
|
|
128
|
+
(game.maxPlayers && numPlayers > game.maxPlayers))
|
|
129
|
+
) {
|
|
130
|
+
ctx.throw(400, 'Invalid numPlayers');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const matchID = await CreateMatch({
|
|
134
|
+
ctx,
|
|
135
|
+
db,
|
|
136
|
+
game,
|
|
137
|
+
numPlayers,
|
|
138
|
+
setupData,
|
|
139
|
+
uuid,
|
|
140
|
+
unlisted,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const body: LobbyAPI.CreatedMatch = { matchID };
|
|
144
|
+
ctx.body = body;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* List matches for a given game.
|
|
149
|
+
*
|
|
150
|
+
* This does not return matches that are marked as unlisted.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} name - The name of the game.
|
|
153
|
+
* @return - Array of match objects.
|
|
154
|
+
*/
|
|
155
|
+
router.get('/games/:name', async (ctx) => {
|
|
156
|
+
const gameName = ctx.params.name;
|
|
157
|
+
const isGameoverString = unwrapQuery(ctx.query.isGameover);
|
|
158
|
+
const updatedBeforeString = unwrapQuery(ctx.query.updatedBefore);
|
|
159
|
+
const updatedAfterString = unwrapQuery(ctx.query.updatedAfter);
|
|
160
|
+
|
|
161
|
+
let isGameover: boolean | undefined;
|
|
162
|
+
if (isGameoverString) {
|
|
163
|
+
if (isGameoverString.toLowerCase() === 'true') {
|
|
164
|
+
isGameover = true;
|
|
165
|
+
} else if (isGameoverString.toLowerCase() === 'false') {
|
|
166
|
+
isGameover = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let updatedBefore: number | undefined;
|
|
170
|
+
if (updatedBeforeString) {
|
|
171
|
+
const parsedNumber = Number.parseInt(updatedBeforeString, 10);
|
|
172
|
+
if (parsedNumber > 0) {
|
|
173
|
+
updatedBefore = parsedNumber;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let updatedAfter: number | undefined;
|
|
177
|
+
if (updatedAfterString) {
|
|
178
|
+
const parsedNumber = Number.parseInt(updatedAfterString, 10);
|
|
179
|
+
if (parsedNumber > 0) {
|
|
180
|
+
updatedAfter = parsedNumber;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const matchList = await db.listMatches({
|
|
184
|
+
gameName,
|
|
185
|
+
where: {
|
|
186
|
+
isGameover,
|
|
187
|
+
updatedAfter,
|
|
188
|
+
updatedBefore,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const matches = [];
|
|
192
|
+
for (const matchID of matchList) {
|
|
193
|
+
const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
|
|
194
|
+
metadata: true,
|
|
195
|
+
});
|
|
196
|
+
if (!metadata.unlisted) {
|
|
197
|
+
matches.push(createClientMatchData(matchID, metadata));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const body: LobbyAPI.MatchList = { matches };
|
|
201
|
+
ctx.body = body;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get data about a specific match.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} name - The name of the game.
|
|
208
|
+
* @param {string} id - The ID of the match.
|
|
209
|
+
* @return - A match object.
|
|
210
|
+
*/
|
|
211
|
+
router.get('/games/:name/:id', async (ctx) => {
|
|
212
|
+
const matchID = ctx.params.id;
|
|
213
|
+
const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
|
|
214
|
+
metadata: true,
|
|
215
|
+
});
|
|
216
|
+
if (!metadata) {
|
|
217
|
+
ctx.throw(404, 'Match ' + matchID + ' not found');
|
|
218
|
+
}
|
|
219
|
+
const body: LobbyAPI.Match = createClientMatchData(matchID, metadata);
|
|
220
|
+
ctx.body = body;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Join a given match.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} name - The name of the game.
|
|
227
|
+
* @param {string} id - The ID of the match.
|
|
228
|
+
* @param {string} playerID - The ID of the player who joins. If not sent, will be assigned to the first index available.
|
|
229
|
+
* @param {string} playerName - The name of the player who joins.
|
|
230
|
+
* @param {object} data - The default data of the player in the match.
|
|
231
|
+
* @return - Player ID and credentials to use when interacting in the joined match.
|
|
232
|
+
*/
|
|
233
|
+
router.post('/games/:name/:id/join', koaBody(), async (ctx) => {
|
|
234
|
+
let playerID = ctx.request.body.playerID;
|
|
235
|
+
const playerName = ctx.request.body.playerName;
|
|
236
|
+
const data = ctx.request.body.data;
|
|
237
|
+
const matchID = ctx.params.id;
|
|
238
|
+
if (!playerName) {
|
|
239
|
+
ctx.throw(403, 'playerName is required');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
|
|
243
|
+
metadata: true,
|
|
244
|
+
});
|
|
245
|
+
if (!metadata) {
|
|
246
|
+
ctx.throw(404, 'Match ' + matchID + ' not found');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (typeof playerID === 'undefined' || playerID === null) {
|
|
250
|
+
playerID = getFirstAvailablePlayerID(metadata.players);
|
|
251
|
+
if (playerID === undefined) {
|
|
252
|
+
const numPlayers = getNumPlayers(metadata.players);
|
|
253
|
+
ctx.throw(
|
|
254
|
+
409,
|
|
255
|
+
`Match ${matchID} reached maximum number of players (${numPlayers})`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!metadata.players[playerID]) {
|
|
261
|
+
ctx.throw(404, 'Player ' + playerID + ' not found');
|
|
262
|
+
}
|
|
263
|
+
if (metadata.players[playerID].name) {
|
|
264
|
+
ctx.throw(409, 'Player ' + playerID + ' not available');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (data) {
|
|
268
|
+
metadata.players[playerID].data = data;
|
|
269
|
+
}
|
|
270
|
+
metadata.players[playerID].name = playerName;
|
|
271
|
+
const playerCredentials = await auth.generateCredentials(ctx);
|
|
272
|
+
metadata.players[playerID].credentials = playerCredentials;
|
|
273
|
+
|
|
274
|
+
await db.setMetadata(matchID, metadata);
|
|
275
|
+
|
|
276
|
+
const body: LobbyAPI.JoinedMatch = { playerID, playerCredentials };
|
|
277
|
+
ctx.body = body;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Leave a given match.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} name - The name of the game.
|
|
284
|
+
* @param {string} id - The ID of the match.
|
|
285
|
+
* @param {string} playerID - The ID of the player who leaves.
|
|
286
|
+
* @param {string} credentials - The credentials of the player who leaves.
|
|
287
|
+
* @return - Nothing.
|
|
288
|
+
*/
|
|
289
|
+
router.post('/games/:name/:id/leave', koaBody(), async (ctx) => {
|
|
290
|
+
const matchID = ctx.params.id;
|
|
291
|
+
const playerID = ctx.request.body.playerID;
|
|
292
|
+
const credentials = ctx.request.body.credentials;
|
|
293
|
+
const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
|
|
294
|
+
metadata: true,
|
|
295
|
+
});
|
|
296
|
+
if (typeof playerID === 'undefined' || playerID === null) {
|
|
297
|
+
ctx.throw(403, 'playerID is required');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!metadata) {
|
|
301
|
+
ctx.throw(404, 'Match ' + matchID + ' not found');
|
|
302
|
+
}
|
|
303
|
+
if (!metadata.players[playerID]) {
|
|
304
|
+
ctx.throw(404, 'Player ' + playerID + ' not found');
|
|
305
|
+
}
|
|
306
|
+
const isAuthorized = await auth.authenticateCredentials({
|
|
307
|
+
playerID,
|
|
308
|
+
credentials,
|
|
309
|
+
metadata,
|
|
310
|
+
});
|
|
311
|
+
if (!isAuthorized) {
|
|
312
|
+
ctx.throw(403, 'Invalid credentials ' + credentials);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
delete metadata.players[playerID].name;
|
|
316
|
+
delete metadata.players[playerID].credentials;
|
|
317
|
+
const hasPlayers = Object.values(metadata.players).some(({ name }) => name);
|
|
318
|
+
await (hasPlayers
|
|
319
|
+
? db.setMetadata(matchID, metadata) // Update metadata.
|
|
320
|
+
: db.wipe(matchID)); // Delete match.
|
|
321
|
+
ctx.body = {};
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Start a new match based on another existing match.
|
|
326
|
+
*
|
|
327
|
+
* @param {string} name - The name of the game.
|
|
328
|
+
* @param {string} id - The ID of the match.
|
|
329
|
+
* @param {string} playerID - The ID of the player creating the match.
|
|
330
|
+
* @param {string} credentials - The credentials of the player creating the match.
|
|
331
|
+
* @param {boolean} unlisted - Whether the match should be excluded from public listing.
|
|
332
|
+
* @return - The ID of the new match.
|
|
333
|
+
*/
|
|
334
|
+
router.post('/games/:name/:id/playAgain', koaBody(), async (ctx) => {
|
|
335
|
+
const gameName = ctx.params.name;
|
|
336
|
+
const matchID = ctx.params.id;
|
|
337
|
+
const playerID = ctx.request.body.playerID;
|
|
338
|
+
const credentials = ctx.request.body.credentials;
|
|
339
|
+
const unlisted = ctx.request.body.unlisted;
|
|
340
|
+
const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
|
|
341
|
+
metadata: true,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (typeof playerID === 'undefined' || playerID === null) {
|
|
345
|
+
ctx.throw(403, 'playerID is required');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!metadata) {
|
|
349
|
+
ctx.throw(404, 'Match ' + matchID + ' not found');
|
|
350
|
+
}
|
|
351
|
+
if (!metadata.players[playerID]) {
|
|
352
|
+
ctx.throw(404, 'Player ' + playerID + ' not found');
|
|
353
|
+
}
|
|
354
|
+
const isAuthorized = await auth.authenticateCredentials({
|
|
355
|
+
playerID,
|
|
356
|
+
credentials,
|
|
357
|
+
metadata,
|
|
358
|
+
});
|
|
359
|
+
if (!isAuthorized) {
|
|
360
|
+
ctx.throw(403, 'Invalid credentials ' + credentials);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check if nextMatch is already set, if so, return that id.
|
|
364
|
+
if (metadata.nextMatchID) {
|
|
365
|
+
ctx.body = { nextMatchID: metadata.nextMatchID };
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// User-data to pass to the game setup function.
|
|
370
|
+
const setupData = ctx.request.body.setupData || metadata.setupData;
|
|
371
|
+
// The number of players for this game instance.
|
|
372
|
+
const numPlayers =
|
|
373
|
+
Number.parseInt(ctx.request.body.numPlayers) ||
|
|
374
|
+
// eslint-disable-next-line unicorn/explicit-length-check
|
|
375
|
+
Object.keys(metadata.players).length;
|
|
376
|
+
|
|
377
|
+
const game = games.find((g) => g.name === gameName);
|
|
378
|
+
const nextMatchID = await CreateMatch({
|
|
379
|
+
ctx,
|
|
380
|
+
db,
|
|
381
|
+
game,
|
|
382
|
+
numPlayers,
|
|
383
|
+
setupData,
|
|
384
|
+
uuid,
|
|
385
|
+
unlisted,
|
|
386
|
+
});
|
|
387
|
+
metadata.nextMatchID = nextMatchID;
|
|
388
|
+
|
|
389
|
+
await db.setMetadata(matchID, metadata);
|
|
390
|
+
|
|
391
|
+
const body: LobbyAPI.NextMatch = { nextMatchID };
|
|
392
|
+
ctx.body = body;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const updatePlayerMetadata = async (ctx: Koa.Context) => {
|
|
396
|
+
const matchID = ctx.params.id;
|
|
397
|
+
const playerID = ctx.request.body.playerID;
|
|
398
|
+
const credentials = ctx.request.body.credentials;
|
|
399
|
+
const newName = ctx.request.body.newName;
|
|
400
|
+
const data = ctx.request.body.data;
|
|
401
|
+
const { metadata } = await (db as StorageAPI.Async).fetch(matchID, {
|
|
402
|
+
metadata: true,
|
|
403
|
+
});
|
|
404
|
+
if (typeof playerID === 'undefined') {
|
|
405
|
+
ctx.throw(403, 'playerID is required');
|
|
406
|
+
}
|
|
407
|
+
if (data === undefined && !newName) {
|
|
408
|
+
ctx.throw(403, 'newName or data is required');
|
|
409
|
+
}
|
|
410
|
+
if (newName && typeof newName !== 'string') {
|
|
411
|
+
ctx.throw(403, `newName must be a string, got ${typeof newName}`);
|
|
412
|
+
}
|
|
413
|
+
if (!metadata) {
|
|
414
|
+
ctx.throw(404, 'Match ' + matchID + ' not found');
|
|
415
|
+
}
|
|
416
|
+
if (!metadata.players[playerID]) {
|
|
417
|
+
ctx.throw(404, 'Player ' + playerID + ' not found');
|
|
418
|
+
}
|
|
419
|
+
const isAuthorized = await auth.authenticateCredentials({
|
|
420
|
+
playerID,
|
|
421
|
+
credentials,
|
|
422
|
+
metadata,
|
|
423
|
+
});
|
|
424
|
+
if (!isAuthorized) {
|
|
425
|
+
ctx.throw(403, 'Invalid credentials ' + credentials);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (newName) {
|
|
429
|
+
metadata.players[playerID].name = newName;
|
|
430
|
+
}
|
|
431
|
+
if (data) {
|
|
432
|
+
metadata.players[playerID].data = data;
|
|
433
|
+
}
|
|
434
|
+
await db.setMetadata(matchID, metadata);
|
|
435
|
+
ctx.body = {};
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Change the name of a player in a given match.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} name - The name of the game.
|
|
442
|
+
* @param {string} id - The ID of the match.
|
|
443
|
+
* @param {string} playerID - The ID of the player.
|
|
444
|
+
* @param {string} credentials - The credentials of the player.
|
|
445
|
+
* @param {object} newName - The new name of the player in the match.
|
|
446
|
+
* @return - Nothing.
|
|
447
|
+
*/
|
|
448
|
+
router.post('/games/:name/:id/rename', koaBody(), async (ctx) => {
|
|
449
|
+
console.warn(
|
|
450
|
+
'This endpoint /rename is deprecated. Please use /update instead.'
|
|
451
|
+
);
|
|
452
|
+
await updatePlayerMetadata(ctx);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Update the player's data for a given match.
|
|
457
|
+
*
|
|
458
|
+
* @param {string} name - The name of the game.
|
|
459
|
+
* @param {string} id - The ID of the match.
|
|
460
|
+
* @param {string} playerID - The ID of the player.
|
|
461
|
+
* @param {string} credentials - The credentials of the player.
|
|
462
|
+
* @param {object} newName - The new name of the player in the match.
|
|
463
|
+
* @param {object} data - The new data of the player in the match.
|
|
464
|
+
* @return - Nothing.
|
|
465
|
+
*/
|
|
466
|
+
router.post('/games/:name/:id/update', koaBody(), updatePlayerMetadata);
|
|
467
|
+
|
|
468
|
+
return router;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
export const configureApp = (
|
|
472
|
+
app: Server.App,
|
|
473
|
+
router: Router<any, Server.AppCtx>,
|
|
474
|
+
origins: CorsOptions['origin']
|
|
475
|
+
): void => {
|
|
476
|
+
app.use(
|
|
477
|
+
cors({
|
|
478
|
+
// Set Access-Control-Allow-Origin header for allowed origins.
|
|
479
|
+
origin: (ctx) => {
|
|
480
|
+
const origin = ctx.get('Origin');
|
|
481
|
+
return isOriginAllowed(origin, origins) ? origin : '';
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// If API_SECRET is set, then require that requests set an
|
|
487
|
+
// api-secret header that is set to the same value.
|
|
488
|
+
app.use(async (ctx, next) => {
|
|
489
|
+
if (
|
|
490
|
+
!!process.env.API_SECRET &&
|
|
491
|
+
ctx.request.headers['api-secret'] !== process.env.API_SECRET
|
|
492
|
+
) {
|
|
493
|
+
ctx.throw(403, 'Invalid API secret');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await next();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Check if a request’s origin header is allowed for CORS.
|
|
504
|
+
* Adapted from `cors` package: https://github.com/expressjs/cors
|
|
505
|
+
* @param origin Request origin to test.
|
|
506
|
+
* @param allowedOrigin Origin(s) that are allowed to connect via CORS.
|
|
507
|
+
* @returns `true` if the origin matched at least one of the allowed origins.
|
|
508
|
+
*/
|
|
509
|
+
function isOriginAllowed(
|
|
510
|
+
origin: string,
|
|
511
|
+
allowedOrigin: CorsOptions['origin']
|
|
512
|
+
): boolean {
|
|
513
|
+
if (Array.isArray(allowedOrigin)) {
|
|
514
|
+
for (const entry of allowedOrigin) {
|
|
515
|
+
if (isOriginAllowed(origin, entry)) {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
} else if (typeof allowedOrigin === 'string') {
|
|
521
|
+
return origin === allowedOrigin;
|
|
522
|
+
} else if (allowedOrigin instanceof RegExp) {
|
|
523
|
+
return allowedOrigin.test(origin);
|
|
524
|
+
} else {
|
|
525
|
+
return !!allowedOrigin;
|
|
526
|
+
}
|
|
527
|
+
}
|