@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,991 @@
1
+ /*
2
+ * Copyright 2017 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 { INVALID_MOVE } from './constants';
10
+ import { applyMiddleware, createStore } from 'redux';
11
+ import { CreateGameReducer, TransientHandlingMiddleware } from './reducer';
12
+ import { InitializeGame } from './initialize';
13
+ import {
14
+ makeMove,
15
+ gameEvent,
16
+ sync,
17
+ update,
18
+ reset,
19
+ undo,
20
+ redo,
21
+ patch,
22
+ } from './action-creators';
23
+ import { error } from '../core/logger';
24
+ import type { Game, State, SyncInfo } from '../types';
25
+
26
+ jest.mock('../core/logger', () => ({
27
+ info: jest.fn(),
28
+ error: jest.fn(),
29
+ }));
30
+
31
+ const game: Game = {
32
+ moves: {
33
+ A: ({ G }) => G,
34
+ B: () => ({ moved: true }),
35
+ C: () => ({ victory: true }),
36
+ Invalid: () => INVALID_MOVE,
37
+ },
38
+ endIf: ({ G, ctx }) => (G.victory ? ctx.currentPlayer : undefined),
39
+ };
40
+ const reducer = CreateGameReducer({ game });
41
+ const initialState = InitializeGame({ game });
42
+
43
+ test('_stateID is incremented', () => {
44
+ let state = initialState;
45
+ state = reducer(state, makeMove('A'));
46
+ expect(state._stateID).toBe(1);
47
+ state = reducer(state, gameEvent('endTurn'));
48
+ expect(state._stateID).toBe(2);
49
+ });
50
+
51
+ test('move returns INVALID_MOVE', () => {
52
+ const game: Game = {
53
+ moves: {
54
+ A: () => INVALID_MOVE,
55
+ },
56
+ };
57
+ const reducer = CreateGameReducer({ game });
58
+ const state = reducer(initialState, makeMove('A'));
59
+ expect(error).toBeCalledWith('invalid move: A args: undefined');
60
+ expect(state._stateID).toBe(0);
61
+ });
62
+
63
+ test('makeMove', () => {
64
+ let state = initialState;
65
+ expect(state._stateID).toBe(0);
66
+
67
+ state = reducer(state, makeMove('unknown'));
68
+ expect(state._stateID).toBe(0);
69
+ expect(state.G).not.toMatchObject({ moved: true });
70
+ expect(error).toBeCalledWith('disallowed move: unknown');
71
+
72
+ state = reducer(state, makeMove('A'));
73
+ expect(state._stateID).toBe(1);
74
+ expect(state.G).not.toMatchObject({ moved: true });
75
+
76
+ state = reducer(state, makeMove('B'));
77
+ expect(state._stateID).toBe(2);
78
+ expect(state.G).toMatchObject({ moved: true });
79
+
80
+ state.ctx.gameover = true;
81
+
82
+ state = reducer(state, makeMove('B'));
83
+ expect(state._stateID).toBe(2);
84
+ expect(error).toBeCalledWith('cannot make move after game end');
85
+
86
+ state = reducer(state, gameEvent('endTurn'));
87
+ expect(state._stateID).toBe(2);
88
+ expect(error).toBeCalledWith('cannot call event after game end');
89
+ });
90
+
91
+ test('disable move by invalid playerIDs', () => {
92
+ let state = initialState;
93
+ expect(state._stateID).toBe(0);
94
+
95
+ // playerID="1" cannot move right now.
96
+ state = reducer(state, makeMove('A', null, '1'));
97
+ expect(state._stateID).toBe(0);
98
+
99
+ // playerID="1" cannot call events right now.
100
+ state = reducer(state, gameEvent('endTurn', null, '1'));
101
+ expect(state._stateID).toBe(0);
102
+
103
+ // playerID="0" can move.
104
+ state = reducer(state, makeMove('A', null, '0'));
105
+ expect(state._stateID).toBe(1);
106
+
107
+ // playerID=undefined can always move.
108
+ state = reducer(state, makeMove('A'));
109
+ expect(state._stateID).toBe(2);
110
+ });
111
+
112
+ test('sync', () => {
113
+ const state = reducer(
114
+ undefined,
115
+ sync({ state: { G: 'restored' } } as SyncInfo)
116
+ );
117
+ expect(state).toEqual({ G: 'restored' });
118
+ });
119
+
120
+ test('update', () => {
121
+ const state = reducer(undefined, update({ G: 'restored' } as State, []));
122
+ expect(state).toEqual({ G: 'restored' });
123
+ });
124
+
125
+ test('valid patch', () => {
126
+ const originalState = { _stateID: 0, G: 'patch' } as State;
127
+ const state = reducer(
128
+ originalState,
129
+ patch(0, 1, [{ op: 'replace', path: '/_stateID', value: 1 }], [])
130
+ );
131
+ expect(state).toEqual({ _stateID: 1, G: 'patch' });
132
+ });
133
+
134
+ test('invalid patch', () => {
135
+ const originalState = { _stateID: 0, G: 'patch' } as State;
136
+ const { transients, ...state } = reducer(
137
+ originalState,
138
+ patch(0, 1, [{ op: 'replace', path: '/_stateIDD', value: 1 }], [])
139
+ );
140
+ expect(state).toEqual(originalState);
141
+ expect(transients.error.type).toEqual('update/patch_failed');
142
+ // It's an array.
143
+ expect(transients.error.payload.length).toEqual(1);
144
+ // It looks like the standard rfc6902 error language.
145
+ expect(transients.error.payload[0].toString()).toContain('/_stateIDD');
146
+ });
147
+
148
+ test('reset', () => {
149
+ let state = reducer(initialState, makeMove('A'));
150
+ expect(state).not.toEqual(initialState);
151
+ state = reducer(state, reset(initialState));
152
+ expect(state).toEqual(initialState);
153
+ });
154
+
155
+ test('victory', () => {
156
+ let state = reducer(initialState, makeMove('A'));
157
+ state = reducer(state, gameEvent('endTurn'));
158
+ expect(state.ctx.gameover).toEqual(undefined);
159
+ state = reducer(state, makeMove('B'));
160
+ state = reducer(state, gameEvent('endTurn'));
161
+ expect(state.ctx.gameover).toEqual(undefined);
162
+ state = reducer(state, makeMove('C'));
163
+ expect(state.ctx.gameover).toEqual('0');
164
+ });
165
+
166
+ test('endTurn', () => {
167
+ {
168
+ const state = reducer(initialState, gameEvent('endTurn'));
169
+ expect(state.ctx.turn).toBe(2);
170
+ }
171
+
172
+ {
173
+ const reducer = CreateGameReducer({ game, isClient: true });
174
+ const state = reducer(initialState, gameEvent('endTurn'));
175
+ expect(state.ctx.turn).toBe(1);
176
+ }
177
+ });
178
+
179
+ test('light client when multiplayer=true', () => {
180
+ const game: Game = {
181
+ moves: { A: () => ({ win: true }) },
182
+ endIf: ({ G }) => G.win,
183
+ };
184
+
185
+ {
186
+ const reducer = CreateGameReducer({ game });
187
+ let state = InitializeGame({ game });
188
+ expect(state.ctx.gameover).toBe(undefined);
189
+ state = reducer(state, makeMove('A'));
190
+ expect(state.ctx.gameover).toBe(true);
191
+ }
192
+
193
+ {
194
+ const reducer = CreateGameReducer({ game, isClient: true });
195
+ let state = InitializeGame({ game });
196
+ expect(state.ctx.gameover).toBe(undefined);
197
+ state = reducer(state, makeMove('A'));
198
+ expect(state.ctx.gameover).toBe(undefined);
199
+ }
200
+ });
201
+
202
+ test('disable optimistic updates', () => {
203
+ const game: Game = {
204
+ moves: {
205
+ A: {
206
+ move: () => ({ A: true }),
207
+ client: false,
208
+ },
209
+ },
210
+ };
211
+
212
+ {
213
+ const reducer = CreateGameReducer({ game });
214
+ let state = InitializeGame({ game });
215
+ expect(state.G).not.toMatchObject({ A: true });
216
+ state = reducer(state, makeMove('A'));
217
+ expect(state.G).toMatchObject({ A: true });
218
+ }
219
+
220
+ {
221
+ const reducer = CreateGameReducer({ game, isClient: true });
222
+ let state = InitializeGame({ game });
223
+ expect(state.G).not.toMatchObject({ A: true });
224
+ state = reducer(state, makeMove('A'));
225
+ expect(state.G).not.toMatchObject({ A: true });
226
+ }
227
+ });
228
+
229
+ test('numPlayers', () => {
230
+ const numPlayers = 4;
231
+ const state = InitializeGame({ game, numPlayers });
232
+ expect(state.ctx.numPlayers).toBe(4);
233
+ });
234
+
235
+ test('deltalog', () => {
236
+ let state = initialState;
237
+
238
+ const actionA = makeMove('A');
239
+ const actionB = makeMove('B');
240
+ const actionC = gameEvent('endTurn');
241
+
242
+ state = reducer(state, actionA);
243
+ expect(state.deltalog).toEqual([
244
+ {
245
+ action: actionA,
246
+ _stateID: 0,
247
+ phase: null,
248
+ turn: 1,
249
+ },
250
+ ]);
251
+ state = reducer(state, actionB);
252
+ expect(state.deltalog).toEqual([
253
+ {
254
+ action: actionB,
255
+ _stateID: 1,
256
+ phase: null,
257
+ turn: 1,
258
+ },
259
+ ]);
260
+ state = reducer(state, actionC);
261
+ expect(state.deltalog).toEqual([
262
+ {
263
+ action: actionC,
264
+ _stateID: 2,
265
+ phase: null,
266
+ turn: 1,
267
+ },
268
+ ]);
269
+ });
270
+
271
+ describe('Events API', () => {
272
+ const fn = ({ events }) => (events ? {} : { error: true });
273
+
274
+ const game: Game = {
275
+ setup: () => ({}),
276
+ phases: { A: {} },
277
+ turn: {
278
+ onBegin: fn,
279
+ onEnd: fn,
280
+ onMove: fn,
281
+ },
282
+ };
283
+
284
+ const reducer = CreateGameReducer({ game });
285
+ let state = InitializeGame({ game });
286
+
287
+ test('is attached at the beginning', () => {
288
+ expect(state.G).not.toEqual({ error: true });
289
+ });
290
+
291
+ test('is attached at the end of turns', () => {
292
+ state = reducer(state, gameEvent('endTurn'));
293
+ expect(state.G).not.toEqual({ error: true });
294
+ });
295
+
296
+ test('is attached at the end of phases', () => {
297
+ state = reducer(state, gameEvent('endPhase'));
298
+ expect(state.G).not.toEqual({ error: true });
299
+ });
300
+ });
301
+
302
+ describe('Plugin Invalid Action API', () => {
303
+ const pluginName = 'validator';
304
+ const message = 'G.value must divide by 5';
305
+ const game: Game<{ value: number }> = {
306
+ setup: () => ({ value: 5 }),
307
+ plugins: [
308
+ {
309
+ name: pluginName,
310
+ isInvalid: ({ G }) => {
311
+ if (G.value % 5 !== 0) return message;
312
+ return false;
313
+ },
314
+ },
315
+ ],
316
+ moves: {
317
+ setValue: ({ G }, arg: number) => {
318
+ G.value = arg;
319
+ },
320
+ },
321
+ phases: {
322
+ unenterable: {
323
+ onBegin: () => ({ value: 13 }),
324
+ },
325
+ enterable: {
326
+ onBegin: () => ({ value: 25 }),
327
+ },
328
+ },
329
+ };
330
+
331
+ let state: State;
332
+ beforeEach(() => {
333
+ state = InitializeGame({ game });
334
+ });
335
+
336
+ describe('multiplayer client', () => {
337
+ const reducer = CreateGameReducer({ game });
338
+
339
+ test('move is cancelled if plugin declares it invalid', () => {
340
+ state = reducer(state, makeMove('setValue', [6], '0'));
341
+ expect(state.G).toMatchObject({ value: 5 });
342
+ expect(state['transients'].error).toEqual({
343
+ type: 'action/plugin_invalid',
344
+ payload: { plugin: pluginName, message },
345
+ });
346
+ });
347
+
348
+ test('move is processed if no plugin declares it invalid', () => {
349
+ state = reducer(state, makeMove('setValue', [15], '0'));
350
+ expect(state.G).toMatchObject({ value: 15 });
351
+ expect(state['transients']).toBeUndefined();
352
+ });
353
+
354
+ test('event is cancelled if plugin declares it invalid', () => {
355
+ state = reducer(state, gameEvent('setPhase', 'unenterable', '0'));
356
+ expect(state.G).toMatchObject({ value: 5 });
357
+ expect(state.ctx.phase).toBe(null);
358
+ expect(state['transients'].error).toEqual({
359
+ type: 'action/plugin_invalid',
360
+ payload: { plugin: pluginName, message },
361
+ });
362
+ });
363
+
364
+ test('event is processed if no plugin declares it invalid', () => {
365
+ state = reducer(state, gameEvent('setPhase', 'enterable', '0'));
366
+ expect(state.G).toMatchObject({ value: 25 });
367
+ expect(state.ctx.phase).toBe('enterable');
368
+ expect(state['transients']).toBeUndefined();
369
+ });
370
+ });
371
+
372
+ describe('local client', () => {
373
+ const reducer = CreateGameReducer({ game, isClient: true });
374
+
375
+ test('move is cancelled if plugin declares it invalid', () => {
376
+ state = reducer(state, makeMove('setValue', [6], '0'));
377
+ expect(state.G).toMatchObject({ value: 5 });
378
+ expect(state['transients'].error).toEqual({
379
+ type: 'action/plugin_invalid',
380
+ payload: { plugin: pluginName, message },
381
+ });
382
+ });
383
+
384
+ test('move is processed if no plugin declares it invalid', () => {
385
+ state = reducer(state, makeMove('setValue', [15], '0'));
386
+ expect(state.G).toMatchObject({ value: 15 });
387
+ expect(state['transients']).toBeUndefined();
388
+ });
389
+ });
390
+ });
391
+
392
+ describe('Random inside setup()', () => {
393
+ const game1: Game = {
394
+ seed: 'seed1',
395
+ setup: (ctx) => ({ n: ctx.random.D6() }),
396
+ };
397
+
398
+ const game2: Game = {
399
+ seed: 'seed2',
400
+ setup: (ctx) => ({ n: ctx.random.D6() }),
401
+ };
402
+
403
+ const game3: Game = {
404
+ seed: 'seed2',
405
+ setup: (ctx) => ({ n: ctx.random.D6() }),
406
+ };
407
+
408
+ test('setting seed', () => {
409
+ const state1 = InitializeGame({ game: game1 });
410
+ const state2 = InitializeGame({ game: game2 });
411
+ const state3 = InitializeGame({ game: game3 });
412
+
413
+ expect(state1.G.n).not.toBe(state2.G.n);
414
+ expect(state2.G.n).toBe(state3.G.n);
415
+ });
416
+ });
417
+
418
+ describe('redact', () => {
419
+ const game: Game = {
420
+ setup: () => ({
421
+ isASecret: false,
422
+ }),
423
+ moves: {
424
+ A: {
425
+ move: ({ G }) => G,
426
+ redact: ({ G }) => G.isASecret,
427
+ },
428
+ B: ({ G }) => {
429
+ return { ...G, isASecret: true };
430
+ },
431
+ },
432
+ };
433
+
434
+ const reducer = CreateGameReducer({ game });
435
+
436
+ let state = InitializeGame({ game });
437
+
438
+ test('move A is not secret and is not redact', () => {
439
+ state = reducer(state, makeMove('A', ['not redact'], '0'));
440
+ expect(state.G).toMatchObject({
441
+ isASecret: false,
442
+ });
443
+ const [lastLogEntry] = state.deltalog.slice(-1);
444
+ expect(lastLogEntry).toMatchObject({
445
+ action: {
446
+ payload: {
447
+ type: 'A',
448
+ args: ['not redact'],
449
+ },
450
+ },
451
+ redact: false,
452
+ });
453
+ });
454
+
455
+ test('move A is secret and is redact', () => {
456
+ state = reducer(state, makeMove('B', ['not redact'], '0'));
457
+ state = reducer(state, makeMove('A', ['redact'], '0'));
458
+ expect(state.G).toMatchObject({
459
+ isASecret: true,
460
+ });
461
+ const [lastLogEntry] = state.deltalog.slice(-1);
462
+ expect(lastLogEntry).toMatchObject({
463
+ action: {
464
+ payload: {
465
+ type: 'A',
466
+ args: ['redact'],
467
+ },
468
+ },
469
+ redact: true,
470
+ });
471
+ });
472
+ });
473
+
474
+ describe('undo / redo', () => {
475
+ const game: Game = {
476
+ seed: 0,
477
+ moves: {
478
+ move: ({ G }, arg: string) => ({ ...G, [arg]: true }),
479
+ roll: ({ G, random }) => {
480
+ G.roll = random.D6();
481
+ },
482
+ },
483
+ turn: {
484
+ stages: {
485
+ special: {},
486
+ },
487
+ },
488
+ };
489
+
490
+ beforeEach(() => {
491
+ jest.clearAllMocks();
492
+ });
493
+
494
+ const reducer = CreateGameReducer({ game });
495
+
496
+ const initialState = InitializeGame({ game });
497
+
498
+ // TODO: Check if this test is still actually required after removal of APIs from ctx
499
+ test('plugin APIs are not included in undo state', () => {
500
+ let state = reducer(initialState, makeMove('move', 'A', '0'));
501
+ state = reducer(state, makeMove('move', 'B', '0'));
502
+ expect(state.G).toMatchObject({ A: true, B: true });
503
+ expect(state._undo[1].ctx).not.toHaveProperty('events');
504
+ expect(state._undo[1].ctx).not.toHaveProperty('random');
505
+ });
506
+
507
+ test('undo restores previous state after move', () => {
508
+ const initial = reducer(initialState, makeMove('move', 'A', '0'));
509
+ let newState = reducer(initial, makeMove('roll', null, '0'));
510
+ newState = reducer(newState, undo());
511
+ expect(newState.G).toEqual(initial.G);
512
+ expect(newState.ctx).toEqual(initial.ctx);
513
+ expect(newState.plugins).toEqual(initial.plugins);
514
+ });
515
+
516
+ test('undo restores previous state after event', () => {
517
+ const initial = reducer(
518
+ initialState,
519
+ gameEvent('setStage', 'special', '0')
520
+ );
521
+ let newState = reducer(initial, gameEvent('endStage', undefined, '0'));
522
+ expect(error).not.toBeCalled();
523
+ // Make sure we actually modified the stage.
524
+ expect(newState.ctx.activePlayers).not.toEqual(initial.ctx.activePlayers);
525
+ newState = reducer(newState, undo());
526
+ expect(error).not.toBeCalled();
527
+ expect(newState.G).toEqual(initial.G);
528
+ expect(newState.ctx).toEqual(initial.ctx);
529
+ expect(newState.plugins).toEqual(initial.plugins);
530
+ });
531
+
532
+ test('redo restores undone state', () => {
533
+ let state = initialState;
534
+ // Make two moves.
535
+ const state1 = (state = reducer(state, makeMove('move', 'A', '0')));
536
+ const state2 = (state = reducer(state, makeMove('roll', null, '0')));
537
+ // Undo both of them.
538
+ state = reducer(state, undo());
539
+ state = reducer(state, undo());
540
+ // Redo one of them.
541
+ state = reducer(state, redo());
542
+ expect(state.G).toEqual(state1.G);
543
+ expect(state.ctx).toEqual(state1.ctx);
544
+ expect(state.plugins).toEqual(state1.plugins);
545
+ // Redo a second time.
546
+ state = reducer(state, redo());
547
+ expect(state.G).toEqual(state2.G);
548
+ expect(state.ctx).toEqual(state2.ctx);
549
+ expect(state.plugins).toEqual(state2.plugins);
550
+ });
551
+
552
+ test('can undo redone state', () => {
553
+ let state = reducer(initialState, makeMove('move', 'A', '0'));
554
+ state = reducer(state, undo());
555
+ state = reducer(state, redo());
556
+ state = reducer(state, undo());
557
+ expect(state.G).toMatchObject(initialState.G);
558
+ expect(state.ctx).toMatchObject(initialState.ctx);
559
+ expect(state.plugins).toMatchObject(initialState.plugins);
560
+ });
561
+
562
+ test('undo has no effect if nothing to undo', () => {
563
+ let state = reducer(initialState, undo());
564
+ state = reducer(state, undo());
565
+ state = reducer(state, undo());
566
+ expect(state.G).toMatchObject(initialState.G);
567
+ expect(state.ctx).toMatchObject(initialState.ctx);
568
+ expect(state.plugins).toMatchObject(initialState.plugins);
569
+ });
570
+
571
+ test('redo works after multiple undos', () => {
572
+ let state = reducer(initialState, makeMove('move', 'A', '0'));
573
+ state = reducer(state, undo());
574
+ state = reducer(state, undo());
575
+ state = reducer(state, undo());
576
+ state = reducer(state, redo());
577
+ state = reducer(state, makeMove('move', 'C', '0'));
578
+ expect(state.G).toMatchObject({ A: true, C: true });
579
+
580
+ state = reducer(state, undo());
581
+ expect(state.G).toMatchObject({ A: true });
582
+
583
+ state = reducer(state, redo());
584
+ expect(state.G).toMatchObject({ A: true, C: true });
585
+ });
586
+
587
+ test('redo only resets deltalog if nothing to redo', () => {
588
+ const state = reducer(initialState, makeMove('move', 'A', '0'));
589
+ expect(reducer(state, redo())).toMatchObject({
590
+ ...state,
591
+ deltalog: [],
592
+ transients: {
593
+ error: {
594
+ type: 'action/action_invalid',
595
+ },
596
+ },
597
+ });
598
+ });
599
+ });
600
+
601
+ test('disable undo / redo', () => {
602
+ const game: Game = {
603
+ seed: 0,
604
+ disableUndo: true,
605
+ moves: {
606
+ move: ({ G }, arg: string) => ({ ...G, [arg]: true }),
607
+ },
608
+ };
609
+
610
+ const reducer = CreateGameReducer({ game });
611
+
612
+ let state = InitializeGame({ game });
613
+
614
+ state = reducer(state, makeMove('move', 'A', '0'));
615
+ expect(state.G).toMatchObject({ A: true });
616
+ expect(state._undo).toEqual([]);
617
+ expect(state._redo).toEqual([]);
618
+
619
+ state = reducer(state, makeMove('move', 'B', '0'));
620
+ expect(state.G).toMatchObject({ A: true, B: true });
621
+ expect(state._undo).toEqual([]);
622
+ expect(state._redo).toEqual([]);
623
+
624
+ state = reducer(state, undo());
625
+ expect(state.G).toMatchObject({ A: true, B: true });
626
+ expect(state._undo).toEqual([]);
627
+ expect(state._redo).toEqual([]);
628
+
629
+ state = reducer(state, undo());
630
+ expect(state.G).toMatchObject({ A: true, B: true });
631
+ expect(state._undo).toEqual([]);
632
+ expect(state._redo).toEqual([]);
633
+
634
+ state = reducer(state, redo());
635
+ expect(state.G).toMatchObject({ A: true, B: true });
636
+ expect(state._undo).toEqual([]);
637
+ expect(state._redo).toEqual([]);
638
+ });
639
+
640
+ describe('undo stack', () => {
641
+ const game: Game = {
642
+ moves: {
643
+ basic: () => {},
644
+ endTurn: ({ events }) => {
645
+ events.endTurn();
646
+ },
647
+ },
648
+ };
649
+
650
+ const reducer = CreateGameReducer({ game });
651
+ let state = InitializeGame({ game });
652
+
653
+ test('contains initial state at start of game', () => {
654
+ expect(state._undo).toHaveLength(1);
655
+ expect(state._undo[0].ctx).toEqual(state.ctx);
656
+ expect(state._undo[0].plugins).toEqual(state.plugins);
657
+ });
658
+
659
+ test('grows when a move is made', () => {
660
+ state = reducer(state, makeMove('basic', null, '0'));
661
+ expect(state._undo).toHaveLength(2);
662
+ expect(state._undo[1].moveType).toBe('basic');
663
+ expect(state._undo[1].ctx).toEqual(state.ctx);
664
+ expect(state._undo[1].plugins).toEqual(state.plugins);
665
+ });
666
+
667
+ test('shrinks when a move is undone', () => {
668
+ state = reducer(state, undo());
669
+ expect(state._undo).toHaveLength(1);
670
+ expect(state._undo[0].ctx).toEqual(state.ctx);
671
+ expect(state._undo[0].plugins).toEqual(state.plugins);
672
+ });
673
+
674
+ test('grows when a move is redone', () => {
675
+ state = reducer(state, redo());
676
+ expect(state._undo).toHaveLength(2);
677
+ expect(state._undo[1].moveType).toBe('basic');
678
+ expect(state._undo[1].ctx).toEqual(state.ctx);
679
+ expect(state._undo[1].plugins).toEqual(state.plugins);
680
+ });
681
+
682
+ test('is reset when a turn ends', () => {
683
+ state = reducer(state, makeMove('endTurn'));
684
+ expect(state._undo).toHaveLength(1);
685
+ expect(state._undo[0].ctx).toEqual(state.ctx);
686
+ expect(state._undo[0].plugins).toEqual(state.plugins);
687
+ expect(state._undo[0].moveType).toBe('endTurn');
688
+ });
689
+
690
+ test('can’t undo at the start of a turn', () => {
691
+ const newState = reducer(state, undo());
692
+ expect(newState).toMatchObject({
693
+ ...state,
694
+ deltalog: [],
695
+ transients: {
696
+ error: {
697
+ type: 'action/action_invalid',
698
+ },
699
+ },
700
+ });
701
+ });
702
+
703
+ test('can’t undo another player’s move', () => {
704
+ state = reducer(state, makeMove('basic', null, '1'));
705
+ const newState = reducer(state, undo('0'));
706
+ expect(newState).toMatchObject({
707
+ ...state,
708
+ deltalog: [],
709
+ transients: {
710
+ error: {
711
+ type: 'action/action_invalid',
712
+ },
713
+ },
714
+ });
715
+ });
716
+ });
717
+
718
+ describe('redo stack', () => {
719
+ const game: Game = {
720
+ moves: {
721
+ basic: () => {},
722
+ endTurn: ({ events }) => {
723
+ events.endTurn();
724
+ },
725
+ },
726
+ };
727
+
728
+ const reducer = CreateGameReducer({ game });
729
+ let state = InitializeGame({ game });
730
+
731
+ test('is empty at start of game', () => {
732
+ expect(state._redo).toHaveLength(0);
733
+ });
734
+
735
+ test('grows when a move is undone', () => {
736
+ state = reducer(state, makeMove('basic', null, '0'));
737
+ state = reducer(state, undo());
738
+ expect(state._redo).toHaveLength(1);
739
+ expect(state._redo[0].moveType).toBe('basic');
740
+ });
741
+
742
+ test('shrinks when a move is redone', () => {
743
+ state = reducer(state, redo());
744
+ expect(state._redo).toHaveLength(0);
745
+ });
746
+
747
+ test('is reset when a move is made', () => {
748
+ state = reducer(state, makeMove('basic', null, '0'));
749
+ state = reducer(state, undo());
750
+ state = reducer(state, undo());
751
+ expect(state._redo).toHaveLength(2);
752
+ state = reducer(state, makeMove('basic', null, '0'));
753
+ expect(state._redo).toHaveLength(0);
754
+ });
755
+
756
+ test('is reset when a turn ends', () => {
757
+ state = reducer(state, makeMove('basic', null, '0'));
758
+ state = reducer(state, undo());
759
+ expect(state._redo).toHaveLength(1);
760
+ state = reducer(state, makeMove('endTurn'));
761
+ expect(state._redo).toHaveLength(0);
762
+ });
763
+
764
+ test('can’t redo another player’s undo', () => {
765
+ state = reducer(state, makeMove('basic', null, '1'));
766
+ state = reducer(state, undo('1'));
767
+ expect(state._redo).toHaveLength(1);
768
+ const newState = reducer(state, redo('0'));
769
+ expect(state._redo).toHaveLength(1);
770
+ expect(newState).toMatchObject({
771
+ ...state,
772
+ deltalog: [],
773
+ transients: {
774
+ error: {
775
+ type: 'action/action_invalid',
776
+ },
777
+ },
778
+ });
779
+ });
780
+ });
781
+
782
+ describe('undo / redo with stages', () => {
783
+ const game: Game = {
784
+ setup: () => ({ A: false, B: false, C: false }),
785
+ turn: {
786
+ activePlayers: { currentPlayer: 'start' },
787
+ stages: {
788
+ start: {
789
+ moves: {
790
+ moveA: {
791
+ move: ({ G, events }, moveAisReversible) => {
792
+ events.setStage('A');
793
+ return { ...G, moveAisReversible, A: true };
794
+ },
795
+ undoable: ({ G }) => G.moveAisReversible > 0,
796
+ },
797
+ },
798
+ },
799
+ A: {
800
+ moves: {
801
+ moveB: {
802
+ move: ({ G, events }) => {
803
+ events.setStage('B');
804
+ return { ...G, B: true };
805
+ },
806
+ undoable: false,
807
+ },
808
+ },
809
+ },
810
+ B: {
811
+ moves: {
812
+ moveC: {
813
+ move: ({ G, events }) => {
814
+ events.setStage('C');
815
+ return { ...G, C: true };
816
+ },
817
+ undoable: true,
818
+ },
819
+ },
820
+ },
821
+ C: {
822
+ moves: {},
823
+ },
824
+ },
825
+ },
826
+ };
827
+
828
+ const reducer = CreateGameReducer({ game });
829
+
830
+ let state = InitializeGame({ game });
831
+
832
+ test('moveA sets state & moves player to stage A (undoable)', () => {
833
+ state = reducer(state, makeMove('moveA', true, '0'));
834
+ expect(state.G).toMatchObject({
835
+ moveAisReversible: true,
836
+ A: true,
837
+ B: false,
838
+ C: false,
839
+ });
840
+ expect(state.ctx.activePlayers['0']).toBe('A');
841
+ });
842
+
843
+ test('undo undoes last move (moveA)', () => {
844
+ state = reducer(state, undo('0'));
845
+ expect(state.G).toMatchObject({
846
+ A: false,
847
+ B: false,
848
+ C: false,
849
+ });
850
+ expect(state.ctx.activePlayers['0']).toBe('start');
851
+ });
852
+
853
+ test('redo redoes moveA', () => {
854
+ state = reducer(state, redo('0'));
855
+ expect(state.G).toMatchObject({
856
+ moveAisReversible: true,
857
+ A: true,
858
+ B: false,
859
+ C: false,
860
+ });
861
+ expect(state.ctx.activePlayers['0']).toBe('A');
862
+ });
863
+
864
+ test('undo undoes last move after redo (moveA)', () => {
865
+ state = reducer(state, undo('0'));
866
+ expect(state.G).toMatchObject({
867
+ A: false,
868
+ B: false,
869
+ C: false,
870
+ });
871
+ expect(state.ctx.activePlayers['0']).toBe('start');
872
+ });
873
+
874
+ test('moveA sets state & moves player to stage A (not undoable)', () => {
875
+ state = reducer(state, makeMove('moveA', false, '0'));
876
+ expect(state.G).toMatchObject({
877
+ moveAisReversible: false,
878
+ A: true,
879
+ B: false,
880
+ C: false,
881
+ });
882
+ expect(state.ctx.activePlayers['0']).toBe('A');
883
+ });
884
+
885
+ test('moveB sets state & moves player to stage B', () => {
886
+ state = reducer(state, makeMove('moveB', [], '0'));
887
+ expect(state.G).toMatchObject({
888
+ moveAisReversible: false,
889
+ A: true,
890
+ B: true,
891
+ C: false,
892
+ });
893
+ expect(state.ctx.activePlayers['0']).toBe('B');
894
+ });
895
+
896
+ test('undo doesn’t undo last move if not undoable (moveB)', () => {
897
+ state = reducer(state, undo('0'));
898
+ expect(state.G).toMatchObject({
899
+ moveAisReversible: false,
900
+ A: true,
901
+ B: true,
902
+ C: false,
903
+ });
904
+ expect(state.ctx.activePlayers['0']).toBe('B');
905
+ });
906
+
907
+ test('moveC sets state & moves player to stage C', () => {
908
+ state = reducer(state, makeMove('moveC', [], '0'));
909
+ expect(state.G).toMatchObject({
910
+ moveAisReversible: false,
911
+ A: true,
912
+ B: true,
913
+ C: true,
914
+ });
915
+ expect(state.ctx.activePlayers['0']).toBe('C');
916
+ });
917
+
918
+ test('undo undoes last move (moveC)', () => {
919
+ state = reducer(state, undo('0'));
920
+ expect(state.G).toMatchObject({
921
+ moveAisReversible: false,
922
+ A: true,
923
+ B: true,
924
+ C: false,
925
+ });
926
+ expect(state.ctx.activePlayers['0']).toBe('B');
927
+ });
928
+
929
+ test('redo redoes moveC', () => {
930
+ state = reducer(state, redo('0'));
931
+ expect(state.G).toMatchObject({
932
+ moveAisReversible: false,
933
+ A: true,
934
+ B: true,
935
+ C: true,
936
+ });
937
+ expect(state.ctx.activePlayers['0']).toBe('C');
938
+ });
939
+
940
+ test('undo undoes last move after redo (moveC)', () => {
941
+ state = reducer(state, undo('0'));
942
+ expect(state.G).toMatchObject({
943
+ moveAisReversible: false,
944
+ A: true,
945
+ B: true,
946
+ C: false,
947
+ });
948
+ expect(state.ctx.activePlayers['0']).toBe('B');
949
+ });
950
+
951
+ test('undo doesn’t undo last move if not undoable after undo/redo', () => {
952
+ state = reducer(state, undo('0'));
953
+ expect(state.G).toMatchObject({
954
+ moveAisReversible: false,
955
+ A: true,
956
+ B: true,
957
+ C: false,
958
+ });
959
+ expect(state.ctx.activePlayers['0']).toBe('B');
960
+ });
961
+ });
962
+
963
+ describe('TransientHandlingMiddleware', () => {
964
+ const middleware = applyMiddleware(TransientHandlingMiddleware);
965
+ let store = null;
966
+
967
+ beforeEach(() => {
968
+ store = createStore(reducer, initialState, middleware);
969
+ });
970
+
971
+ test('regular dispatch result has no transients', () => {
972
+ const result = store.dispatch(makeMove('A'));
973
+ expect(result).toEqual(
974
+ expect.not.objectContaining({ transients: expect.anything() })
975
+ );
976
+ expect(result).toEqual(
977
+ expect.not.objectContaining({ stripTransientsResult: expect.anything() })
978
+ );
979
+ });
980
+
981
+ test('failing dispatch result contains transients', () => {
982
+ const result = store.dispatch(makeMove('Invalid'));
983
+ expect(result).toMatchObject({
984
+ transients: {
985
+ error: {
986
+ type: 'action/invalid_move',
987
+ },
988
+ },
989
+ });
990
+ });
991
+ });