@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,4087 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var Koa = require('koa');
6
+ var Router = require('@koa/router');
7
+ var koaBody = require('koa-body');
8
+ var nanoid = require('nanoid');
9
+ var cors = require('@koa/cors');
10
+ var produce = require('immer');
11
+ var isPlainObject = require('lodash.isplainobject');
12
+ var IO = require('koa-socket-2');
13
+ var PQueue = require('p-queue');
14
+ var rfc6902 = require('rfc6902');
15
+ var redux = require('redux');
16
+
17
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
18
+
19
+ var Koa__default = /*#__PURE__*/_interopDefaultLegacy(Koa);
20
+ var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
21
+ var koaBody__default = /*#__PURE__*/_interopDefaultLegacy(koaBody);
22
+ var cors__default = /*#__PURE__*/_interopDefaultLegacy(cors);
23
+ var produce__default = /*#__PURE__*/_interopDefaultLegacy(produce);
24
+ var isPlainObject__default = /*#__PURE__*/_interopDefaultLegacy(isPlainObject);
25
+ var IO__default = /*#__PURE__*/_interopDefaultLegacy(IO);
26
+ var PQueue__default = /*#__PURE__*/_interopDefaultLegacy(PQueue);
27
+
28
+ /**
29
+ * Moves can return this when they want to indicate
30
+ * that the combination of arguments is illegal and
31
+ * the move ought to be discarded.
32
+ */
33
+ const INVALID_MOVE = 'INVALID_MOVE';
34
+
35
+ /*
36
+ * Copyright 2018 The boardgame.io Authors
37
+ *
38
+ * Use of this source code is governed by a MIT-style
39
+ * license that can be found in the LICENSE file or at
40
+ * https://opensource.org/licenses/MIT.
41
+ */
42
+ /**
43
+ * Plugin that allows using Immer to make immutable changes
44
+ * to G by just mutating it.
45
+ */
46
+ const ImmerPlugin = {
47
+ name: 'plugin-immer',
48
+ fnWrap: (move) => (context, ...args) => {
49
+ let isInvalid = false;
50
+ const newG = produce__default["default"](context.G, (G) => {
51
+ const result = move({ ...context, G }, ...args);
52
+ if (result === INVALID_MOVE) {
53
+ isInvalid = true;
54
+ return;
55
+ }
56
+ return result;
57
+ });
58
+ if (isInvalid)
59
+ return INVALID_MOVE;
60
+ return newG;
61
+ },
62
+ };
63
+
64
+ // Inlined version of Alea from https://github.com/davidbau/seedrandom.
65
+ // Converted to Typescript October 2020.
66
+ class Alea {
67
+ constructor(seed) {
68
+ const mash = Mash();
69
+ // Apply the seeding algorithm from Baagoe.
70
+ this.c = 1;
71
+ this.s0 = mash(' ');
72
+ this.s1 = mash(' ');
73
+ this.s2 = mash(' ');
74
+ this.s0 -= mash(seed);
75
+ if (this.s0 < 0) {
76
+ this.s0 += 1;
77
+ }
78
+ this.s1 -= mash(seed);
79
+ if (this.s1 < 0) {
80
+ this.s1 += 1;
81
+ }
82
+ this.s2 -= mash(seed);
83
+ if (this.s2 < 0) {
84
+ this.s2 += 1;
85
+ }
86
+ }
87
+ next() {
88
+ const t = 2091639 * this.s0 + this.c * 2.3283064365386963e-10; // 2^-32
89
+ this.s0 = this.s1;
90
+ this.s1 = this.s2;
91
+ return (this.s2 = t - (this.c = Math.trunc(t)));
92
+ }
93
+ }
94
+ function Mash() {
95
+ let n = 0xefc8249d;
96
+ const mash = function (data) {
97
+ const str = data.toString();
98
+ for (let i = 0; i < str.length; i++) {
99
+ n += str.charCodeAt(i);
100
+ let h = 0.02519603282416938 * n;
101
+ n = h >>> 0;
102
+ h -= n;
103
+ h *= n;
104
+ n = h >>> 0;
105
+ h -= n;
106
+ n += h * 0x100000000; // 2^32
107
+ }
108
+ return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
109
+ };
110
+ return mash;
111
+ }
112
+ function copy(f, t) {
113
+ t.c = f.c;
114
+ t.s0 = f.s0;
115
+ t.s1 = f.s1;
116
+ t.s2 = f.s2;
117
+ return t;
118
+ }
119
+ function alea(seed, state) {
120
+ const xg = new Alea(seed);
121
+ const prng = xg.next.bind(xg);
122
+ if (state)
123
+ copy(state, xg);
124
+ prng.state = () => copy(xg, {});
125
+ return prng;
126
+ }
127
+
128
+ /*
129
+ * Copyright 2017 The boardgame.io Authors
130
+ *
131
+ * Use of this source code is governed by a MIT-style
132
+ * license that can be found in the LICENSE file or at
133
+ * https://opensource.org/licenses/MIT.
134
+ */
135
+ /**
136
+ * Random
137
+ *
138
+ * Calls that require a pseudorandom number generator.
139
+ * Uses a seed from ctx, and also persists the PRNG
140
+ * state in ctx so that moves can stay pure.
141
+ */
142
+ class Random {
143
+ /**
144
+ * constructor
145
+ * @param {object} ctx - The ctx object to initialize from.
146
+ */
147
+ constructor(state) {
148
+ // If we are on the client, the seed is not present.
149
+ // Just use a temporary seed to execute the move without
150
+ // crashing it. The move state itself is discarded,
151
+ // so the actual value doesn't matter.
152
+ this.state = state || { seed: '0' };
153
+ this.used = false;
154
+ }
155
+ /**
156
+ * Generates a new seed from the current date / time.
157
+ */
158
+ static seed() {
159
+ return Date.now().toString(36).slice(-10);
160
+ }
161
+ isUsed() {
162
+ return this.used;
163
+ }
164
+ getState() {
165
+ return this.state;
166
+ }
167
+ /**
168
+ * Generate a random number.
169
+ */
170
+ _random() {
171
+ this.used = true;
172
+ const R = this.state;
173
+ const seed = R.prngstate ? '' : R.seed;
174
+ const rand = alea(seed, R.prngstate);
175
+ const number = rand();
176
+ this.state = {
177
+ ...R,
178
+ prngstate: rand.state(),
179
+ };
180
+ return number;
181
+ }
182
+ api() {
183
+ const random = this._random.bind(this);
184
+ const SpotValue = {
185
+ D4: 4,
186
+ D6: 6,
187
+ D8: 8,
188
+ D10: 10,
189
+ D12: 12,
190
+ D20: 20,
191
+ };
192
+ // Generate functions for predefined dice values D4 - D20.
193
+ const predefined = {};
194
+ for (const key in SpotValue) {
195
+ const spotvalue = SpotValue[key];
196
+ predefined[key] = (diceCount) => {
197
+ return diceCount === undefined
198
+ ? Math.floor(random() * spotvalue) + 1
199
+ : Array.from({ length: diceCount }).map(() => Math.floor(random() * spotvalue) + 1);
200
+ };
201
+ }
202
+ function Die(spotvalue = 6, diceCount) {
203
+ return diceCount === undefined
204
+ ? Math.floor(random() * spotvalue) + 1
205
+ : Array.from({ length: diceCount }).map(() => Math.floor(random() * spotvalue) + 1);
206
+ }
207
+ return {
208
+ /**
209
+ * Similar to Die below, but with fixed spot values.
210
+ * Supports passing a diceCount
211
+ * if not defined, defaults to 1 and returns the value directly.
212
+ * if defined, returns an array containing the random dice values.
213
+ *
214
+ * D4: (diceCount) => value
215
+ * D6: (diceCount) => value
216
+ * D8: (diceCount) => value
217
+ * D10: (diceCount) => value
218
+ * D12: (diceCount) => value
219
+ * D20: (diceCount) => value
220
+ */
221
+ ...predefined,
222
+ /**
223
+ * Roll a die of specified spot value.
224
+ *
225
+ * @param {number} spotvalue - The die dimension (default: 6).
226
+ * @param {number} diceCount - number of dice to throw.
227
+ * if not defined, defaults to 1 and returns the value directly.
228
+ * if defined, returns an array containing the random dice values.
229
+ */
230
+ Die,
231
+ /**
232
+ * Generate a random number between 0 and 1.
233
+ */
234
+ Number: () => {
235
+ return random();
236
+ },
237
+ /**
238
+ * Shuffle an array.
239
+ *
240
+ * @param {Array} deck - The array to shuffle. Does not mutate
241
+ * the input, but returns the shuffled array.
242
+ */
243
+ Shuffle: (deck) => {
244
+ const clone = [...deck];
245
+ let sourceIndex = deck.length;
246
+ let destinationIndex = 0;
247
+ const shuffled = Array.from({ length: sourceIndex });
248
+ while (sourceIndex) {
249
+ const randomIndex = Math.trunc(sourceIndex * random());
250
+ shuffled[destinationIndex++] = clone[randomIndex];
251
+ clone[randomIndex] = clone[--sourceIndex];
252
+ }
253
+ return shuffled;
254
+ },
255
+ _private: this,
256
+ };
257
+ }
258
+ }
259
+
260
+ /*
261
+ * Copyright 2018 The boardgame.io Authors
262
+ *
263
+ * Use of this source code is governed by a MIT-style
264
+ * license that can be found in the LICENSE file or at
265
+ * https://opensource.org/licenses/MIT.
266
+ */
267
+ const RandomPlugin = {
268
+ name: 'random',
269
+ noClient: ({ api }) => {
270
+ return api._private.isUsed();
271
+ },
272
+ flush: ({ api }) => {
273
+ return api._private.getState();
274
+ },
275
+ api: ({ data }) => {
276
+ const random = new Random(data);
277
+ return random.api();
278
+ },
279
+ setup: ({ game }) => {
280
+ let { seed } = game;
281
+ if (seed === undefined) {
282
+ seed = Random.seed();
283
+ }
284
+ return { seed };
285
+ },
286
+ playerView: () => undefined,
287
+ };
288
+
289
+ /*
290
+ * Copyright 2017 The boardgame.io Authors
291
+ *
292
+ * Use of this source code is governed by a MIT-style
293
+ * license that can be found in the LICENSE file or at
294
+ * https://opensource.org/licenses/MIT.
295
+ */
296
+ const MAKE_MOVE = 'MAKE_MOVE';
297
+ const GAME_EVENT = 'GAME_EVENT';
298
+ const REDO = 'REDO';
299
+ const RESET = 'RESET';
300
+ const SYNC = 'SYNC';
301
+ const UNDO = 'UNDO';
302
+ const UPDATE = 'UPDATE';
303
+ const PATCH = 'PATCH';
304
+ const PLUGIN = 'PLUGIN';
305
+ const STRIP_TRANSIENTS = 'STRIP_TRANSIENTS';
306
+
307
+ /*
308
+ * Copyright 2017 The boardgame.io Authors
309
+ *
310
+ * Use of this source code is governed by a MIT-style
311
+ * license that can be found in the LICENSE file or at
312
+ * https://opensource.org/licenses/MIT.
313
+ */
314
+ /**
315
+ * Generate a game event to be dispatched to the flow reducer.
316
+ *
317
+ * @param {string} type - The event type.
318
+ * @param {Array} args - Additional arguments.
319
+ * @param {string} playerID - The ID of the player making this action.
320
+ * @param {string} credentials - (optional) The credentials for the player making this action.
321
+ */
322
+ const gameEvent = (type, args, playerID, credentials) => ({
323
+ type: GAME_EVENT,
324
+ payload: { type, args, playerID, credentials },
325
+ });
326
+ /**
327
+ * Generate an automatic game event that is a side-effect of a move.
328
+ * @param {string} type - The event type.
329
+ * @param {Array} args - Additional arguments.
330
+ * @param {string} playerID - The ID of the player making this action.
331
+ * @param {string} credentials - (optional) The credentials for the player making this action.
332
+ */
333
+ const automaticGameEvent = (type, args, playerID, credentials) => ({
334
+ type: GAME_EVENT,
335
+ payload: { type, args, playerID, credentials },
336
+ automatic: true,
337
+ });
338
+ /**
339
+ * Private action used to strip transient metadata (e.g. errors) from the game
340
+ * state.
341
+ */
342
+ const stripTransients = () => ({
343
+ type: STRIP_TRANSIENTS,
344
+ });
345
+
346
+ var GameMethod;
347
+ (function (GameMethod) {
348
+ GameMethod["MOVE"] = "MOVE";
349
+ GameMethod["GAME_ON_END"] = "GAME_ON_END";
350
+ GameMethod["PHASE_ON_BEGIN"] = "PHASE_ON_BEGIN";
351
+ GameMethod["PHASE_ON_END"] = "PHASE_ON_END";
352
+ GameMethod["TURN_ON_BEGIN"] = "TURN_ON_BEGIN";
353
+ GameMethod["TURN_ON_MOVE"] = "TURN_ON_MOVE";
354
+ GameMethod["TURN_ON_END"] = "TURN_ON_END";
355
+ })(GameMethod || (GameMethod = {}));
356
+
357
+ /*
358
+ * Copyright 2018 The boardgame.io Authors
359
+ *
360
+ * Use of this source code is governed by a MIT-style
361
+ * license that can be found in the LICENSE file or at
362
+ * https://opensource.org/licenses/MIT.
363
+ */
364
+ var Errors;
365
+ (function (Errors) {
366
+ Errors["CalledOutsideHook"] = "Events must be called from moves or the `onBegin`, `onEnd`, and `onMove` hooks.\nThis error probably means you called an event from other game code, like an `endIf` trigger or one of the `turn.order` methods.";
367
+ Errors["EndTurnInOnEnd"] = "`endTurn` is disallowed in `onEnd` hooks \u2014 the turn is already ending.";
368
+ Errors["MaxTurnEndings"] = "Maximum number of turn endings exceeded for this update.\nThis likely means game code is triggering an infinite loop.";
369
+ Errors["PhaseEventInOnEnd"] = "`setPhase` & `endPhase` are disallowed in a phase\u2019s `onEnd` hook \u2014 the phase is already ending.\nIf you\u2019re trying to dynamically choose the next phase when a phase ends, use the phase\u2019s `next` trigger.";
370
+ Errors["StageEventInOnEnd"] = "`setStage`, `endStage` & `setActivePlayers` are disallowed in `onEnd` hooks.";
371
+ Errors["StageEventInPhaseBegin"] = "`setStage`, `endStage` & `setActivePlayers` are disallowed in a phase\u2019s `onBegin` hook.\nUse `setActivePlayers` in a `turn.onBegin` hook or declare stages with `turn.activePlayers` instead.";
372
+ Errors["StageEventInTurnBegin"] = "`setStage` & `endStage` are disallowed in `turn.onBegin`.\nUse `setActivePlayers` or declare stages with `turn.activePlayers` instead.";
373
+ })(Errors || (Errors = {}));
374
+ /**
375
+ * Events
376
+ */
377
+ class Events {
378
+ constructor(flow, ctx, playerID) {
379
+ this.flow = flow;
380
+ this.playerID = playerID;
381
+ this.dispatch = [];
382
+ this.initialTurn = ctx.turn;
383
+ this.updateTurnContext(ctx, undefined);
384
+ // This is an arbitrarily large upper threshold, which could be made
385
+ // configurable via a game option if the need arises.
386
+ this.maxEndedTurnsPerAction = ctx.numPlayers * 100;
387
+ }
388
+ api() {
389
+ const events = {
390
+ _private: this,
391
+ };
392
+ for (const type of this.flow.eventNames) {
393
+ events[type] = (...args) => {
394
+ this.dispatch.push({
395
+ type,
396
+ args,
397
+ phase: this.currentPhase,
398
+ turn: this.currentTurn,
399
+ calledFrom: this.currentMethod,
400
+ // Used to capture a stack trace in case it is needed later.
401
+ error: new Error('Events Plugin Error'),
402
+ });
403
+ };
404
+ }
405
+ return events;
406
+ }
407
+ isUsed() {
408
+ return this.dispatch.length > 0;
409
+ }
410
+ updateTurnContext(ctx, methodType) {
411
+ this.currentPhase = ctx.phase;
412
+ this.currentTurn = ctx.turn;
413
+ this.currentMethod = methodType;
414
+ }
415
+ unsetCurrentMethod() {
416
+ this.currentMethod = undefined;
417
+ }
418
+ /**
419
+ * Updates ctx with the triggered events.
420
+ * @param {object} state - The state object { G, ctx }.
421
+ */
422
+ update(state) {
423
+ const initialState = state;
424
+ const stateWithError = ({ stack }, message) => ({
425
+ ...initialState,
426
+ plugins: {
427
+ ...initialState.plugins,
428
+ events: {
429
+ ...initialState.plugins.events,
430
+ data: { error: message + '\n' + stack },
431
+ },
432
+ },
433
+ });
434
+ EventQueue: for (let i = 0; i < this.dispatch.length; i++) {
435
+ const event = this.dispatch[i];
436
+ const turnHasEnded = event.turn !== state.ctx.turn;
437
+ // This protects against potential infinite loops if specific events are called on hooks.
438
+ // The moment we exceed the defined threshold, we just bail out of all phases.
439
+ const endedTurns = this.currentTurn - this.initialTurn;
440
+ if (endedTurns >= this.maxEndedTurnsPerAction) {
441
+ return stateWithError(event.error, Errors.MaxTurnEndings);
442
+ }
443
+ if (event.calledFrom === undefined) {
444
+ return stateWithError(event.error, Errors.CalledOutsideHook);
445
+ }
446
+ // Stop processing events once the game has finished.
447
+ if (state.ctx.gameover)
448
+ break EventQueue;
449
+ switch (event.type) {
450
+ case 'endStage':
451
+ case 'setStage':
452
+ case 'setActivePlayers': {
453
+ switch (event.calledFrom) {
454
+ // Disallow all stage events in onEnd and phase.onBegin hooks.
455
+ case GameMethod.TURN_ON_END:
456
+ case GameMethod.PHASE_ON_END:
457
+ return stateWithError(event.error, Errors.StageEventInOnEnd);
458
+ case GameMethod.PHASE_ON_BEGIN:
459
+ return stateWithError(event.error, Errors.StageEventInPhaseBegin);
460
+ // Disallow setStage & endStage in turn.onBegin hooks.
461
+ case GameMethod.TURN_ON_BEGIN:
462
+ if (event.type === 'setActivePlayers')
463
+ break;
464
+ return stateWithError(event.error, Errors.StageEventInTurnBegin);
465
+ }
466
+ // If the turn already ended, don't try to process stage events.
467
+ if (turnHasEnded)
468
+ continue EventQueue;
469
+ break;
470
+ }
471
+ case 'endTurn': {
472
+ if (event.calledFrom === GameMethod.TURN_ON_END ||
473
+ event.calledFrom === GameMethod.PHASE_ON_END) {
474
+ return stateWithError(event.error, Errors.EndTurnInOnEnd);
475
+ }
476
+ // If the turn already ended some other way,
477
+ // don't try to end the turn again.
478
+ if (turnHasEnded)
479
+ continue EventQueue;
480
+ break;
481
+ }
482
+ case 'endPhase':
483
+ case 'setPhase': {
484
+ if (event.calledFrom === GameMethod.PHASE_ON_END) {
485
+ return stateWithError(event.error, Errors.PhaseEventInOnEnd);
486
+ }
487
+ // If the phase already ended some other way,
488
+ // don't try to end the phase again.
489
+ if (event.phase !== state.ctx.phase)
490
+ continue EventQueue;
491
+ break;
492
+ }
493
+ }
494
+ const action = automaticGameEvent(event.type, event.args, this.playerID);
495
+ state = this.flow.processEvent(state, action);
496
+ }
497
+ return state;
498
+ }
499
+ }
500
+
501
+ /*
502
+ * Copyright 2020 The boardgame.io Authors
503
+ *
504
+ * Use of this source code is governed by a MIT-style
505
+ * license that can be found in the LICENSE file or at
506
+ * https://opensource.org/licenses/MIT.
507
+ */
508
+ const EventsPlugin = {
509
+ name: 'events',
510
+ noClient: ({ api }) => api._private.isUsed(),
511
+ isInvalid: ({ data }) => data.error || false,
512
+ // Update the events plugin’s internal turn context each time a move
513
+ // or hook is called. This allows events called after turn or phase
514
+ // endings to dispatch the current turn and phase correctly.
515
+ fnWrap: (method, methodType) => (context, ...args) => {
516
+ const api = context.events;
517
+ if (api)
518
+ api._private.updateTurnContext(context.ctx, methodType);
519
+ const G = method(context, ...args);
520
+ if (api)
521
+ api._private.unsetCurrentMethod();
522
+ return G;
523
+ },
524
+ dangerouslyFlushRawState: ({ state, api }) => api._private.update(state),
525
+ api: ({ game, ctx, playerID }) => new Events(game.flow, ctx, playerID).api(),
526
+ };
527
+
528
+ /*
529
+ * Copyright 2018 The boardgame.io Authors
530
+ *
531
+ * Use of this source code is governed by a MIT-style
532
+ * license that can be found in the LICENSE file or at
533
+ * https://opensource.org/licenses/MIT.
534
+ */
535
+ /**
536
+ * Plugin that makes it possible to add metadata to log entries.
537
+ * During a move, you can set metadata using ctx.log.setMetadata and it will be
538
+ * available on the log entry for that move.
539
+ */
540
+ const LogPlugin = {
541
+ name: 'log',
542
+ flush: () => ({}),
543
+ api: ({ data }) => {
544
+ return {
545
+ setMetadata: (metadata) => {
546
+ data.metadata = metadata;
547
+ },
548
+ };
549
+ },
550
+ setup: () => ({}),
551
+ };
552
+
553
+ /**
554
+ * Check if a value can be serialized (e.g. using `JSON.stringify`).
555
+ * Adapted from: https://stackoverflow.com/a/30712764/3829557
556
+ */
557
+ function isSerializable(value) {
558
+ // Primitives are OK.
559
+ if (value === undefined ||
560
+ value === null ||
561
+ typeof value === 'boolean' ||
562
+ typeof value === 'number' ||
563
+ typeof value === 'string') {
564
+ return true;
565
+ }
566
+ // A non-primitive value that is neither a POJO or an array cannot be serialized.
567
+ if (!isPlainObject__default["default"](value) && !Array.isArray(value)) {
568
+ return false;
569
+ }
570
+ // Recurse entries if the value is an object or array.
571
+ for (const key in value) {
572
+ if (!isSerializable(value[key]))
573
+ return false;
574
+ }
575
+ return true;
576
+ }
577
+ /**
578
+ * Plugin that checks whether state is serializable, in order to avoid
579
+ * network serialization bugs.
580
+ */
581
+ const SerializablePlugin = {
582
+ name: 'plugin-serializable',
583
+ fnWrap: (move) => (context, ...args) => {
584
+ const result = move(context, ...args);
585
+ // Check state in non-production environments.
586
+ if (process.env.NODE_ENV !== 'production' && !isSerializable(result)) {
587
+ throw new Error('Move state is not JSON-serialiazable.\n' +
588
+ 'See https://boardgame.io/documentation/#/?id=state for more information.');
589
+ }
590
+ return result;
591
+ },
592
+ };
593
+
594
+ /*
595
+ * Copyright 2018 The boardgame.io Authors
596
+ *
597
+ * Use of this source code is governed by a MIT-style
598
+ * license that can be found in the LICENSE file or at
599
+ * https://opensource.org/licenses/MIT.
600
+ */
601
+ const production = process.env.NODE_ENV === 'production';
602
+ const logfn = production ? () => { } : (...msg) => console.log(...msg);
603
+ const errorfn = (...msg) => console.error(...msg);
604
+ function info(msg) {
605
+ logfn(`INFO: ${msg}`);
606
+ }
607
+ function error(error) {
608
+ errorfn('ERROR:', error);
609
+ }
610
+
611
+ /*
612
+ * Copyright 2018 The boardgame.io Authors
613
+ *
614
+ * Use of this source code is governed by a MIT-style
615
+ * license that can be found in the LICENSE file or at
616
+ * https://opensource.org/licenses/MIT.
617
+ */
618
+ /**
619
+ * List of plugins that are always added.
620
+ */
621
+ const CORE_PLUGINS = [ImmerPlugin, RandomPlugin, LogPlugin, SerializablePlugin];
622
+ const DEFAULT_PLUGINS = [...CORE_PLUGINS, EventsPlugin];
623
+ /**
624
+ * Allow plugins to intercept actions and process them.
625
+ */
626
+ const ProcessAction = (state, action, opts) => {
627
+ // TODO(#723): Extend error handling to plugins.
628
+ opts.game.plugins
629
+ .filter((plugin) => plugin.action !== undefined)
630
+ .filter((plugin) => plugin.name === action.payload.type)
631
+ .forEach((plugin) => {
632
+ const name = plugin.name;
633
+ const pluginState = state.plugins[name] || { data: {} };
634
+ const data = plugin.action(pluginState.data, action.payload);
635
+ state = {
636
+ ...state,
637
+ plugins: {
638
+ ...state.plugins,
639
+ [name]: { ...pluginState, data },
640
+ },
641
+ };
642
+ });
643
+ return state;
644
+ };
645
+ /**
646
+ * The APIs created by various plugins are stored in the plugins
647
+ * section of the state object:
648
+ *
649
+ * {
650
+ * G: {},
651
+ * ctx: {},
652
+ * plugins: {
653
+ * plugin-a: {
654
+ * data: {}, // this is generated by the plugin at Setup / Flush.
655
+ * api: {}, // this is ephemeral and generated by Enhance.
656
+ * }
657
+ * }
658
+ * }
659
+ *
660
+ * This function retrieves plugin APIs and returns them as an object
661
+ * for consumption as used by move contexts.
662
+ */
663
+ const GetAPIs = ({ plugins }) => Object.entries(plugins || {}).reduce((apis, [name, { api }]) => {
664
+ apis[name] = api;
665
+ return apis;
666
+ }, {});
667
+ /**
668
+ * Applies the provided plugins to the given move / flow function.
669
+ *
670
+ * @param methodToWrap - The move function or hook to apply the plugins to.
671
+ * @param methodType - The type of the move or hook being wrapped.
672
+ * @param plugins - The list of plugins.
673
+ */
674
+ const FnWrap = (methodToWrap, methodType, plugins) => {
675
+ return [...CORE_PLUGINS, ...plugins, EventsPlugin]
676
+ .filter((plugin) => plugin.fnWrap !== undefined)
677
+ .reduce((method, { fnWrap }) => fnWrap(method, methodType), methodToWrap);
678
+ };
679
+ /**
680
+ * Allows the plugin to generate its initial state.
681
+ */
682
+ const Setup = (state, opts) => {
683
+ [...DEFAULT_PLUGINS, ...opts.game.plugins]
684
+ .filter((plugin) => plugin.setup !== undefined)
685
+ .forEach((plugin) => {
686
+ const name = plugin.name;
687
+ const data = plugin.setup({
688
+ G: state.G,
689
+ ctx: state.ctx,
690
+ game: opts.game,
691
+ });
692
+ state = {
693
+ ...state,
694
+ plugins: {
695
+ ...state.plugins,
696
+ [name]: { data },
697
+ },
698
+ };
699
+ });
700
+ return state;
701
+ };
702
+ /**
703
+ * Invokes the plugin before a move or event.
704
+ * The API that the plugin generates is stored inside
705
+ * the `plugins` section of the state (which is subsequently
706
+ * merged into ctx).
707
+ */
708
+ const Enhance = (state, opts) => {
709
+ [...DEFAULT_PLUGINS, ...opts.game.plugins]
710
+ .filter((plugin) => plugin.api !== undefined)
711
+ .forEach((plugin) => {
712
+ const name = plugin.name;
713
+ const pluginState = state.plugins[name] || { data: {} };
714
+ const api = plugin.api({
715
+ G: state.G,
716
+ ctx: state.ctx,
717
+ data: pluginState.data,
718
+ game: opts.game,
719
+ playerID: opts.playerID,
720
+ });
721
+ state = {
722
+ ...state,
723
+ plugins: {
724
+ ...state.plugins,
725
+ [name]: { ...pluginState, api },
726
+ },
727
+ };
728
+ });
729
+ return state;
730
+ };
731
+ /**
732
+ * Allows plugins to update their state after a move / event.
733
+ */
734
+ const Flush = (state, opts) => {
735
+ // We flush the events plugin first, then custom plugins and the core plugins.
736
+ // This means custom plugins cannot use the events API but will be available in event hooks.
737
+ // Note that plugins are flushed in reverse, to allow custom plugins calling each other.
738
+ [...CORE_PLUGINS, ...opts.game.plugins, EventsPlugin]
739
+ .reverse()
740
+ .forEach((plugin) => {
741
+ const name = plugin.name;
742
+ const pluginState = state.plugins[name] || { data: {} };
743
+ if (plugin.flush) {
744
+ const newData = plugin.flush({
745
+ G: state.G,
746
+ ctx: state.ctx,
747
+ game: opts.game,
748
+ api: pluginState.api,
749
+ data: pluginState.data,
750
+ });
751
+ state = {
752
+ ...state,
753
+ plugins: {
754
+ ...state.plugins,
755
+ [plugin.name]: { data: newData },
756
+ },
757
+ };
758
+ }
759
+ else if (plugin.dangerouslyFlushRawState) {
760
+ state = plugin.dangerouslyFlushRawState({
761
+ state,
762
+ game: opts.game,
763
+ api: pluginState.api,
764
+ data: pluginState.data,
765
+ });
766
+ // Remove everything other than data.
767
+ const data = state.plugins[name].data;
768
+ state = {
769
+ ...state,
770
+ plugins: {
771
+ ...state.plugins,
772
+ [plugin.name]: { data },
773
+ },
774
+ };
775
+ }
776
+ });
777
+ return state;
778
+ };
779
+ /**
780
+ * Allows plugins to indicate if they should not be materialized on the client.
781
+ * This will cause the client to discard the state update and wait for the
782
+ * master instead.
783
+ */
784
+ const NoClient = (state, opts) => {
785
+ return [...DEFAULT_PLUGINS, ...opts.game.plugins]
786
+ .filter((plugin) => plugin.noClient !== undefined)
787
+ .map((plugin) => {
788
+ const name = plugin.name;
789
+ const pluginState = state.plugins[name];
790
+ if (pluginState) {
791
+ return plugin.noClient({
792
+ G: state.G,
793
+ ctx: state.ctx,
794
+ game: opts.game,
795
+ api: pluginState.api,
796
+ data: pluginState.data,
797
+ });
798
+ }
799
+ return false;
800
+ })
801
+ .includes(true);
802
+ };
803
+ /**
804
+ * Allows plugins to indicate if the entire action should be thrown out
805
+ * as invalid. This will cancel the entire state update.
806
+ */
807
+ const IsInvalid = (state, opts) => {
808
+ const firstInvalidReturn = [...DEFAULT_PLUGINS, ...opts.game.plugins]
809
+ .filter((plugin) => plugin.isInvalid !== undefined)
810
+ .map((plugin) => {
811
+ const { name } = plugin;
812
+ const pluginState = state.plugins[name];
813
+ const message = plugin.isInvalid({
814
+ G: state.G,
815
+ ctx: state.ctx,
816
+ game: opts.game,
817
+ data: pluginState && pluginState.data,
818
+ });
819
+ return message ? { plugin: name, message } : false;
820
+ })
821
+ .find((value) => value);
822
+ return firstInvalidReturn || false;
823
+ };
824
+ /**
825
+ * Update plugin state after move/event & check if plugins consider the update to be valid.
826
+ * @returns Tuple of `[updatedState]` or `[originalState, invalidError]`.
827
+ */
828
+ const FlushAndValidate = (state, opts) => {
829
+ const updatedState = Flush(state, opts);
830
+ const isInvalid = IsInvalid(updatedState, opts);
831
+ if (!isInvalid)
832
+ return [updatedState];
833
+ const { plugin, message } = isInvalid;
834
+ error(`${plugin} plugin declared action invalid:\n${message}`);
835
+ return [state, isInvalid];
836
+ };
837
+ /**
838
+ * Allows plugins to customize their data for specific players.
839
+ * For example, a plugin may want to share no data with the client, or
840
+ * want to keep some player data secret from opponents.
841
+ */
842
+ const PlayerView = ({ G, ctx, plugins = {} }, { game, playerID }) => {
843
+ [...DEFAULT_PLUGINS, ...game.plugins].forEach(({ name, playerView }) => {
844
+ if (!playerView)
845
+ return;
846
+ const { data } = plugins[name] || { data: {} };
847
+ const newData = playerView({ G, ctx, game, data, playerID });
848
+ plugins = {
849
+ ...plugins,
850
+ [name]: { data: newData },
851
+ };
852
+ });
853
+ return plugins;
854
+ };
855
+
856
+ /**
857
+ * Adjust the given options to use the new minMoves/maxMoves if a legacy moveLimit was given
858
+ * @param options The options object to apply backwards compatibility to
859
+ * @param enforceMinMoves Use moveLimit to set both minMoves and maxMoves
860
+ */
861
+ function supportDeprecatedMoveLimit(options, enforceMinMoves = false) {
862
+ if (options.moveLimit) {
863
+ if (enforceMinMoves) {
864
+ options.minMoves = options.moveLimit;
865
+ }
866
+ options.maxMoves = options.moveLimit;
867
+ delete options.moveLimit;
868
+ }
869
+ }
870
+
871
+ /*
872
+ * Copyright 2017 The boardgame.io Authors
873
+ *
874
+ * Use of this source code is governed by a MIT-style
875
+ * license that can be found in the LICENSE file or at
876
+ * https://opensource.org/licenses/MIT.
877
+ */
878
+ function SetActivePlayers(ctx, arg) {
879
+ let activePlayers = {};
880
+ let _prevActivePlayers = [];
881
+ let _nextActivePlayers = null;
882
+ let _activePlayersMinMoves = {};
883
+ let _activePlayersMaxMoves = {};
884
+ if (Array.isArray(arg)) {
885
+ // support a simple array of player IDs as active players
886
+ const value = {};
887
+ arg.forEach((v) => (value[v] = Stage.NULL));
888
+ activePlayers = value;
889
+ }
890
+ else {
891
+ // process active players argument object
892
+ // stages previously did not enforce minMoves, this behaviour is kept intentionally
893
+ supportDeprecatedMoveLimit(arg);
894
+ if (arg.next) {
895
+ _nextActivePlayers = arg.next;
896
+ }
897
+ if (arg.revert) {
898
+ _prevActivePlayers = [
899
+ ...ctx._prevActivePlayers,
900
+ {
901
+ activePlayers: ctx.activePlayers,
902
+ _activePlayersMinMoves: ctx._activePlayersMinMoves,
903
+ _activePlayersMaxMoves: ctx._activePlayersMaxMoves,
904
+ _activePlayersNumMoves: ctx._activePlayersNumMoves,
905
+ },
906
+ ];
907
+ }
908
+ if (arg.currentPlayer !== undefined) {
909
+ ApplyActivePlayerArgument(activePlayers, _activePlayersMinMoves, _activePlayersMaxMoves, ctx.currentPlayer, arg.currentPlayer);
910
+ }
911
+ if (arg.others !== undefined) {
912
+ for (let i = 0; i < ctx.playOrder.length; i++) {
913
+ const id = ctx.playOrder[i];
914
+ if (id !== ctx.currentPlayer) {
915
+ ApplyActivePlayerArgument(activePlayers, _activePlayersMinMoves, _activePlayersMaxMoves, id, arg.others);
916
+ }
917
+ }
918
+ }
919
+ if (arg.all !== undefined) {
920
+ for (let i = 0; i < ctx.playOrder.length; i++) {
921
+ const id = ctx.playOrder[i];
922
+ ApplyActivePlayerArgument(activePlayers, _activePlayersMinMoves, _activePlayersMaxMoves, id, arg.all);
923
+ }
924
+ }
925
+ if (arg.value) {
926
+ for (const id in arg.value) {
927
+ ApplyActivePlayerArgument(activePlayers, _activePlayersMinMoves, _activePlayersMaxMoves, id, arg.value[id]);
928
+ }
929
+ }
930
+ if (arg.minMoves) {
931
+ for (const id in activePlayers) {
932
+ if (_activePlayersMinMoves[id] === undefined) {
933
+ _activePlayersMinMoves[id] = arg.minMoves;
934
+ }
935
+ }
936
+ }
937
+ if (arg.maxMoves) {
938
+ for (const id in activePlayers) {
939
+ if (_activePlayersMaxMoves[id] === undefined) {
940
+ _activePlayersMaxMoves[id] = arg.maxMoves;
941
+ }
942
+ }
943
+ }
944
+ }
945
+ if (Object.keys(activePlayers).length === 0) {
946
+ activePlayers = null;
947
+ }
948
+ if (Object.keys(_activePlayersMinMoves).length === 0) {
949
+ _activePlayersMinMoves = null;
950
+ }
951
+ if (Object.keys(_activePlayersMaxMoves).length === 0) {
952
+ _activePlayersMaxMoves = null;
953
+ }
954
+ const _activePlayersNumMoves = {};
955
+ for (const id in activePlayers) {
956
+ _activePlayersNumMoves[id] = 0;
957
+ }
958
+ return {
959
+ ...ctx,
960
+ activePlayers,
961
+ _activePlayersMinMoves,
962
+ _activePlayersMaxMoves,
963
+ _activePlayersNumMoves,
964
+ _prevActivePlayers,
965
+ _nextActivePlayers,
966
+ };
967
+ }
968
+ /**
969
+ * Update activePlayers, setting it to previous, next or null values
970
+ * when it becomes empty.
971
+ * @param ctx
972
+ */
973
+ function UpdateActivePlayersOnceEmpty(ctx) {
974
+ let { activePlayers, _activePlayersMinMoves, _activePlayersMaxMoves, _activePlayersNumMoves, _prevActivePlayers, _nextActivePlayers, } = ctx;
975
+ if (activePlayers && Object.keys(activePlayers).length === 0) {
976
+ if (_nextActivePlayers) {
977
+ ctx = SetActivePlayers(ctx, _nextActivePlayers);
978
+ ({
979
+ activePlayers,
980
+ _activePlayersMinMoves,
981
+ _activePlayersMaxMoves,
982
+ _activePlayersNumMoves,
983
+ _prevActivePlayers,
984
+ } = ctx);
985
+ }
986
+ else if (_prevActivePlayers.length > 0) {
987
+ const lastIndex = _prevActivePlayers.length - 1;
988
+ ({
989
+ activePlayers,
990
+ _activePlayersMinMoves,
991
+ _activePlayersMaxMoves,
992
+ _activePlayersNumMoves,
993
+ } = _prevActivePlayers[lastIndex]);
994
+ _prevActivePlayers = _prevActivePlayers.slice(0, lastIndex);
995
+ }
996
+ else {
997
+ activePlayers = null;
998
+ _activePlayersMinMoves = null;
999
+ _activePlayersMaxMoves = null;
1000
+ }
1001
+ }
1002
+ return {
1003
+ ...ctx,
1004
+ activePlayers,
1005
+ _activePlayersMinMoves,
1006
+ _activePlayersMaxMoves,
1007
+ _activePlayersNumMoves,
1008
+ _prevActivePlayers,
1009
+ };
1010
+ }
1011
+ /**
1012
+ * Apply an active player argument to the given player ID
1013
+ * @param {Object} activePlayers
1014
+ * @param {Object} _activePlayersMinMoves
1015
+ * @param {Object} _activePlayersMaxMoves
1016
+ * @param {String} playerID The player to apply the parameter to
1017
+ * @param {(String|Object)} arg An active player argument
1018
+ */
1019
+ function ApplyActivePlayerArgument(activePlayers, _activePlayersMinMoves, _activePlayersMaxMoves, playerID, arg) {
1020
+ if (typeof arg !== 'object' || arg === Stage.NULL) {
1021
+ arg = { stage: arg };
1022
+ }
1023
+ if (arg.stage !== undefined) {
1024
+ // stages previously did not enforce minMoves, this behaviour is kept intentionally
1025
+ supportDeprecatedMoveLimit(arg);
1026
+ activePlayers[playerID] = arg.stage;
1027
+ if (arg.minMoves)
1028
+ _activePlayersMinMoves[playerID] = arg.minMoves;
1029
+ if (arg.maxMoves)
1030
+ _activePlayersMaxMoves[playerID] = arg.maxMoves;
1031
+ }
1032
+ }
1033
+ /**
1034
+ * Converts a playOrderPos index into its value in playOrder.
1035
+ * @param {Array} playOrder - An array of player ID's.
1036
+ * @param {number} playOrderPos - An index into the above.
1037
+ */
1038
+ function getCurrentPlayer(playOrder, playOrderPos) {
1039
+ // convert to string in case playOrder is set to number[]
1040
+ return playOrder[playOrderPos] + '';
1041
+ }
1042
+ /**
1043
+ * Called at the start of a turn to initialize turn order state.
1044
+ *
1045
+ * TODO: This is called inside StartTurn, which is called from
1046
+ * both UpdateTurn and StartPhase (so it's called at the beginning
1047
+ * of a new phase as well as between turns). We should probably
1048
+ * split it into two.
1049
+ */
1050
+ function InitTurnOrderState(state, turn) {
1051
+ let { G, ctx } = state;
1052
+ const { numPlayers } = ctx;
1053
+ const pluginAPIs = GetAPIs(state);
1054
+ const context = { ...pluginAPIs, G, ctx };
1055
+ const order = turn.order;
1056
+ let playOrder = [...Array.from({ length: numPlayers })].map((_, i) => i + '');
1057
+ if (order.playOrder !== undefined) {
1058
+ playOrder = order.playOrder(context);
1059
+ }
1060
+ const playOrderPos = order.first(context);
1061
+ const posType = typeof playOrderPos;
1062
+ if (posType !== 'number') {
1063
+ error(`invalid value returned by turn.order.first — expected number got ${posType} “${playOrderPos}”.`);
1064
+ }
1065
+ const currentPlayer = getCurrentPlayer(playOrder, playOrderPos);
1066
+ ctx = { ...ctx, currentPlayer, playOrderPos, playOrder };
1067
+ ctx = SetActivePlayers(ctx, turn.activePlayers || {});
1068
+ return ctx;
1069
+ }
1070
+ /**
1071
+ * Called at the end of each turn to update the turn order state.
1072
+ * @param {object} G - The game object G.
1073
+ * @param {object} ctx - The game object ctx.
1074
+ * @param {object} turn - A turn object for this phase.
1075
+ * @param {string} endTurnArg - An optional argument to endTurn that
1076
+ may specify the next player.
1077
+ */
1078
+ function UpdateTurnOrderState(state, currentPlayer, turn, endTurnArg) {
1079
+ const order = turn.order;
1080
+ let { G, ctx } = state;
1081
+ let playOrderPos = ctx.playOrderPos;
1082
+ let endPhase = false;
1083
+ if (endTurnArg && endTurnArg !== true) {
1084
+ if (typeof endTurnArg !== 'object') {
1085
+ error(`invalid argument to endTurn: ${endTurnArg}`);
1086
+ }
1087
+ Object.keys(endTurnArg).forEach((arg) => {
1088
+ switch (arg) {
1089
+ case 'remove':
1090
+ currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos);
1091
+ break;
1092
+ case 'next':
1093
+ playOrderPos = ctx.playOrder.indexOf(endTurnArg.next);
1094
+ currentPlayer = endTurnArg.next;
1095
+ break;
1096
+ default:
1097
+ error(`invalid argument to endTurn: ${arg}`);
1098
+ }
1099
+ });
1100
+ }
1101
+ else {
1102
+ const pluginAPIs = GetAPIs(state);
1103
+ const context = { ...pluginAPIs, G, ctx };
1104
+ const t = order.next(context);
1105
+ const type = typeof t;
1106
+ if (t !== undefined && type !== 'number') {
1107
+ error(`invalid value returned by turn.order.next — expected number or undefined got ${type} “${t}”.`);
1108
+ }
1109
+ if (t === undefined) {
1110
+ endPhase = true;
1111
+ }
1112
+ else {
1113
+ playOrderPos = t;
1114
+ currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos);
1115
+ }
1116
+ }
1117
+ ctx = {
1118
+ ...ctx,
1119
+ playOrderPos,
1120
+ currentPlayer,
1121
+ };
1122
+ return { endPhase, ctx };
1123
+ }
1124
+ /**
1125
+ * Set of different turn orders possible in a phase.
1126
+ * These are meant to be passed to the `turn` setting
1127
+ * in the flow objects.
1128
+ *
1129
+ * Each object defines the first player when the phase / game
1130
+ * begins, and also a function `next` to determine who the
1131
+ * next player is when the turn ends.
1132
+ *
1133
+ * The phase ends if next() returns undefined.
1134
+ */
1135
+ const TurnOrder = {
1136
+ /**
1137
+ * DEFAULT
1138
+ *
1139
+ * The default round-robin turn order.
1140
+ */
1141
+ DEFAULT: {
1142
+ first: ({ ctx }) => ctx.turn === 0
1143
+ ? ctx.playOrderPos
1144
+ : (ctx.playOrderPos + 1) % ctx.playOrder.length,
1145
+ next: ({ ctx }) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
1146
+ },
1147
+ /**
1148
+ * RESET
1149
+ *
1150
+ * Similar to DEFAULT, but starts from 0 each time.
1151
+ */
1152
+ RESET: {
1153
+ first: () => 0,
1154
+ next: ({ ctx }) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
1155
+ },
1156
+ /**
1157
+ * CONTINUE
1158
+ *
1159
+ * Similar to DEFAULT, but starts with the player who ended the last phase.
1160
+ */
1161
+ CONTINUE: {
1162
+ first: ({ ctx }) => ctx.playOrderPos,
1163
+ next: ({ ctx }) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
1164
+ },
1165
+ /**
1166
+ * ONCE
1167
+ *
1168
+ * Another round-robin turn order, but goes around just once.
1169
+ * The phase ends after all players have played.
1170
+ */
1171
+ ONCE: {
1172
+ first: () => 0,
1173
+ next: ({ ctx }) => {
1174
+ if (ctx.playOrderPos < ctx.playOrder.length - 1) {
1175
+ return ctx.playOrderPos + 1;
1176
+ }
1177
+ },
1178
+ },
1179
+ /**
1180
+ * CUSTOM
1181
+ *
1182
+ * Identical to DEFAULT, but also sets playOrder at the
1183
+ * beginning of the phase.
1184
+ *
1185
+ * @param {Array} playOrder - The play order.
1186
+ */
1187
+ CUSTOM: (playOrder) => ({
1188
+ playOrder: () => playOrder,
1189
+ first: () => 0,
1190
+ next: ({ ctx }) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
1191
+ }),
1192
+ /**
1193
+ * CUSTOM_FROM
1194
+ *
1195
+ * Identical to DEFAULT, but also sets playOrder at the
1196
+ * beginning of the phase to a value specified by a field
1197
+ * in G.
1198
+ *
1199
+ * @param {string} playOrderField - Field in G.
1200
+ */
1201
+ CUSTOM_FROM: (playOrderField) => ({
1202
+ playOrder: ({ G }) => G[playOrderField],
1203
+ first: () => 0,
1204
+ next: ({ ctx }) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
1205
+ }),
1206
+ };
1207
+ const Stage = {
1208
+ NULL: null,
1209
+ };
1210
+
1211
+ /*
1212
+ * Copyright 2017 The boardgame.io Authors
1213
+ *
1214
+ * Use of this source code is governed by a MIT-style
1215
+ * license that can be found in the LICENSE file or at
1216
+ * https://opensource.org/licenses/MIT.
1217
+ */
1218
+ /**
1219
+ * Flow
1220
+ *
1221
+ * Creates a reducer that updates ctx (analogous to how moves update G).
1222
+ */
1223
+ function Flow({ moves, phases, endIf, onEnd, turn, events, plugins, }) {
1224
+ // Attach defaults.
1225
+ if (moves === undefined) {
1226
+ moves = {};
1227
+ }
1228
+ if (events === undefined) {
1229
+ events = {};
1230
+ }
1231
+ if (plugins === undefined) {
1232
+ plugins = [];
1233
+ }
1234
+ if (phases === undefined) {
1235
+ phases = {};
1236
+ }
1237
+ if (!endIf)
1238
+ endIf = () => undefined;
1239
+ if (!onEnd)
1240
+ onEnd = ({ G }) => G;
1241
+ if (!turn)
1242
+ turn = {};
1243
+ const phaseMap = { ...phases };
1244
+ if ('' in phaseMap) {
1245
+ error('cannot specify phase with empty name');
1246
+ }
1247
+ phaseMap[''] = {};
1248
+ const moveMap = {};
1249
+ const moveNames = new Set();
1250
+ let startingPhase = null;
1251
+ Object.keys(moves).forEach((name) => moveNames.add(name));
1252
+ const HookWrapper = (hook, hookType) => {
1253
+ const withPlugins = FnWrap(hook, hookType, plugins);
1254
+ return (state) => {
1255
+ const pluginAPIs = GetAPIs(state);
1256
+ return withPlugins({
1257
+ ...pluginAPIs,
1258
+ G: state.G,
1259
+ ctx: state.ctx,
1260
+ playerID: state.playerID,
1261
+ });
1262
+ };
1263
+ };
1264
+ const TriggerWrapper = (trigger) => {
1265
+ return (state) => {
1266
+ const pluginAPIs = GetAPIs(state);
1267
+ return trigger({
1268
+ ...pluginAPIs,
1269
+ G: state.G,
1270
+ ctx: state.ctx,
1271
+ });
1272
+ };
1273
+ };
1274
+ const wrapped = {
1275
+ onEnd: HookWrapper(onEnd, GameMethod.GAME_ON_END),
1276
+ endIf: TriggerWrapper(endIf),
1277
+ };
1278
+ for (const phase in phaseMap) {
1279
+ const phaseConfig = phaseMap[phase];
1280
+ if (phaseConfig.start === true) {
1281
+ startingPhase = phase;
1282
+ }
1283
+ if (phaseConfig.moves !== undefined) {
1284
+ for (const move of Object.keys(phaseConfig.moves)) {
1285
+ moveMap[phase + '.' + move] = phaseConfig.moves[move];
1286
+ moveNames.add(move);
1287
+ }
1288
+ }
1289
+ if (phaseConfig.endIf === undefined) {
1290
+ phaseConfig.endIf = () => undefined;
1291
+ }
1292
+ if (phaseConfig.onBegin === undefined) {
1293
+ phaseConfig.onBegin = ({ G }) => G;
1294
+ }
1295
+ if (phaseConfig.onEnd === undefined) {
1296
+ phaseConfig.onEnd = ({ G }) => G;
1297
+ }
1298
+ if (phaseConfig.turn === undefined) {
1299
+ phaseConfig.turn = turn;
1300
+ }
1301
+ if (phaseConfig.turn.order === undefined) {
1302
+ phaseConfig.turn.order = TurnOrder.DEFAULT;
1303
+ }
1304
+ if (phaseConfig.turn.onBegin === undefined) {
1305
+ phaseConfig.turn.onBegin = ({ G }) => G;
1306
+ }
1307
+ if (phaseConfig.turn.onEnd === undefined) {
1308
+ phaseConfig.turn.onEnd = ({ G }) => G;
1309
+ }
1310
+ if (phaseConfig.turn.endIf === undefined) {
1311
+ phaseConfig.turn.endIf = () => false;
1312
+ }
1313
+ if (phaseConfig.turn.onMove === undefined) {
1314
+ phaseConfig.turn.onMove = ({ G }) => G;
1315
+ }
1316
+ if (phaseConfig.turn.stages === undefined) {
1317
+ phaseConfig.turn.stages = {};
1318
+ }
1319
+ // turns previously treated moveLimit as both minMoves and maxMoves, this behaviour is kept intentionally
1320
+ supportDeprecatedMoveLimit(phaseConfig.turn, true);
1321
+ for (const stage in phaseConfig.turn.stages) {
1322
+ const stageConfig = phaseConfig.turn.stages[stage];
1323
+ const moves = stageConfig.moves || {};
1324
+ for (const move of Object.keys(moves)) {
1325
+ const key = phase + '.' + stage + '.' + move;
1326
+ moveMap[key] = moves[move];
1327
+ moveNames.add(move);
1328
+ }
1329
+ }
1330
+ phaseConfig.wrapped = {
1331
+ onBegin: HookWrapper(phaseConfig.onBegin, GameMethod.PHASE_ON_BEGIN),
1332
+ onEnd: HookWrapper(phaseConfig.onEnd, GameMethod.PHASE_ON_END),
1333
+ endIf: TriggerWrapper(phaseConfig.endIf),
1334
+ };
1335
+ phaseConfig.turn.wrapped = {
1336
+ onMove: HookWrapper(phaseConfig.turn.onMove, GameMethod.TURN_ON_MOVE),
1337
+ onBegin: HookWrapper(phaseConfig.turn.onBegin, GameMethod.TURN_ON_BEGIN),
1338
+ onEnd: HookWrapper(phaseConfig.turn.onEnd, GameMethod.TURN_ON_END),
1339
+ endIf: TriggerWrapper(phaseConfig.turn.endIf),
1340
+ };
1341
+ if (typeof phaseConfig.next !== 'function') {
1342
+ const { next } = phaseConfig;
1343
+ phaseConfig.next = () => next || null;
1344
+ }
1345
+ phaseConfig.wrapped.next = TriggerWrapper(phaseConfig.next);
1346
+ }
1347
+ function GetPhase(ctx) {
1348
+ return ctx.phase ? phaseMap[ctx.phase] : phaseMap[''];
1349
+ }
1350
+ function OnMove(state) {
1351
+ return state;
1352
+ }
1353
+ function Process(state, events) {
1354
+ const phasesEnded = new Set();
1355
+ const turnsEnded = new Set();
1356
+ for (let i = 0; i < events.length; i++) {
1357
+ const { fn, arg, ...rest } = events[i];
1358
+ // Detect a loop of EndPhase calls.
1359
+ // This could potentially even be an infinite loop
1360
+ // if the endIf condition of each phase blindly
1361
+ // returns true. The moment we detect a single
1362
+ // loop, we just bail out of all phases.
1363
+ if (fn === EndPhase) {
1364
+ turnsEnded.clear();
1365
+ const phase = state.ctx.phase;
1366
+ if (phasesEnded.has(phase)) {
1367
+ const ctx = { ...state.ctx, phase: null };
1368
+ return { ...state, ctx };
1369
+ }
1370
+ phasesEnded.add(phase);
1371
+ }
1372
+ // Process event.
1373
+ const next = [];
1374
+ state = fn(state, {
1375
+ ...rest,
1376
+ arg,
1377
+ next,
1378
+ });
1379
+ if (fn === EndGame) {
1380
+ break;
1381
+ }
1382
+ // Check if we should end the game.
1383
+ const shouldEndGame = ShouldEndGame(state);
1384
+ if (shouldEndGame) {
1385
+ events.push({
1386
+ fn: EndGame,
1387
+ arg: shouldEndGame,
1388
+ turn: state.ctx.turn,
1389
+ phase: state.ctx.phase,
1390
+ automatic: true,
1391
+ });
1392
+ continue;
1393
+ }
1394
+ // Check if we should end the phase.
1395
+ const shouldEndPhase = ShouldEndPhase(state);
1396
+ if (shouldEndPhase) {
1397
+ events.push({
1398
+ fn: EndPhase,
1399
+ arg: shouldEndPhase,
1400
+ turn: state.ctx.turn,
1401
+ phase: state.ctx.phase,
1402
+ automatic: true,
1403
+ });
1404
+ continue;
1405
+ }
1406
+ // Check if we should end the turn.
1407
+ if ([OnMove, UpdateStage, UpdateActivePlayers].includes(fn)) {
1408
+ const shouldEndTurn = ShouldEndTurn(state);
1409
+ if (shouldEndTurn) {
1410
+ events.push({
1411
+ fn: EndTurn,
1412
+ arg: shouldEndTurn,
1413
+ turn: state.ctx.turn,
1414
+ phase: state.ctx.phase,
1415
+ automatic: true,
1416
+ });
1417
+ continue;
1418
+ }
1419
+ }
1420
+ events.push(...next);
1421
+ }
1422
+ return state;
1423
+ }
1424
+ ///////////
1425
+ // Start //
1426
+ ///////////
1427
+ function StartGame(state, { next }) {
1428
+ next.push({ fn: StartPhase });
1429
+ return state;
1430
+ }
1431
+ function StartPhase(state, { next }) {
1432
+ let { G, ctx } = state;
1433
+ const phaseConfig = GetPhase(ctx);
1434
+ // Run any phase setup code provided by the user.
1435
+ G = phaseConfig.wrapped.onBegin(state);
1436
+ next.push({ fn: StartTurn });
1437
+ return { ...state, G, ctx };
1438
+ }
1439
+ function StartTurn(state, { currentPlayer }) {
1440
+ let { ctx } = state;
1441
+ const phaseConfig = GetPhase(ctx);
1442
+ // Initialize the turn order state.
1443
+ if (currentPlayer) {
1444
+ ctx = { ...ctx, currentPlayer };
1445
+ if (phaseConfig.turn.activePlayers) {
1446
+ ctx = SetActivePlayers(ctx, phaseConfig.turn.activePlayers);
1447
+ }
1448
+ }
1449
+ else {
1450
+ // This is only called at the beginning of the phase
1451
+ // when there is no currentPlayer yet.
1452
+ ctx = InitTurnOrderState(state, phaseConfig.turn);
1453
+ }
1454
+ const turn = ctx.turn + 1;
1455
+ ctx = { ...ctx, turn, numMoves: 0, _prevActivePlayers: [] };
1456
+ const G = phaseConfig.turn.wrapped.onBegin({ ...state, ctx });
1457
+ return { ...state, G, ctx, _undo: [], _redo: [] };
1458
+ }
1459
+ ////////////
1460
+ // Update //
1461
+ ////////////
1462
+ function UpdatePhase(state, { arg, next, phase }) {
1463
+ const phaseConfig = GetPhase({ phase });
1464
+ let { ctx } = state;
1465
+ if (arg && arg.next) {
1466
+ if (arg.next in phaseMap) {
1467
+ ctx = { ...ctx, phase: arg.next };
1468
+ }
1469
+ else {
1470
+ error('invalid phase: ' + arg.next);
1471
+ return state;
1472
+ }
1473
+ }
1474
+ else {
1475
+ ctx = { ...ctx, phase: phaseConfig.wrapped.next(state) || null };
1476
+ }
1477
+ state = { ...state, ctx };
1478
+ // Start the new phase.
1479
+ next.push({ fn: StartPhase });
1480
+ return state;
1481
+ }
1482
+ function UpdateTurn(state, { arg, currentPlayer, next }) {
1483
+ let { G, ctx } = state;
1484
+ const phaseConfig = GetPhase(ctx);
1485
+ // Update turn order state.
1486
+ const { endPhase, ctx: newCtx } = UpdateTurnOrderState(state, currentPlayer, phaseConfig.turn, arg);
1487
+ ctx = newCtx;
1488
+ state = { ...state, G, ctx };
1489
+ if (endPhase) {
1490
+ next.push({ fn: EndPhase, turn: ctx.turn, phase: ctx.phase });
1491
+ }
1492
+ else {
1493
+ next.push({ fn: StartTurn, currentPlayer: ctx.currentPlayer });
1494
+ }
1495
+ return state;
1496
+ }
1497
+ function UpdateStage(state, { arg, playerID }) {
1498
+ if (typeof arg === 'string' || arg === Stage.NULL) {
1499
+ arg = { stage: arg };
1500
+ }
1501
+ if (typeof arg !== 'object')
1502
+ return state;
1503
+ // `arg` should be of type `StageArg`, loose typing as `any` here for historic reasons
1504
+ // stages previously did not enforce minMoves, this behaviour is kept intentionally
1505
+ supportDeprecatedMoveLimit(arg);
1506
+ let { ctx } = state;
1507
+ let { activePlayers, _activePlayersMinMoves, _activePlayersMaxMoves, _activePlayersNumMoves, } = ctx;
1508
+ // Checking if stage is valid, even Stage.NULL
1509
+ if (arg.stage !== undefined) {
1510
+ if (activePlayers === null) {
1511
+ activePlayers = {};
1512
+ }
1513
+ activePlayers[playerID] = arg.stage;
1514
+ _activePlayersNumMoves[playerID] = 0;
1515
+ if (arg.minMoves) {
1516
+ if (_activePlayersMinMoves === null) {
1517
+ _activePlayersMinMoves = {};
1518
+ }
1519
+ _activePlayersMinMoves[playerID] = arg.minMoves;
1520
+ }
1521
+ if (arg.maxMoves) {
1522
+ if (_activePlayersMaxMoves === null) {
1523
+ _activePlayersMaxMoves = {};
1524
+ }
1525
+ _activePlayersMaxMoves[playerID] = arg.maxMoves;
1526
+ }
1527
+ }
1528
+ ctx = {
1529
+ ...ctx,
1530
+ activePlayers,
1531
+ _activePlayersMinMoves,
1532
+ _activePlayersMaxMoves,
1533
+ _activePlayersNumMoves,
1534
+ };
1535
+ return { ...state, ctx };
1536
+ }
1537
+ function UpdateActivePlayers(state, { arg }) {
1538
+ return { ...state, ctx: SetActivePlayers(state.ctx, arg) };
1539
+ }
1540
+ ///////////////
1541
+ // ShouldEnd //
1542
+ ///////////////
1543
+ function ShouldEndGame(state) {
1544
+ return wrapped.endIf(state);
1545
+ }
1546
+ function ShouldEndPhase(state) {
1547
+ const phaseConfig = GetPhase(state.ctx);
1548
+ return phaseConfig.wrapped.endIf(state);
1549
+ }
1550
+ function ShouldEndTurn(state) {
1551
+ const phaseConfig = GetPhase(state.ctx);
1552
+ // End the turn if the required number of moves has been made.
1553
+ const currentPlayerMoves = state.ctx.numMoves || 0;
1554
+ if (phaseConfig.turn.maxMoves &&
1555
+ currentPlayerMoves >= phaseConfig.turn.maxMoves) {
1556
+ return true;
1557
+ }
1558
+ return phaseConfig.turn.wrapped.endIf(state);
1559
+ }
1560
+ /////////
1561
+ // End //
1562
+ /////////
1563
+ function EndGame(state, { arg, phase }) {
1564
+ state = EndPhase(state, { phase });
1565
+ if (arg === undefined) {
1566
+ arg = true;
1567
+ }
1568
+ state = { ...state, ctx: { ...state.ctx, gameover: arg } };
1569
+ // Run game end hook.
1570
+ const G = wrapped.onEnd(state);
1571
+ return { ...state, G };
1572
+ }
1573
+ function EndPhase(state, { arg, next, turn: initialTurn, automatic }) {
1574
+ // End the turn first.
1575
+ state = EndTurn(state, { turn: initialTurn, force: true, automatic: true });
1576
+ const { phase, turn } = state.ctx;
1577
+ if (next) {
1578
+ next.push({ fn: UpdatePhase, arg, phase });
1579
+ }
1580
+ // If we aren't in a phase, there is nothing else to do.
1581
+ if (phase === null) {
1582
+ return state;
1583
+ }
1584
+ // Run any cleanup code for the phase that is about to end.
1585
+ const phaseConfig = GetPhase(state.ctx);
1586
+ const G = phaseConfig.wrapped.onEnd(state);
1587
+ // Reset the phase.
1588
+ const ctx = { ...state.ctx, phase: null };
1589
+ // Add log entry.
1590
+ const action = gameEvent('endPhase', arg);
1591
+ const { _stateID } = state;
1592
+ const logEntry = { action, _stateID, turn, phase };
1593
+ if (automatic)
1594
+ logEntry.automatic = true;
1595
+ const deltalog = [...(state.deltalog || []), logEntry];
1596
+ return { ...state, G, ctx, deltalog };
1597
+ }
1598
+ function EndTurn(state, { arg, next, turn: initialTurn, force, automatic, playerID }) {
1599
+ // This is not the turn that EndTurn was originally
1600
+ // called for. The turn was probably ended some other way.
1601
+ if (initialTurn !== state.ctx.turn) {
1602
+ return state;
1603
+ }
1604
+ const { currentPlayer, numMoves, phase, turn } = state.ctx;
1605
+ const phaseConfig = GetPhase(state.ctx);
1606
+ // Prevent ending the turn if minMoves haven't been reached.
1607
+ const currentPlayerMoves = numMoves || 0;
1608
+ if (!force &&
1609
+ phaseConfig.turn.minMoves &&
1610
+ currentPlayerMoves < phaseConfig.turn.minMoves) {
1611
+ info(`cannot end turn before making ${phaseConfig.turn.minMoves} moves`);
1612
+ return state;
1613
+ }
1614
+ // Run turn-end triggers.
1615
+ const G = phaseConfig.turn.wrapped.onEnd(state);
1616
+ if (next) {
1617
+ next.push({ fn: UpdateTurn, arg, currentPlayer });
1618
+ }
1619
+ // Reset activePlayers.
1620
+ let ctx = { ...state.ctx, activePlayers: null };
1621
+ // Remove player from playerOrder
1622
+ if (arg && arg.remove) {
1623
+ playerID = playerID || currentPlayer;
1624
+ const playOrder = ctx.playOrder.filter((i) => i != playerID);
1625
+ const playOrderPos = ctx.playOrderPos > playOrder.length - 1 ? 0 : ctx.playOrderPos;
1626
+ ctx = { ...ctx, playOrder, playOrderPos };
1627
+ if (playOrder.length === 0) {
1628
+ next.push({ fn: EndPhase, turn, phase });
1629
+ return state;
1630
+ }
1631
+ }
1632
+ // Create log entry.
1633
+ const action = gameEvent('endTurn', arg);
1634
+ const { _stateID } = state;
1635
+ const logEntry = { action, _stateID, turn, phase };
1636
+ if (automatic)
1637
+ logEntry.automatic = true;
1638
+ const deltalog = [...(state.deltalog || []), logEntry];
1639
+ return { ...state, G, ctx, deltalog, _undo: [], _redo: [] };
1640
+ }
1641
+ function EndStage(state, { arg, next, automatic, playerID }) {
1642
+ playerID = playerID || state.ctx.currentPlayer;
1643
+ let { ctx, _stateID } = state;
1644
+ let { activePlayers, _activePlayersNumMoves, _activePlayersMinMoves, _activePlayersMaxMoves, phase, turn, } = ctx;
1645
+ const playerInStage = activePlayers !== null && playerID in activePlayers;
1646
+ const phaseConfig = GetPhase(ctx);
1647
+ if (!arg && playerInStage) {
1648
+ const stage = phaseConfig.turn.stages[activePlayers[playerID]];
1649
+ if (stage && stage.next) {
1650
+ arg = stage.next;
1651
+ }
1652
+ }
1653
+ // Checking if arg is a valid stage, even Stage.NULL
1654
+ if (next) {
1655
+ next.push({ fn: UpdateStage, arg, playerID });
1656
+ }
1657
+ // If player isn’t in a stage, there is nothing else to do.
1658
+ if (!playerInStage)
1659
+ return state;
1660
+ // Prevent ending the stage if minMoves haven't been reached.
1661
+ const currentPlayerMoves = _activePlayersNumMoves[playerID] || 0;
1662
+ if (_activePlayersMinMoves &&
1663
+ _activePlayersMinMoves[playerID] &&
1664
+ currentPlayerMoves < _activePlayersMinMoves[playerID]) {
1665
+ info(`cannot end stage before making ${_activePlayersMinMoves[playerID]} moves`);
1666
+ return state;
1667
+ }
1668
+ // Remove player from activePlayers.
1669
+ activePlayers = { ...activePlayers };
1670
+ delete activePlayers[playerID];
1671
+ if (_activePlayersMinMoves) {
1672
+ // Remove player from _activePlayersMinMoves.
1673
+ _activePlayersMinMoves = { ..._activePlayersMinMoves };
1674
+ delete _activePlayersMinMoves[playerID];
1675
+ }
1676
+ if (_activePlayersMaxMoves) {
1677
+ // Remove player from _activePlayersMaxMoves.
1678
+ _activePlayersMaxMoves = { ..._activePlayersMaxMoves };
1679
+ delete _activePlayersMaxMoves[playerID];
1680
+ }
1681
+ ctx = UpdateActivePlayersOnceEmpty({
1682
+ ...ctx,
1683
+ activePlayers,
1684
+ _activePlayersMinMoves,
1685
+ _activePlayersMaxMoves,
1686
+ });
1687
+ // Create log entry.
1688
+ const action = gameEvent('endStage', arg);
1689
+ const logEntry = { action, _stateID, turn, phase };
1690
+ if (automatic)
1691
+ logEntry.automatic = true;
1692
+ const deltalog = [...(state.deltalog || []), logEntry];
1693
+ return { ...state, ctx, deltalog };
1694
+ }
1695
+ /**
1696
+ * Retrieves the relevant move that can be played by playerID.
1697
+ *
1698
+ * If ctx.activePlayers is set (i.e. one or more players are in some stage),
1699
+ * then it attempts to find the move inside the stages config for
1700
+ * that turn. If the stage for a player is '', then the player is
1701
+ * allowed to make a move (as determined by the phase config), but
1702
+ * isn't restricted to a particular set as defined in the stage config.
1703
+ *
1704
+ * If not, it then looks for the move inside the phase.
1705
+ *
1706
+ * If it doesn't find the move there, it looks at the global move definition.
1707
+ *
1708
+ * @param {object} ctx
1709
+ * @param {string} name
1710
+ * @param {string} playerID
1711
+ */
1712
+ function GetMove(ctx, name, playerID) {
1713
+ const phaseConfig = GetPhase(ctx);
1714
+ const stages = phaseConfig.turn.stages;
1715
+ const { activePlayers } = ctx;
1716
+ if (activePlayers &&
1717
+ activePlayers[playerID] !== undefined &&
1718
+ activePlayers[playerID] !== Stage.NULL &&
1719
+ stages[activePlayers[playerID]] !== undefined &&
1720
+ stages[activePlayers[playerID]].moves !== undefined) {
1721
+ // Check if moves are defined for the player's stage.
1722
+ const stage = stages[activePlayers[playerID]];
1723
+ const moves = stage.moves;
1724
+ if (name in moves) {
1725
+ return moves[name];
1726
+ }
1727
+ }
1728
+ else if (phaseConfig.moves) {
1729
+ // Check if moves are defined for the current phase.
1730
+ if (name in phaseConfig.moves) {
1731
+ return phaseConfig.moves[name];
1732
+ }
1733
+ }
1734
+ else if (name in moves) {
1735
+ // Check for the move globally.
1736
+ return moves[name];
1737
+ }
1738
+ return null;
1739
+ }
1740
+ function ProcessMove(state, action) {
1741
+ const { playerID, type } = action;
1742
+ const { currentPlayer, activePlayers, _activePlayersMaxMoves } = state.ctx;
1743
+ const move = GetMove(state.ctx, type, playerID);
1744
+ const shouldCount = !move || typeof move === 'function' || move.noLimit !== true;
1745
+ let { numMoves, _activePlayersNumMoves } = state.ctx;
1746
+ if (shouldCount) {
1747
+ if (playerID === currentPlayer)
1748
+ numMoves++;
1749
+ if (activePlayers)
1750
+ _activePlayersNumMoves[playerID]++;
1751
+ }
1752
+ state = {
1753
+ ...state,
1754
+ ctx: {
1755
+ ...state.ctx,
1756
+ numMoves,
1757
+ _activePlayersNumMoves,
1758
+ },
1759
+ };
1760
+ if (_activePlayersMaxMoves &&
1761
+ _activePlayersNumMoves[playerID] >= _activePlayersMaxMoves[playerID]) {
1762
+ state = EndStage(state, { playerID, automatic: true });
1763
+ }
1764
+ const phaseConfig = GetPhase(state.ctx);
1765
+ const G = phaseConfig.turn.wrapped.onMove({ ...state, playerID });
1766
+ state = { ...state, G };
1767
+ const events = [{ fn: OnMove }];
1768
+ return Process(state, events);
1769
+ }
1770
+ function SetStageEvent(state, playerID, arg) {
1771
+ return Process(state, [{ fn: EndStage, arg, playerID }]);
1772
+ }
1773
+ function EndStageEvent(state, playerID) {
1774
+ return Process(state, [{ fn: EndStage, playerID }]);
1775
+ }
1776
+ function SetActivePlayersEvent(state, _playerID, arg) {
1777
+ return Process(state, [{ fn: UpdateActivePlayers, arg }]);
1778
+ }
1779
+ function SetPhaseEvent(state, _playerID, newPhase) {
1780
+ return Process(state, [
1781
+ {
1782
+ fn: EndPhase,
1783
+ phase: state.ctx.phase,
1784
+ turn: state.ctx.turn,
1785
+ arg: { next: newPhase },
1786
+ },
1787
+ ]);
1788
+ }
1789
+ function EndPhaseEvent(state) {
1790
+ return Process(state, [
1791
+ { fn: EndPhase, phase: state.ctx.phase, turn: state.ctx.turn },
1792
+ ]);
1793
+ }
1794
+ function EndTurnEvent(state, _playerID, arg) {
1795
+ return Process(state, [
1796
+ { fn: EndTurn, turn: state.ctx.turn, phase: state.ctx.phase, arg },
1797
+ ]);
1798
+ }
1799
+ function PassEvent(state, _playerID, arg) {
1800
+ return Process(state, [
1801
+ {
1802
+ fn: EndTurn,
1803
+ turn: state.ctx.turn,
1804
+ phase: state.ctx.phase,
1805
+ force: true,
1806
+ arg,
1807
+ },
1808
+ ]);
1809
+ }
1810
+ function EndGameEvent(state, _playerID, arg) {
1811
+ return Process(state, [
1812
+ { fn: EndGame, turn: state.ctx.turn, phase: state.ctx.phase, arg },
1813
+ ]);
1814
+ }
1815
+ const eventHandlers = {
1816
+ endStage: EndStageEvent,
1817
+ setStage: SetStageEvent,
1818
+ endTurn: EndTurnEvent,
1819
+ pass: PassEvent,
1820
+ endPhase: EndPhaseEvent,
1821
+ setPhase: SetPhaseEvent,
1822
+ endGame: EndGameEvent,
1823
+ setActivePlayers: SetActivePlayersEvent,
1824
+ };
1825
+ const enabledEventNames = [];
1826
+ if (events.endTurn !== false) {
1827
+ enabledEventNames.push('endTurn');
1828
+ }
1829
+ if (events.pass !== false) {
1830
+ enabledEventNames.push('pass');
1831
+ }
1832
+ if (events.endPhase !== false) {
1833
+ enabledEventNames.push('endPhase');
1834
+ }
1835
+ if (events.setPhase !== false) {
1836
+ enabledEventNames.push('setPhase');
1837
+ }
1838
+ if (events.endGame !== false) {
1839
+ enabledEventNames.push('endGame');
1840
+ }
1841
+ if (events.setActivePlayers !== false) {
1842
+ enabledEventNames.push('setActivePlayers');
1843
+ }
1844
+ if (events.endStage !== false) {
1845
+ enabledEventNames.push('endStage');
1846
+ }
1847
+ if (events.setStage !== false) {
1848
+ enabledEventNames.push('setStage');
1849
+ }
1850
+ function ProcessEvent(state, action) {
1851
+ const { type, playerID, args } = action.payload;
1852
+ if (typeof eventHandlers[type] !== 'function')
1853
+ return state;
1854
+ return eventHandlers[type](state, playerID, ...(Array.isArray(args) ? args : [args]));
1855
+ }
1856
+ function IsPlayerActive(_G, ctx, playerID) {
1857
+ if (ctx.activePlayers) {
1858
+ return playerID in ctx.activePlayers;
1859
+ }
1860
+ return ctx.currentPlayer === playerID;
1861
+ }
1862
+ return {
1863
+ ctx: (numPlayers) => ({
1864
+ numPlayers,
1865
+ turn: 0,
1866
+ currentPlayer: '0',
1867
+ playOrder: [...Array.from({ length: numPlayers })].map((_, i) => i + ''),
1868
+ playOrderPos: 0,
1869
+ phase: startingPhase,
1870
+ activePlayers: null,
1871
+ }),
1872
+ init: (state) => {
1873
+ return Process(state, [{ fn: StartGame }]);
1874
+ },
1875
+ isPlayerActive: IsPlayerActive,
1876
+ eventHandlers,
1877
+ eventNames: Object.keys(eventHandlers),
1878
+ enabledEventNames,
1879
+ moveMap,
1880
+ moveNames: [...moveNames.values()],
1881
+ processMove: ProcessMove,
1882
+ processEvent: ProcessEvent,
1883
+ getMove: GetMove,
1884
+ };
1885
+ }
1886
+
1887
+ /*
1888
+ * Copyright 2017 The boardgame.io Authors
1889
+ *
1890
+ * Use of this source code is governed by a MIT-style
1891
+ * license that can be found in the LICENSE file or at
1892
+ * https://opensource.org/licenses/MIT.
1893
+ */
1894
+ function IsProcessed(game) {
1895
+ return game.processMove !== undefined;
1896
+ }
1897
+ /**
1898
+ * Helper to generate the game move reducer. The returned
1899
+ * reducer has the following signature:
1900
+ *
1901
+ * (G, action, ctx) => {}
1902
+ *
1903
+ * You can roll your own if you like, or use any Redux
1904
+ * addon to generate such a reducer.
1905
+ *
1906
+ * The convention used in this framework is to
1907
+ * have action.type contain the name of the move, and
1908
+ * action.args contain any additional arguments as an
1909
+ * Array.
1910
+ */
1911
+ function ProcessGameConfig(game) {
1912
+ // The Game() function has already been called on this
1913
+ // config object, so just pass it through.
1914
+ if (IsProcessed(game)) {
1915
+ return game;
1916
+ }
1917
+ if (game.name === undefined)
1918
+ game.name = 'default';
1919
+ if (game.deltaState === undefined)
1920
+ game.deltaState = false;
1921
+ if (game.disableUndo === undefined)
1922
+ game.disableUndo = false;
1923
+ if (game.setup === undefined)
1924
+ game.setup = () => ({});
1925
+ if (game.moves === undefined)
1926
+ game.moves = {};
1927
+ if (game.playerView === undefined)
1928
+ game.playerView = ({ G }) => G;
1929
+ if (game.plugins === undefined)
1930
+ game.plugins = [];
1931
+ game.plugins.forEach((plugin) => {
1932
+ if (plugin.name === undefined) {
1933
+ throw new Error('Plugin missing name attribute');
1934
+ }
1935
+ if (plugin.name.includes(' ')) {
1936
+ throw new Error(plugin.name + ': Plugin name must not include spaces');
1937
+ }
1938
+ });
1939
+ if (game.name.includes(' ')) {
1940
+ throw new Error(game.name + ': Game name must not include spaces');
1941
+ }
1942
+ const flow = Flow(game);
1943
+ return {
1944
+ ...game,
1945
+ flow,
1946
+ moveNames: flow.moveNames,
1947
+ pluginNames: game.plugins.map((p) => p.name),
1948
+ processMove: (state, action) => {
1949
+ let moveFn = flow.getMove(state.ctx, action.type, action.playerID);
1950
+ if (IsLongFormMove(moveFn)) {
1951
+ moveFn = moveFn.move;
1952
+ }
1953
+ if (moveFn instanceof Function) {
1954
+ const fn = FnWrap(moveFn, GameMethod.MOVE, game.plugins);
1955
+ let args = [];
1956
+ if (action.args !== undefined) {
1957
+ args = Array.isArray(action.args) ? action.args : [action.args];
1958
+ }
1959
+ const context = {
1960
+ ...GetAPIs(state),
1961
+ G: state.G,
1962
+ ctx: state.ctx,
1963
+ playerID: action.playerID,
1964
+ };
1965
+ return fn(context, ...args);
1966
+ }
1967
+ error(`invalid move object: ${action.type}`);
1968
+ return state.G;
1969
+ },
1970
+ };
1971
+ }
1972
+ function IsLongFormMove(move) {
1973
+ return move instanceof Object && move.move !== undefined;
1974
+ }
1975
+
1976
+ /*
1977
+ * Copyright 2020 The boardgame.io Authors
1978
+ *
1979
+ * Use of this source code is governed by a MIT-style
1980
+ * license that can be found in the LICENSE file or at
1981
+ * https://opensource.org/licenses/MIT.
1982
+ */
1983
+ /**
1984
+ * Creates the initial game state.
1985
+ */
1986
+ function InitializeGame({ game, numPlayers, setupData, }) {
1987
+ game = ProcessGameConfig(game);
1988
+ if (!numPlayers) {
1989
+ numPlayers = 2;
1990
+ }
1991
+ const ctx = game.flow.ctx(numPlayers);
1992
+ let state = {
1993
+ // User managed state.
1994
+ G: {},
1995
+ // Framework managed state.
1996
+ ctx,
1997
+ // Plugin related state.
1998
+ plugins: {},
1999
+ };
2000
+ // Run plugins over initial state.
2001
+ state = Setup(state, { game });
2002
+ state = Enhance(state, { game, playerID: undefined });
2003
+ const pluginAPIs = GetAPIs(state);
2004
+ state.G = game.setup({ ...pluginAPIs, ctx: state.ctx }, setupData);
2005
+ let initial = {
2006
+ ...state,
2007
+ // List of {G, ctx} pairs that can be undone.
2008
+ _undo: [],
2009
+ // List of {G, ctx} pairs that can be redone.
2010
+ _redo: [],
2011
+ // A monotonically non-decreasing ID to ensure that
2012
+ // state updates are only allowed from clients that
2013
+ // are at the same version that the server.
2014
+ _stateID: 0,
2015
+ };
2016
+ initial = game.flow.init(initial);
2017
+ [initial] = FlushAndValidate(initial, { game });
2018
+ // Initialize undo stack.
2019
+ if (!game.disableUndo) {
2020
+ initial._undo = [
2021
+ {
2022
+ G: initial.G,
2023
+ ctx: initial.ctx,
2024
+ plugins: initial.plugins,
2025
+ },
2026
+ ];
2027
+ }
2028
+ return initial;
2029
+ }
2030
+
2031
+ /**
2032
+ * Creates a new match metadata object.
2033
+ */
2034
+ const createMetadata = ({ game, unlisted, setupData, numPlayers, }) => {
2035
+ const metadata = {
2036
+ gameName: game.name,
2037
+ unlisted: !!unlisted,
2038
+ players: {},
2039
+ createdAt: Date.now(),
2040
+ updatedAt: Date.now(),
2041
+ };
2042
+ if (setupData !== undefined)
2043
+ metadata.setupData = setupData;
2044
+ for (let playerIndex = 0; playerIndex < numPlayers; playerIndex++) {
2045
+ metadata.players[playerIndex] = { id: playerIndex };
2046
+ }
2047
+ return metadata;
2048
+ };
2049
+ /**
2050
+ * Creates initial state and metadata for a new match.
2051
+ * If the provided `setupData` doesn’t pass the game’s validation,
2052
+ * an error object is returned instead.
2053
+ */
2054
+ const createMatch = ({ game, numPlayers, setupData, unlisted, }) => {
2055
+ if (!numPlayers || typeof numPlayers !== 'number')
2056
+ numPlayers = 2;
2057
+ const setupDataError = game.validateSetupData && game.validateSetupData(setupData, numPlayers);
2058
+ if (setupDataError !== undefined)
2059
+ return { setupDataError };
2060
+ const metadata = createMetadata({ game, numPlayers, setupData, unlisted });
2061
+ const initialState = InitializeGame({ game, numPlayers, setupData });
2062
+ return { metadata, initialState };
2063
+ };
2064
+ /**
2065
+ * Given players, returns the count of players.
2066
+ */
2067
+ const getNumPlayers = (players) => Object.keys(players).length;
2068
+ /**
2069
+ * Given players, tries to find the ID of the first player that can be joined.
2070
+ * Returns `undefined` if there’s no available ID.
2071
+ */
2072
+ const getFirstAvailablePlayerID = (players) => {
2073
+ const numPlayers = getNumPlayers(players);
2074
+ // Try to get the first index available
2075
+ for (let i = 0; i < numPlayers; i++) {
2076
+ if (typeof players[i].name === 'undefined' || players[i].name === null) {
2077
+ return String(i);
2078
+ }
2079
+ }
2080
+ };
2081
+
2082
+ /*
2083
+ * Copyright 2018 The boardgame.io Authors
2084
+ *
2085
+ * Use of this source code is governed by a MIT-style
2086
+ * license that can be found in the LICENSE file or at
2087
+ * https://opensource.org/licenses/MIT.
2088
+ */
2089
+ /**
2090
+ * Creates a new match.
2091
+ *
2092
+ * @param {object} db - The storage API.
2093
+ * @param {object} game - The game config object.
2094
+ * @param {number} numPlayers - The number of players.
2095
+ * @param {object} setupData - User-defined object that's available
2096
+ * during game setup.
2097
+ * @param {object } lobbyConfig - Configuration options for the lobby.
2098
+ * @param {boolean} unlisted - Whether the match should be excluded from public listing.
2099
+ */
2100
+ const CreateMatch = async ({ ctx, db, uuid, ...opts }) => {
2101
+ const matchID = uuid();
2102
+ const match = createMatch(opts);
2103
+ if ('setupDataError' in match) {
2104
+ ctx.throw(400, match.setupDataError);
2105
+ }
2106
+ else {
2107
+ await db.createMatch(matchID, match);
2108
+ return matchID;
2109
+ }
2110
+ };
2111
+ /**
2112
+ * Create a metadata object without secret credentials to return to the client.
2113
+ *
2114
+ * @param {string} matchID - The identifier of the match the metadata belongs to.
2115
+ * @param {object} metadata - The match metadata object to strip credentials from.
2116
+ * @return - A metadata object without player credentials.
2117
+ */
2118
+ const createClientMatchData = (matchID, metadata) => {
2119
+ return {
2120
+ ...metadata,
2121
+ matchID,
2122
+ players: Object.values(metadata.players).map((player) => {
2123
+ // strip away credentials
2124
+ const { credentials, ...strippedInfo } = player;
2125
+ return strippedInfo;
2126
+ }),
2127
+ };
2128
+ };
2129
+ /** Utility extracting `string` from a query if it is `string[]`. */
2130
+ const unwrapQuery = (query) => (Array.isArray(query) ? query[0] : query);
2131
+ const configureRouter = ({ router, db, auth, games, uuid = () => nanoid.nanoid(11), }) => {
2132
+ /**
2133
+ * List available games.
2134
+ *
2135
+ * @return - Array of game names as string.
2136
+ */
2137
+ router.get('/games', async (ctx) => {
2138
+ const body = games.map((game) => game.name);
2139
+ ctx.body = body;
2140
+ });
2141
+ /**
2142
+ * Create a new match of a given game.
2143
+ *
2144
+ * @param {string} name - The name of the game of the new match.
2145
+ * @param {number} numPlayers - The number of players.
2146
+ * @param {object} setupData - User-defined object that's available
2147
+ * during game setup.
2148
+ * @param {boolean} unlisted - Whether the match should be excluded from public listing.
2149
+ * @return - The ID of the created match.
2150
+ */
2151
+ router.post('/games/:name/create', koaBody__default["default"](), async (ctx) => {
2152
+ // The name of the game (for example: tic-tac-toe).
2153
+ const gameName = ctx.params.name;
2154
+ // User-data to pass to the game setup function.
2155
+ const setupData = ctx.request.body.setupData;
2156
+ // Whether the game should be excluded from public listing.
2157
+ const unlisted = ctx.request.body.unlisted;
2158
+ // The number of players for this game instance.
2159
+ const numPlayers = Number.parseInt(ctx.request.body.numPlayers);
2160
+ const game = games.find((g) => g.name === gameName);
2161
+ if (!game)
2162
+ ctx.throw(404, 'Game ' + gameName + ' not found');
2163
+ if (ctx.request.body.numPlayers !== undefined &&
2164
+ (Number.isNaN(numPlayers) ||
2165
+ (game.minPlayers && numPlayers < game.minPlayers) ||
2166
+ (game.maxPlayers && numPlayers > game.maxPlayers))) {
2167
+ ctx.throw(400, 'Invalid numPlayers');
2168
+ }
2169
+ const matchID = await CreateMatch({
2170
+ ctx,
2171
+ db,
2172
+ game,
2173
+ numPlayers,
2174
+ setupData,
2175
+ uuid,
2176
+ unlisted,
2177
+ });
2178
+ const body = { matchID };
2179
+ ctx.body = body;
2180
+ });
2181
+ /**
2182
+ * List matches for a given game.
2183
+ *
2184
+ * This does not return matches that are marked as unlisted.
2185
+ *
2186
+ * @param {string} name - The name of the game.
2187
+ * @return - Array of match objects.
2188
+ */
2189
+ router.get('/games/:name', async (ctx) => {
2190
+ const gameName = ctx.params.name;
2191
+ const isGameoverString = unwrapQuery(ctx.query.isGameover);
2192
+ const updatedBeforeString = unwrapQuery(ctx.query.updatedBefore);
2193
+ const updatedAfterString = unwrapQuery(ctx.query.updatedAfter);
2194
+ let isGameover;
2195
+ if (isGameoverString) {
2196
+ if (isGameoverString.toLowerCase() === 'true') {
2197
+ isGameover = true;
2198
+ }
2199
+ else if (isGameoverString.toLowerCase() === 'false') {
2200
+ isGameover = false;
2201
+ }
2202
+ }
2203
+ let updatedBefore;
2204
+ if (updatedBeforeString) {
2205
+ const parsedNumber = Number.parseInt(updatedBeforeString, 10);
2206
+ if (parsedNumber > 0) {
2207
+ updatedBefore = parsedNumber;
2208
+ }
2209
+ }
2210
+ let updatedAfter;
2211
+ if (updatedAfterString) {
2212
+ const parsedNumber = Number.parseInt(updatedAfterString, 10);
2213
+ if (parsedNumber > 0) {
2214
+ updatedAfter = parsedNumber;
2215
+ }
2216
+ }
2217
+ const matchList = await db.listMatches({
2218
+ gameName,
2219
+ where: {
2220
+ isGameover,
2221
+ updatedAfter,
2222
+ updatedBefore,
2223
+ },
2224
+ });
2225
+ const matches = [];
2226
+ for (const matchID of matchList) {
2227
+ const { metadata } = await db.fetch(matchID, {
2228
+ metadata: true,
2229
+ });
2230
+ if (!metadata.unlisted) {
2231
+ matches.push(createClientMatchData(matchID, metadata));
2232
+ }
2233
+ }
2234
+ const body = { matches };
2235
+ ctx.body = body;
2236
+ });
2237
+ /**
2238
+ * Get data about a specific match.
2239
+ *
2240
+ * @param {string} name - The name of the game.
2241
+ * @param {string} id - The ID of the match.
2242
+ * @return - A match object.
2243
+ */
2244
+ router.get('/games/:name/:id', async (ctx) => {
2245
+ const matchID = ctx.params.id;
2246
+ const { metadata } = await db.fetch(matchID, {
2247
+ metadata: true,
2248
+ });
2249
+ if (!metadata) {
2250
+ ctx.throw(404, 'Match ' + matchID + ' not found');
2251
+ }
2252
+ const body = createClientMatchData(matchID, metadata);
2253
+ ctx.body = body;
2254
+ });
2255
+ /**
2256
+ * Join a given match.
2257
+ *
2258
+ * @param {string} name - The name of the game.
2259
+ * @param {string} id - The ID of the match.
2260
+ * @param {string} playerID - The ID of the player who joins. If not sent, will be assigned to the first index available.
2261
+ * @param {string} playerName - The name of the player who joins.
2262
+ * @param {object} data - The default data of the player in the match.
2263
+ * @return - Player ID and credentials to use when interacting in the joined match.
2264
+ */
2265
+ router.post('/games/:name/:id/join', koaBody__default["default"](), async (ctx) => {
2266
+ let playerID = ctx.request.body.playerID;
2267
+ const playerName = ctx.request.body.playerName;
2268
+ const data = ctx.request.body.data;
2269
+ const matchID = ctx.params.id;
2270
+ if (!playerName) {
2271
+ ctx.throw(403, 'playerName is required');
2272
+ }
2273
+ const { metadata } = await db.fetch(matchID, {
2274
+ metadata: true,
2275
+ });
2276
+ if (!metadata) {
2277
+ ctx.throw(404, 'Match ' + matchID + ' not found');
2278
+ }
2279
+ if (typeof playerID === 'undefined' || playerID === null) {
2280
+ playerID = getFirstAvailablePlayerID(metadata.players);
2281
+ if (playerID === undefined) {
2282
+ const numPlayers = getNumPlayers(metadata.players);
2283
+ ctx.throw(409, `Match ${matchID} reached maximum number of players (${numPlayers})`);
2284
+ }
2285
+ }
2286
+ if (!metadata.players[playerID]) {
2287
+ ctx.throw(404, 'Player ' + playerID + ' not found');
2288
+ }
2289
+ if (metadata.players[playerID].name) {
2290
+ ctx.throw(409, 'Player ' + playerID + ' not available');
2291
+ }
2292
+ if (data) {
2293
+ metadata.players[playerID].data = data;
2294
+ }
2295
+ metadata.players[playerID].name = playerName;
2296
+ const playerCredentials = await auth.generateCredentials(ctx);
2297
+ metadata.players[playerID].credentials = playerCredentials;
2298
+ await db.setMetadata(matchID, metadata);
2299
+ const body = { playerID, playerCredentials };
2300
+ ctx.body = body;
2301
+ });
2302
+ /**
2303
+ * Leave a given match.
2304
+ *
2305
+ * @param {string} name - The name of the game.
2306
+ * @param {string} id - The ID of the match.
2307
+ * @param {string} playerID - The ID of the player who leaves.
2308
+ * @param {string} credentials - The credentials of the player who leaves.
2309
+ * @return - Nothing.
2310
+ */
2311
+ router.post('/games/:name/:id/leave', koaBody__default["default"](), async (ctx) => {
2312
+ const matchID = ctx.params.id;
2313
+ const playerID = ctx.request.body.playerID;
2314
+ const credentials = ctx.request.body.credentials;
2315
+ const { metadata } = await db.fetch(matchID, {
2316
+ metadata: true,
2317
+ });
2318
+ if (typeof playerID === 'undefined' || playerID === null) {
2319
+ ctx.throw(403, 'playerID is required');
2320
+ }
2321
+ if (!metadata) {
2322
+ ctx.throw(404, 'Match ' + matchID + ' not found');
2323
+ }
2324
+ if (!metadata.players[playerID]) {
2325
+ ctx.throw(404, 'Player ' + playerID + ' not found');
2326
+ }
2327
+ const isAuthorized = await auth.authenticateCredentials({
2328
+ playerID,
2329
+ credentials,
2330
+ metadata,
2331
+ });
2332
+ if (!isAuthorized) {
2333
+ ctx.throw(403, 'Invalid credentials ' + credentials);
2334
+ }
2335
+ delete metadata.players[playerID].name;
2336
+ delete metadata.players[playerID].credentials;
2337
+ const hasPlayers = Object.values(metadata.players).some(({ name }) => name);
2338
+ await (hasPlayers
2339
+ ? db.setMetadata(matchID, metadata) // Update metadata.
2340
+ : db.wipe(matchID)); // Delete match.
2341
+ ctx.body = {};
2342
+ });
2343
+ /**
2344
+ * Start a new match based on another existing match.
2345
+ *
2346
+ * @param {string} name - The name of the game.
2347
+ * @param {string} id - The ID of the match.
2348
+ * @param {string} playerID - The ID of the player creating the match.
2349
+ * @param {string} credentials - The credentials of the player creating the match.
2350
+ * @param {boolean} unlisted - Whether the match should be excluded from public listing.
2351
+ * @return - The ID of the new match.
2352
+ */
2353
+ router.post('/games/:name/:id/playAgain', koaBody__default["default"](), async (ctx) => {
2354
+ const gameName = ctx.params.name;
2355
+ const matchID = ctx.params.id;
2356
+ const playerID = ctx.request.body.playerID;
2357
+ const credentials = ctx.request.body.credentials;
2358
+ const unlisted = ctx.request.body.unlisted;
2359
+ const { metadata } = await db.fetch(matchID, {
2360
+ metadata: true,
2361
+ });
2362
+ if (typeof playerID === 'undefined' || playerID === null) {
2363
+ ctx.throw(403, 'playerID is required');
2364
+ }
2365
+ if (!metadata) {
2366
+ ctx.throw(404, 'Match ' + matchID + ' not found');
2367
+ }
2368
+ if (!metadata.players[playerID]) {
2369
+ ctx.throw(404, 'Player ' + playerID + ' not found');
2370
+ }
2371
+ const isAuthorized = await auth.authenticateCredentials({
2372
+ playerID,
2373
+ credentials,
2374
+ metadata,
2375
+ });
2376
+ if (!isAuthorized) {
2377
+ ctx.throw(403, 'Invalid credentials ' + credentials);
2378
+ }
2379
+ // Check if nextMatch is already set, if so, return that id.
2380
+ if (metadata.nextMatchID) {
2381
+ ctx.body = { nextMatchID: metadata.nextMatchID };
2382
+ return;
2383
+ }
2384
+ // User-data to pass to the game setup function.
2385
+ const setupData = ctx.request.body.setupData || metadata.setupData;
2386
+ // The number of players for this game instance.
2387
+ const numPlayers = Number.parseInt(ctx.request.body.numPlayers) ||
2388
+ // eslint-disable-next-line unicorn/explicit-length-check
2389
+ Object.keys(metadata.players).length;
2390
+ const game = games.find((g) => g.name === gameName);
2391
+ const nextMatchID = await CreateMatch({
2392
+ ctx,
2393
+ db,
2394
+ game,
2395
+ numPlayers,
2396
+ setupData,
2397
+ uuid,
2398
+ unlisted,
2399
+ });
2400
+ metadata.nextMatchID = nextMatchID;
2401
+ await db.setMetadata(matchID, metadata);
2402
+ const body = { nextMatchID };
2403
+ ctx.body = body;
2404
+ });
2405
+ const updatePlayerMetadata = async (ctx) => {
2406
+ const matchID = ctx.params.id;
2407
+ const playerID = ctx.request.body.playerID;
2408
+ const credentials = ctx.request.body.credentials;
2409
+ const newName = ctx.request.body.newName;
2410
+ const data = ctx.request.body.data;
2411
+ const { metadata } = await db.fetch(matchID, {
2412
+ metadata: true,
2413
+ });
2414
+ if (typeof playerID === 'undefined') {
2415
+ ctx.throw(403, 'playerID is required');
2416
+ }
2417
+ if (data === undefined && !newName) {
2418
+ ctx.throw(403, 'newName or data is required');
2419
+ }
2420
+ if (newName && typeof newName !== 'string') {
2421
+ ctx.throw(403, `newName must be a string, got ${typeof newName}`);
2422
+ }
2423
+ if (!metadata) {
2424
+ ctx.throw(404, 'Match ' + matchID + ' not found');
2425
+ }
2426
+ if (!metadata.players[playerID]) {
2427
+ ctx.throw(404, 'Player ' + playerID + ' not found');
2428
+ }
2429
+ const isAuthorized = await auth.authenticateCredentials({
2430
+ playerID,
2431
+ credentials,
2432
+ metadata,
2433
+ });
2434
+ if (!isAuthorized) {
2435
+ ctx.throw(403, 'Invalid credentials ' + credentials);
2436
+ }
2437
+ if (newName) {
2438
+ metadata.players[playerID].name = newName;
2439
+ }
2440
+ if (data) {
2441
+ metadata.players[playerID].data = data;
2442
+ }
2443
+ await db.setMetadata(matchID, metadata);
2444
+ ctx.body = {};
2445
+ };
2446
+ /**
2447
+ * Change the name of a player in a given match.
2448
+ *
2449
+ * @param {string} name - The name of the game.
2450
+ * @param {string} id - The ID of the match.
2451
+ * @param {string} playerID - The ID of the player.
2452
+ * @param {string} credentials - The credentials of the player.
2453
+ * @param {object} newName - The new name of the player in the match.
2454
+ * @return - Nothing.
2455
+ */
2456
+ router.post('/games/:name/:id/rename', koaBody__default["default"](), async (ctx) => {
2457
+ console.warn('This endpoint /rename is deprecated. Please use /update instead.');
2458
+ await updatePlayerMetadata(ctx);
2459
+ });
2460
+ /**
2461
+ * Update the player's data for a given match.
2462
+ *
2463
+ * @param {string} name - The name of the game.
2464
+ * @param {string} id - The ID of the match.
2465
+ * @param {string} playerID - The ID of the player.
2466
+ * @param {string} credentials - The credentials of the player.
2467
+ * @param {object} newName - The new name of the player in the match.
2468
+ * @param {object} data - The new data of the player in the match.
2469
+ * @return - Nothing.
2470
+ */
2471
+ router.post('/games/:name/:id/update', koaBody__default["default"](), updatePlayerMetadata);
2472
+ return router;
2473
+ };
2474
+ const configureApp = (app, router, origins) => {
2475
+ app.use(cors__default["default"]({
2476
+ // Set Access-Control-Allow-Origin header for allowed origins.
2477
+ origin: (ctx) => {
2478
+ const origin = ctx.get('Origin');
2479
+ return isOriginAllowed(origin, origins) ? origin : '';
2480
+ },
2481
+ }));
2482
+ // If API_SECRET is set, then require that requests set an
2483
+ // api-secret header that is set to the same value.
2484
+ app.use(async (ctx, next) => {
2485
+ if (!!process.env.API_SECRET &&
2486
+ ctx.request.headers['api-secret'] !== process.env.API_SECRET) {
2487
+ ctx.throw(403, 'Invalid API secret');
2488
+ }
2489
+ await next();
2490
+ });
2491
+ app.use(router.routes()).use(router.allowedMethods());
2492
+ };
2493
+ /**
2494
+ * Check if a request’s origin header is allowed for CORS.
2495
+ * Adapted from `cors` package: https://github.com/expressjs/cors
2496
+ * @param origin Request origin to test.
2497
+ * @param allowedOrigin Origin(s) that are allowed to connect via CORS.
2498
+ * @returns `true` if the origin matched at least one of the allowed origins.
2499
+ */
2500
+ function isOriginAllowed(origin, allowedOrigin) {
2501
+ if (Array.isArray(allowedOrigin)) {
2502
+ for (const entry of allowedOrigin) {
2503
+ if (isOriginAllowed(origin, entry)) {
2504
+ return true;
2505
+ }
2506
+ }
2507
+ return false;
2508
+ }
2509
+ else if (typeof allowedOrigin === 'string') {
2510
+ return origin === allowedOrigin;
2511
+ }
2512
+ else if (allowedOrigin instanceof RegExp) {
2513
+ return allowedOrigin.test(origin);
2514
+ }
2515
+ else {
2516
+ return !!allowedOrigin;
2517
+ }
2518
+ }
2519
+
2520
+ var Type;
2521
+ (function (Type) {
2522
+ Type[Type["SYNC"] = 0] = "SYNC";
2523
+ Type[Type["ASYNC"] = 1] = "ASYNC";
2524
+ })(Type || (Type = {}));
2525
+ /**
2526
+ * Type guard that checks if a storage implementation is synchronous.
2527
+ */
2528
+ function isSynchronous(storageAPI) {
2529
+ return storageAPI.type() === Type.SYNC;
2530
+ }
2531
+ class Async {
2532
+ /* istanbul ignore next */
2533
+ type() {
2534
+ /* istanbul ignore next */
2535
+ return Type.ASYNC;
2536
+ }
2537
+ /**
2538
+ * Create a new match.
2539
+ *
2540
+ * This might just need to call setState and setMetadata in
2541
+ * most implementations.
2542
+ *
2543
+ * However, it exists as a separate call so that the
2544
+ * implementation can provision things differently when
2545
+ * a match is created. For example, it might stow away the
2546
+ * initial match state in a separate field for easier retrieval.
2547
+ */
2548
+ /* istanbul ignore next */
2549
+ async createMatch(matchID, opts) {
2550
+ if (this.createGame) {
2551
+ console.warn('The database connector does not implement a createMatch method.', '\nUsing the deprecated createGame method instead.');
2552
+ return this.createGame(matchID, opts);
2553
+ }
2554
+ else {
2555
+ console.error('The database connector does not implement a createMatch method.');
2556
+ }
2557
+ }
2558
+ /**
2559
+ * Return all matches.
2560
+ */
2561
+ /* istanbul ignore next */
2562
+ async listMatches(opts) {
2563
+ if (this.listGames) {
2564
+ console.warn('The database connector does not implement a listMatches method.', '\nUsing the deprecated listGames method instead.');
2565
+ return this.listGames(opts);
2566
+ }
2567
+ else {
2568
+ console.error('The database connector does not implement a listMatches method.');
2569
+ }
2570
+ }
2571
+ }
2572
+ class Sync {
2573
+ type() {
2574
+ return Type.SYNC;
2575
+ }
2576
+ /**
2577
+ * Connect.
2578
+ */
2579
+ connect() {
2580
+ return;
2581
+ }
2582
+ /**
2583
+ * Create a new match.
2584
+ *
2585
+ * This might just need to call setState and setMetadata in
2586
+ * most implementations.
2587
+ *
2588
+ * However, it exists as a separate call so that the
2589
+ * implementation can provision things differently when
2590
+ * a match is created. For example, it might stow away the
2591
+ * initial match state in a separate field for easier retrieval.
2592
+ */
2593
+ /* istanbul ignore next */
2594
+ createMatch(matchID, opts) {
2595
+ if (this.createGame) {
2596
+ console.warn('The database connector does not implement a createMatch method.', '\nUsing the deprecated createGame method instead.');
2597
+ return this.createGame(matchID, opts);
2598
+ }
2599
+ else {
2600
+ console.error('The database connector does not implement a createMatch method.');
2601
+ }
2602
+ }
2603
+ /**
2604
+ * Return all matches.
2605
+ */
2606
+ /* istanbul ignore next */
2607
+ listMatches(opts) {
2608
+ if (this.listGames) {
2609
+ console.warn('The database connector does not implement a listMatches method.', '\nUsing the deprecated listGames method instead.');
2610
+ return this.listGames(opts);
2611
+ }
2612
+ else {
2613
+ console.error('The database connector does not implement a listMatches method.');
2614
+ }
2615
+ }
2616
+ }
2617
+
2618
+ /*
2619
+ * Copyright 2017 The boardgame.io Authors
2620
+ *
2621
+ * Use of this source code is governed by a MIT-style
2622
+ * license that can be found in the LICENSE file or at
2623
+ * https://opensource.org/licenses/MIT.
2624
+ */
2625
+ /**
2626
+ * InMemory data storage.
2627
+ */
2628
+ class InMemory extends Sync {
2629
+ /**
2630
+ * Creates a new InMemory storage.
2631
+ */
2632
+ constructor() {
2633
+ super();
2634
+ this.state = new Map();
2635
+ this.initial = new Map();
2636
+ this.metadata = new Map();
2637
+ this.log = new Map();
2638
+ }
2639
+ /**
2640
+ * Create a new match.
2641
+ *
2642
+ * @override
2643
+ */
2644
+ createMatch(matchID, opts) {
2645
+ this.initial.set(matchID, opts.initialState);
2646
+ this.setState(matchID, opts.initialState);
2647
+ this.setMetadata(matchID, opts.metadata);
2648
+ }
2649
+ /**
2650
+ * Write the match metadata to the in-memory object.
2651
+ */
2652
+ setMetadata(matchID, metadata) {
2653
+ this.metadata.set(matchID, metadata);
2654
+ }
2655
+ /**
2656
+ * Write the match state to the in-memory object.
2657
+ */
2658
+ setState(matchID, state, deltalog) {
2659
+ if (deltalog && deltalog.length > 0) {
2660
+ const log = this.log.get(matchID) || [];
2661
+ this.log.set(matchID, [...log, ...deltalog]);
2662
+ }
2663
+ this.state.set(matchID, state);
2664
+ }
2665
+ /**
2666
+ * Fetches state for a particular matchID.
2667
+ */
2668
+ fetch(matchID, opts) {
2669
+ const result = {};
2670
+ if (opts.state) {
2671
+ result.state = this.state.get(matchID);
2672
+ }
2673
+ if (opts.metadata) {
2674
+ result.metadata = this.metadata.get(matchID);
2675
+ }
2676
+ if (opts.log) {
2677
+ result.log = this.log.get(matchID) || [];
2678
+ }
2679
+ if (opts.initialState) {
2680
+ result.initialState = this.initial.get(matchID);
2681
+ }
2682
+ return result;
2683
+ }
2684
+ /**
2685
+ * Remove the match state from the in-memory object.
2686
+ */
2687
+ wipe(matchID) {
2688
+ this.state.delete(matchID);
2689
+ this.metadata.delete(matchID);
2690
+ }
2691
+ /**
2692
+ * Return all keys.
2693
+ *
2694
+ * @override
2695
+ */
2696
+ listMatches(opts) {
2697
+ return [...this.metadata.entries()]
2698
+ .filter(([, metadata]) => {
2699
+ if (!opts) {
2700
+ return true;
2701
+ }
2702
+ if (opts.gameName !== undefined &&
2703
+ metadata.gameName !== opts.gameName) {
2704
+ return false;
2705
+ }
2706
+ if (opts.where !== undefined) {
2707
+ if (opts.where.isGameover !== undefined) {
2708
+ const isGameover = metadata.gameover !== undefined;
2709
+ if (isGameover !== opts.where.isGameover) {
2710
+ return false;
2711
+ }
2712
+ }
2713
+ if (opts.where.updatedBefore !== undefined &&
2714
+ metadata.updatedAt >= opts.where.updatedBefore) {
2715
+ return false;
2716
+ }
2717
+ if (opts.where.updatedAfter !== undefined &&
2718
+ metadata.updatedAt <= opts.where.updatedAfter) {
2719
+ return false;
2720
+ }
2721
+ }
2722
+ return true;
2723
+ })
2724
+ .map(([key]) => key);
2725
+ }
2726
+ }
2727
+
2728
+ /**
2729
+ * FlatFile data storage.
2730
+ */
2731
+ class FlatFile extends Async {
2732
+ constructor({ dir, logging, ttl }) {
2733
+ super();
2734
+ this.games = require('node-persist');
2735
+ this.dir = dir;
2736
+ this.logging = logging || false;
2737
+ this.ttl = ttl || false;
2738
+ this.fileQueues = {};
2739
+ }
2740
+ async chainRequest(key, request) {
2741
+ if (!(key in this.fileQueues))
2742
+ this.fileQueues[key] = Promise.resolve();
2743
+ this.fileQueues[key] = this.fileQueues[key].then(request, request);
2744
+ return this.fileQueues[key];
2745
+ }
2746
+ async getItem(key) {
2747
+ return this.chainRequest(key, () => this.games.getItem(key));
2748
+ }
2749
+ async setItem(key, value) {
2750
+ return this.chainRequest(key, () => this.games.setItem(key, value));
2751
+ }
2752
+ async removeItem(key) {
2753
+ return this.chainRequest(key, () => this.games.removeItem(key));
2754
+ }
2755
+ async connect() {
2756
+ await this.games.init({
2757
+ dir: this.dir,
2758
+ logging: this.logging,
2759
+ ttl: this.ttl,
2760
+ });
2761
+ return;
2762
+ }
2763
+ /**
2764
+ * Create new match.
2765
+ *
2766
+ * @param matchID
2767
+ * @param opts
2768
+ * @override
2769
+ */
2770
+ async createMatch(matchID, opts) {
2771
+ // Store initial state separately for easy retrieval later.
2772
+ const key = InitialStateKey(matchID);
2773
+ await this.setItem(key, opts.initialState);
2774
+ await this.setState(matchID, opts.initialState);
2775
+ await this.setMetadata(matchID, opts.metadata);
2776
+ }
2777
+ async fetch(matchID, opts) {
2778
+ const result = {};
2779
+ if (opts.state) {
2780
+ result.state = (await this.getItem(matchID));
2781
+ }
2782
+ if (opts.metadata) {
2783
+ const key = MetadataKey(matchID);
2784
+ result.metadata = (await this.getItem(key));
2785
+ }
2786
+ if (opts.log) {
2787
+ const key = LogKey(matchID);
2788
+ result.log = (await this.getItem(key));
2789
+ }
2790
+ if (opts.initialState) {
2791
+ const key = InitialStateKey(matchID);
2792
+ result.initialState = (await this.getItem(key));
2793
+ }
2794
+ return result;
2795
+ }
2796
+ async clear() {
2797
+ return this.games.clear();
2798
+ }
2799
+ async setState(id, state, deltalog) {
2800
+ if (deltalog && deltalog.length > 0) {
2801
+ const key = LogKey(id);
2802
+ const log = (await this.getItem(key)) || [];
2803
+ await this.setItem(key, [...log, ...deltalog]);
2804
+ }
2805
+ return await this.setItem(id, state);
2806
+ }
2807
+ async setMetadata(id, metadata) {
2808
+ const key = MetadataKey(id);
2809
+ return await this.setItem(key, metadata);
2810
+ }
2811
+ async wipe(id) {
2812
+ const keys = await this.games.keys();
2813
+ if (!keys.includes(id))
2814
+ return;
2815
+ await this.removeItem(id);
2816
+ await this.removeItem(InitialStateKey(id));
2817
+ await this.removeItem(LogKey(id));
2818
+ await this.removeItem(MetadataKey(id));
2819
+ }
2820
+ /**
2821
+ * List matches IDs.
2822
+ *
2823
+ * @param opts
2824
+ * @override
2825
+ */
2826
+ async listMatches(opts) {
2827
+ const keys = await this.games.keys();
2828
+ const suffix = ':metadata';
2829
+ const arr = await Promise.all(keys.map(async (k) => {
2830
+ if (!k.endsWith(suffix)) {
2831
+ return false;
2832
+ }
2833
+ const matchID = k.slice(0, k.length - suffix.length);
2834
+ if (!opts) {
2835
+ return matchID;
2836
+ }
2837
+ const game = await this.fetch(matchID, {
2838
+ state: true,
2839
+ metadata: true,
2840
+ });
2841
+ if (opts.gameName && opts.gameName !== game.metadata.gameName) {
2842
+ return false;
2843
+ }
2844
+ if (opts.where !== undefined) {
2845
+ if (typeof opts.where.isGameover !== 'undefined') {
2846
+ const isGameover = typeof game.metadata.gameover !== 'undefined';
2847
+ if (isGameover !== opts.where.isGameover) {
2848
+ return false;
2849
+ }
2850
+ }
2851
+ if (typeof opts.where.updatedBefore !== 'undefined' &&
2852
+ game.metadata.updatedAt >= opts.where.updatedBefore) {
2853
+ return false;
2854
+ }
2855
+ if (typeof opts.where.updatedAfter !== 'undefined' &&
2856
+ game.metadata.updatedAt <= opts.where.updatedAfter) {
2857
+ return false;
2858
+ }
2859
+ }
2860
+ return matchID;
2861
+ }));
2862
+ return arr.filter((r) => typeof r === 'string');
2863
+ }
2864
+ }
2865
+ function InitialStateKey(matchID) {
2866
+ return `${matchID}:initial`;
2867
+ }
2868
+ function MetadataKey(matchID) {
2869
+ return `${matchID}:metadata`;
2870
+ }
2871
+ function LogKey(matchID) {
2872
+ return `${matchID}:log`;
2873
+ }
2874
+
2875
+ const DBFromEnv = () => {
2876
+ return process.env.FLATFILE_DIR
2877
+ ? new FlatFile({
2878
+ dir: process.env.FLATFILE_DIR,
2879
+ })
2880
+ : new InMemory();
2881
+ };
2882
+
2883
+ /**
2884
+ * Verifies that a match has metadata and is using credentials.
2885
+ */
2886
+ const doesMatchRequireAuthentication = (matchData) => {
2887
+ if (!matchData)
2888
+ return false;
2889
+ const { players } = matchData;
2890
+ const hasCredentials = Object.values(players).some((player) => !!(player && player.credentials));
2891
+ return hasCredentials;
2892
+ };
2893
+ /**
2894
+ * The default `authenticateCredentials` method.
2895
+ * Verifies that the provided credentials match the player’s metadata.
2896
+ */
2897
+ const areCredentialsAuthentic = (actionCredentials, playerMetadata) => {
2898
+ if (!actionCredentials)
2899
+ return false;
2900
+ if (!playerMetadata)
2901
+ return false;
2902
+ return actionCredentials === playerMetadata.credentials;
2903
+ };
2904
+ /**
2905
+ * Extracts a player’s metadata from the match data object.
2906
+ */
2907
+ const extractPlayerMetadata = (matchData, playerID) => {
2908
+ if (matchData && matchData.players) {
2909
+ return matchData.players[playerID];
2910
+ }
2911
+ };
2912
+ /**
2913
+ * Class that provides authentication methods to the lobby server & transport.
2914
+ */
2915
+ class Auth {
2916
+ constructor(opts = {}) {
2917
+ this.shouldAuthenticate = doesMatchRequireAuthentication;
2918
+ this.authenticate = areCredentialsAuthentic;
2919
+ /**
2920
+ * Generate credentials string from the Koa context.
2921
+ */
2922
+ this.generateCredentials = () => nanoid.nanoid();
2923
+ if (typeof opts.authenticateCredentials === 'function') {
2924
+ this.authenticate = opts.authenticateCredentials;
2925
+ this.shouldAuthenticate = () => true;
2926
+ }
2927
+ if (typeof opts.generateCredentials === 'function') {
2928
+ this.generateCredentials = opts.generateCredentials;
2929
+ }
2930
+ }
2931
+ /**
2932
+ * Resolves to true if the provided credentials are valid for the given
2933
+ * metadata and player IDs, or if the match does not require authentication.
2934
+ */
2935
+ authenticateCredentials({ playerID, credentials, metadata, }) {
2936
+ const playerMetadata = extractPlayerMetadata(metadata, playerID);
2937
+ return this.shouldAuthenticate(metadata)
2938
+ ? this.authenticate(credentials, playerMetadata)
2939
+ : true;
2940
+ }
2941
+ }
2942
+
2943
+ /*
2944
+ * Copyright 2017 The boardgame.io Authors
2945
+ *
2946
+ * Use of this source code is governed by a MIT-style
2947
+ * license that can be found in the LICENSE file or at
2948
+ * https://opensource.org/licenses/MIT.
2949
+ */
2950
+ var UpdateErrorType;
2951
+ (function (UpdateErrorType) {
2952
+ // The action’s credentials were missing or invalid
2953
+ UpdateErrorType["UnauthorizedAction"] = "update/unauthorized_action";
2954
+ // The action’s matchID was not found
2955
+ UpdateErrorType["MatchNotFound"] = "update/match_not_found";
2956
+ // Could not apply Patch operation (rfc6902).
2957
+ UpdateErrorType["PatchFailed"] = "update/patch_failed";
2958
+ })(UpdateErrorType || (UpdateErrorType = {}));
2959
+ var ActionErrorType;
2960
+ (function (ActionErrorType) {
2961
+ // The action contained a stale state ID
2962
+ ActionErrorType["StaleStateId"] = "action/stale_state_id";
2963
+ // The requested move is unknown or not currently available
2964
+ ActionErrorType["UnavailableMove"] = "action/unavailable_move";
2965
+ // The move declared it was invalid (INVALID_MOVE constant)
2966
+ ActionErrorType["InvalidMove"] = "action/invalid_move";
2967
+ // The player making the action is not currently active
2968
+ ActionErrorType["InactivePlayer"] = "action/inactive_player";
2969
+ // The game has finished
2970
+ ActionErrorType["GameOver"] = "action/gameover";
2971
+ // The requested action is disabled (e.g. undo/redo, events)
2972
+ ActionErrorType["ActionDisabled"] = "action/action_disabled";
2973
+ // The requested action is not currently possible
2974
+ ActionErrorType["ActionInvalid"] = "action/action_invalid";
2975
+ // The requested action was declared invalid by a plugin
2976
+ ActionErrorType["PluginActionInvalid"] = "action/plugin_invalid";
2977
+ })(ActionErrorType || (ActionErrorType = {}));
2978
+
2979
+ /*
2980
+ * Copyright 2017 The boardgame.io Authors
2981
+ *
2982
+ * Use of this source code is governed by a MIT-style
2983
+ * license that can be found in the LICENSE file or at
2984
+ * https://opensource.org/licenses/MIT.
2985
+ */
2986
+ /**
2987
+ * Check if the payload for the passed action contains a playerID.
2988
+ */
2989
+ const actionHasPlayerID = (action) => action.payload.playerID !== null && action.payload.playerID !== undefined;
2990
+ /**
2991
+ * Returns true if a move can be undone.
2992
+ */
2993
+ const CanUndoMove = (G, ctx, move) => {
2994
+ function HasUndoable(move) {
2995
+ return move.undoable !== undefined;
2996
+ }
2997
+ function IsFunction(undoable) {
2998
+ return undoable instanceof Function;
2999
+ }
3000
+ if (!HasUndoable(move)) {
3001
+ return true;
3002
+ }
3003
+ if (IsFunction(move.undoable)) {
3004
+ return move.undoable({ G, ctx });
3005
+ }
3006
+ return move.undoable;
3007
+ };
3008
+ /**
3009
+ * Update the undo and redo stacks for a move or event.
3010
+ */
3011
+ function updateUndoRedoState(state, opts) {
3012
+ if (opts.game.disableUndo)
3013
+ return state;
3014
+ const undoEntry = {
3015
+ G: state.G,
3016
+ ctx: state.ctx,
3017
+ plugins: state.plugins,
3018
+ playerID: opts.action.payload.playerID || state.ctx.currentPlayer,
3019
+ };
3020
+ if (opts.action.type === 'MAKE_MOVE') {
3021
+ undoEntry.moveType = opts.action.payload.type;
3022
+ }
3023
+ return {
3024
+ ...state,
3025
+ _undo: [...state._undo, undoEntry],
3026
+ // Always reset redo stack when making a move or event
3027
+ _redo: [],
3028
+ };
3029
+ }
3030
+ /**
3031
+ * Process state, adding the initial deltalog for this action.
3032
+ */
3033
+ function initializeDeltalog(state, action, move) {
3034
+ // Create a log entry for this action.
3035
+ const logEntry = {
3036
+ action,
3037
+ _stateID: state._stateID,
3038
+ turn: state.ctx.turn,
3039
+ phase: state.ctx.phase,
3040
+ };
3041
+ const pluginLogMetadata = state.plugins.log.data.metadata;
3042
+ if (pluginLogMetadata !== undefined) {
3043
+ logEntry.metadata = pluginLogMetadata;
3044
+ }
3045
+ if (typeof move === 'object' && move.redact === true) {
3046
+ logEntry.redact = true;
3047
+ }
3048
+ else if (typeof move === 'object' && move.redact instanceof Function) {
3049
+ logEntry.redact = move.redact({ G: state.G, ctx: state.ctx });
3050
+ }
3051
+ return {
3052
+ ...state,
3053
+ deltalog: [logEntry],
3054
+ };
3055
+ }
3056
+ /**
3057
+ * Update plugin state after move/event & check if plugins consider the action to be valid.
3058
+ * @param state Current version of state in the reducer.
3059
+ * @param oldState State to revert to in case of error.
3060
+ * @param pluginOpts Plugin configuration options.
3061
+ * @returns Tuple of the new state updated after flushing plugins and the old
3062
+ * state augmented with an error if a plugin declared the action invalid.
3063
+ */
3064
+ function flushAndValidatePlugins(state, oldState, pluginOpts) {
3065
+ const [newState, isInvalid] = FlushAndValidate(state, pluginOpts);
3066
+ if (!isInvalid)
3067
+ return [newState];
3068
+ return [
3069
+ newState,
3070
+ WithError(oldState, ActionErrorType.PluginActionInvalid, isInvalid),
3071
+ ];
3072
+ }
3073
+ /**
3074
+ * ExtractTransientsFromState
3075
+ *
3076
+ * Split out transients from the a TransientState
3077
+ */
3078
+ function ExtractTransients(transientState) {
3079
+ if (!transientState) {
3080
+ // We preserve null for the state for legacy callers, but the transient
3081
+ // field should be undefined if not present to be consistent with the
3082
+ // code path below.
3083
+ return [null, undefined];
3084
+ }
3085
+ const { transients, ...state } = transientState;
3086
+ return [state, transients];
3087
+ }
3088
+ /**
3089
+ * WithError
3090
+ *
3091
+ * Augment a State instance with transient error information.
3092
+ */
3093
+ function WithError(state, errorType, payload) {
3094
+ const error = {
3095
+ type: errorType,
3096
+ payload,
3097
+ };
3098
+ return {
3099
+ ...state,
3100
+ transients: {
3101
+ error,
3102
+ },
3103
+ };
3104
+ }
3105
+ /**
3106
+ * Middleware for processing TransientState associated with the reducer
3107
+ * returned by CreateGameReducer.
3108
+ * This should pretty much be used everywhere you want realistic state
3109
+ * transitions and error handling.
3110
+ */
3111
+ const TransientHandlingMiddleware = (store) => (next) => (action) => {
3112
+ const result = next(action);
3113
+ switch (action.type) {
3114
+ case STRIP_TRANSIENTS: {
3115
+ return result;
3116
+ }
3117
+ default: {
3118
+ const [, transients] = ExtractTransients(store.getState());
3119
+ if (typeof transients !== 'undefined') {
3120
+ store.dispatch(stripTransients());
3121
+ // Dev Note: If parent middleware needs to correlate the spawned
3122
+ // StripTransients action to the triggering action, instrument here.
3123
+ //
3124
+ // This is a bit tricky; for more details, see:
3125
+ // https://github.com/boardgameio/boardgame.io/pull/940#discussion_r636200648
3126
+ return {
3127
+ ...result,
3128
+ transients,
3129
+ };
3130
+ }
3131
+ return result;
3132
+ }
3133
+ }
3134
+ };
3135
+ /**
3136
+ * CreateGameReducer
3137
+ *
3138
+ * Creates the main game state reducer.
3139
+ */
3140
+ function CreateGameReducer({ game, isClient, }) {
3141
+ game = ProcessGameConfig(game);
3142
+ /**
3143
+ * GameReducer
3144
+ *
3145
+ * Redux reducer that maintains the overall game state.
3146
+ * @param {object} state - The state before the action.
3147
+ * @param {object} action - A Redux action.
3148
+ */
3149
+ return (stateWithTransients = null, action) => {
3150
+ let [state /*, transients */] = ExtractTransients(stateWithTransients);
3151
+ switch (action.type) {
3152
+ case STRIP_TRANSIENTS: {
3153
+ // This action indicates that transient metadata in the state has been
3154
+ // consumed and should now be stripped from the state..
3155
+ return state;
3156
+ }
3157
+ case GAME_EVENT: {
3158
+ state = { ...state, deltalog: [] };
3159
+ // Process game events only on the server.
3160
+ // These events like `endTurn` typically
3161
+ // contain code that may rely on secret state
3162
+ // and cannot be computed on the client.
3163
+ if (isClient) {
3164
+ return state;
3165
+ }
3166
+ // Disallow events once the game is over.
3167
+ if (state.ctx.gameover !== undefined) {
3168
+ error(`cannot call event after game end`);
3169
+ return WithError(state, ActionErrorType.GameOver);
3170
+ }
3171
+ // Ignore the event if the player isn't active.
3172
+ if (actionHasPlayerID(action) &&
3173
+ !game.flow.isPlayerActive(state.G, state.ctx, action.payload.playerID)) {
3174
+ error(`disallowed event: ${action.payload.type}`);
3175
+ return WithError(state, ActionErrorType.InactivePlayer);
3176
+ }
3177
+ // Execute plugins.
3178
+ state = Enhance(state, {
3179
+ game,
3180
+ isClient: false,
3181
+ playerID: action.payload.playerID,
3182
+ });
3183
+ // Process event.
3184
+ let newState = game.flow.processEvent(state, action);
3185
+ // Execute plugins.
3186
+ let stateWithError;
3187
+ [newState, stateWithError] = flushAndValidatePlugins(newState, state, {
3188
+ game,
3189
+ isClient: false,
3190
+ });
3191
+ if (stateWithError)
3192
+ return stateWithError;
3193
+ // Update undo / redo state.
3194
+ newState = updateUndoRedoState(newState, { game, action });
3195
+ return { ...newState, _stateID: state._stateID + 1 };
3196
+ }
3197
+ case MAKE_MOVE: {
3198
+ const oldState = (state = { ...state, deltalog: [] });
3199
+ // Check whether the move is allowed at this time.
3200
+ const move = game.flow.getMove(state.ctx, action.payload.type, action.payload.playerID || state.ctx.currentPlayer);
3201
+ if (move === null) {
3202
+ error(`disallowed move: ${action.payload.type}`);
3203
+ return WithError(state, ActionErrorType.UnavailableMove);
3204
+ }
3205
+ // Don't run move on client if move says so.
3206
+ if (isClient && move.client === false) {
3207
+ return state;
3208
+ }
3209
+ // Disallow moves once the game is over.
3210
+ if (state.ctx.gameover !== undefined) {
3211
+ error(`cannot make move after game end`);
3212
+ return WithError(state, ActionErrorType.GameOver);
3213
+ }
3214
+ // Ignore the move if the player isn't active.
3215
+ if (actionHasPlayerID(action) &&
3216
+ !game.flow.isPlayerActive(state.G, state.ctx, action.payload.playerID)) {
3217
+ error(`disallowed move: ${action.payload.type}`);
3218
+ return WithError(state, ActionErrorType.InactivePlayer);
3219
+ }
3220
+ // Execute plugins.
3221
+ state = Enhance(state, {
3222
+ game,
3223
+ isClient,
3224
+ playerID: action.payload.playerID,
3225
+ });
3226
+ // Process the move.
3227
+ const G = game.processMove(state, action.payload);
3228
+ // The game declared the move as invalid.
3229
+ if (G === INVALID_MOVE) {
3230
+ error(`invalid move: ${action.payload.type} args: ${action.payload.args}`);
3231
+ // TODO(#723): Marshal a nice error payload with the processed move.
3232
+ return WithError(state, ActionErrorType.InvalidMove);
3233
+ }
3234
+ const newState = { ...state, G };
3235
+ // Some plugin indicated that it is not suitable to be
3236
+ // materialized on the client (and must wait for the server
3237
+ // response instead).
3238
+ if (isClient && NoClient(newState, { game })) {
3239
+ return state;
3240
+ }
3241
+ state = newState;
3242
+ // If we're on the client, just process the move
3243
+ // and no triggers in multiplayer mode.
3244
+ // These will be processed on the server, which
3245
+ // will send back a state update.
3246
+ if (isClient) {
3247
+ let stateWithError;
3248
+ [state, stateWithError] = flushAndValidatePlugins(state, oldState, {
3249
+ game,
3250
+ isClient: true,
3251
+ });
3252
+ if (stateWithError)
3253
+ return stateWithError;
3254
+ return {
3255
+ ...state,
3256
+ _stateID: state._stateID + 1,
3257
+ };
3258
+ }
3259
+ // On the server, construct the deltalog.
3260
+ state = initializeDeltalog(state, action, move);
3261
+ // Allow the flow reducer to process any triggers that happen after moves.
3262
+ state = game.flow.processMove(state, action.payload);
3263
+ let stateWithError;
3264
+ [state, stateWithError] = flushAndValidatePlugins(state, oldState, {
3265
+ game,
3266
+ });
3267
+ if (stateWithError)
3268
+ return stateWithError;
3269
+ // Update undo / redo state.
3270
+ state = updateUndoRedoState(state, { game, action });
3271
+ return {
3272
+ ...state,
3273
+ _stateID: state._stateID + 1,
3274
+ };
3275
+ }
3276
+ case RESET:
3277
+ case UPDATE:
3278
+ case SYNC: {
3279
+ return action.state;
3280
+ }
3281
+ case UNDO: {
3282
+ state = { ...state, deltalog: [] };
3283
+ if (game.disableUndo) {
3284
+ error('Undo is not enabled');
3285
+ return WithError(state, ActionErrorType.ActionDisabled);
3286
+ }
3287
+ const { G, ctx, _undo, _redo, _stateID } = state;
3288
+ if (_undo.length < 2) {
3289
+ error(`No moves to undo`);
3290
+ return WithError(state, ActionErrorType.ActionInvalid);
3291
+ }
3292
+ const last = _undo[_undo.length - 1];
3293
+ const restore = _undo[_undo.length - 2];
3294
+ // Only allow players to undo their own moves.
3295
+ if (actionHasPlayerID(action) &&
3296
+ action.payload.playerID !== last.playerID) {
3297
+ error(`Cannot undo other players' moves`);
3298
+ return WithError(state, ActionErrorType.ActionInvalid);
3299
+ }
3300
+ // If undoing a move, check it is undoable.
3301
+ if (last.moveType) {
3302
+ const lastMove = game.flow.getMove(restore.ctx, last.moveType, last.playerID);
3303
+ if (!CanUndoMove(G, ctx, lastMove)) {
3304
+ error(`Move cannot be undone`);
3305
+ return WithError(state, ActionErrorType.ActionInvalid);
3306
+ }
3307
+ }
3308
+ state = initializeDeltalog(state, action);
3309
+ return {
3310
+ ...state,
3311
+ G: restore.G,
3312
+ ctx: restore.ctx,
3313
+ plugins: restore.plugins,
3314
+ _stateID: _stateID + 1,
3315
+ _undo: _undo.slice(0, -1),
3316
+ _redo: [last, ..._redo],
3317
+ };
3318
+ }
3319
+ case REDO: {
3320
+ state = { ...state, deltalog: [] };
3321
+ if (game.disableUndo) {
3322
+ error('Redo is not enabled');
3323
+ return WithError(state, ActionErrorType.ActionDisabled);
3324
+ }
3325
+ const { _undo, _redo, _stateID } = state;
3326
+ if (_redo.length === 0) {
3327
+ error(`No moves to redo`);
3328
+ return WithError(state, ActionErrorType.ActionInvalid);
3329
+ }
3330
+ const first = _redo[0];
3331
+ // Only allow players to redo their own undos.
3332
+ if (actionHasPlayerID(action) &&
3333
+ action.payload.playerID !== first.playerID) {
3334
+ error(`Cannot redo other players' moves`);
3335
+ return WithError(state, ActionErrorType.ActionInvalid);
3336
+ }
3337
+ state = initializeDeltalog(state, action);
3338
+ return {
3339
+ ...state,
3340
+ G: first.G,
3341
+ ctx: first.ctx,
3342
+ plugins: first.plugins,
3343
+ _stateID: _stateID + 1,
3344
+ _undo: [..._undo, first],
3345
+ _redo: _redo.slice(1),
3346
+ };
3347
+ }
3348
+ case PLUGIN: {
3349
+ // TODO(#723): Expose error semantics to plugin processing.
3350
+ return ProcessAction(state, action, { game });
3351
+ }
3352
+ case PATCH: {
3353
+ const oldState = state;
3354
+ const newState = JSON.parse(JSON.stringify(oldState));
3355
+ const patchError = rfc6902.applyPatch(newState, action.patch);
3356
+ const hasError = patchError.some((entry) => entry !== null);
3357
+ if (hasError) {
3358
+ error(`Patch ${JSON.stringify(action.patch)} apply failed`);
3359
+ return WithError(oldState, UpdateErrorType.PatchFailed, patchError);
3360
+ }
3361
+ else {
3362
+ return newState;
3363
+ }
3364
+ }
3365
+ default: {
3366
+ return state;
3367
+ }
3368
+ }
3369
+ };
3370
+ }
3371
+
3372
+ /*
3373
+ * Copyright 2018 The boardgame.io Authors
3374
+ *
3375
+ * Use of this source code is governed by a MIT-style
3376
+ * license that can be found in the LICENSE file or at
3377
+ * https://opensource.org/licenses/MIT.
3378
+ */
3379
+ /**
3380
+ * Filter match data to get a player metadata object with credentials stripped.
3381
+ */
3382
+ const filterMatchData = (matchData) => Object.values(matchData.players).map((player) => {
3383
+ const { credentials, ...filteredData } = player;
3384
+ return filteredData;
3385
+ });
3386
+ /**
3387
+ * Remove player credentials from action payload
3388
+ */
3389
+ const stripCredentialsFromAction = (action) => {
3390
+ const { credentials, ...payload } = action.payload;
3391
+ return { ...action, payload };
3392
+ };
3393
+ /**
3394
+ * Master
3395
+ *
3396
+ * Class that runs the game and maintains the authoritative state.
3397
+ * It uses the transportAPI to communicate with clients and the
3398
+ * storageAPI to communicate with the database.
3399
+ */
3400
+ class Master {
3401
+ constructor(game, storageAPI, transportAPI, auth) {
3402
+ this.game = ProcessGameConfig(game);
3403
+ this.storageAPI = storageAPI;
3404
+ this.transportAPI = transportAPI;
3405
+ this.subscribeCallback = () => { };
3406
+ this.auth = auth;
3407
+ }
3408
+ subscribe(fn) {
3409
+ this.subscribeCallback = fn;
3410
+ }
3411
+ /**
3412
+ * Called on each move / event made by the client.
3413
+ * Computes the new value of the game state and returns it
3414
+ * along with a deltalog.
3415
+ */
3416
+ async onUpdate(credAction, stateID, matchID, playerID) {
3417
+ if (!credAction || !credAction.payload) {
3418
+ return { error: 'missing action or action payload' };
3419
+ }
3420
+ let metadata;
3421
+ if (isSynchronous(this.storageAPI)) {
3422
+ ({ metadata } = this.storageAPI.fetch(matchID, { metadata: true }));
3423
+ }
3424
+ else {
3425
+ ({ metadata } = await this.storageAPI.fetch(matchID, { metadata: true }));
3426
+ }
3427
+ if (this.auth) {
3428
+ const isAuthentic = await this.auth.authenticateCredentials({
3429
+ playerID,
3430
+ credentials: credAction.payload.credentials,
3431
+ metadata,
3432
+ });
3433
+ if (!isAuthentic) {
3434
+ return { error: 'unauthorized action' };
3435
+ }
3436
+ }
3437
+ const action = stripCredentialsFromAction(credAction);
3438
+ const key = matchID;
3439
+ let state;
3440
+ if (isSynchronous(this.storageAPI)) {
3441
+ ({ state } = this.storageAPI.fetch(key, { state: true }));
3442
+ }
3443
+ else {
3444
+ ({ state } = await this.storageAPI.fetch(key, { state: true }));
3445
+ }
3446
+ if (state === undefined) {
3447
+ error(`game not found, matchID=[${key}]`);
3448
+ return { error: 'game not found' };
3449
+ }
3450
+ if (state.ctx.gameover !== undefined) {
3451
+ error(`game over - matchID=[${key}] - playerID=[${playerID}]` +
3452
+ ` - action[${action.payload.type}]`);
3453
+ return;
3454
+ }
3455
+ const reducer = CreateGameReducer({
3456
+ game: this.game,
3457
+ });
3458
+ const middleware = redux.applyMiddleware(TransientHandlingMiddleware);
3459
+ const store = redux.createStore(reducer, state, middleware);
3460
+ // Only allow UNDO / REDO if there is exactly one player
3461
+ // that can make moves right now and the person doing the
3462
+ // action is that player.
3463
+ if (action.type == UNDO || action.type == REDO) {
3464
+ const hasActivePlayers = state.ctx.activePlayers !== null;
3465
+ const isCurrentPlayer = state.ctx.currentPlayer === playerID;
3466
+ if (
3467
+ // If activePlayers is empty, non-current players can’t undo.
3468
+ (!hasActivePlayers && !isCurrentPlayer) ||
3469
+ // If player is not active or multiple players are active, can’t undo.
3470
+ (hasActivePlayers &&
3471
+ (state.ctx.activePlayers[playerID] === undefined ||
3472
+ Object.keys(state.ctx.activePlayers).length > 1))) {
3473
+ error(`playerID=[${playerID}] cannot undo / redo right now`);
3474
+ return;
3475
+ }
3476
+ }
3477
+ // Check whether the player is active.
3478
+ if (!this.game.flow.isPlayerActive(state.G, state.ctx, playerID)) {
3479
+ error(`player not active - playerID=[${playerID}]` +
3480
+ ` - action[${action.payload.type}]`);
3481
+ return;
3482
+ }
3483
+ // Get move for further checks
3484
+ const move = action.type == MAKE_MOVE
3485
+ ? this.game.flow.getMove(state.ctx, action.payload.type, playerID)
3486
+ : null;
3487
+ // Check whether the player is allowed to make the move.
3488
+ if (action.type == MAKE_MOVE && !move) {
3489
+ error(`move not processed - canPlayerMakeMove=false - playerID=[${playerID}]` +
3490
+ ` - action[${action.payload.type}]`);
3491
+ return;
3492
+ }
3493
+ // Check if action's stateID is different than store's stateID
3494
+ // and if move does not have ignoreStaleStateID truthy.
3495
+ if (state._stateID !== stateID &&
3496
+ !(move && IsLongFormMove(move) && move.ignoreStaleStateID)) {
3497
+ error(`invalid stateID, was=[${stateID}], expected=[${state._stateID}]` +
3498
+ ` - playerID=[${playerID}] - action[${action.payload.type}]`);
3499
+ return;
3500
+ }
3501
+ const prevState = store.getState();
3502
+ // Update server's version of the store.
3503
+ store.dispatch(action);
3504
+ state = store.getState();
3505
+ this.subscribeCallback({
3506
+ state,
3507
+ action,
3508
+ matchID,
3509
+ });
3510
+ if (this.game.deltaState) {
3511
+ this.transportAPI.sendAll({
3512
+ type: 'patch',
3513
+ args: [matchID, stateID, prevState, state],
3514
+ });
3515
+ }
3516
+ else {
3517
+ this.transportAPI.sendAll({
3518
+ type: 'update',
3519
+ args: [matchID, state],
3520
+ });
3521
+ }
3522
+ const { deltalog, ...stateWithoutDeltalog } = state;
3523
+ let newMetadata;
3524
+ if (metadata &&
3525
+ (metadata.gameover === undefined || metadata.gameover === null)) {
3526
+ newMetadata = {
3527
+ ...metadata,
3528
+ updatedAt: Date.now(),
3529
+ };
3530
+ if (state.ctx.gameover !== undefined) {
3531
+ newMetadata.gameover = state.ctx.gameover;
3532
+ }
3533
+ }
3534
+ if (isSynchronous(this.storageAPI)) {
3535
+ this.storageAPI.setState(key, stateWithoutDeltalog, deltalog);
3536
+ if (newMetadata)
3537
+ this.storageAPI.setMetadata(key, newMetadata);
3538
+ }
3539
+ else {
3540
+ const writes = [
3541
+ this.storageAPI.setState(key, stateWithoutDeltalog, deltalog),
3542
+ ];
3543
+ if (newMetadata) {
3544
+ writes.push(this.storageAPI.setMetadata(key, newMetadata));
3545
+ }
3546
+ await Promise.all(writes);
3547
+ }
3548
+ }
3549
+ /**
3550
+ * Called when the client connects / reconnects.
3551
+ * Returns the latest game state and the entire log.
3552
+ */
3553
+ async onSync(matchID, playerID, credentials, numPlayers = 2) {
3554
+ const key = matchID;
3555
+ const fetchOpts = {
3556
+ state: true,
3557
+ metadata: true,
3558
+ log: true,
3559
+ initialState: true,
3560
+ };
3561
+ const fetchResult = isSynchronous(this.storageAPI)
3562
+ ? this.storageAPI.fetch(key, fetchOpts)
3563
+ : await this.storageAPI.fetch(key, fetchOpts);
3564
+ let { state, initialState, log, metadata } = fetchResult;
3565
+ if (this.auth && playerID !== undefined && playerID !== null) {
3566
+ const isAuthentic = await this.auth.authenticateCredentials({
3567
+ playerID,
3568
+ credentials,
3569
+ metadata,
3570
+ });
3571
+ if (!isAuthentic) {
3572
+ return { error: 'unauthorized' };
3573
+ }
3574
+ }
3575
+ // If the game doesn't exist, then create one on demand.
3576
+ // TODO: Move this out of the sync call.
3577
+ if (state === undefined) {
3578
+ const match = createMatch({
3579
+ game: this.game,
3580
+ unlisted: true,
3581
+ numPlayers,
3582
+ setupData: undefined,
3583
+ });
3584
+ if ('setupDataError' in match) {
3585
+ return { error: 'game requires setupData' };
3586
+ }
3587
+ initialState = state = match.initialState;
3588
+ metadata = match.metadata;
3589
+ this.subscribeCallback({ state, matchID });
3590
+ if (isSynchronous(this.storageAPI)) {
3591
+ this.storageAPI.createMatch(key, { initialState, metadata });
3592
+ }
3593
+ else {
3594
+ await this.storageAPI.createMatch(key, { initialState, metadata });
3595
+ }
3596
+ }
3597
+ const filteredMetadata = metadata ? filterMatchData(metadata) : undefined;
3598
+ const syncInfo = {
3599
+ state,
3600
+ log,
3601
+ filteredMetadata,
3602
+ initialState,
3603
+ };
3604
+ this.transportAPI.send({
3605
+ playerID,
3606
+ type: 'sync',
3607
+ args: [matchID, syncInfo],
3608
+ });
3609
+ return;
3610
+ }
3611
+ /**
3612
+ * Called when a client connects or disconnects.
3613
+ * Updates and sends out metadata to reflect the player’s connection status.
3614
+ */
3615
+ async onConnectionChange(matchID, playerID, credentials, connected) {
3616
+ const key = matchID;
3617
+ // Ignore changes for clients without a playerID, e.g. spectators.
3618
+ if (playerID === undefined || playerID === null) {
3619
+ return;
3620
+ }
3621
+ let metadata;
3622
+ if (isSynchronous(this.storageAPI)) {
3623
+ ({ metadata } = this.storageAPI.fetch(key, { metadata: true }));
3624
+ }
3625
+ else {
3626
+ ({ metadata } = await this.storageAPI.fetch(key, { metadata: true }));
3627
+ }
3628
+ if (metadata === undefined) {
3629
+ error(`metadata not found for matchID=[${key}]`);
3630
+ return { error: 'metadata not found' };
3631
+ }
3632
+ if (metadata.players[playerID] === undefined) {
3633
+ error(`Player not in the match, matchID=[${key}] playerID=[${playerID}]`);
3634
+ return { error: 'player not in the match' };
3635
+ }
3636
+ if (this.auth) {
3637
+ const isAuthentic = await this.auth.authenticateCredentials({
3638
+ playerID,
3639
+ credentials,
3640
+ metadata,
3641
+ });
3642
+ if (!isAuthentic) {
3643
+ return { error: 'unauthorized' };
3644
+ }
3645
+ }
3646
+ metadata.players[playerID].isConnected = connected;
3647
+ const filteredMetadata = filterMatchData(metadata);
3648
+ this.transportAPI.sendAll({
3649
+ type: 'matchData',
3650
+ args: [matchID, filteredMetadata],
3651
+ });
3652
+ if (isSynchronous(this.storageAPI)) {
3653
+ this.storageAPI.setMetadata(key, metadata);
3654
+ }
3655
+ else {
3656
+ await this.storageAPI.setMetadata(key, metadata);
3657
+ }
3658
+ }
3659
+ async onChatMessage(matchID, chatMessage, credentials) {
3660
+ const key = matchID;
3661
+ if (this.auth) {
3662
+ const { metadata } = await this.storageAPI.fetch(key, {
3663
+ metadata: true,
3664
+ });
3665
+ if (!(chatMessage && typeof chatMessage.sender === 'string')) {
3666
+ return { error: 'unauthorized' };
3667
+ }
3668
+ const isAuthentic = await this.auth.authenticateCredentials({
3669
+ playerID: chatMessage.sender,
3670
+ credentials,
3671
+ metadata,
3672
+ });
3673
+ if (!isAuthentic) {
3674
+ return { error: 'unauthorized' };
3675
+ }
3676
+ }
3677
+ this.transportAPI.sendAll({
3678
+ type: 'chat',
3679
+ args: [matchID, chatMessage],
3680
+ });
3681
+ }
3682
+ }
3683
+
3684
+ const applyPlayerView = (game, playerID, state) => ({
3685
+ ...state,
3686
+ G: game.playerView({ G: state.G, ctx: state.ctx, playerID }),
3687
+ plugins: PlayerView(state, { playerID, game }),
3688
+ deltalog: undefined,
3689
+ _undo: [],
3690
+ _redo: [],
3691
+ });
3692
+ /** Gets a function that filters the TransportData for a given player and game. */
3693
+ const getFilterPlayerView = (game) => (playerID, payload) => {
3694
+ switch (payload.type) {
3695
+ case 'patch': {
3696
+ const [matchID, stateID, prevState, state] = payload.args;
3697
+ const log = redactLog(state.deltalog, playerID);
3698
+ const filteredState = applyPlayerView(game, playerID, state);
3699
+ const newStateID = state._stateID;
3700
+ const prevFilteredState = applyPlayerView(game, playerID, prevState);
3701
+ const patch = rfc6902.createPatch(prevFilteredState, filteredState);
3702
+ return {
3703
+ type: 'patch',
3704
+ args: [matchID, stateID, newStateID, patch, log],
3705
+ };
3706
+ }
3707
+ case 'update': {
3708
+ const [matchID, state] = payload.args;
3709
+ const log = redactLog(state.deltalog, playerID);
3710
+ const filteredState = applyPlayerView(game, playerID, state);
3711
+ return {
3712
+ type: 'update',
3713
+ args: [matchID, filteredState, log],
3714
+ };
3715
+ }
3716
+ case 'sync': {
3717
+ const [matchID, syncInfo] = payload.args;
3718
+ const filteredState = applyPlayerView(game, playerID, syncInfo.state);
3719
+ const log = redactLog(syncInfo.log, playerID);
3720
+ const newSyncInfo = {
3721
+ ...syncInfo,
3722
+ state: filteredState,
3723
+ log,
3724
+ };
3725
+ return {
3726
+ type: 'sync',
3727
+ args: [matchID, newSyncInfo],
3728
+ };
3729
+ }
3730
+ default: {
3731
+ return payload;
3732
+ }
3733
+ }
3734
+ };
3735
+ /**
3736
+ * Redact the log.
3737
+ *
3738
+ * @param {Array} log - The game log (or deltalog).
3739
+ * @param {String} playerID - The playerID that this log is
3740
+ * to be sent to.
3741
+ */
3742
+ function redactLog(log, playerID) {
3743
+ if (log === undefined) {
3744
+ return log;
3745
+ }
3746
+ return log.map((logEvent) => {
3747
+ // filter for all other players and spectators.
3748
+ if (playerID !== null && +playerID === +logEvent.action.payload.playerID) {
3749
+ return logEvent;
3750
+ }
3751
+ if (logEvent.redact !== true) {
3752
+ return logEvent;
3753
+ }
3754
+ const payload = {
3755
+ ...logEvent.action.payload,
3756
+ args: null,
3757
+ };
3758
+ const filteredEvent = {
3759
+ ...logEvent,
3760
+ action: { ...logEvent.action, payload },
3761
+ };
3762
+ const { redact, ...remaining } = filteredEvent;
3763
+ return remaining;
3764
+ });
3765
+ }
3766
+
3767
+ class InMemoryPubSub {
3768
+ constructor() {
3769
+ this.callbacks = new Map();
3770
+ }
3771
+ publish(channelId, payload) {
3772
+ if (!this.callbacks.has(channelId)) {
3773
+ return;
3774
+ }
3775
+ const allCallbacks = this.callbacks.get(channelId);
3776
+ for (const callback of allCallbacks) {
3777
+ callback(payload);
3778
+ }
3779
+ }
3780
+ subscribe(channelId, callback) {
3781
+ if (!this.callbacks.has(channelId)) {
3782
+ this.callbacks.set(channelId, []);
3783
+ }
3784
+ this.callbacks.get(channelId).push(callback);
3785
+ }
3786
+ unsubscribeAll(channelId) {
3787
+ if (this.callbacks.has(channelId)) {
3788
+ this.callbacks.delete(channelId);
3789
+ }
3790
+ }
3791
+ }
3792
+
3793
+ /*
3794
+ * Copyright 2018 The boardgame.io Authors
3795
+ *
3796
+ * Use of this source code is governed by a MIT-style
3797
+ * license that can be found in the LICENSE file or at
3798
+ * https://opensource.org/licenses/MIT.
3799
+ */
3800
+ const PING_TIMEOUT = 20 * 1e3;
3801
+ const PING_INTERVAL = 10 * 1e3;
3802
+ const emit = (socket, { type, args }) => {
3803
+ socket.emit(type, ...args);
3804
+ };
3805
+ function getPubSubChannelId(matchID) {
3806
+ return `MATCH-${matchID}`;
3807
+ }
3808
+ /**
3809
+ * API that's exposed by SocketIO for the Master to send
3810
+ * information to the clients.
3811
+ */
3812
+ const TransportAPI = (matchID, socket, filterPlayerView, pubSub) => {
3813
+ const send = ({ playerID, ...data }) => {
3814
+ emit(socket, filterPlayerView(playerID, data));
3815
+ };
3816
+ /**
3817
+ * Send a message to all clients.
3818
+ */
3819
+ const sendAll = (payload) => {
3820
+ pubSub.publish(getPubSubChannelId(matchID), payload);
3821
+ };
3822
+ return { send, sendAll };
3823
+ };
3824
+ /**
3825
+ * Transport interface that uses socket.io
3826
+ */
3827
+ class SocketIO {
3828
+ constructor({ https, socketAdapter, socketOpts, pubSub } = {}) {
3829
+ this.clientInfo = new Map();
3830
+ this.roomInfo = new Map();
3831
+ this.perMatchQueue = new Map();
3832
+ this.https = https;
3833
+ this.socketAdapter = socketAdapter;
3834
+ this.socketOpts = socketOpts;
3835
+ this.pubSub = pubSub || new InMemoryPubSub();
3836
+ }
3837
+ /**
3838
+ * Unregister client data for a socket.
3839
+ */
3840
+ removeClient(socketID) {
3841
+ // Get client data for this socket ID.
3842
+ const client = this.clientInfo.get(socketID);
3843
+ if (!client)
3844
+ return;
3845
+ // Remove client from list of connected sockets for this match.
3846
+ const { matchID } = client;
3847
+ const matchClients = this.roomInfo.get(matchID);
3848
+ matchClients.delete(socketID);
3849
+ // If the match is now empty, delete its promise queue & client ID list.
3850
+ if (matchClients.size === 0) {
3851
+ this.unsubscribePubSubChannel(matchID);
3852
+ this.roomInfo.delete(matchID);
3853
+ this.deleteMatchQueue(matchID);
3854
+ }
3855
+ // Remove client data from the client map.
3856
+ this.clientInfo.delete(socketID);
3857
+ }
3858
+ /**
3859
+ * Register client data for a socket.
3860
+ */
3861
+ addClient(client, game) {
3862
+ const { matchID, socket } = client;
3863
+ // Add client to list of connected sockets for this match.
3864
+ let matchClients = this.roomInfo.get(matchID);
3865
+ if (matchClients === undefined) {
3866
+ this.subscribePubSubChannel(matchID, game);
3867
+ matchClients = new Set();
3868
+ this.roomInfo.set(matchID, matchClients);
3869
+ }
3870
+ matchClients.add(socket.id);
3871
+ // Register data for this socket in the client map.
3872
+ this.clientInfo.set(socket.id, client);
3873
+ }
3874
+ subscribePubSubChannel(matchID, game) {
3875
+ const filterPlayerView = getFilterPlayerView(game);
3876
+ const broadcast = (payload) => {
3877
+ this.roomInfo.get(matchID).forEach((clientID) => {
3878
+ const client = this.clientInfo.get(clientID);
3879
+ const data = filterPlayerView(client.playerID, payload);
3880
+ emit(client.socket, data);
3881
+ });
3882
+ };
3883
+ this.pubSub.subscribe(getPubSubChannelId(matchID), broadcast);
3884
+ }
3885
+ unsubscribePubSubChannel(matchID) {
3886
+ this.pubSub.unsubscribeAll(getPubSubChannelId(matchID));
3887
+ }
3888
+ init(app, games, origins = []) {
3889
+ const io = new IO__default["default"]({
3890
+ ioOptions: {
3891
+ pingTimeout: PING_TIMEOUT,
3892
+ pingInterval: PING_INTERVAL,
3893
+ cors: {
3894
+ origins,
3895
+ },
3896
+ ...this.socketOpts,
3897
+ },
3898
+ });
3899
+ app.context.io = io;
3900
+ io.attach(app, !!this.https, this.https);
3901
+ if (this.socketAdapter) {
3902
+ io.adapter(this.socketAdapter);
3903
+ }
3904
+ for (const game of games) {
3905
+ const nsp = app._io.of(game.name);
3906
+ const filterPlayerView = getFilterPlayerView(game);
3907
+ nsp.on('connection', (socket) => {
3908
+ socket.on('update', async (...args) => {
3909
+ const [action, stateID, matchID, playerID] = args;
3910
+ const master = new Master(game, app.context.db, TransportAPI(matchID, socket, filterPlayerView, this.pubSub), app.context.auth);
3911
+ const matchQueue = this.getMatchQueue(matchID);
3912
+ await matchQueue.add(() => master.onUpdate(action, stateID, matchID, playerID));
3913
+ });
3914
+ socket.on('sync', async (...args) => {
3915
+ const [matchID, playerID, credentials] = args;
3916
+ socket.join(matchID);
3917
+ this.removeClient(socket.id);
3918
+ const requestingClient = { socket, matchID, playerID, credentials };
3919
+ const transport = TransportAPI(matchID, socket, filterPlayerView, this.pubSub);
3920
+ const master = new Master(game, app.context.db, transport, app.context.auth);
3921
+ const syncResponse = await master.onSync(...args);
3922
+ if (syncResponse && syncResponse.error === 'unauthorized') {
3923
+ return;
3924
+ }
3925
+ this.addClient(requestingClient, game);
3926
+ await master.onConnectionChange(matchID, playerID, credentials, true);
3927
+ });
3928
+ socket.on('disconnect', async () => {
3929
+ const client = this.clientInfo.get(socket.id);
3930
+ this.removeClient(socket.id);
3931
+ if (client) {
3932
+ const { matchID, playerID, credentials } = client;
3933
+ const master = new Master(game, app.context.db, TransportAPI(matchID, socket, filterPlayerView, this.pubSub), app.context.auth);
3934
+ await master.onConnectionChange(matchID, playerID, credentials, false);
3935
+ }
3936
+ });
3937
+ socket.on('chat', async (...args) => {
3938
+ const [matchID] = args;
3939
+ const master = new Master(game, app.context.db, TransportAPI(matchID, socket, filterPlayerView, this.pubSub), app.context.auth);
3940
+ master.onChatMessage(...args);
3941
+ });
3942
+ });
3943
+ }
3944
+ }
3945
+ /**
3946
+ * Create a PQueue for a given matchID if none exists and return it.
3947
+ * @param matchID
3948
+ * @returns
3949
+ */
3950
+ getMatchQueue(matchID) {
3951
+ if (!this.perMatchQueue.has(matchID)) {
3952
+ // PQueue should process only one action at a time.
3953
+ this.perMatchQueue.set(matchID, new PQueue__default["default"]({ concurrency: 1 }));
3954
+ }
3955
+ return this.perMatchQueue.get(matchID);
3956
+ }
3957
+ /**
3958
+ * Delete a PQueue for a given matchID.
3959
+ * @param matchID
3960
+ */
3961
+ deleteMatchQueue(matchID) {
3962
+ this.perMatchQueue.delete(matchID);
3963
+ }
3964
+ }
3965
+
3966
+ /*
3967
+ * Copyright 2017 The boardgame.io Authors
3968
+ *
3969
+ * Use of this source code is governed by a MIT-style
3970
+ * license that can be found in the LICENSE file or at
3971
+ * https://opensource.org/licenses/MIT.
3972
+ */
3973
+ /**
3974
+ * Build config object from server run arguments.
3975
+ */
3976
+ const createServerRunConfig = (portOrConfig, callback) => portOrConfig && typeof portOrConfig === 'object'
3977
+ ? {
3978
+ ...portOrConfig,
3979
+ callback: portOrConfig.callback || callback,
3980
+ }
3981
+ : { port: portOrConfig, callback };
3982
+ const getPortFromServer = (server) => {
3983
+ const address = server.address();
3984
+ if (typeof address === 'string')
3985
+ return address;
3986
+ if (address === null)
3987
+ return null;
3988
+ return address.port;
3989
+ };
3990
+ /**
3991
+ * Instantiate a game server.
3992
+ *
3993
+ * @param games - The games that this server will handle.
3994
+ * @param db - The interface with the database.
3995
+ * @param transport - The interface with the clients.
3996
+ * @param authenticateCredentials - Function to test player credentials.
3997
+ * @param origins - Allowed origins to use this server, e.g. `['http://localhost:3000']`.
3998
+ * @param apiOrigins - Allowed origins to use the Lobby API, defaults to `origins`.
3999
+ * @param generateCredentials - Method for API to generate player credentials.
4000
+ * @param https - HTTPS configuration options passed through to the TLS module.
4001
+ * @param lobbyConfig - Configuration options for the Lobby API server.
4002
+ */
4003
+ function Server({ games, db, transport, https, uuid, origins, apiOrigins = origins, generateCredentials = uuid, authenticateCredentials, }) {
4004
+ const app = new Koa__default["default"]();
4005
+ games = games.map((game) => ProcessGameConfig(game));
4006
+ if (db === undefined) {
4007
+ db = DBFromEnv();
4008
+ }
4009
+ app.context.db = db;
4010
+ const auth = new Auth({ authenticateCredentials, generateCredentials });
4011
+ app.context.auth = auth;
4012
+ if (transport === undefined) {
4013
+ transport = new SocketIO({ https });
4014
+ }
4015
+ if (origins === undefined) {
4016
+ console.warn('Server `origins` option is not set.\n' +
4017
+ 'Since boardgame.io@0.45, CORS is not enabled by default and you must ' +
4018
+ 'explicitly set the origins that are allowed to connect to the server.\n' +
4019
+ 'See https://boardgame.io/documentation/#/api/Server');
4020
+ }
4021
+ transport.init(app, games, origins);
4022
+ const router = new Router__default["default"]();
4023
+ return {
4024
+ app,
4025
+ db,
4026
+ auth,
4027
+ router,
4028
+ transport,
4029
+ run: async (portOrConfig, callback) => {
4030
+ const serverRunConfig = createServerRunConfig(portOrConfig, callback);
4031
+ configureRouter({ router, db, games, uuid, auth });
4032
+ // DB
4033
+ await db.connect();
4034
+ // Lobby API
4035
+ const lobbyConfig = serverRunConfig.lobbyConfig;
4036
+ let apiServer;
4037
+ if (!lobbyConfig || !lobbyConfig.apiPort) {
4038
+ configureApp(app, router, apiOrigins);
4039
+ }
4040
+ else {
4041
+ // Run API in a separate Koa app.
4042
+ const api = new Koa__default["default"]();
4043
+ api.context.db = db;
4044
+ api.context.auth = auth;
4045
+ configureApp(api, router, apiOrigins);
4046
+ await new Promise((resolve) => {
4047
+ apiServer = api.listen(lobbyConfig.apiPort, resolve);
4048
+ });
4049
+ if (lobbyConfig.apiCallback)
4050
+ lobbyConfig.apiCallback();
4051
+ info(`API serving on ${getPortFromServer(apiServer)}...`);
4052
+ }
4053
+ // Run Game Server (+ API, if necessary).
4054
+ let appServer;
4055
+ await new Promise((resolve) => {
4056
+ appServer = app.listen(serverRunConfig.port, resolve);
4057
+ });
4058
+ if (serverRunConfig.callback)
4059
+ serverRunConfig.callback();
4060
+ info(`App serving on ${getPortFromServer(appServer)}...`);
4061
+ return { apiServer, appServer };
4062
+ },
4063
+ kill: (servers) => {
4064
+ if (servers.apiServer) {
4065
+ servers.apiServer.close();
4066
+ }
4067
+ servers.appServer.close();
4068
+ },
4069
+ };
4070
+ }
4071
+
4072
+ const LOCALHOST = /localhost:\d+/;
4073
+ const Origins = {
4074
+ LOCALHOST,
4075
+ LOCALHOST_IN_DEVELOPMENT: process.env.NODE_ENV === 'production' ? false : LOCALHOST,
4076
+ };
4077
+
4078
+ exports.FlatFile = FlatFile;
4079
+ exports.Master = Master;
4080
+ exports.Origins = Origins;
4081
+ exports.Server = Server;
4082
+ exports.SocketIO = SocketIO;
4083
+ exports.TransportAPI = TransportAPI;
4084
+ exports.configureApp = configureApp;
4085
+ exports.configureRouter = configureRouter;
4086
+ exports.createServerRunConfig = createServerRunConfig;
4087
+ exports.getPortFromServer = getPortFromServer;