@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.
Files changed (296) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/ai/package.json +7 -0
  4. package/client/package.json +7 -0
  5. package/core/package.json +7 -0
  6. package/debug/package.json +7 -0
  7. package/dist/boardgameio.es.js +14238 -0
  8. package/dist/boardgameio.js +14277 -0
  9. package/dist/boardgameio.min.js +16 -0
  10. package/dist/cjs/Debug-9d141c06.js +9586 -0
  11. package/dist/cjs/ai-e0e8a768.js +377 -0
  12. package/dist/cjs/ai.js +20 -0
  13. package/dist/cjs/client-76dec77b.js +258 -0
  14. package/dist/cjs/client-a22d7500.js +524 -0
  15. package/dist/cjs/client.js +26 -0
  16. package/dist/cjs/core.js +52 -0
  17. package/dist/cjs/debug.js +18 -0
  18. package/dist/cjs/filter-player-view-bb02e2f6.js +89 -0
  19. package/dist/cjs/initialize-267fcd69.js +61 -0
  20. package/dist/cjs/internal.js +25 -0
  21. package/dist/cjs/master-2904879d.js +320 -0
  22. package/dist/cjs/master.js +18 -0
  23. package/dist/cjs/multiplayer.js +23 -0
  24. package/dist/cjs/plugin-random-7425844d.js +229 -0
  25. package/dist/cjs/plugins.js +59 -0
  26. package/dist/cjs/react-native.js +182 -0
  27. package/dist/cjs/react.js +727 -0
  28. package/dist/cjs/reducer-16eec232.js +1203 -0
  29. package/dist/cjs/server.js +4087 -0
  30. package/dist/cjs/socketio-7a0837eb.js +478 -0
  31. package/dist/cjs/testing.js +30 -0
  32. package/dist/cjs/transport-b1874dfa.js +37 -0
  33. package/dist/cjs/turn-order-b2ff8740.js +1136 -0
  34. package/dist/cjs/util-fcfd8fb8.js +140 -0
  35. package/dist/esm/Debug-0141fe2d.js +9577 -0
  36. package/dist/esm/ai-5c06e761.js +371 -0
  37. package/dist/esm/ai.js +8 -0
  38. package/dist/esm/client-2e653027.js +522 -0
  39. package/dist/esm/client-5f57c3f2.js +255 -0
  40. package/dist/esm/client.js +16 -0
  41. package/dist/esm/core.js +40 -0
  42. package/dist/esm/debug.js +10 -0
  43. package/dist/esm/filter-player-view-2c6cc96f.js +87 -0
  44. package/dist/esm/initialize-11d626ca.js +59 -0
  45. package/dist/esm/internal.js +10 -0
  46. package/dist/esm/master-fa8f2e43.js +318 -0
  47. package/dist/esm/master.js +10 -0
  48. package/dist/esm/multiplayer.js +14 -0
  49. package/dist/esm/plugin-random-087f861e.js +226 -0
  50. package/dist/esm/plugins.js +55 -0
  51. package/dist/esm/react-native.js +173 -0
  52. package/dist/esm/react.js +716 -0
  53. package/dist/esm/reducer-c46da7e5.js +1198 -0
  54. package/dist/esm/socketio-c22ffa65.js +455 -0
  55. package/dist/esm/testing.js +26 -0
  56. package/dist/esm/transport-ce07b771.js +35 -0
  57. package/dist/esm/turn-order-376d315e.js +1091 -0
  58. package/dist/esm/util-b6147cef.js +135 -0
  59. package/dist/types/packages/ai.d.ts +5 -0
  60. package/dist/types/packages/client.d.ts +3 -0
  61. package/dist/types/packages/core.d.ts +5 -0
  62. package/dist/types/packages/debug.d.ts +2 -0
  63. package/dist/types/packages/internal.d.ts +8 -0
  64. package/dist/types/packages/master.d.ts +2 -0
  65. package/dist/types/packages/multiplayer.d.ts +3 -0
  66. package/dist/types/packages/plugins.d.ts +3 -0
  67. package/dist/types/packages/react-native.d.ts +2 -0
  68. package/dist/types/packages/react.d.ts +3 -0
  69. package/dist/types/packages/server.d.ts +6 -0
  70. package/dist/types/packages/testing.d.ts +1 -0
  71. package/dist/types/src/ai/ai.d.ts +53 -0
  72. package/dist/types/src/ai/ai.test.d.ts +1 -0
  73. package/dist/types/src/ai/bot.d.ts +40 -0
  74. package/dist/types/src/ai/mcts-bot.d.ts +60 -0
  75. package/dist/types/src/ai/random-bot.d.ts +27 -0
  76. package/dist/types/src/client/client.d.ts +104 -0
  77. package/dist/types/src/client/client.test.d.ts +1 -0
  78. package/dist/types/src/client/debug/tests/debug.test.d.ts +1 -0
  79. package/dist/types/src/client/manager.d.ts +61 -0
  80. package/dist/types/src/client/react.d.ts +75 -0
  81. package/dist/types/src/client/react.ssr.test.d.ts +4 -0
  82. package/dist/types/src/client/react.test.d.ts +1 -0
  83. package/dist/types/src/client/transport/dummy.d.ts +18 -0
  84. package/dist/types/src/client/transport/local.d.ts +59 -0
  85. package/dist/types/src/client/transport/local.test.d.ts +1 -0
  86. package/dist/types/src/client/transport/socketio.d.ts +45 -0
  87. package/dist/types/src/client/transport/socketio.test.d.ts +1 -0
  88. package/dist/types/src/client/transport/transport.d.ts +50 -0
  89. package/dist/types/src/client/transport/transport.test.d.ts +1 -0
  90. package/dist/types/src/core/action-creators.d.ts +144 -0
  91. package/dist/types/src/core/action-types.d.ts +10 -0
  92. package/dist/types/src/core/backwards-compatibility.d.ts +12 -0
  93. package/dist/types/src/core/constants.d.ts +6 -0
  94. package/dist/types/src/core/errors.d.ts +15 -0
  95. package/dist/types/src/core/flow.d.ts +28 -0
  96. package/dist/types/src/core/flow.test.d.ts +1 -0
  97. package/dist/types/src/core/game-methods.d.ts +9 -0
  98. package/dist/types/src/core/game.d.ts +26 -0
  99. package/dist/types/src/core/game.test.d.ts +1 -0
  100. package/dist/types/src/core/initialize.d.ts +9 -0
  101. package/dist/types/src/core/logger.d.ts +2 -0
  102. package/dist/types/src/core/player-view.d.ts +7 -0
  103. package/dist/types/src/core/player-view.test.d.ts +1 -0
  104. package/dist/types/src/core/reducer.d.ts +155 -0
  105. package/dist/types/src/core/reducer.test.d.ts +1 -0
  106. package/dist/types/src/core/turn-order.d.ts +179 -0
  107. package/dist/types/src/core/turn-order.test.d.ts +8 -0
  108. package/dist/types/src/lobby/client.d.ts +194 -0
  109. package/dist/types/src/lobby/client.test.d.ts +1 -0
  110. package/dist/types/src/lobby/connection.d.ts +44 -0
  111. package/dist/types/src/lobby/connection.test.d.ts +1 -0
  112. package/dist/types/src/lobby/create-match-form.d.ts +26 -0
  113. package/dist/types/src/lobby/login-form.d.ts +23 -0
  114. package/dist/types/src/lobby/match-instance.d.ts +31 -0
  115. package/dist/types/src/lobby/react.d.ts +113 -0
  116. package/dist/types/src/lobby/react.ssr.test.d.ts +4 -0
  117. package/dist/types/src/lobby/react.test.d.ts +1 -0
  118. package/dist/types/src/master/filter-player-view.d.ts +96 -0
  119. package/dist/types/src/master/filter-player-view.test.d.ts +1 -0
  120. package/dist/types/src/master/master.d.ts +94 -0
  121. package/dist/types/src/master/master.test.d.ts +1 -0
  122. package/dist/types/src/plugins/events/events.d.ts +54 -0
  123. package/dist/types/src/plugins/events/events.test.d.ts +1 -0
  124. package/dist/types/src/plugins/main.d.ts +75 -0
  125. package/dist/types/src/plugins/main.test.d.ts +1 -0
  126. package/dist/types/src/plugins/plugin-events.d.ts +5 -0
  127. package/dist/types/src/plugins/plugin-immer.d.ts +7 -0
  128. package/dist/types/src/plugins/plugin-immer.test.d.ts +1 -0
  129. package/dist/types/src/plugins/plugin-log.d.ts +14 -0
  130. package/dist/types/src/plugins/plugin-log.test.d.ts +1 -0
  131. package/dist/types/src/plugins/plugin-player.d.ts +29 -0
  132. package/dist/types/src/plugins/plugin-player.test.d.ts +1 -0
  133. package/dist/types/src/plugins/plugin-random.d.ts +4 -0
  134. package/dist/types/src/plugins/plugin-serializable.d.ts +7 -0
  135. package/dist/types/src/plugins/plugin-serializable.test.d.ts +1 -0
  136. package/dist/types/src/plugins/random/random.alea.d.ts +19 -0
  137. package/dist/types/src/plugins/random/random.d.ts +54 -0
  138. package/dist/types/src/plugins/random/random.test.d.ts +1 -0
  139. package/dist/types/src/server/api.d.ts +13 -0
  140. package/dist/types/src/server/api.test.d.ts +1 -0
  141. package/dist/types/src/server/auth.d.ts +38 -0
  142. package/dist/types/src/server/auth.test.d.ts +1 -0
  143. package/dist/types/src/server/cors.d.ts +4 -0
  144. package/dist/types/src/server/cors.test.d.ts +1 -0
  145. package/dist/types/src/server/db/base.d.ts +192 -0
  146. package/dist/types/src/server/db/flatfile.d.ts +44 -0
  147. package/dist/types/src/server/db/flatfile.test.d.ts +1 -0
  148. package/dist/types/src/server/db/index.d.ts +4 -0
  149. package/dist/types/src/server/db/index.test.d.ts +1 -0
  150. package/dist/types/src/server/db/inmemory.d.ts +43 -0
  151. package/dist/types/src/server/db/inmemory.test.d.ts +1 -0
  152. package/dist/types/src/server/db/localstorage.d.ts +7 -0
  153. package/dist/types/src/server/db/localstorage.test.d.ts +1 -0
  154. package/dist/types/src/server/index.d.ts +68 -0
  155. package/dist/types/src/server/index.test.d.ts +1 -0
  156. package/dist/types/src/server/transport/pubsub/generic-pub-sub.d.ts +6 -0
  157. package/dist/types/src/server/transport/pubsub/in-memory-pub-sub.d.ts +7 -0
  158. package/dist/types/src/server/transport/pubsub/in-memory-pub-sub.test.d.ts +1 -0
  159. package/dist/types/src/server/transport/socketio-simultaneous.test.d.ts +1 -0
  160. package/dist/types/src/server/transport/socketio.d.ts +65 -0
  161. package/dist/types/src/server/transport/socketio.test.d.ts +1 -0
  162. package/dist/types/src/server/util.d.ts +35 -0
  163. package/dist/types/src/testing/mock-random.d.ts +15 -0
  164. package/dist/types/src/testing/mock-random.test.d.ts +1 -0
  165. package/dist/types/src/types.d.ts +387 -0
  166. package/internal/package.json +7 -0
  167. package/master/package.json +7 -0
  168. package/multiplayer/package.json +7 -0
  169. package/package.json +211 -0
  170. package/plugins/package.json +7 -0
  171. package/react/package.json +7 -0
  172. package/react-native/package.json +7 -0
  173. package/server/package.json +6 -0
  174. package/src/ai/ai.test.ts +433 -0
  175. package/src/ai/ai.ts +84 -0
  176. package/src/ai/bot.ts +122 -0
  177. package/src/ai/mcts-bot.ts +331 -0
  178. package/src/ai/random-bot.ts +20 -0
  179. package/src/client/client.test.ts +993 -0
  180. package/src/client/client.ts +588 -0
  181. package/src/client/debug/Debug.svelte +239 -0
  182. package/src/client/debug/Menu.svelte +65 -0
  183. package/src/client/debug/ai/AI.svelte +215 -0
  184. package/src/client/debug/ai/Options.svelte +48 -0
  185. package/src/client/debug/info/Info.svelte +22 -0
  186. package/src/client/debug/info/Item.svelte +24 -0
  187. package/src/client/debug/log/Log.svelte +157 -0
  188. package/src/client/debug/log/LogEvent.svelte +149 -0
  189. package/src/client/debug/log/LogMetadata.svelte +7 -0
  190. package/src/client/debug/log/PhaseMarker.svelte +27 -0
  191. package/src/client/debug/log/TurnMarker.svelte +23 -0
  192. package/src/client/debug/main/ClientSwitcher.svelte +59 -0
  193. package/src/client/debug/main/Controls.svelte +58 -0
  194. package/src/client/debug/main/Hotkey.svelte +84 -0
  195. package/src/client/debug/main/InteractiveFunction.svelte +85 -0
  196. package/src/client/debug/main/Main.svelte +121 -0
  197. package/src/client/debug/main/Move.svelte +68 -0
  198. package/src/client/debug/main/PlayerInfo.svelte +70 -0
  199. package/src/client/debug/mcts/Action.svelte +22 -0
  200. package/src/client/debug/mcts/MCTS.svelte +78 -0
  201. package/src/client/debug/mcts/Table.svelte +98 -0
  202. package/src/client/debug/tests/JSONTree.mock.svelte +3 -0
  203. package/src/client/debug/tests/debug.test.ts +183 -0
  204. package/src/client/debug/utils/shortcuts.js +50 -0
  205. package/src/client/debug/utils/shortcuts.test.js +49 -0
  206. package/src/client/manager.ts +177 -0
  207. package/src/client/react-native.js +136 -0
  208. package/src/client/react-native.test.js +229 -0
  209. package/src/client/react.ssr.test.tsx +24 -0
  210. package/src/client/react.test.tsx +213 -0
  211. package/src/client/react.tsx +192 -0
  212. package/src/client/transport/dummy.ts +19 -0
  213. package/src/client/transport/local.test.ts +353 -0
  214. package/src/client/transport/local.ts +230 -0
  215. package/src/client/transport/socketio.test.ts +328 -0
  216. package/src/client/transport/socketio.ts +210 -0
  217. package/src/client/transport/transport.test.ts +27 -0
  218. package/src/client/transport/transport.ts +95 -0
  219. package/src/core/action-creators.ts +159 -0
  220. package/src/core/action-types.ts +18 -0
  221. package/src/core/backwards-compatibility.ts +23 -0
  222. package/src/core/constants.ts +6 -0
  223. package/src/core/errors.ts +35 -0
  224. package/src/core/flow.test.ts +2433 -0
  225. package/src/core/flow.ts +897 -0
  226. package/src/core/game-methods.ts +9 -0
  227. package/src/core/game.test.ts +286 -0
  228. package/src/core/game.ts +114 -0
  229. package/src/core/initialize.ts +77 -0
  230. package/src/core/logger.test.js +90 -0
  231. package/src/core/logger.ts +18 -0
  232. package/src/core/player-view.test.ts +50 -0
  233. package/src/core/player-view.ts +39 -0
  234. package/src/core/reducer.test.ts +991 -0
  235. package/src/core/reducer.ts +532 -0
  236. package/src/core/turn-order.test.ts +1123 -0
  237. package/src/core/turn-order.ts +473 -0
  238. package/src/lobby/client.test.ts +385 -0
  239. package/src/lobby/client.ts +358 -0
  240. package/src/lobby/connection.test.ts +207 -0
  241. package/src/lobby/connection.ts +162 -0
  242. package/src/lobby/create-match-form.tsx +122 -0
  243. package/src/lobby/login-form.tsx +75 -0
  244. package/src/lobby/match-instance.tsx +135 -0
  245. package/src/lobby/react.ssr.test.tsx +22 -0
  246. package/src/lobby/react.test.tsx +594 -0
  247. package/src/lobby/react.tsx +402 -0
  248. package/src/master/filter-player-view.test.ts +381 -0
  249. package/src/master/filter-player-view.ts +102 -0
  250. package/src/master/master.test.ts +1068 -0
  251. package/src/master/master.ts +492 -0
  252. package/src/plugins/events/events.test.ts +108 -0
  253. package/src/plugins/events/events.ts +209 -0
  254. package/src/plugins/main.test.ts +411 -0
  255. package/src/plugins/main.ts +314 -0
  256. package/src/plugins/plugin-events.ts +40 -0
  257. package/src/plugins/plugin-immer.test.ts +86 -0
  258. package/src/plugins/plugin-immer.ts +37 -0
  259. package/src/plugins/plugin-log.test.ts +37 -0
  260. package/src/plugins/plugin-log.ts +40 -0
  261. package/src/plugins/plugin-player.test.ts +172 -0
  262. package/src/plugins/plugin-player.ts +100 -0
  263. package/src/plugins/plugin-random.ts +40 -0
  264. package/src/plugins/plugin-serializable.test.ts +40 -0
  265. package/src/plugins/plugin-serializable.ts +55 -0
  266. package/src/plugins/random/random.alea.ts +109 -0
  267. package/src/plugins/random/random.test.ts +167 -0
  268. package/src/plugins/random/random.ts +198 -0
  269. package/src/server/api.test.ts +1699 -0
  270. package/src/server/api.ts +527 -0
  271. package/src/server/auth.test.ts +275 -0
  272. package/src/server/auth.ts +89 -0
  273. package/src/server/cors.test.ts +121 -0
  274. package/src/server/cors.ts +7 -0
  275. package/src/server/db/base.ts +296 -0
  276. package/src/server/db/flatfile.test.ts +221 -0
  277. package/src/server/db/flatfile.ts +228 -0
  278. package/src/server/db/index.test.ts +8 -0
  279. package/src/server/db/index.ts +12 -0
  280. package/src/server/db/inmemory.test.ts +143 -0
  281. package/src/server/db/inmemory.ts +143 -0
  282. package/src/server/db/localstorage.test.ts +73 -0
  283. package/src/server/db/localstorage.ts +44 -0
  284. package/src/server/index.test.ts +265 -0
  285. package/src/server/index.ts +175 -0
  286. package/src/server/transport/pubsub/generic-pub-sub.ts +11 -0
  287. package/src/server/transport/pubsub/in-memory-pub-sub.test.ts +47 -0
  288. package/src/server/transport/pubsub/in-memory-pub-sub.ts +28 -0
  289. package/src/server/transport/socketio-simultaneous.test.ts +603 -0
  290. package/src/server/transport/socketio.test.ts +303 -0
  291. package/src/server/transport/socketio.ts +279 -0
  292. package/src/server/util.ts +85 -0
  293. package/src/testing/mock-random.test.ts +45 -0
  294. package/src/testing/mock-random.ts +27 -0
  295. package/src/types.ts +511 -0
  296. package/testing/package.json +7 -0
@@ -0,0 +1,1068 @@
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 * as ActionCreators from '../core/action-creators';
10
+ import { InitializeGame } from '../core/initialize';
11
+ import { InMemory } from '../server/db/inmemory';
12
+ import { Master } from './master';
13
+ import { error } from '../core/logger';
14
+ import type { Game, Server, State, LogEntry } from '../types';
15
+ import { Auth } from '../server/auth';
16
+ import * as StorageAPI from '../server/db/base';
17
+ import * as dateMock from 'jest-date-mock';
18
+ import { PlayerView } from '../core/player-view';
19
+ import { INVALID_MOVE } from '../core/constants';
20
+
21
+ jest.mock('../core/logger', () => ({
22
+ info: jest.fn(),
23
+ error: jest.fn(),
24
+ }));
25
+
26
+ beforeEach(() => {
27
+ dateMock.clear();
28
+ });
29
+
30
+ class InMemoryAsync extends StorageAPI.Async {
31
+ db: InMemory;
32
+
33
+ constructor() {
34
+ super();
35
+ this.db = new InMemory();
36
+ }
37
+
38
+ async connect() {
39
+ await this.sleep();
40
+ }
41
+
42
+ private sleep(): Promise<void> {
43
+ const interval = Math.round(Math.random() * 50 + 50);
44
+ return new Promise((resolve) => void setTimeout(resolve, interval));
45
+ }
46
+
47
+ async createMatch(id: string, opts: StorageAPI.CreateMatchOpts) {
48
+ await this.sleep();
49
+ this.db.createMatch(id, opts);
50
+ }
51
+
52
+ async setMetadata(matchID: string, metadata: Server.MatchData) {
53
+ await this.sleep();
54
+ this.db.setMetadata(matchID, metadata);
55
+ }
56
+
57
+ async setState(matchID: string, state: State, deltalog?: LogEntry[]) {
58
+ await this.sleep();
59
+ this.db.setState(matchID, state, deltalog);
60
+ }
61
+
62
+ async fetch<O extends StorageAPI.FetchOpts>(
63
+ matchID: string,
64
+ opts: O
65
+ ): Promise<StorageAPI.FetchResult<O>> {
66
+ await this.sleep();
67
+ return this.db.fetch(matchID, opts);
68
+ }
69
+
70
+ async wipe(matchID: string) {
71
+ await this.sleep();
72
+ this.db.wipe(matchID);
73
+ }
74
+
75
+ async listMatches(opts?: StorageAPI.ListMatchesOpts): Promise<string[]> {
76
+ await this.sleep();
77
+ return this.db.listMatches(opts);
78
+ }
79
+ }
80
+
81
+ const game: Game = { seed: 0 };
82
+
83
+ function TransportAPI(send = jest.fn(), sendAll = jest.fn()) {
84
+ return { send, sendAll };
85
+ }
86
+
87
+ function validateNotTransientState(state: any) {
88
+ expect(state).toEqual(
89
+ expect.not.objectContaining({ transients: expect.anything() })
90
+ );
91
+ }
92
+
93
+ describe('sync', () => {
94
+ const send = jest.fn();
95
+ const db = new InMemory();
96
+ const master = new Master(game, db, TransportAPI(send));
97
+
98
+ beforeEach(() => {
99
+ jest.clearAllMocks();
100
+ });
101
+
102
+ test('causes server to respond', async () => {
103
+ await master.onSync('matchID', '0', undefined, 2);
104
+ expect(send).toHaveBeenCalledWith(
105
+ expect.objectContaining({
106
+ type: 'sync',
107
+ })
108
+ );
109
+ });
110
+
111
+ test('sync a second time does not create a game', async () => {
112
+ const fetchResult = db.fetch('matchID', { metadata: true });
113
+ await master.onSync('matchID', '0', undefined, 2);
114
+ expect(db.fetch('matchID', { metadata: true })).toMatchObject(fetchResult);
115
+ });
116
+
117
+ test('should not have metadata', async () => {
118
+ db.setState('oldGameID', {} as State);
119
+ await master.onSync('oldGameID', '0');
120
+ // [0][0] = first call, first argument
121
+ expect(send.mock.calls[0][0].args[1].filteredMetadata).toBeUndefined();
122
+ });
123
+
124
+ test('should have metadata', async () => {
125
+ const db = new InMemory();
126
+ const metadata = {
127
+ gameName: 'tic-tac-toe',
128
+ setupData: {},
129
+ players: {
130
+ '0': {
131
+ id: 0,
132
+ credentials: 'qS2m4Ujb_',
133
+ name: 'Alice',
134
+ },
135
+ '1': {
136
+ id: 1,
137
+ credentials: 'nIQtXFybDD',
138
+ name: 'Bob',
139
+ },
140
+ },
141
+ createdAt: 0,
142
+ updatedAt: 0,
143
+ };
144
+ db.createMatch('matchID', { metadata, initialState: {} as State });
145
+ const masterWithMetadata = new Master(game, db, TransportAPI(send));
146
+ await masterWithMetadata.onSync('matchID', '0', undefined, 2);
147
+
148
+ const expectedMetadata = [
149
+ { id: 0, name: 'Alice' },
150
+ { id: 1, name: 'Bob' },
151
+ ];
152
+ expect(send.mock.calls[0][0].args[1].filteredMetadata).toMatchObject(
153
+ expectedMetadata
154
+ );
155
+ });
156
+
157
+ test('should not create match for games that require setupData', async () => {
158
+ const game: Game = {
159
+ validateSetupData: () => 'requires setupData',
160
+ };
161
+ const db = new InMemory();
162
+ const master = new Master(game, db, TransportAPI(send));
163
+
164
+ const matchID = 'matchID';
165
+ const res = await master.onSync(matchID, '0', undefined, 2);
166
+
167
+ expect(res).toEqual({ error: 'game requires setupData' });
168
+ expect(send).not.toHaveBeenCalled();
169
+ expect(db.fetch(matchID, { state: true })).toEqual({ state: undefined });
170
+ });
171
+ });
172
+
173
+ describe('update', () => {
174
+ const send = jest.fn();
175
+ const sendAll = jest.fn();
176
+ const game: Game = {
177
+ moves: {
178
+ A: ({ G }) => G,
179
+ },
180
+ };
181
+ let db;
182
+ let master;
183
+ const action = ActionCreators.gameEvent('endTurn');
184
+
185
+ beforeEach(async () => {
186
+ db = new InMemory();
187
+ master = new Master(game, db, TransportAPI(send, sendAll));
188
+ await master.onSync('matchID', '0', undefined, 2);
189
+ jest.clearAllMocks();
190
+ });
191
+
192
+ test('basic', async () => {
193
+ await master.onUpdate(action, 0, 'matchID', '0');
194
+ expect(sendAll).toBeCalled();
195
+ const value = sendAll.mock.calls[0][0];
196
+ expect(value.type).toBe('update');
197
+ expect(value.args[0]).toBe('matchID');
198
+ expect(value.args[1]).toMatchObject({
199
+ G: {},
200
+ _stateID: 1,
201
+ ctx: {
202
+ currentPlayer: '1',
203
+ numPlayers: 2,
204
+ phase: null,
205
+ playOrder: ['0', '1'],
206
+ playOrderPos: 1,
207
+ turn: 2,
208
+ },
209
+ });
210
+ });
211
+
212
+ test('missing action', async () => {
213
+ const { error } = await master.onUpdate(null, 0, 'matchID', '0');
214
+ expect(sendAll).not.toHaveBeenCalled();
215
+ expect(error).toBe('missing action or action payload');
216
+ });
217
+
218
+ test('missing action payload', async () => {
219
+ const { error } = await master.onUpdate({}, 0, 'matchID', '0');
220
+ expect(sendAll).not.toHaveBeenCalled();
221
+ expect(error).toBe('missing action or action payload');
222
+ });
223
+
224
+ test('invalid matchID', async () => {
225
+ await master.onUpdate(action, 0, 'default:unknown', '1');
226
+ expect(sendAll).not.toHaveBeenCalled();
227
+ expect(error).toHaveBeenCalledWith(
228
+ `game not found, matchID=[default:unknown]`
229
+ );
230
+ });
231
+
232
+ test('invalid stateID', async () => {
233
+ await master.onUpdate(action, 100, 'matchID', '0');
234
+ expect(sendAll).not.toHaveBeenCalled();
235
+ expect(error).toHaveBeenCalledWith(
236
+ `invalid stateID, was=[100], expected=[0] - playerID=[0] - action[endTurn]`
237
+ );
238
+ });
239
+
240
+ test('invalid playerID', async () => {
241
+ await master.onUpdate(action, 0, 'matchID', '100');
242
+ await master.onUpdate(ActionCreators.makeMove('move'), 1, 'matchID', '100');
243
+ expect(sendAll).not.toHaveBeenCalled();
244
+ expect(error).toHaveBeenCalledWith(
245
+ `player not active - playerID=[100] - action[move]`
246
+ );
247
+ });
248
+
249
+ test('invalid move', async () => {
250
+ await master.onUpdate(ActionCreators.makeMove('move'), 0, 'matchID', '0');
251
+ expect(sendAll).not.toHaveBeenCalled();
252
+ expect(error).toHaveBeenCalledWith(
253
+ `move not processed - canPlayerMakeMove=false - playerID=[0] - action[move]`
254
+ );
255
+ });
256
+
257
+ test('valid matchID / stateID / playerID', async () => {
258
+ await master.onUpdate(action, 0, 'matchID', '0');
259
+ expect(sendAll).toHaveBeenCalled();
260
+ });
261
+
262
+ test('allow execution of moves with ignoreStaleStateID truthy', async () => {
263
+ const game: Game = {
264
+ setup: () => {
265
+ const G = {
266
+ players: {
267
+ '0': {
268
+ cards: ['card3'],
269
+ },
270
+ '1': {
271
+ cards: [],
272
+ },
273
+ },
274
+ cards: ['card0', 'card1', 'card2'],
275
+ discardedCards: [],
276
+ };
277
+ return G;
278
+ },
279
+ playerView: PlayerView.STRIP_SECRETS,
280
+ turn: {
281
+ activePlayers: { currentPlayer: { stage: 'A' } },
282
+ stages: {
283
+ A: {
284
+ moves: {
285
+ A: ({ G, playerID }) => {
286
+ const card = G.players[playerID].cards.shift();
287
+ G.discardedCards.push(card);
288
+ },
289
+ B: {
290
+ move: ({ G, playerID }) => {
291
+ const card = G.cards.pop();
292
+ G.players[playerID].cards.push(card);
293
+ },
294
+ ignoreStaleStateID: true,
295
+ },
296
+ },
297
+ },
298
+ },
299
+ },
300
+ };
301
+
302
+ const send = jest.fn();
303
+ const master = new Master(
304
+ game,
305
+ new InMemory(),
306
+ TransportAPI(send, sendAll)
307
+ );
308
+
309
+ const setActivePlayers = ActionCreators.gameEvent(
310
+ 'setActivePlayers',
311
+ [{ all: 'A' }],
312
+ '0'
313
+ );
314
+ const actionA = ActionCreators.makeMove('A', null, '0');
315
+ const actionB = ActionCreators.makeMove('B', null, '1');
316
+ const actionC = ActionCreators.makeMove('B', null, '0');
317
+
318
+ // test: simultaneous moves
319
+ await master.onSync('matchID', '0', undefined, 2);
320
+ await master.onUpdate(actionA, 0, 'matchID', '0');
321
+ await master.onUpdate(setActivePlayers, 1, 'matchID', '0');
322
+ await Promise.all([
323
+ master.onUpdate(actionB, 2, 'matchID', '1'),
324
+ master.onUpdate(actionC, 2, 'matchID', '0'),
325
+ ]);
326
+ await Promise.all([
327
+ master.onSync('matchID', '0', undefined, 2),
328
+ master.onSync('matchID', '1', undefined, 2),
329
+ ]);
330
+
331
+ const G = sendAll.mock.calls[sendAll.mock.calls.length - 1][0].args[1].G;
332
+
333
+ expect(G).toMatchObject({
334
+ players: {
335
+ '0': {
336
+ cards: ['card1'],
337
+ },
338
+ },
339
+ cards: ['card0'],
340
+ discardedCards: ['card3'],
341
+ });
342
+ });
343
+
344
+ describe('undo / redo', () => {
345
+ test('player 0 can undo', async () => {
346
+ const move = ActionCreators.makeMove('A', null, '0');
347
+ await master.onUpdate(move, 0, 'matchID', '0');
348
+ expect(error).not.toHaveBeenCalled();
349
+ await master.onUpdate(ActionCreators.undo(), 1, 'matchID', '0');
350
+ expect(error).not.toHaveBeenCalled();
351
+
352
+ // Negative case: All moves already undone.
353
+ await master.onUpdate(ActionCreators.undo(), 2, 'matchID', '0');
354
+ expect(error).toHaveBeenCalledWith(`No moves to undo`);
355
+ });
356
+
357
+ test('player 1 can’t undo', async () => {
358
+ await master.onUpdate(ActionCreators.undo(), 2, 'matchID', '1');
359
+ expect(error).toHaveBeenCalledWith(
360
+ `playerID=[1] cannot undo / redo right now`
361
+ );
362
+ });
363
+
364
+ test('player can’t undo with multiple active players', async () => {
365
+ const setActivePlayers = ActionCreators.gameEvent(
366
+ 'setActivePlayers',
367
+ [{ all: 'A' }],
368
+ '0'
369
+ );
370
+ await master.onUpdate(setActivePlayers, 0, 'matchID', '0');
371
+ await master.onUpdate(ActionCreators.undo('0'), 1, 'matchID', '0');
372
+ expect(error).toHaveBeenCalledWith(
373
+ `playerID=[0] cannot undo / redo right now`
374
+ );
375
+ });
376
+
377
+ test('player can undo if they are the only active player', async () => {
378
+ const move = ActionCreators.makeMove('A', null, '0');
379
+ await master.onUpdate(move, 0, 'matchID', '0');
380
+ expect(error).not.toHaveBeenCalled();
381
+ const endStage = ActionCreators.gameEvent('endStage', undefined, '0');
382
+ await master.onUpdate(endStage, 1, 'matchID', '0');
383
+ expect(error).not.toBeCalled();
384
+ await master.onUpdate(ActionCreators.undo(), 2, 'matchID', '0');
385
+ expect(error).not.toBeCalled();
386
+
387
+ // Clean-up active players.
388
+ const endStage2 = ActionCreators.gameEvent('endStage', undefined, '1');
389
+ await master.onUpdate(endStage2, 3, 'matchID', '1');
390
+ });
391
+ });
392
+
393
+ test('game over', async () => {
394
+ let event = ActionCreators.gameEvent('endGame');
395
+ await master.onUpdate(event, 0, 'matchID', '0');
396
+ event = ActionCreators.gameEvent('endTurn');
397
+ await master.onUpdate(event, 1, 'matchID', '0');
398
+ expect(error).toHaveBeenCalledWith(
399
+ `game over - matchID=[matchID] - playerID=[0] - action[endTurn]`
400
+ );
401
+ });
402
+
403
+ test('writes gameover to metadata', async () => {
404
+ const id = 'gameWithMetadata';
405
+ const db = new InMemory();
406
+ const dbMetadata = {
407
+ gameName: 'tic-tac-toe',
408
+ setupData: {},
409
+ players: { '0': { id: 0 }, '1': { id: 1 } },
410
+ createdAt: 0,
411
+ updatedAt: 0,
412
+ };
413
+ db.setMetadata(id, dbMetadata);
414
+ const masterWithMetadata = new Master(game, db, TransportAPI(send));
415
+ await masterWithMetadata.onSync(id, '0', undefined, 2);
416
+
417
+ const gameOverArg = 'gameOverArg';
418
+ const event = ActionCreators.gameEvent('endGame', gameOverArg);
419
+ await masterWithMetadata.onUpdate(event, 0, id, '0');
420
+ const { metadata } = db.fetch(id, { metadata: true });
421
+ expect(metadata.gameover).toEqual(gameOverArg);
422
+ });
423
+
424
+ test('writes gameover to metadata with null gameover', async () => {
425
+ const id = 'gameWithMetadataNullGameover';
426
+ const db = new InMemory();
427
+ const dbMetadata = {
428
+ gameName: 'tic-tac-toe',
429
+ gameover: null,
430
+ setupData: {},
431
+ players: { '0': { id: 0 }, '1': { id: 1 } },
432
+ createdAt: 0,
433
+ updatedAt: 0,
434
+ };
435
+ const masterWithMetadata = new Master(game, db, TransportAPI(send));
436
+ await masterWithMetadata.onSync(id, '0', undefined, 2);
437
+ db.setMetadata(id, dbMetadata);
438
+
439
+ const gameOverArg = 'gameOverArg';
440
+ const event = ActionCreators.gameEvent('endGame', gameOverArg);
441
+ await masterWithMetadata.onUpdate(event, 0, id, '0');
442
+ const { metadata } = db.fetch(id, { metadata: true });
443
+ expect(metadata.gameover).toEqual(gameOverArg);
444
+ });
445
+
446
+ test('writes gameover to metadata with async storage API', async () => {
447
+ const id = 'gameWithMetadata';
448
+ const db = new InMemoryAsync();
449
+ const dbMetadata = {
450
+ gameName: 'tic-tac-toe',
451
+ setupData: {},
452
+ players: { '0': { id: 0 }, '1': { id: 1 } },
453
+ createdAt: 0,
454
+ updatedAt: 0,
455
+ };
456
+ await db.setMetadata(id, dbMetadata);
457
+ const masterWithMetadata = new Master(game, db, TransportAPI(send));
458
+ await masterWithMetadata.onSync(id, '0', undefined, 2);
459
+
460
+ const gameOverArg = 'gameOverArg';
461
+ const event = ActionCreators.gameEvent('endGame', gameOverArg);
462
+ await masterWithMetadata.onUpdate(event, 0, id, '0');
463
+ const { metadata } = await db.fetch(id, { metadata: true });
464
+ expect(metadata.gameover).toEqual(gameOverArg);
465
+ });
466
+
467
+ test('writes updatedAt to metadata with async storage API', async () => {
468
+ const id = 'gameWithMetadata';
469
+ const db = new InMemoryAsync();
470
+ const dbMetadata = {
471
+ gameName: 'tic-tac-toe',
472
+ setupData: {},
473
+ players: { '0': { id: 0 }, '1': { id: 1 } },
474
+ createdAt: 0,
475
+ updatedAt: 0,
476
+ };
477
+ await db.setMetadata(id, dbMetadata);
478
+ const masterWithMetadata = new Master(game, db, TransportAPI(send));
479
+ await masterWithMetadata.onSync(id, '0', undefined, 2);
480
+
481
+ const updatedAt = new Date(2020, 3, 4, 5, 6, 7);
482
+ dateMock.advanceTo(updatedAt);
483
+ const event = ActionCreators.gameEvent('endTurn', null, '0');
484
+ await masterWithMetadata.onUpdate(event, 0, id, '0');
485
+ const { metadata } = await db.fetch(id, { metadata: true });
486
+ expect(metadata.updatedAt).toEqual(updatedAt.getTime());
487
+ });
488
+
489
+ test('processes update if there is no metadata', async () => {
490
+ const id = 'gameWithoutMetadata';
491
+ const db = new InMemory();
492
+ const masterWithoutMetadata = new Master(game, db, TransportAPI(send));
493
+ // Store state manually to bypass automatic metadata initialization on sync.
494
+ let state = InitializeGame({ game });
495
+ expect(state.ctx.turn).toBe(1);
496
+ db.setState(id, state);
497
+ // Dispatch update to end the turn.
498
+ const event = ActionCreators.gameEvent('endTurn', null, '0');
499
+ await masterWithoutMetadata.onUpdate(event, 0, id, '0');
500
+ // Confirm the turn ended.
501
+ let metadata: undefined | Server.MatchData;
502
+ ({ state, metadata } = db.fetch(id, { state: true, metadata: true }));
503
+ expect(state.ctx.turn).toBe(2);
504
+ expect(metadata).toBeUndefined();
505
+ });
506
+
507
+ test('processes update if there is no metadata with async DB', async () => {
508
+ const id = 'gameWithoutMetadata';
509
+ const db = new InMemoryAsync();
510
+ const masterWithoutMetadata = new Master(game, db, TransportAPI(send));
511
+ // Store state manually to bypass automatic metadata initialization on sync.
512
+ let state = InitializeGame({ game });
513
+ expect(state.ctx.turn).toBe(1);
514
+ await db.setState(id, state);
515
+ // Dispatch update to end the turn.
516
+ const event = ActionCreators.gameEvent('endTurn', null, '0');
517
+ await masterWithoutMetadata.onUpdate(event, 0, id, '0');
518
+ // Confirm the turn ended.
519
+ let metadata: undefined | Server.MatchData;
520
+ ({ state, metadata } = await db.fetch(id, { state: true, metadata: true }));
521
+ expect(state.ctx.turn).toBe(2);
522
+ expect(metadata).toBeUndefined();
523
+ });
524
+ });
525
+
526
+ describe('patch', () => {
527
+ const send = jest.fn();
528
+ const sendAll = jest.fn();
529
+ const db = new InMemory();
530
+ const master = new Master(
531
+ {
532
+ seed: 0,
533
+ deltaState: true,
534
+ setup: () => {
535
+ return {
536
+ players: {
537
+ '0': {
538
+ cards: ['card3'],
539
+ },
540
+ '1': {
541
+ cards: [],
542
+ },
543
+ },
544
+ cards: ['card0', 'card1', 'card2'],
545
+ discardedCards: [],
546
+ };
547
+ },
548
+ playerView: PlayerView.STRIP_SECRETS,
549
+ turn: {
550
+ activePlayers: { currentPlayer: { stage: 'A' } },
551
+ stages: {
552
+ A: {
553
+ moves: {
554
+ Invalid: () => {
555
+ return INVALID_MOVE;
556
+ },
557
+ A: {
558
+ client: false,
559
+ move: ({ G, playerID }) => {
560
+ const card = G.players[playerID].cards.shift();
561
+ G.discardedCards.push(card);
562
+ },
563
+ },
564
+ B: {
565
+ client: false,
566
+ ignoreStaleStateID: true,
567
+ move: ({ G, playerID }) => {
568
+ const card = G.cards.pop();
569
+ G.players[playerID].cards.push(card);
570
+ },
571
+ },
572
+ },
573
+ },
574
+ },
575
+ },
576
+ },
577
+ db,
578
+ TransportAPI(send, sendAll)
579
+ );
580
+ const move = ActionCreators.makeMove('A', null, '0');
581
+ const action = ActionCreators.gameEvent('endTurn');
582
+
583
+ beforeAll(async () => {
584
+ master.subscribe(({ state }) => {
585
+ validateNotTransientState(state);
586
+ });
587
+ await master.onSync('matchID', '0', undefined, 2);
588
+ });
589
+
590
+ beforeEach(() => {
591
+ jest.clearAllMocks();
592
+ });
593
+
594
+ test('basic', async () => {
595
+ await master.onUpdate(move, 0, 'matchID', '0');
596
+ expect(sendAll).toBeCalled();
597
+
598
+ const value = sendAll.mock.calls[0][0];
599
+ expect(value.type).toBe('patch');
600
+ expect(value.args[0]).toBe('matchID');
601
+ expect(value.args[1]).toBe(0);
602
+ // prevState -- had a card
603
+ expect(value.args[2].G.players[0].cards).toEqual(['card3']);
604
+ // state -- doesnt have a card anymore
605
+ expect(value.args[3].G.players[0].cards).toEqual([]);
606
+ });
607
+
608
+ test('invalid matchID', async () => {
609
+ await master.onUpdate(action, 1, 'default:unknown', '1');
610
+ expect(sendAll).not.toHaveBeenCalled();
611
+ expect(error).toHaveBeenCalledWith(
612
+ `game not found, matchID=[default:unknown]`
613
+ );
614
+ });
615
+
616
+ test('invalid stateID', async () => {
617
+ await master.onUpdate(action, 100, 'matchID', '0');
618
+ expect(sendAll).not.toHaveBeenCalled();
619
+ expect(error).toHaveBeenCalledWith(
620
+ `invalid stateID, was=[100], expected=[1] - playerID=[0] - action[endTurn]`
621
+ );
622
+ });
623
+
624
+ test('invalid playerID', async () => {
625
+ await master.onUpdate(action, 1, 'matchID', '102');
626
+ await master.onUpdate(ActionCreators.makeMove('move'), 1, 'matchID', '102');
627
+ expect(sendAll).not.toHaveBeenCalled();
628
+ expect(error).toHaveBeenCalledWith(
629
+ `player not active - playerID=[102] - action[move]`
630
+ );
631
+ });
632
+
633
+ test('disallowed move', async () => {
634
+ await master.onUpdate(ActionCreators.makeMove('move'), 1, 'matchID', '0');
635
+ expect(sendAll).not.toHaveBeenCalled();
636
+ expect(error).toHaveBeenCalledWith(
637
+ `move not processed - canPlayerMakeMove=false - playerID=[0] - action[move]`
638
+ );
639
+ });
640
+
641
+ test('invalid move', async () => {
642
+ await master.onUpdate(
643
+ ActionCreators.makeMove('Invalid', null, '0'),
644
+ 1,
645
+ 'matchID',
646
+ '0'
647
+ );
648
+ expect(sendAll).toHaveBeenCalled();
649
+ expect(error).toHaveBeenCalledWith('invalid move: Invalid args: null');
650
+ });
651
+
652
+ test('valid matchID / stateID / playerID', async () => {
653
+ await master.onUpdate(action, 1, 'matchID', '0');
654
+ expect(sendAll).toHaveBeenCalled();
655
+ });
656
+
657
+ describe('undo / redo', () => {
658
+ test('player 0 can undo', async () => {
659
+ await master.onUpdate(ActionCreators.undo(), 2, 'matchID', '1');
660
+ // The master allows this, but the reducer does not.
661
+ expect(error).toHaveBeenCalledWith(`No moves to undo`);
662
+ });
663
+
664
+ test('player 1 can’t undo', async () => {
665
+ await master.onUpdate(ActionCreators.undo(), 2, 'matchID', '0');
666
+ expect(error).toHaveBeenCalledWith(
667
+ `playerID=[0] cannot undo / redo right now`
668
+ );
669
+ });
670
+
671
+ test('player can’t undo with multiple active players', async () => {
672
+ const setActivePlayers = ActionCreators.gameEvent(
673
+ 'setActivePlayers',
674
+ [{ all: 'A' }],
675
+ '0'
676
+ );
677
+ await master.onUpdate(setActivePlayers, 2, 'matchID', '0');
678
+ await master.onUpdate(ActionCreators.undo('0'), 3, 'matchID', '0');
679
+ expect(error).toHaveBeenCalledWith(
680
+ `playerID=[0] cannot undo / redo right now`
681
+ );
682
+ });
683
+
684
+ test('player can undo if they are the only active player', async () => {
685
+ const endStage = ActionCreators.gameEvent('endStage', undefined, '1');
686
+ await master.onUpdate(endStage, 2, 'matchID', '1');
687
+ await master.onUpdate(ActionCreators.undo('0'), 3, 'matchID', '1');
688
+ // The master allows this, but the reducer does not.
689
+ expect(error).toHaveBeenCalledWith(`Cannot undo other players' moves`);
690
+
691
+ // Clean-up active players.
692
+ const endStage2 = ActionCreators.gameEvent('endStage', undefined, '1');
693
+ await master.onUpdate(endStage2, 4, 'matchID', '1');
694
+ });
695
+ });
696
+
697
+ test('game over', async () => {
698
+ let event = ActionCreators.gameEvent('endGame');
699
+ await master.onUpdate(event, 3, 'matchID', '1');
700
+ event = ActionCreators.gameEvent('endTurn');
701
+ await master.onUpdate(event, 3, 'matchID', '1');
702
+ expect(error).toHaveBeenCalledWith(
703
+ `game over - matchID=[matchID] - playerID=[1] - action[endTurn]`
704
+ );
705
+ });
706
+ });
707
+
708
+ describe('connectionChange', () => {
709
+ const send = jest.fn();
710
+ const sendAll = jest.fn();
711
+
712
+ const db = new InMemory();
713
+ const master = new Master(game, db, TransportAPI(send, sendAll));
714
+
715
+ const metadata = {
716
+ gameName: 'tic-tac-toe',
717
+ setupData: {},
718
+ players: {
719
+ '0': {
720
+ id: 0,
721
+ credentials: 'qS2m4Ujb_',
722
+ name: 'Alice',
723
+ },
724
+ '1': {
725
+ id: 1,
726
+ credentials: 'nIQtXFybDD',
727
+ name: 'Bob',
728
+ isConnected: true,
729
+ },
730
+ },
731
+ createdAt: 0,
732
+ updatedAt: 0,
733
+ };
734
+ db.createMatch('matchID', { metadata, initialState: {} as State });
735
+
736
+ beforeEach(() => {
737
+ master.subscribe(({ state }) => {
738
+ validateNotTransientState(state);
739
+ });
740
+ jest.clearAllMocks();
741
+ });
742
+
743
+ test('changes players metadata', async () => {
744
+ await master.onConnectionChange('matchID', '0', undefined, true);
745
+
746
+ const expectedPlayerData = { id: 0, name: 'Alice', isConnected: true };
747
+ const {
748
+ metadata: { players },
749
+ } = db.fetch('matchID', { metadata: true });
750
+ expect(players['0']).toMatchObject(expectedPlayerData);
751
+ });
752
+
753
+ test('sends metadata to all', async () => {
754
+ await master.onConnectionChange('matchID', '1', undefined, false);
755
+ const expectedMetadata = [
756
+ { id: 0, name: 'Alice', isConnected: true },
757
+ { id: 1, name: 'Bob', isConnected: false },
758
+ ];
759
+ const sentMessage = sendAll.mock.calls[0][0];
760
+ expect(sentMessage.type).toEqual('matchData');
761
+ expect(sentMessage.args[1]).toMatchObject(expectedMetadata);
762
+ });
763
+
764
+ test('invalid matchID', async () => {
765
+ const result = await master.onConnectionChange(
766
+ 'invalidMatchID',
767
+ '0',
768
+ undefined,
769
+ true
770
+ );
771
+ expect(error).toHaveBeenCalledWith(
772
+ 'metadata not found for matchID=[invalidMatchID]'
773
+ );
774
+ expect(result && result.error).toEqual('metadata not found');
775
+ });
776
+
777
+ test('invalid playerID', async () => {
778
+ const result = await master.onConnectionChange(
779
+ 'matchID',
780
+ '3',
781
+ undefined,
782
+ true
783
+ );
784
+ expect(error).toHaveBeenCalledWith(
785
+ 'Player not in the match, matchID=[matchID] playerID=[3]'
786
+ );
787
+ expect(result && result.error).toEqual('player not in the match');
788
+ });
789
+
790
+ test('processes connection change with an async db', async () => {
791
+ const asyncDb = new InMemoryAsync();
792
+ const masterWithAsyncDb = new Master(
793
+ game,
794
+ asyncDb,
795
+ TransportAPI(send, sendAll)
796
+ );
797
+ await asyncDb.createMatch('matchID', {
798
+ metadata,
799
+ initialState: {} as State,
800
+ });
801
+
802
+ await masterWithAsyncDb.onConnectionChange('matchID', '0', undefined, true);
803
+
804
+ expect(sendAll).toHaveBeenCalled();
805
+ });
806
+ });
807
+
808
+ describe('subscribe', () => {
809
+ const callback = jest.fn();
810
+
811
+ let master;
812
+ beforeAll(() => {
813
+ master = new Master({}, new InMemory(), TransportAPI(jest.fn(), jest.fn()));
814
+ master.subscribe(callback);
815
+ });
816
+
817
+ test('sync', async () => {
818
+ master.onSync('matchID', '0');
819
+ expect(callback).toBeCalledWith({
820
+ matchID: 'matchID',
821
+ state: expect.objectContaining({ _stateID: 0 }),
822
+ });
823
+ });
824
+
825
+ test('update', async () => {
826
+ const action = ActionCreators.gameEvent('endTurn');
827
+ master.onUpdate(action, 0, 'matchID', '0');
828
+ expect(callback).toBeCalledWith({
829
+ matchID: 'matchID',
830
+ action,
831
+ state: expect.objectContaining({ _stateID: 1 }),
832
+ });
833
+ });
834
+ });
835
+
836
+ describe('authentication', () => {
837
+ const send = jest.fn();
838
+ const sendAll = jest.fn();
839
+ const game = { seed: 0 };
840
+ const matchID = 'matchID';
841
+ let storage = new InMemoryAsync();
842
+
843
+ const resetTestEnvironment = async () => {
844
+ send.mockReset();
845
+ sendAll.mockReset();
846
+ storage = new InMemoryAsync();
847
+ const master = new Master(game, storage, TransportAPI());
848
+ await master.onSync(matchID, '0', undefined, 2);
849
+ };
850
+
851
+ describe('onUpdate', () => {
852
+ const action = ActionCreators.gameEvent('endTurn');
853
+
854
+ beforeEach(resetTestEnvironment);
855
+
856
+ test('auth failure', async () => {
857
+ const authenticateCredentials = () => false;
858
+ const master = new Master(
859
+ game,
860
+ storage,
861
+ TransportAPI(send, sendAll),
862
+ new Auth({ authenticateCredentials })
863
+ );
864
+ const ret = await master.onUpdate(action, 0, matchID, '0');
865
+ expect(ret && ret.error).toBe('unauthorized action');
866
+ expect(sendAll).not.toHaveBeenCalled();
867
+ });
868
+
869
+ test('auth success', async () => {
870
+ const authenticateCredentials = () => true;
871
+ const master = new Master(
872
+ game,
873
+ storage,
874
+ TransportAPI(send, sendAll),
875
+ new Auth({ authenticateCredentials })
876
+ );
877
+ const ret = await master.onUpdate(action, 0, matchID, '0');
878
+ expect(ret).toBeUndefined();
879
+ expect(sendAll).toHaveBeenCalled();
880
+ });
881
+
882
+ test('default', async () => {
883
+ const master = new Master(
884
+ game,
885
+ storage,
886
+ TransportAPI(send, sendAll),
887
+ new Auth()
888
+ );
889
+ const ret = await master.onUpdate(action, 0, matchID, '0');
890
+ expect(ret).toBeUndefined();
891
+ expect(sendAll).toHaveBeenCalled();
892
+ });
893
+ });
894
+
895
+ describe('onSync', () => {
896
+ beforeEach(resetTestEnvironment);
897
+
898
+ test('auth failure', async () => {
899
+ const authenticateCredentials = () => false;
900
+ const master = new Master(
901
+ game,
902
+ storage,
903
+ TransportAPI(send, sendAll),
904
+ new Auth({ authenticateCredentials })
905
+ );
906
+ const ret = await master.onSync(matchID, '0');
907
+ expect(ret && ret.error).toBe('unauthorized');
908
+ expect(send).not.toHaveBeenCalled();
909
+ });
910
+
911
+ test('auth success', async () => {
912
+ const authenticateCredentials = () => true;
913
+ const master = new Master(
914
+ game,
915
+ storage,
916
+ TransportAPI(send, sendAll),
917
+ new Auth({ authenticateCredentials })
918
+ );
919
+ const ret = await master.onSync(matchID, '0');
920
+ expect(ret).toBeUndefined();
921
+ expect(send).toHaveBeenCalled();
922
+ });
923
+
924
+ test('spectators don’t need to authenticate', async () => {
925
+ const authenticateCredentials = () => false;
926
+ const master = new Master(
927
+ game,
928
+ storage,
929
+ TransportAPI(send, sendAll),
930
+ new Auth({ authenticateCredentials })
931
+ );
932
+ const ret = await master.onSync(matchID, null);
933
+ expect(ret).toBeUndefined();
934
+ expect(send).toHaveBeenCalled();
935
+ });
936
+ });
937
+
938
+ describe('onConnectionChange', () => {
939
+ beforeEach(resetTestEnvironment);
940
+
941
+ test('auth failure', async () => {
942
+ const authenticateCredentials = () => false;
943
+ const master = new Master(
944
+ game,
945
+ storage,
946
+ TransportAPI(send, sendAll),
947
+ new Auth({ authenticateCredentials })
948
+ );
949
+ const ret = await master.onConnectionChange(matchID, '0', null, true);
950
+ expect(ret && ret.error).toBe('unauthorized');
951
+ expect(sendAll).not.toHaveBeenCalled();
952
+ });
953
+
954
+ test('auth success', async () => {
955
+ const authenticateCredentials = () => true;
956
+ const master = new Master(
957
+ game,
958
+ storage,
959
+ TransportAPI(send, sendAll),
960
+ new Auth({ authenticateCredentials })
961
+ );
962
+ const ret = await master.onConnectionChange(matchID, '0', null, true);
963
+ expect(ret).toBeUndefined();
964
+ expect(sendAll).toHaveBeenCalled();
965
+ });
966
+
967
+ test('spectators are ignored', async () => {
968
+ const authenticateCredentials = jest.fn();
969
+ const master = new Master(
970
+ game,
971
+ storage,
972
+ TransportAPI(send, sendAll),
973
+ new Auth({ authenticateCredentials })
974
+ );
975
+ const ret = await master.onConnectionChange(matchID, null, null, true);
976
+ expect(ret).toBeUndefined();
977
+ expect(authenticateCredentials).not.toHaveBeenCalled();
978
+ expect(sendAll).not.toHaveBeenCalled();
979
+ });
980
+ });
981
+
982
+ describe('onChatMessage', () => {
983
+ const chatMessage = {
984
+ id: 'uuid',
985
+ payload: { message: 'foo' },
986
+ sender: '0',
987
+ };
988
+
989
+ beforeEach(resetTestEnvironment);
990
+
991
+ test('auth success', async () => {
992
+ const authenticateCredentials = () => true;
993
+ const master = new Master(
994
+ game,
995
+ storage,
996
+ TransportAPI(send, sendAll),
997
+ new Auth({ authenticateCredentials })
998
+ );
999
+ const ret = await master.onChatMessage(matchID, chatMessage, undefined);
1000
+ expect(ret).toBeUndefined();
1001
+ expect(sendAll).toHaveBeenCalled();
1002
+ });
1003
+
1004
+ test('auth failure', async () => {
1005
+ const authenticateCredentials = () => false;
1006
+ const master = new Master(
1007
+ game,
1008
+ storage,
1009
+ TransportAPI(send, sendAll),
1010
+ new Auth({ authenticateCredentials })
1011
+ );
1012
+ const ret = await master.onChatMessage(matchID, chatMessage, undefined);
1013
+ expect(ret && ret.error).toBe('unauthorized');
1014
+ expect(sendAll).not.toHaveBeenCalled();
1015
+ });
1016
+
1017
+ test('invalid packet', async () => {
1018
+ const authenticateCredentials = () => true;
1019
+ const master = new Master(
1020
+ game,
1021
+ storage,
1022
+ TransportAPI(send, sendAll),
1023
+ new Auth({ authenticateCredentials })
1024
+ );
1025
+ const ret = await master.onChatMessage(matchID, undefined, undefined);
1026
+ expect(ret && ret.error).toBe('unauthorized');
1027
+ expect(sendAll).not.toHaveBeenCalled();
1028
+ });
1029
+
1030
+ test('default', async () => {
1031
+ const master = new Master(
1032
+ game,
1033
+ storage,
1034
+ TransportAPI(send, sendAll),
1035
+ new Auth()
1036
+ );
1037
+ const ret = await master.onChatMessage(matchID, chatMessage, undefined);
1038
+ expect(ret).toBeUndefined();
1039
+ expect(sendAll).toHaveBeenCalled();
1040
+ });
1041
+ });
1042
+ });
1043
+
1044
+ describe('chat', () => {
1045
+ const send = jest.fn();
1046
+ const sendAll = jest.fn();
1047
+ const db = new InMemory();
1048
+ const master = new Master(game, db, TransportAPI(send, sendAll));
1049
+
1050
+ beforeEach(() => {
1051
+ jest.clearAllMocks();
1052
+ });
1053
+
1054
+ test('Sends chat messages to all', async () => {
1055
+ master.onChatMessage(
1056
+ 'matchID',
1057
+ { id: 'uuid', sender: '0', payload: { message: 'foo' } },
1058
+ undefined
1059
+ );
1060
+ expect(sendAll.mock.calls[0][0]).toEqual({
1061
+ type: 'chat',
1062
+ args: [
1063
+ 'matchID',
1064
+ { id: 'uuid', sender: '0', payload: { message: 'foo' } },
1065
+ ],
1066
+ });
1067
+ });
1068
+ });