@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,433 @@
1
+ /*
2
+ * Copyright 2018 The boardgame.io Authors
3
+ *
4
+ * Use of this source code is governed by a MIT-style
5
+ * license that can be found in the LICENSE file or at
6
+ * https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ import { InitializeGame } from '../core/initialize';
10
+ import { Client } from '../client/client';
11
+ import { MAKE_MOVE, GAME_EVENT } from '../core/action-types';
12
+ import { makeMove } from '../core/action-creators';
13
+ import { Step, Simulate } from './ai';
14
+ import { RandomBot } from './random-bot';
15
+ import { MCTSBot } from './mcts-bot';
16
+ import type { Node } from './mcts-bot';
17
+ import { ProcessGameConfig } from '../core/game';
18
+ import { Stage } from '../core/turn-order';
19
+ import type { AnyFn, Game, Ctx } from '../types';
20
+
21
+ function IsVictory(cells) {
22
+ const positions = [
23
+ [0, 1, 2],
24
+ [3, 4, 5],
25
+ [6, 7, 8],
26
+ [0, 3, 6],
27
+ [1, 4, 7],
28
+ [2, 5, 8],
29
+ [0, 4, 8],
30
+ [2, 4, 6],
31
+ ];
32
+
33
+ const isRowComplete = (row) => {
34
+ const symbols = row.map((i) => cells[i]);
35
+ return symbols.every((i) => i !== null && i === symbols[0]);
36
+ };
37
+
38
+ return positions.map((row) => isRowComplete(row)).includes(true);
39
+ }
40
+
41
+ const TicTacToe = ProcessGameConfig({
42
+ setup: () => ({
43
+ cells: Array.from({ length: 9 }).fill(null),
44
+ }),
45
+
46
+ moves: {
47
+ clickCell({ G, ctx }, id: number) {
48
+ const cells = [...G.cells];
49
+ if (cells[id] === null) {
50
+ cells[id] = ctx.currentPlayer;
51
+ }
52
+ return { ...G, cells };
53
+ },
54
+ },
55
+
56
+ turn: { minMoves: 1, maxMoves: 1 },
57
+
58
+ endIf: ({ G, ctx }) => {
59
+ if (IsVictory(G.cells)) {
60
+ return { winner: ctx.currentPlayer };
61
+ }
62
+
63
+ if (G.cells.filter((t) => t == null).length === 0) {
64
+ return { draw: true };
65
+ }
66
+ },
67
+ });
68
+
69
+ const enumerate = (G: any, ctx: Ctx, playerID: string) => {
70
+ const r = [];
71
+ for (let i = 0; i < 9; i++) {
72
+ if (G.cells[i] === null) {
73
+ r.push(makeMove('clickCell', [i], playerID));
74
+ }
75
+ }
76
+ return r;
77
+ };
78
+
79
+ describe('Step', () => {
80
+ test('advances game state', async () => {
81
+ const client = Client<{ moved: boolean }>({
82
+ game: {
83
+ setup: () => ({ moved: false }),
84
+
85
+ moves: {
86
+ clickCell({ G }) {
87
+ return { moved: !G.moved };
88
+ },
89
+ },
90
+
91
+ endIf({ G }) {
92
+ if (G.moved) return true;
93
+ },
94
+
95
+ ai: {
96
+ enumerate: () => [{ move: 'clickCell' }],
97
+ },
98
+ },
99
+ });
100
+
101
+ const bot = new RandomBot({ enumerate: client.game.ai.enumerate });
102
+ expect(client.getState().G).toEqual({ moved: false });
103
+ await Step(client, bot);
104
+ expect(client.getState().G).toEqual({ moved: true });
105
+ });
106
+
107
+ test('does not crash on empty action', async () => {
108
+ const client = Client({
109
+ game: {
110
+ ai: {
111
+ enumerate: () => [],
112
+ },
113
+ },
114
+ });
115
+ const bot = new RandomBot({ enumerate: client.game.ai.enumerate });
116
+ await expect(Step(client, bot)).resolves.toBeUndefined();
117
+ });
118
+
119
+ test('works with stages', async () => {
120
+ const client = Client({
121
+ game: {
122
+ moves: {
123
+ A: ({ G }) => {
124
+ G.moved = true;
125
+ },
126
+ },
127
+
128
+ turn: {
129
+ activePlayers: { currentPlayer: 'stage' },
130
+ },
131
+
132
+ ai: {
133
+ enumerate: () => [{ move: 'A' }],
134
+ },
135
+ },
136
+ });
137
+
138
+ const bot = new RandomBot({ enumerate: client.game.ai.enumerate });
139
+ expect(client.getState().G).not.toEqual({ moved: true });
140
+ await Step(client, bot);
141
+ expect(client.getState().G).toEqual({ moved: true });
142
+ });
143
+ });
144
+
145
+ describe('Simulate', () => {
146
+ const bots = {
147
+ '0': new RandomBot({ seed: 'test', enumerate }),
148
+ '1': new RandomBot({ seed: 'test', enumerate }),
149
+ };
150
+
151
+ test('multiple bots', async () => {
152
+ const state = InitializeGame({ game: TicTacToe });
153
+ const { state: endState } = await Simulate({
154
+ game: TicTacToe,
155
+ bots,
156
+ state,
157
+ });
158
+ expect(endState.ctx.gameover).not.toBe(undefined);
159
+ });
160
+
161
+ test('single bot', async () => {
162
+ const bot = new RandomBot({ seed: 'test', enumerate });
163
+ const state = InitializeGame({ game: TicTacToe });
164
+ const { state: endState } = await Simulate({
165
+ game: TicTacToe,
166
+ bots: bot,
167
+ state,
168
+ depth: 10,
169
+ });
170
+ expect(endState.ctx.gameover).not.toBe(undefined);
171
+ });
172
+
173
+ test('with activePlayers', async () => {
174
+ const game = ProcessGameConfig({
175
+ moves: {
176
+ A: ({ G }) => {
177
+ G.moved = true;
178
+ },
179
+ },
180
+ turn: {
181
+ activePlayers: { currentPlayer: Stage.NULL },
182
+ },
183
+ endIf: ({ G }) => G.moved,
184
+ });
185
+
186
+ const bot = new RandomBot({
187
+ seed: 'test',
188
+ enumerate: () => [makeMove('A')],
189
+ });
190
+
191
+ const state = InitializeGame({ game });
192
+ const { state: endState } = await Simulate({
193
+ game,
194
+ bots: bot,
195
+ state,
196
+ depth: 1,
197
+ });
198
+ expect(endState.ctx.gameover).not.toBe(undefined);
199
+ });
200
+ });
201
+
202
+ describe('Bot', () => {
203
+ test('random', () => {
204
+ const b = new RandomBot({ enumerate: () => [] });
205
+ expect(b.random()).toBeGreaterThanOrEqual(0);
206
+ expect(b.random()).toBeLessThan(1);
207
+ });
208
+
209
+ test('enumerate - makeMove', () => {
210
+ const enumerate = () => [makeMove('move')];
211
+ const b = new RandomBot({ enumerate });
212
+ expect(b.enumerate(undefined, undefined, undefined)[0].type).toBe(
213
+ MAKE_MOVE
214
+ );
215
+ });
216
+
217
+ test('enumerate - translate to makeMove', () => {
218
+ const enumerate = () => [{ move: 'move' }];
219
+ const b = new RandomBot({ enumerate });
220
+ expect(b.enumerate(undefined, undefined, undefined)[0].type).toBe(
221
+ MAKE_MOVE
222
+ );
223
+ });
224
+
225
+ test('enumerate - translate to gameEvent', () => {
226
+ const enumerate = () => [{ event: 'endTurn' }];
227
+ const b = new RandomBot({ enumerate });
228
+ expect(b.enumerate(undefined, undefined, undefined)[0].type).toBe(
229
+ GAME_EVENT
230
+ );
231
+ });
232
+
233
+ test('enumerate - unrecognized', () => {
234
+ const enumerate = (() =>
235
+ [{ unknown: true }] as unknown) as Game['ai']['enumerate'];
236
+ const b = new RandomBot({ enumerate });
237
+ expect(b.enumerate(undefined, undefined, undefined)).toEqual([undefined]);
238
+ });
239
+ });
240
+
241
+ describe('MCTSBot', () => {
242
+ test('game that never ends', async () => {
243
+ const game: Game = {};
244
+ const state = InitializeGame({ game });
245
+ const bot = new MCTSBot({ seed: 'test', game, enumerate: () => [] });
246
+ const { state: endState } = await Simulate({ game, bots: bot, state });
247
+ expect(endState.ctx.turn).toBe(1);
248
+ });
249
+
250
+ test('RandomBot vs. MCTSBot', async () => {
251
+ const bots = {
252
+ '0': new RandomBot({ seed: 'test', enumerate }),
253
+ '1': new MCTSBot({
254
+ iterations: 200,
255
+ seed: 'test',
256
+ game: TicTacToe,
257
+ enumerate,
258
+ }),
259
+ };
260
+
261
+ const initialState = InitializeGame({ game: TicTacToe });
262
+
263
+ for (let i = 0; i < 5; i++) {
264
+ const state = initialState;
265
+ const { state: endState } = await Simulate({
266
+ game: TicTacToe,
267
+ bots,
268
+ state,
269
+ });
270
+ expect(endState.ctx.gameover).not.toEqual({ winner: '0' });
271
+ }
272
+ });
273
+
274
+ test('MCTSBot vs. MCTSBot', async () => {
275
+ const initialState = InitializeGame({ game: TicTacToe });
276
+ const iterations = 400;
277
+
278
+ for (let i = 0; i < 5; i++) {
279
+ const bots = {
280
+ '0': new MCTSBot({
281
+ seed: i,
282
+ game: TicTacToe,
283
+ enumerate,
284
+ iterations,
285
+ playoutDepth: 50,
286
+ }),
287
+ '1': new MCTSBot({
288
+ seed: i,
289
+ game: TicTacToe,
290
+ enumerate,
291
+ iterations,
292
+ }),
293
+ };
294
+ const state = initialState;
295
+ const { state: endState } = await Simulate({
296
+ game: TicTacToe,
297
+ bots,
298
+ state,
299
+ });
300
+ expect(endState.ctx.gameover).toEqual({ draw: true });
301
+ }
302
+ });
303
+
304
+ test('with activePlayers', async () => {
305
+ const game = ProcessGameConfig({
306
+ setup: () => ({ moves: 0 }),
307
+ moves: {
308
+ A: ({ G }) => {
309
+ G.moves++;
310
+ },
311
+ },
312
+ turn: {
313
+ activePlayers: { currentPlayer: Stage.NULL },
314
+ },
315
+ endIf: ({ G }) => G.moves > 5,
316
+ });
317
+
318
+ const bot = new MCTSBot({
319
+ seed: 'test',
320
+ game,
321
+ enumerate: () => [makeMove('A')],
322
+ });
323
+
324
+ const state = InitializeGame({ game });
325
+ const { state: endState } = await Simulate({
326
+ game,
327
+ bots: bot,
328
+ state,
329
+ depth: 10,
330
+ });
331
+ expect(endState.ctx.gameover).not.toBe(undefined);
332
+ });
333
+
334
+ test('objectives', async () => {
335
+ const objectives = () => ({
336
+ 'play-on-square-0': {
337
+ checker: (G) => G.cells[0] !== null,
338
+ weight: 10,
339
+ },
340
+ });
341
+
342
+ const state = InitializeGame({ game: TicTacToe });
343
+
344
+ for (let i = 0; i < 10; i++) {
345
+ const bot = new MCTSBot({
346
+ iterations: 200,
347
+ seed: i,
348
+ game: TicTacToe,
349
+ enumerate,
350
+ objectives,
351
+ });
352
+
353
+ const { action } = await bot.play(state, '0');
354
+ expect(action.payload.args).toEqual([0]);
355
+ }
356
+ });
357
+
358
+ test('async mode', async () => {
359
+ const initialState = InitializeGame({ game: TicTacToe });
360
+ const bot = new MCTSBot({
361
+ seed: '0',
362
+ game: TicTacToe,
363
+ enumerate,
364
+ iterations: 10,
365
+ playoutDepth: 10,
366
+ });
367
+ bot.setOpt('async', true);
368
+ const action = await bot.play(initialState, '0');
369
+ expect(action).not.toBeUndefined();
370
+ });
371
+
372
+ describe('iterations & playout depth', () => {
373
+ test('set opts', () => {
374
+ const bot = new MCTSBot({ game: TicTacToe, enumerate: jest.fn() });
375
+ bot.setOpt('iterations', 1);
376
+ expect(bot.opts()['iterations'].value).toBe(1);
377
+ });
378
+
379
+ test('setOpt works on invalid key', () => {
380
+ const bot = new RandomBot({ enumerate: jest.fn() });
381
+ const setInvalidKey = () => bot.setOpt('unknown', 1);
382
+ const getInvalidKey = () => bot.getOpt('unknown');
383
+ expect(setInvalidKey).not.toThrow();
384
+ expect(getInvalidKey).toThrow();
385
+ });
386
+
387
+ test('functions', () => {
388
+ const state = InitializeGame({ game: TicTacToe });
389
+
390
+ // jump ahead in the game because the example iterations
391
+ // and playoutDepth functions are based on the turn
392
+ state.ctx.turn = 8;
393
+
394
+ const { turn, currentPlayer } = state.ctx;
395
+
396
+ const enumerateSpy = jest.fn(enumerate);
397
+
398
+ const bot = new MCTSBot({
399
+ game: TicTacToe,
400
+ enumerate: enumerateSpy,
401
+ iterations: (G, ctx) => ctx.turn * 100,
402
+ playoutDepth: (G, ctx) => ctx.turn * 10,
403
+ });
404
+
405
+ expect(
406
+ (bot.iterations as AnyFn)(null, { turn } as Ctx, currentPlayer)
407
+ ).toBe(turn * 100);
408
+ expect(
409
+ (bot.playoutDepth as AnyFn)(null, { turn } as Ctx, currentPlayer)
410
+ ).toBe(turn * 10);
411
+
412
+ // try the playout() function which requests the playoutDepth value
413
+ bot.playout({ state } as Node);
414
+
415
+ expect(enumerateSpy).toHaveBeenCalledWith(
416
+ state.G,
417
+ state.ctx,
418
+ currentPlayer
419
+ );
420
+
421
+ // then try the play() function which requests the iterations value
422
+ enumerateSpy.mockClear();
423
+
424
+ bot.play(state, currentPlayer);
425
+
426
+ expect(enumerateSpy).toHaveBeenCalledWith(
427
+ state.G,
428
+ state.ctx,
429
+ currentPlayer
430
+ );
431
+ });
432
+ });
433
+ });
package/src/ai/ai.ts ADDED
@@ -0,0 +1,84 @@
1
+ /*
2
+ * Copyright 2018 The boardgame.io Authors
3
+ *
4
+ * Use of this source code is governed by a MIT-style
5
+ * license that can be found in the LICENSE file or at
6
+ * https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ import { CreateGameReducer } from '../core/reducer';
10
+ import { Bot } from './bot';
11
+ import type { Game, PlayerID, State, Store } from '../types';
12
+
13
+ /**
14
+ * Make a single move on the client with a bot.
15
+ *
16
+ * @param {...object} client - The game client.
17
+ * @param {...object} bot - The bot.
18
+ */
19
+ export async function Step(client: { store: Store }, bot: Bot) {
20
+ const state = client.store.getState();
21
+
22
+ let playerID = state.ctx.currentPlayer;
23
+ if (state.ctx.activePlayers) {
24
+ playerID = Object.keys(state.ctx.activePlayers)[0];
25
+ }
26
+
27
+ const { action, metadata } = await bot.play(state, playerID);
28
+
29
+ if (action) {
30
+ const a = {
31
+ ...action,
32
+ payload: {
33
+ ...action.payload,
34
+ metadata,
35
+ },
36
+ };
37
+ client.store.dispatch(a);
38
+ return a;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Simulates the game till the end or a max depth.
44
+ *
45
+ * @param {...object} game - The game object.
46
+ * @param {...object} bots - An array of bots.
47
+ * @param {...object} state - The game state to start from.
48
+ */
49
+ export async function Simulate({
50
+ game,
51
+ bots,
52
+ state,
53
+ depth,
54
+ }: {
55
+ game: Game;
56
+ bots: Bot | Record<PlayerID, Bot>;
57
+ state: State;
58
+ depth?: number;
59
+ }) {
60
+ if (depth === undefined) depth = 10000;
61
+ const reducer = CreateGameReducer({ game });
62
+
63
+ let metadata = null;
64
+ let iter = 0;
65
+ while (state.ctx.gameover === undefined && iter < depth) {
66
+ let playerID = state.ctx.currentPlayer;
67
+ if (state.ctx.activePlayers) {
68
+ playerID = Object.keys(state.ctx.activePlayers)[0];
69
+ }
70
+
71
+ const bot = bots instanceof Bot ? bots : bots[playerID];
72
+ const t = await bot.play(state, playerID);
73
+
74
+ if (!t.action) {
75
+ break;
76
+ }
77
+
78
+ metadata = t.metadata;
79
+ state = reducer(state, t.action);
80
+ iter++;
81
+ }
82
+
83
+ return { state, metadata };
84
+ }
package/src/ai/bot.ts ADDED
@@ -0,0 +1,122 @@
1
+ /*
2
+ * Copyright 2018 The boardgame.io Authors
3
+ *
4
+ * Use of this source code is governed by a MIT-style
5
+ * license that can be found in the LICENSE file or at
6
+ * https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ import { makeMove, gameEvent } from '../core/action-creators';
10
+ import { alea } from '../plugins/random/random.alea';
11
+ import type { AleaState } from '../plugins/random/random.alea';
12
+ import type { ActionShape, Game, Ctx, PlayerID, State } from '../types';
13
+
14
+ export type BotAction = ActionShape.GameEvent | ActionShape.MakeMove;
15
+
16
+ /**
17
+ * Base class that bots can extend.
18
+ */
19
+ export abstract class Bot {
20
+ private enumerateFn: Game['ai']['enumerate'];
21
+ private seed?: string | number;
22
+ protected iterationCounter: number;
23
+ private _opts: Record<
24
+ string,
25
+ {
26
+ range?: { min: number; max: number };
27
+ value: any;
28
+ }
29
+ >;
30
+ private prngstate?: AleaState;
31
+
32
+ constructor({
33
+ enumerate,
34
+ seed,
35
+ }: {
36
+ enumerate: Game['ai']['enumerate'];
37
+ seed?: string | number;
38
+ }) {
39
+ this.enumerateFn = enumerate;
40
+ this.seed = seed;
41
+ this.iterationCounter = 0;
42
+ this._opts = {};
43
+ }
44
+
45
+ abstract play(
46
+ state: State,
47
+ playerID: PlayerID
48
+ ): Promise<{ action: BotAction; metadata?: any }>;
49
+
50
+ addOpt({
51
+ key,
52
+ range,
53
+ initial,
54
+ }: {
55
+ key: string;
56
+ range?: { min: number; max: number };
57
+ initial: any;
58
+ }) {
59
+ this._opts[key] = {
60
+ range,
61
+ value: initial,
62
+ };
63
+ }
64
+
65
+ getOpt(key: string) {
66
+ return this._opts[key].value;
67
+ }
68
+
69
+ setOpt(key: string, value: any) {
70
+ if (key in this._opts) {
71
+ this._opts[key].value = value;
72
+ }
73
+ }
74
+
75
+ opts() {
76
+ return this._opts;
77
+ }
78
+
79
+ enumerate(G: any, ctx: Ctx, playerID: PlayerID) {
80
+ const actions = this.enumerateFn(G, ctx, playerID);
81
+ return actions.map((a) => {
82
+ if ('payload' in a) {
83
+ return a;
84
+ }
85
+
86
+ if ('move' in a) {
87
+ return makeMove(a.move, a.args, playerID);
88
+ }
89
+
90
+ if ('event' in a) {
91
+ return gameEvent(a.event, a.args, playerID);
92
+ }
93
+ });
94
+ }
95
+
96
+ random<T extends any = any>(arg: T[]): T;
97
+ random(arg?: number): number;
98
+ random<T extends any = any>(arg?: number | T[]) {
99
+ let number: number;
100
+
101
+ if (this.seed !== undefined) {
102
+ const seed = this.prngstate ? '' : this.seed;
103
+ const rand = alea(seed, this.prngstate);
104
+
105
+ number = rand();
106
+ this.prngstate = rand.state();
107
+ } else {
108
+ number = Math.random();
109
+ }
110
+
111
+ if (arg) {
112
+ if (Array.isArray(arg)) {
113
+ const id = Math.floor(number * arg.length);
114
+ return arg[id];
115
+ } else {
116
+ return Math.floor(number * arg);
117
+ }
118
+ }
119
+
120
+ return number;
121
+ }
122
+ }