@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,303 @@
|
|
|
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 { SocketOpts } from './socketio';
|
|
10
|
+
import { TransportAPI, SocketIO } from './socketio';
|
|
11
|
+
import { Auth } from '../auth';
|
|
12
|
+
import { ProcessGameConfig } from '../../core/game';
|
|
13
|
+
import type { Master } from '../../master/master';
|
|
14
|
+
import { error } from '../../core/logger';
|
|
15
|
+
import { getFilterPlayerView } from '../../master/filter-player-view';
|
|
16
|
+
|
|
17
|
+
jest.mock('../../core/logger', () => ({
|
|
18
|
+
info: jest.fn(),
|
|
19
|
+
error: jest.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
type SyncArgs = Parameters<Master['onSync']>;
|
|
23
|
+
|
|
24
|
+
type SocketIOTestAdapterOpts = SocketOpts & {
|
|
25
|
+
clientInfo?: Map<any, any>;
|
|
26
|
+
roomInfo?: Map<any, any>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class SocketIOTestAdapter extends SocketIO {
|
|
30
|
+
constructor({
|
|
31
|
+
clientInfo = new Map(),
|
|
32
|
+
roomInfo = new Map(),
|
|
33
|
+
...args
|
|
34
|
+
}: SocketIOTestAdapterOpts = {}) {
|
|
35
|
+
super(Object.keys(args).length > 0 ? args : undefined);
|
|
36
|
+
this.clientInfo = clientInfo;
|
|
37
|
+
this.roomInfo = roomInfo;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public getPubSub() {
|
|
41
|
+
return this.pubSub;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
jest.mock('../../master/master', () => {
|
|
46
|
+
class Master {
|
|
47
|
+
onUpdate: jest.Mock<any, any>;
|
|
48
|
+
onSync: jest.Mock<any, any>;
|
|
49
|
+
onConnectionChange: jest.Mock<any, any>;
|
|
50
|
+
onChatMessage: jest.Mock<any, any>;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
this.onUpdate = jest.fn();
|
|
54
|
+
this.onSync = jest.fn();
|
|
55
|
+
this.onConnectionChange = jest.fn();
|
|
56
|
+
this.onChatMessage = jest.fn();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { Master };
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
jest.mock('koa-socket-2', () => {
|
|
64
|
+
class MockSocket {
|
|
65
|
+
id: string;
|
|
66
|
+
callbacks: Record<string, (...args: any[]) => any>;
|
|
67
|
+
emit: jest.Mock<any, any>;
|
|
68
|
+
broadcast: { emit: jest.Mock<any, any> };
|
|
69
|
+
|
|
70
|
+
constructor() {
|
|
71
|
+
this.id = 'id';
|
|
72
|
+
this.callbacks = {};
|
|
73
|
+
this.emit = jest.fn();
|
|
74
|
+
this.broadcast = { emit: jest.fn() };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async receive(type, ...args) {
|
|
78
|
+
await this.callbacks[type](args[0], args[1], args[2], args[3], args[4]);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
on(type, callback) {
|
|
83
|
+
this.callbacks[type] = callback;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
to() {
|
|
87
|
+
return {
|
|
88
|
+
broadcast: this.broadcast,
|
|
89
|
+
emit: this.emit,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
join() {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class MockIO {
|
|
97
|
+
socket: MockSocket;
|
|
98
|
+
socketAdapter: any;
|
|
99
|
+
|
|
100
|
+
constructor() {
|
|
101
|
+
this.socket = new MockSocket();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
adapter(socketAdapter) {
|
|
105
|
+
this.socketAdapter = socketAdapter;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
attach(app) {
|
|
109
|
+
app.io = app._io = this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
of() {
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
on(type, callback) {
|
|
117
|
+
callback(this.socket);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return MockIO;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('basic', () => {
|
|
125
|
+
const auth = new Auth({ authenticateCredentials: () => true });
|
|
126
|
+
const app: any = { context: { auth } };
|
|
127
|
+
const games = [ProcessGameConfig({ seed: 0 })];
|
|
128
|
+
let clientInfo;
|
|
129
|
+
let roomInfo;
|
|
130
|
+
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
clientInfo = new Map();
|
|
133
|
+
roomInfo = new Map();
|
|
134
|
+
const transport = new SocketIOTestAdapter({ clientInfo, roomInfo });
|
|
135
|
+
transport.init(app, games);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('is attached to app', () => {
|
|
139
|
+
expect(app.context.io).toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('socketAdapter', () => {
|
|
144
|
+
const auth = new Auth({ authenticateCredentials: () => true });
|
|
145
|
+
const app: any = { context: { auth } };
|
|
146
|
+
const games = [ProcessGameConfig({ seed: 0 })];
|
|
147
|
+
|
|
148
|
+
const socketAdapter = jest.fn();
|
|
149
|
+
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
const transport = new SocketIOTestAdapter({ socketAdapter });
|
|
152
|
+
transport.init(app, games);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('socketAdapter is passed', () => {
|
|
156
|
+
expect(app.io.socketAdapter).toBe(socketAdapter);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('TransportAPI', () => {
|
|
161
|
+
let io;
|
|
162
|
+
let api;
|
|
163
|
+
const matchID = 'matchID';
|
|
164
|
+
|
|
165
|
+
beforeAll(() => {
|
|
166
|
+
const auth = new Auth({ authenticateCredentials: () => true });
|
|
167
|
+
const app: any = { context: { auth } };
|
|
168
|
+
const games = [ProcessGameConfig({ seed: 0 })];
|
|
169
|
+
const clientInfo = new Map();
|
|
170
|
+
const roomInfo = new Map();
|
|
171
|
+
const transport = new SocketIOTestAdapter({ clientInfo, roomInfo });
|
|
172
|
+
transport.init(app, games);
|
|
173
|
+
io = app.context.io;
|
|
174
|
+
const socket = io.socket;
|
|
175
|
+
const filterPlayerView = getFilterPlayerView(games[0]);
|
|
176
|
+
api = TransportAPI(
|
|
177
|
+
matchID,
|
|
178
|
+
socket,
|
|
179
|
+
filterPlayerView,
|
|
180
|
+
transport.getPubSub()
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
beforeEach(async () => {
|
|
185
|
+
io.socket.emit = jest.fn();
|
|
186
|
+
io.socket.id = '0';
|
|
187
|
+
const args0: SyncArgs = [matchID, '0', undefined];
|
|
188
|
+
await io.socket.receive('sync', ...args0);
|
|
189
|
+
io.socket.id = '1';
|
|
190
|
+
const args1: SyncArgs = [matchID, '1', undefined];
|
|
191
|
+
await io.socket.receive('sync', ...args1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('send', () => {
|
|
195
|
+
io.socket.id = '0';
|
|
196
|
+
api.send({ type: 'A', playerID: '0', args: [] });
|
|
197
|
+
expect(io.socket.emit).toHaveBeenCalledWith('A');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('sendAll - function', () => {
|
|
201
|
+
api.sendAll({ type: 'A', args: [] });
|
|
202
|
+
expect(io.socket.emit).toHaveBeenCalledTimes(2);
|
|
203
|
+
expect(io.socket.emit).toHaveBeenCalledWith('A');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('sync / update', () => {
|
|
208
|
+
const auth = new Auth({ authenticateCredentials: () => true });
|
|
209
|
+
const app: any = { context: { auth } };
|
|
210
|
+
const games = [ProcessGameConfig({ seed: 0 })];
|
|
211
|
+
const transport = new SocketIOTestAdapter();
|
|
212
|
+
transport.init(app, games);
|
|
213
|
+
const io = app.context.io;
|
|
214
|
+
|
|
215
|
+
test('sync', () => {
|
|
216
|
+
io.socket.receive('sync', 'matchID', '0');
|
|
217
|
+
expect(error).not.toBeCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('update', () => {
|
|
221
|
+
io.socket.receive('update');
|
|
222
|
+
expect(error).not.toBeCalled();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('chat', () => {
|
|
227
|
+
const app: any = { context: {} };
|
|
228
|
+
const games = [ProcessGameConfig({ seed: 0 })];
|
|
229
|
+
const transport = new SocketIOTestAdapter();
|
|
230
|
+
transport.init(app, games);
|
|
231
|
+
const io = app.context.io;
|
|
232
|
+
|
|
233
|
+
test('chat message', async () => {
|
|
234
|
+
await io.socket.receive('chat', 'matchID', { message: 'foo' });
|
|
235
|
+
expect(error).not.toBeCalled();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('connect / disconnect', () => {
|
|
240
|
+
const auth = new Auth({ authenticateCredentials: () => true });
|
|
241
|
+
const app: any = { context: { auth } };
|
|
242
|
+
const games = [ProcessGameConfig({ seed: 0 })];
|
|
243
|
+
let clientInfo;
|
|
244
|
+
let roomInfo;
|
|
245
|
+
let io;
|
|
246
|
+
|
|
247
|
+
beforeAll(() => {
|
|
248
|
+
clientInfo = new Map();
|
|
249
|
+
roomInfo = new Map();
|
|
250
|
+
const transport = new SocketIOTestAdapter({ clientInfo, roomInfo });
|
|
251
|
+
transport.init(app, games);
|
|
252
|
+
io = app.context.io;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('0 and 1 connect', async () => {
|
|
256
|
+
io.socket.id = '0';
|
|
257
|
+
const args0: SyncArgs = ['matchID', '0', undefined, 2];
|
|
258
|
+
await io.socket.receive('sync', ...args0);
|
|
259
|
+
io.socket.id = '1';
|
|
260
|
+
const args1: SyncArgs = ['matchID', '1', undefined, 2];
|
|
261
|
+
await io.socket.receive('sync', ...args1);
|
|
262
|
+
|
|
263
|
+
expect(clientInfo.get('0')).toMatchObject({
|
|
264
|
+
matchID: 'matchID',
|
|
265
|
+
playerID: '0',
|
|
266
|
+
});
|
|
267
|
+
expect(clientInfo.get('1')).toMatchObject({
|
|
268
|
+
matchID: 'matchID',
|
|
269
|
+
playerID: '1',
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('0 disconnects', async () => {
|
|
274
|
+
io.socket.id = '0';
|
|
275
|
+
await io.socket.receive('disconnect');
|
|
276
|
+
|
|
277
|
+
expect(clientInfo.get('0')).toBeUndefined();
|
|
278
|
+
expect(clientInfo.get('1')).toMatchObject({
|
|
279
|
+
matchID: 'matchID',
|
|
280
|
+
playerID: '1',
|
|
281
|
+
});
|
|
282
|
+
expect([...roomInfo.get('matchID')]).toEqual(['1']);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('unknown player disconnects', async () => {
|
|
286
|
+
io.socket.id = 'unknown';
|
|
287
|
+
await io.socket.receive('disconnect');
|
|
288
|
+
|
|
289
|
+
expect(clientInfo.get('0')).toBeUndefined();
|
|
290
|
+
expect(clientInfo.get('1')).toMatchObject({
|
|
291
|
+
matchID: 'matchID',
|
|
292
|
+
playerID: '1',
|
|
293
|
+
});
|
|
294
|
+
expect([...roomInfo.get('matchID')]).toEqual(['1']);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('1 disconnects', async () => {
|
|
298
|
+
io.socket.id = '1';
|
|
299
|
+
await io.socket.receive('disconnect');
|
|
300
|
+
expect([...clientInfo.keys()]).toHaveLength(0);
|
|
301
|
+
expect(roomInfo.get('matchID')).toBeUndefined();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
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 IO from 'koa-socket-2';
|
|
11
|
+
import type IOTypes from 'socket.io';
|
|
12
|
+
import type { ServerOptions as HttpsOptions } from 'https';
|
|
13
|
+
import PQueue from 'p-queue';
|
|
14
|
+
import { Master } from '../../master/master';
|
|
15
|
+
import type {
|
|
16
|
+
TransportAPI as MasterTransport,
|
|
17
|
+
TransportData,
|
|
18
|
+
} from '../../master/master';
|
|
19
|
+
import { getFilterPlayerView } from '../../master/filter-player-view';
|
|
20
|
+
import type { Game, Server } from '../../types';
|
|
21
|
+
import type { GenericPubSub } from './pubsub/generic-pub-sub';
|
|
22
|
+
import type { IntermediateTransportData } from '../../master/master';
|
|
23
|
+
import { InMemoryPubSub } from './pubsub/in-memory-pub-sub';
|
|
24
|
+
|
|
25
|
+
const PING_TIMEOUT = 20 * 1e3;
|
|
26
|
+
const PING_INTERVAL = 10 * 1e3;
|
|
27
|
+
|
|
28
|
+
const emit = (socket: IOTypes.Socket, { type, args }: TransportData) => {
|
|
29
|
+
socket.emit(type, ...args);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function getPubSubChannelId(matchID: string): string {
|
|
33
|
+
return `MATCH-${matchID}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* API that's exposed by SocketIO for the Master to send
|
|
38
|
+
* information to the clients.
|
|
39
|
+
*/
|
|
40
|
+
export const TransportAPI = (
|
|
41
|
+
matchID: string,
|
|
42
|
+
socket: IOTypes.Socket,
|
|
43
|
+
filterPlayerView: any,
|
|
44
|
+
pubSub: GenericPubSub<IntermediateTransportData>
|
|
45
|
+
): MasterTransport => {
|
|
46
|
+
const send: MasterTransport['send'] = ({ playerID, ...data }) => {
|
|
47
|
+
emit(socket, filterPlayerView(playerID, data));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send a message to all clients.
|
|
52
|
+
*/
|
|
53
|
+
const sendAll: MasterTransport['sendAll'] = (payload) => {
|
|
54
|
+
pubSub.publish(getPubSubChannelId(matchID), payload);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return { send, sendAll };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export interface SocketOpts {
|
|
61
|
+
https?: HttpsOptions;
|
|
62
|
+
socketOpts?: IOTypes.ServerOptions;
|
|
63
|
+
socketAdapter?: any;
|
|
64
|
+
pubSub?: GenericPubSub<IntermediateTransportData>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface Client {
|
|
68
|
+
matchID: string;
|
|
69
|
+
playerID: string;
|
|
70
|
+
socket: IOTypes.Socket;
|
|
71
|
+
credentials: string | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Transport interface that uses socket.io
|
|
76
|
+
*/
|
|
77
|
+
export class SocketIO {
|
|
78
|
+
protected clientInfo: Map<string, Client>;
|
|
79
|
+
protected roomInfo: Map<string, Set<string>>;
|
|
80
|
+
protected perMatchQueue: Map<string, PQueue>;
|
|
81
|
+
private readonly https: HttpsOptions;
|
|
82
|
+
private readonly socketAdapter: any;
|
|
83
|
+
private readonly socketOpts: IOTypes.ServerOptions;
|
|
84
|
+
protected pubSub: GenericPubSub<IntermediateTransportData>;
|
|
85
|
+
|
|
86
|
+
constructor({ https, socketAdapter, socketOpts, pubSub }: SocketOpts = {}) {
|
|
87
|
+
this.clientInfo = new Map();
|
|
88
|
+
this.roomInfo = new Map();
|
|
89
|
+
this.perMatchQueue = new Map();
|
|
90
|
+
this.https = https;
|
|
91
|
+
this.socketAdapter = socketAdapter;
|
|
92
|
+
this.socketOpts = socketOpts;
|
|
93
|
+
this.pubSub = pubSub || new InMemoryPubSub();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Unregister client data for a socket.
|
|
98
|
+
*/
|
|
99
|
+
private removeClient(socketID: string): void {
|
|
100
|
+
// Get client data for this socket ID.
|
|
101
|
+
const client = this.clientInfo.get(socketID);
|
|
102
|
+
if (!client) return;
|
|
103
|
+
// Remove client from list of connected sockets for this match.
|
|
104
|
+
const { matchID } = client;
|
|
105
|
+
const matchClients = this.roomInfo.get(matchID);
|
|
106
|
+
matchClients.delete(socketID);
|
|
107
|
+
// If the match is now empty, delete its promise queue & client ID list.
|
|
108
|
+
if (matchClients.size === 0) {
|
|
109
|
+
this.unsubscribePubSubChannel(matchID);
|
|
110
|
+
this.roomInfo.delete(matchID);
|
|
111
|
+
this.deleteMatchQueue(matchID);
|
|
112
|
+
}
|
|
113
|
+
// Remove client data from the client map.
|
|
114
|
+
this.clientInfo.delete(socketID);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Register client data for a socket.
|
|
119
|
+
*/
|
|
120
|
+
private addClient(client: Client, game: Game): void {
|
|
121
|
+
const { matchID, socket } = client;
|
|
122
|
+
// Add client to list of connected sockets for this match.
|
|
123
|
+
let matchClients = this.roomInfo.get(matchID);
|
|
124
|
+
if (matchClients === undefined) {
|
|
125
|
+
this.subscribePubSubChannel(matchID, game);
|
|
126
|
+
matchClients = new Set<string>();
|
|
127
|
+
this.roomInfo.set(matchID, matchClients);
|
|
128
|
+
}
|
|
129
|
+
matchClients.add(socket.id);
|
|
130
|
+
// Register data for this socket in the client map.
|
|
131
|
+
this.clientInfo.set(socket.id, client);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private subscribePubSubChannel(matchID: string, game: Game) {
|
|
135
|
+
const filterPlayerView = getFilterPlayerView(game);
|
|
136
|
+
const broadcast = (payload: IntermediateTransportData) => {
|
|
137
|
+
this.roomInfo.get(matchID).forEach((clientID) => {
|
|
138
|
+
const client = this.clientInfo.get(clientID);
|
|
139
|
+
const data = filterPlayerView(client.playerID, payload);
|
|
140
|
+
emit(client.socket, data);
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
this.pubSub.subscribe(getPubSubChannelId(matchID), broadcast);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private unsubscribePubSubChannel(matchID: string) {
|
|
148
|
+
this.pubSub.unsubscribeAll(getPubSubChannelId(matchID));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
init(
|
|
152
|
+
app: Server.App & { _io?: IOTypes.Server },
|
|
153
|
+
games: Game[],
|
|
154
|
+
origins: CorsOptions['origin'] = []
|
|
155
|
+
) {
|
|
156
|
+
const io = new IO({
|
|
157
|
+
ioOptions: {
|
|
158
|
+
pingTimeout: PING_TIMEOUT,
|
|
159
|
+
pingInterval: PING_INTERVAL,
|
|
160
|
+
cors: {
|
|
161
|
+
origins,
|
|
162
|
+
},
|
|
163
|
+
...this.socketOpts,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.context.io = io;
|
|
168
|
+
io.attach(app, !!this.https, this.https);
|
|
169
|
+
|
|
170
|
+
if (this.socketAdapter) {
|
|
171
|
+
io.adapter(this.socketAdapter);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const game of games) {
|
|
175
|
+
const nsp = app._io.of(game.name);
|
|
176
|
+
const filterPlayerView = getFilterPlayerView(game);
|
|
177
|
+
|
|
178
|
+
nsp.on('connection', (socket: IOTypes.Socket) => {
|
|
179
|
+
socket.on('update', async (...args: Parameters<Master['onUpdate']>) => {
|
|
180
|
+
const [action, stateID, matchID, playerID] = args;
|
|
181
|
+
const master = new Master(
|
|
182
|
+
game,
|
|
183
|
+
app.context.db,
|
|
184
|
+
TransportAPI(matchID, socket, filterPlayerView, this.pubSub),
|
|
185
|
+
app.context.auth
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const matchQueue = this.getMatchQueue(matchID);
|
|
189
|
+
await matchQueue.add(() =>
|
|
190
|
+
master.onUpdate(action, stateID, matchID, playerID)
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
socket.on('sync', async (...args: Parameters<Master['onSync']>) => {
|
|
195
|
+
const [matchID, playerID, credentials] = args;
|
|
196
|
+
socket.join(matchID);
|
|
197
|
+
this.removeClient(socket.id);
|
|
198
|
+
const requestingClient = { socket, matchID, playerID, credentials };
|
|
199
|
+
const transport = TransportAPI(
|
|
200
|
+
matchID,
|
|
201
|
+
socket,
|
|
202
|
+
filterPlayerView,
|
|
203
|
+
this.pubSub
|
|
204
|
+
);
|
|
205
|
+
const master = new Master(
|
|
206
|
+
game,
|
|
207
|
+
app.context.db,
|
|
208
|
+
transport,
|
|
209
|
+
app.context.auth
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const syncResponse = await master.onSync(...args);
|
|
213
|
+
if (syncResponse && syncResponse.error === 'unauthorized') {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.addClient(requestingClient, game);
|
|
217
|
+
await master.onConnectionChange(matchID, playerID, credentials, true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
socket.on('disconnect', async () => {
|
|
221
|
+
const client = this.clientInfo.get(socket.id);
|
|
222
|
+
this.removeClient(socket.id);
|
|
223
|
+
if (client) {
|
|
224
|
+
const { matchID, playerID, credentials } = client;
|
|
225
|
+
const master = new Master(
|
|
226
|
+
game,
|
|
227
|
+
app.context.db,
|
|
228
|
+
TransportAPI(matchID, socket, filterPlayerView, this.pubSub),
|
|
229
|
+
app.context.auth
|
|
230
|
+
);
|
|
231
|
+
await master.onConnectionChange(
|
|
232
|
+
matchID,
|
|
233
|
+
playerID,
|
|
234
|
+
credentials,
|
|
235
|
+
false
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
socket.on(
|
|
241
|
+
'chat',
|
|
242
|
+
async (...args: Parameters<Master['onChatMessage']>) => {
|
|
243
|
+
const [matchID] = args;
|
|
244
|
+
const master = new Master(
|
|
245
|
+
game,
|
|
246
|
+
app.context.db,
|
|
247
|
+
TransportAPI(matchID, socket, filterPlayerView, this.pubSub),
|
|
248
|
+
app.context.auth
|
|
249
|
+
);
|
|
250
|
+
master.onChatMessage(...args);
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create a PQueue for a given matchID if none exists and return it.
|
|
259
|
+
* @param matchID
|
|
260
|
+
* @returns
|
|
261
|
+
*/
|
|
262
|
+
getMatchQueue(matchID: string): PQueue {
|
|
263
|
+
if (!this.perMatchQueue.has(matchID)) {
|
|
264
|
+
// PQueue should process only one action at a time.
|
|
265
|
+
this.perMatchQueue.set(matchID, new PQueue({ concurrency: 1 }));
|
|
266
|
+
}
|
|
267
|
+
return this.perMatchQueue.get(matchID);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Delete a PQueue for a given matchID.
|
|
272
|
+
* @param matchID
|
|
273
|
+
*/
|
|
274
|
+
deleteMatchQueue(matchID: string): void {
|
|
275
|
+
this.perMatchQueue.delete(matchID);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { Master };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { InitializeGame } from '../core/initialize';
|
|
2
|
+
import type { Server, State, Game } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new match metadata object.
|
|
6
|
+
*/
|
|
7
|
+
export const createMetadata = ({
|
|
8
|
+
game,
|
|
9
|
+
unlisted,
|
|
10
|
+
setupData,
|
|
11
|
+
numPlayers,
|
|
12
|
+
}: {
|
|
13
|
+
game: Game;
|
|
14
|
+
numPlayers: number;
|
|
15
|
+
setupData?: any;
|
|
16
|
+
unlisted?: boolean;
|
|
17
|
+
}): Server.MatchData => {
|
|
18
|
+
const metadata: Server.MatchData = {
|
|
19
|
+
gameName: game.name,
|
|
20
|
+
unlisted: !!unlisted,
|
|
21
|
+
players: {},
|
|
22
|
+
createdAt: Date.now(),
|
|
23
|
+
updatedAt: Date.now(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (setupData !== undefined) metadata.setupData = setupData;
|
|
27
|
+
|
|
28
|
+
for (let playerIndex = 0; playerIndex < numPlayers; playerIndex++) {
|
|
29
|
+
metadata.players[playerIndex] = { id: playerIndex };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return metadata;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates initial state and metadata for a new match.
|
|
37
|
+
* If the provided `setupData` doesn’t pass the game’s validation,
|
|
38
|
+
* an error object is returned instead.
|
|
39
|
+
*/
|
|
40
|
+
export const createMatch = ({
|
|
41
|
+
game,
|
|
42
|
+
numPlayers,
|
|
43
|
+
setupData,
|
|
44
|
+
unlisted,
|
|
45
|
+
}: {
|
|
46
|
+
game: Game;
|
|
47
|
+
numPlayers: number;
|
|
48
|
+
setupData: any;
|
|
49
|
+
unlisted: boolean;
|
|
50
|
+
}):
|
|
51
|
+
| { metadata: Server.MatchData; initialState: State }
|
|
52
|
+
| { setupDataError: string } => {
|
|
53
|
+
if (!numPlayers || typeof numPlayers !== 'number') numPlayers = 2;
|
|
54
|
+
|
|
55
|
+
const setupDataError =
|
|
56
|
+
game.validateSetupData && game.validateSetupData(setupData, numPlayers);
|
|
57
|
+
if (setupDataError !== undefined) return { setupDataError };
|
|
58
|
+
|
|
59
|
+
const metadata = createMetadata({ game, numPlayers, setupData, unlisted });
|
|
60
|
+
const initialState = InitializeGame({ game, numPlayers, setupData });
|
|
61
|
+
|
|
62
|
+
return { metadata, initialState };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Given players, returns the count of players.
|
|
67
|
+
*/
|
|
68
|
+
export const getNumPlayers = (players: Server.MatchData['players']): number =>
|
|
69
|
+
Object.keys(players).length;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Given players, tries to find the ID of the first player that can be joined.
|
|
73
|
+
* Returns `undefined` if there’s no available ID.
|
|
74
|
+
*/
|
|
75
|
+
export const getFirstAvailablePlayerID = (
|
|
76
|
+
players: Server.MatchData['players']
|
|
77
|
+
): string | undefined => {
|
|
78
|
+
const numPlayers = getNumPlayers(players);
|
|
79
|
+
// Try to get the first index available
|
|
80
|
+
for (let i = 0; i < numPlayers; i++) {
|
|
81
|
+
if (typeof players[i].name === 'undefined' || players[i].name === null) {
|
|
82
|
+
return String(i);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Client } from '../client/client';
|
|
2
|
+
import type { Game } from '../types';
|
|
3
|
+
import { MockRandom } from './mock-random';
|
|
4
|
+
|
|
5
|
+
test('it creates a plugin object', () => {
|
|
6
|
+
const plugin = MockRandom();
|
|
7
|
+
expect(plugin).toEqual({
|
|
8
|
+
name: 'random',
|
|
9
|
+
noClient: expect.any(Function),
|
|
10
|
+
api: expect.any(Function),
|
|
11
|
+
setup: expect.any(Function),
|
|
12
|
+
playerView: expect.any(Function),
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('it can override random API methods', () => {
|
|
17
|
+
const game: Game<{ roll: number }> = {
|
|
18
|
+
moves: {
|
|
19
|
+
roll: ({ G, random }) => {
|
|
20
|
+
G.roll = random.D6();
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
plugins: [MockRandom({ D6: () => 1 })],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const client = Client({ game });
|
|
27
|
+
client.moves.roll();
|
|
28
|
+
expect(client.getState().G.roll).toBe(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('it can use non-overridden API methods', () => {
|
|
32
|
+
const game: Game<{ roll: number }> = {
|
|
33
|
+
moves: {
|
|
34
|
+
roll: ({ G, random }) => {
|
|
35
|
+
G.roll = random.D6();
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
plugins: [MockRandom({ D10: () => 1 })],
|
|
39
|
+
seed: 0,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const client = Client({ game });
|
|
43
|
+
client.moves.roll();
|
|
44
|
+
expect(client.getState().G.roll).toMatchInlineSnapshot(`4`);
|
|
45
|
+
});
|