@mnbroatch/boardgame.io 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/ai/package.json +7 -0
- package/client/package.json +7 -0
- package/core/package.json +7 -0
- package/debug/package.json +7 -0
- package/dist/boardgameio.es.js +14238 -0
- package/dist/boardgameio.js +14277 -0
- package/dist/boardgameio.min.js +16 -0
- package/dist/cjs/Debug-9d141c06.js +9586 -0
- package/dist/cjs/ai-e0e8a768.js +377 -0
- package/dist/cjs/ai.js +20 -0
- package/dist/cjs/client-76dec77b.js +258 -0
- package/dist/cjs/client-a22d7500.js +524 -0
- package/dist/cjs/client.js +26 -0
- package/dist/cjs/core.js +52 -0
- package/dist/cjs/debug.js +18 -0
- package/dist/cjs/filter-player-view-bb02e2f6.js +89 -0
- package/dist/cjs/initialize-267fcd69.js +61 -0
- package/dist/cjs/internal.js +25 -0
- package/dist/cjs/master-2904879d.js +320 -0
- package/dist/cjs/master.js +18 -0
- package/dist/cjs/multiplayer.js +23 -0
- package/dist/cjs/plugin-random-7425844d.js +229 -0
- package/dist/cjs/plugins.js +59 -0
- package/dist/cjs/react-native.js +182 -0
- package/dist/cjs/react.js +727 -0
- package/dist/cjs/reducer-16eec232.js +1203 -0
- package/dist/cjs/server.js +4087 -0
- package/dist/cjs/socketio-7a0837eb.js +478 -0
- package/dist/cjs/testing.js +30 -0
- package/dist/cjs/transport-b1874dfa.js +37 -0
- package/dist/cjs/turn-order-b2ff8740.js +1136 -0
- package/dist/cjs/util-fcfd8fb8.js +140 -0
- package/dist/esm/Debug-0141fe2d.js +9577 -0
- package/dist/esm/ai-5c06e761.js +371 -0
- package/dist/esm/ai.js +8 -0
- package/dist/esm/client-2e653027.js +522 -0
- package/dist/esm/client-5f57c3f2.js +255 -0
- package/dist/esm/client.js +16 -0
- package/dist/esm/core.js +40 -0
- package/dist/esm/debug.js +10 -0
- package/dist/esm/filter-player-view-2c6cc96f.js +87 -0
- package/dist/esm/initialize-11d626ca.js +59 -0
- package/dist/esm/internal.js +10 -0
- package/dist/esm/master-fa8f2e43.js +318 -0
- package/dist/esm/master.js +10 -0
- package/dist/esm/multiplayer.js +14 -0
- package/dist/esm/plugin-random-087f861e.js +226 -0
- package/dist/esm/plugins.js +55 -0
- package/dist/esm/react-native.js +173 -0
- package/dist/esm/react.js +716 -0
- package/dist/esm/reducer-c46da7e5.js +1198 -0
- package/dist/esm/socketio-c22ffa65.js +455 -0
- package/dist/esm/testing.js +26 -0
- package/dist/esm/transport-ce07b771.js +35 -0
- package/dist/esm/turn-order-376d315e.js +1091 -0
- package/dist/esm/util-b6147cef.js +135 -0
- package/dist/types/packages/ai.d.ts +5 -0
- package/dist/types/packages/client.d.ts +3 -0
- package/dist/types/packages/core.d.ts +5 -0
- package/dist/types/packages/debug.d.ts +2 -0
- package/dist/types/packages/internal.d.ts +8 -0
- package/dist/types/packages/master.d.ts +2 -0
- package/dist/types/packages/multiplayer.d.ts +3 -0
- package/dist/types/packages/plugins.d.ts +3 -0
- package/dist/types/packages/react-native.d.ts +2 -0
- package/dist/types/packages/react.d.ts +3 -0
- package/dist/types/packages/server.d.ts +6 -0
- package/dist/types/packages/testing.d.ts +1 -0
- package/dist/types/src/ai/ai.d.ts +53 -0
- package/dist/types/src/ai/ai.test.d.ts +1 -0
- package/dist/types/src/ai/bot.d.ts +40 -0
- package/dist/types/src/ai/mcts-bot.d.ts +60 -0
- package/dist/types/src/ai/random-bot.d.ts +27 -0
- package/dist/types/src/client/client.d.ts +104 -0
- package/dist/types/src/client/client.test.d.ts +1 -0
- package/dist/types/src/client/debug/tests/debug.test.d.ts +1 -0
- package/dist/types/src/client/manager.d.ts +61 -0
- package/dist/types/src/client/react.d.ts +75 -0
- package/dist/types/src/client/react.ssr.test.d.ts +4 -0
- package/dist/types/src/client/react.test.d.ts +1 -0
- package/dist/types/src/client/transport/dummy.d.ts +18 -0
- package/dist/types/src/client/transport/local.d.ts +59 -0
- package/dist/types/src/client/transport/local.test.d.ts +1 -0
- package/dist/types/src/client/transport/socketio.d.ts +45 -0
- package/dist/types/src/client/transport/socketio.test.d.ts +1 -0
- package/dist/types/src/client/transport/transport.d.ts +50 -0
- package/dist/types/src/client/transport/transport.test.d.ts +1 -0
- package/dist/types/src/core/action-creators.d.ts +144 -0
- package/dist/types/src/core/action-types.d.ts +10 -0
- package/dist/types/src/core/backwards-compatibility.d.ts +12 -0
- package/dist/types/src/core/constants.d.ts +6 -0
- package/dist/types/src/core/errors.d.ts +15 -0
- package/dist/types/src/core/flow.d.ts +28 -0
- package/dist/types/src/core/flow.test.d.ts +1 -0
- package/dist/types/src/core/game-methods.d.ts +9 -0
- package/dist/types/src/core/game.d.ts +26 -0
- package/dist/types/src/core/game.test.d.ts +1 -0
- package/dist/types/src/core/initialize.d.ts +9 -0
- package/dist/types/src/core/logger.d.ts +2 -0
- package/dist/types/src/core/player-view.d.ts +7 -0
- package/dist/types/src/core/player-view.test.d.ts +1 -0
- package/dist/types/src/core/reducer.d.ts +155 -0
- package/dist/types/src/core/reducer.test.d.ts +1 -0
- package/dist/types/src/core/turn-order.d.ts +179 -0
- package/dist/types/src/core/turn-order.test.d.ts +8 -0
- package/dist/types/src/lobby/client.d.ts +194 -0
- package/dist/types/src/lobby/client.test.d.ts +1 -0
- package/dist/types/src/lobby/connection.d.ts +44 -0
- package/dist/types/src/lobby/connection.test.d.ts +1 -0
- package/dist/types/src/lobby/create-match-form.d.ts +26 -0
- package/dist/types/src/lobby/login-form.d.ts +23 -0
- package/dist/types/src/lobby/match-instance.d.ts +31 -0
- package/dist/types/src/lobby/react.d.ts +113 -0
- package/dist/types/src/lobby/react.ssr.test.d.ts +4 -0
- package/dist/types/src/lobby/react.test.d.ts +1 -0
- package/dist/types/src/master/filter-player-view.d.ts +96 -0
- package/dist/types/src/master/filter-player-view.test.d.ts +1 -0
- package/dist/types/src/master/master.d.ts +94 -0
- package/dist/types/src/master/master.test.d.ts +1 -0
- package/dist/types/src/plugins/events/events.d.ts +54 -0
- package/dist/types/src/plugins/events/events.test.d.ts +1 -0
- package/dist/types/src/plugins/main.d.ts +75 -0
- package/dist/types/src/plugins/main.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-events.d.ts +5 -0
- package/dist/types/src/plugins/plugin-immer.d.ts +7 -0
- package/dist/types/src/plugins/plugin-immer.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-log.d.ts +14 -0
- package/dist/types/src/plugins/plugin-log.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-player.d.ts +29 -0
- package/dist/types/src/plugins/plugin-player.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-random.d.ts +4 -0
- package/dist/types/src/plugins/plugin-serializable.d.ts +7 -0
- package/dist/types/src/plugins/plugin-serializable.test.d.ts +1 -0
- package/dist/types/src/plugins/random/random.alea.d.ts +19 -0
- package/dist/types/src/plugins/random/random.d.ts +54 -0
- package/dist/types/src/plugins/random/random.test.d.ts +1 -0
- package/dist/types/src/server/api.d.ts +13 -0
- package/dist/types/src/server/api.test.d.ts +1 -0
- package/dist/types/src/server/auth.d.ts +38 -0
- package/dist/types/src/server/auth.test.d.ts +1 -0
- package/dist/types/src/server/cors.d.ts +4 -0
- package/dist/types/src/server/cors.test.d.ts +1 -0
- package/dist/types/src/server/db/base.d.ts +192 -0
- package/dist/types/src/server/db/flatfile.d.ts +44 -0
- package/dist/types/src/server/db/flatfile.test.d.ts +1 -0
- package/dist/types/src/server/db/index.d.ts +4 -0
- package/dist/types/src/server/db/index.test.d.ts +1 -0
- package/dist/types/src/server/db/inmemory.d.ts +43 -0
- package/dist/types/src/server/db/inmemory.test.d.ts +1 -0
- package/dist/types/src/server/db/localstorage.d.ts +7 -0
- package/dist/types/src/server/db/localstorage.test.d.ts +1 -0
- package/dist/types/src/server/index.d.ts +68 -0
- package/dist/types/src/server/index.test.d.ts +1 -0
- package/dist/types/src/server/transport/pubsub/generic-pub-sub.d.ts +6 -0
- package/dist/types/src/server/transport/pubsub/in-memory-pub-sub.d.ts +7 -0
- package/dist/types/src/server/transport/pubsub/in-memory-pub-sub.test.d.ts +1 -0
- package/dist/types/src/server/transport/socketio-simultaneous.test.d.ts +1 -0
- package/dist/types/src/server/transport/socketio.d.ts +65 -0
- package/dist/types/src/server/transport/socketio.test.d.ts +1 -0
- package/dist/types/src/server/util.d.ts +35 -0
- package/dist/types/src/testing/mock-random.d.ts +15 -0
- package/dist/types/src/testing/mock-random.test.d.ts +1 -0
- package/dist/types/src/types.d.ts +387 -0
- package/internal/package.json +7 -0
- package/master/package.json +7 -0
- package/multiplayer/package.json +7 -0
- package/package.json +211 -0
- package/plugins/package.json +7 -0
- package/react/package.json +7 -0
- package/react-native/package.json +7 -0
- package/server/package.json +6 -0
- package/src/ai/ai.test.ts +433 -0
- package/src/ai/ai.ts +84 -0
- package/src/ai/bot.ts +122 -0
- package/src/ai/mcts-bot.ts +331 -0
- package/src/ai/random-bot.ts +20 -0
- package/src/client/client.test.ts +993 -0
- package/src/client/client.ts +588 -0
- package/src/client/debug/Debug.svelte +239 -0
- package/src/client/debug/Menu.svelte +65 -0
- package/src/client/debug/ai/AI.svelte +215 -0
- package/src/client/debug/ai/Options.svelte +48 -0
- package/src/client/debug/info/Info.svelte +22 -0
- package/src/client/debug/info/Item.svelte +24 -0
- package/src/client/debug/log/Log.svelte +157 -0
- package/src/client/debug/log/LogEvent.svelte +149 -0
- package/src/client/debug/log/LogMetadata.svelte +7 -0
- package/src/client/debug/log/PhaseMarker.svelte +27 -0
- package/src/client/debug/log/TurnMarker.svelte +23 -0
- package/src/client/debug/main/ClientSwitcher.svelte +59 -0
- package/src/client/debug/main/Controls.svelte +58 -0
- package/src/client/debug/main/Hotkey.svelte +84 -0
- package/src/client/debug/main/InteractiveFunction.svelte +85 -0
- package/src/client/debug/main/Main.svelte +121 -0
- package/src/client/debug/main/Move.svelte +68 -0
- package/src/client/debug/main/PlayerInfo.svelte +70 -0
- package/src/client/debug/mcts/Action.svelte +22 -0
- package/src/client/debug/mcts/MCTS.svelte +78 -0
- package/src/client/debug/mcts/Table.svelte +98 -0
- package/src/client/debug/tests/JSONTree.mock.svelte +3 -0
- package/src/client/debug/tests/debug.test.ts +183 -0
- package/src/client/debug/utils/shortcuts.js +50 -0
- package/src/client/debug/utils/shortcuts.test.js +49 -0
- package/src/client/manager.ts +177 -0
- package/src/client/react-native.js +136 -0
- package/src/client/react-native.test.js +229 -0
- package/src/client/react.ssr.test.tsx +24 -0
- package/src/client/react.test.tsx +213 -0
- package/src/client/react.tsx +192 -0
- package/src/client/transport/dummy.ts +19 -0
- package/src/client/transport/local.test.ts +353 -0
- package/src/client/transport/local.ts +230 -0
- package/src/client/transport/socketio.test.ts +328 -0
- package/src/client/transport/socketio.ts +210 -0
- package/src/client/transport/transport.test.ts +27 -0
- package/src/client/transport/transport.ts +95 -0
- package/src/core/action-creators.ts +159 -0
- package/src/core/action-types.ts +18 -0
- package/src/core/backwards-compatibility.ts +23 -0
- package/src/core/constants.ts +6 -0
- package/src/core/errors.ts +35 -0
- package/src/core/flow.test.ts +2433 -0
- package/src/core/flow.ts +897 -0
- package/src/core/game-methods.ts +9 -0
- package/src/core/game.test.ts +286 -0
- package/src/core/game.ts +114 -0
- package/src/core/initialize.ts +77 -0
- package/src/core/logger.test.js +90 -0
- package/src/core/logger.ts +18 -0
- package/src/core/player-view.test.ts +50 -0
- package/src/core/player-view.ts +39 -0
- package/src/core/reducer.test.ts +991 -0
- package/src/core/reducer.ts +532 -0
- package/src/core/turn-order.test.ts +1123 -0
- package/src/core/turn-order.ts +473 -0
- package/src/lobby/client.test.ts +385 -0
- package/src/lobby/client.ts +358 -0
- package/src/lobby/connection.test.ts +207 -0
- package/src/lobby/connection.ts +162 -0
- package/src/lobby/create-match-form.tsx +122 -0
- package/src/lobby/login-form.tsx +75 -0
- package/src/lobby/match-instance.tsx +135 -0
- package/src/lobby/react.ssr.test.tsx +22 -0
- package/src/lobby/react.test.tsx +594 -0
- package/src/lobby/react.tsx +402 -0
- package/src/master/filter-player-view.test.ts +381 -0
- package/src/master/filter-player-view.ts +102 -0
- package/src/master/master.test.ts +1068 -0
- package/src/master/master.ts +492 -0
- package/src/plugins/events/events.test.ts +108 -0
- package/src/plugins/events/events.ts +209 -0
- package/src/plugins/main.test.ts +411 -0
- package/src/plugins/main.ts +314 -0
- package/src/plugins/plugin-events.ts +40 -0
- package/src/plugins/plugin-immer.test.ts +86 -0
- package/src/plugins/plugin-immer.ts +37 -0
- package/src/plugins/plugin-log.test.ts +37 -0
- package/src/plugins/plugin-log.ts +40 -0
- package/src/plugins/plugin-player.test.ts +172 -0
- package/src/plugins/plugin-player.ts +100 -0
- package/src/plugins/plugin-random.ts +40 -0
- package/src/plugins/plugin-serializable.test.ts +40 -0
- package/src/plugins/plugin-serializable.ts +55 -0
- package/src/plugins/random/random.alea.ts +109 -0
- package/src/plugins/random/random.test.ts +167 -0
- package/src/plugins/random/random.ts +198 -0
- package/src/server/api.test.ts +1699 -0
- package/src/server/api.ts +527 -0
- package/src/server/auth.test.ts +275 -0
- package/src/server/auth.ts +89 -0
- package/src/server/cors.test.ts +121 -0
- package/src/server/cors.ts +7 -0
- package/src/server/db/base.ts +296 -0
- package/src/server/db/flatfile.test.ts +221 -0
- package/src/server/db/flatfile.ts +228 -0
- package/src/server/db/index.test.ts +8 -0
- package/src/server/db/index.ts +12 -0
- package/src/server/db/inmemory.test.ts +143 -0
- package/src/server/db/inmemory.ts +143 -0
- package/src/server/db/localstorage.test.ts +73 -0
- package/src/server/db/localstorage.ts +44 -0
- package/src/server/index.test.ts +265 -0
- package/src/server/index.ts +175 -0
- package/src/server/transport/pubsub/generic-pub-sub.ts +11 -0
- package/src/server/transport/pubsub/in-memory-pub-sub.test.ts +47 -0
- package/src/server/transport/pubsub/in-memory-pub-sub.ts +28 -0
- package/src/server/transport/socketio-simultaneous.test.ts +603 -0
- package/src/server/transport/socketio.test.ts +303 -0
- package/src/server/transport/socketio.ts +279 -0
- package/src/server/util.ts +85 -0
- package/src/testing/mock-random.test.ts +45 -0
- package/src/testing/mock-random.ts +27 -0
- package/src/types.ts +511 -0
- package/testing/package.json +7 -0
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var turnOrder = require('./turn-order-b2ff8740.js');
|
|
4
|
+
var rfc6902 = require('rfc6902');
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Copyright 2017 The boardgame.io Authors
|
|
8
|
+
*
|
|
9
|
+
* Use of this source code is governed by a MIT-style
|
|
10
|
+
* license that can be found in the LICENSE file or at
|
|
11
|
+
* https://opensource.org/licenses/MIT.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Flow
|
|
15
|
+
*
|
|
16
|
+
* Creates a reducer that updates ctx (analogous to how moves update G).
|
|
17
|
+
*/
|
|
18
|
+
function Flow({ moves, phases, endIf, onEnd, turn, events, plugins, }) {
|
|
19
|
+
// Attach defaults.
|
|
20
|
+
if (moves === undefined) {
|
|
21
|
+
moves = {};
|
|
22
|
+
}
|
|
23
|
+
if (events === undefined) {
|
|
24
|
+
events = {};
|
|
25
|
+
}
|
|
26
|
+
if (plugins === undefined) {
|
|
27
|
+
plugins = [];
|
|
28
|
+
}
|
|
29
|
+
if (phases === undefined) {
|
|
30
|
+
phases = {};
|
|
31
|
+
}
|
|
32
|
+
if (!endIf)
|
|
33
|
+
endIf = () => undefined;
|
|
34
|
+
if (!onEnd)
|
|
35
|
+
onEnd = ({ G }) => G;
|
|
36
|
+
if (!turn)
|
|
37
|
+
turn = {};
|
|
38
|
+
const phaseMap = { ...phases };
|
|
39
|
+
if ('' in phaseMap) {
|
|
40
|
+
turnOrder.error('cannot specify phase with empty name');
|
|
41
|
+
}
|
|
42
|
+
phaseMap[''] = {};
|
|
43
|
+
const moveMap = {};
|
|
44
|
+
const moveNames = new Set();
|
|
45
|
+
let startingPhase = null;
|
|
46
|
+
Object.keys(moves).forEach((name) => moveNames.add(name));
|
|
47
|
+
const HookWrapper = (hook, hookType) => {
|
|
48
|
+
const withPlugins = turnOrder.FnWrap(hook, hookType, plugins);
|
|
49
|
+
return (state) => {
|
|
50
|
+
const pluginAPIs = turnOrder.GetAPIs(state);
|
|
51
|
+
return withPlugins({
|
|
52
|
+
...pluginAPIs,
|
|
53
|
+
G: state.G,
|
|
54
|
+
ctx: state.ctx,
|
|
55
|
+
playerID: state.playerID,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const TriggerWrapper = (trigger) => {
|
|
60
|
+
return (state) => {
|
|
61
|
+
const pluginAPIs = turnOrder.GetAPIs(state);
|
|
62
|
+
return trigger({
|
|
63
|
+
...pluginAPIs,
|
|
64
|
+
G: state.G,
|
|
65
|
+
ctx: state.ctx,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const wrapped = {
|
|
70
|
+
onEnd: HookWrapper(onEnd, turnOrder.GameMethod.GAME_ON_END),
|
|
71
|
+
endIf: TriggerWrapper(endIf),
|
|
72
|
+
};
|
|
73
|
+
for (const phase in phaseMap) {
|
|
74
|
+
const phaseConfig = phaseMap[phase];
|
|
75
|
+
if (phaseConfig.start === true) {
|
|
76
|
+
startingPhase = phase;
|
|
77
|
+
}
|
|
78
|
+
if (phaseConfig.moves !== undefined) {
|
|
79
|
+
for (const move of Object.keys(phaseConfig.moves)) {
|
|
80
|
+
moveMap[phase + '.' + move] = phaseConfig.moves[move];
|
|
81
|
+
moveNames.add(move);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (phaseConfig.endIf === undefined) {
|
|
85
|
+
phaseConfig.endIf = () => undefined;
|
|
86
|
+
}
|
|
87
|
+
if (phaseConfig.onBegin === undefined) {
|
|
88
|
+
phaseConfig.onBegin = ({ G }) => G;
|
|
89
|
+
}
|
|
90
|
+
if (phaseConfig.onEnd === undefined) {
|
|
91
|
+
phaseConfig.onEnd = ({ G }) => G;
|
|
92
|
+
}
|
|
93
|
+
if (phaseConfig.turn === undefined) {
|
|
94
|
+
phaseConfig.turn = turn;
|
|
95
|
+
}
|
|
96
|
+
if (phaseConfig.turn.order === undefined) {
|
|
97
|
+
phaseConfig.turn.order = turnOrder.TurnOrder.DEFAULT;
|
|
98
|
+
}
|
|
99
|
+
if (phaseConfig.turn.onBegin === undefined) {
|
|
100
|
+
phaseConfig.turn.onBegin = ({ G }) => G;
|
|
101
|
+
}
|
|
102
|
+
if (phaseConfig.turn.onEnd === undefined) {
|
|
103
|
+
phaseConfig.turn.onEnd = ({ G }) => G;
|
|
104
|
+
}
|
|
105
|
+
if (phaseConfig.turn.endIf === undefined) {
|
|
106
|
+
phaseConfig.turn.endIf = () => false;
|
|
107
|
+
}
|
|
108
|
+
if (phaseConfig.turn.onMove === undefined) {
|
|
109
|
+
phaseConfig.turn.onMove = ({ G }) => G;
|
|
110
|
+
}
|
|
111
|
+
if (phaseConfig.turn.stages === undefined) {
|
|
112
|
+
phaseConfig.turn.stages = {};
|
|
113
|
+
}
|
|
114
|
+
// turns previously treated moveLimit as both minMoves and maxMoves, this behaviour is kept intentionally
|
|
115
|
+
turnOrder.supportDeprecatedMoveLimit(phaseConfig.turn, true);
|
|
116
|
+
for (const stage in phaseConfig.turn.stages) {
|
|
117
|
+
const stageConfig = phaseConfig.turn.stages[stage];
|
|
118
|
+
const moves = stageConfig.moves || {};
|
|
119
|
+
for (const move of Object.keys(moves)) {
|
|
120
|
+
const key = phase + '.' + stage + '.' + move;
|
|
121
|
+
moveMap[key] = moves[move];
|
|
122
|
+
moveNames.add(move);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
phaseConfig.wrapped = {
|
|
126
|
+
onBegin: HookWrapper(phaseConfig.onBegin, turnOrder.GameMethod.PHASE_ON_BEGIN),
|
|
127
|
+
onEnd: HookWrapper(phaseConfig.onEnd, turnOrder.GameMethod.PHASE_ON_END),
|
|
128
|
+
endIf: TriggerWrapper(phaseConfig.endIf),
|
|
129
|
+
};
|
|
130
|
+
phaseConfig.turn.wrapped = {
|
|
131
|
+
onMove: HookWrapper(phaseConfig.turn.onMove, turnOrder.GameMethod.TURN_ON_MOVE),
|
|
132
|
+
onBegin: HookWrapper(phaseConfig.turn.onBegin, turnOrder.GameMethod.TURN_ON_BEGIN),
|
|
133
|
+
onEnd: HookWrapper(phaseConfig.turn.onEnd, turnOrder.GameMethod.TURN_ON_END),
|
|
134
|
+
endIf: TriggerWrapper(phaseConfig.turn.endIf),
|
|
135
|
+
};
|
|
136
|
+
if (typeof phaseConfig.next !== 'function') {
|
|
137
|
+
const { next } = phaseConfig;
|
|
138
|
+
phaseConfig.next = () => next || null;
|
|
139
|
+
}
|
|
140
|
+
phaseConfig.wrapped.next = TriggerWrapper(phaseConfig.next);
|
|
141
|
+
}
|
|
142
|
+
function GetPhase(ctx) {
|
|
143
|
+
return ctx.phase ? phaseMap[ctx.phase] : phaseMap[''];
|
|
144
|
+
}
|
|
145
|
+
function OnMove(state) {
|
|
146
|
+
return state;
|
|
147
|
+
}
|
|
148
|
+
function Process(state, events) {
|
|
149
|
+
const phasesEnded = new Set();
|
|
150
|
+
const turnsEnded = new Set();
|
|
151
|
+
for (let i = 0; i < events.length; i++) {
|
|
152
|
+
const { fn, arg, ...rest } = events[i];
|
|
153
|
+
// Detect a loop of EndPhase calls.
|
|
154
|
+
// This could potentially even be an infinite loop
|
|
155
|
+
// if the endIf condition of each phase blindly
|
|
156
|
+
// returns true. The moment we detect a single
|
|
157
|
+
// loop, we just bail out of all phases.
|
|
158
|
+
if (fn === EndPhase) {
|
|
159
|
+
turnsEnded.clear();
|
|
160
|
+
const phase = state.ctx.phase;
|
|
161
|
+
if (phasesEnded.has(phase)) {
|
|
162
|
+
const ctx = { ...state.ctx, phase: null };
|
|
163
|
+
return { ...state, ctx };
|
|
164
|
+
}
|
|
165
|
+
phasesEnded.add(phase);
|
|
166
|
+
}
|
|
167
|
+
// Process event.
|
|
168
|
+
const next = [];
|
|
169
|
+
state = fn(state, {
|
|
170
|
+
...rest,
|
|
171
|
+
arg,
|
|
172
|
+
next,
|
|
173
|
+
});
|
|
174
|
+
if (fn === EndGame) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
// Check if we should end the game.
|
|
178
|
+
const shouldEndGame = ShouldEndGame(state);
|
|
179
|
+
if (shouldEndGame) {
|
|
180
|
+
events.push({
|
|
181
|
+
fn: EndGame,
|
|
182
|
+
arg: shouldEndGame,
|
|
183
|
+
turn: state.ctx.turn,
|
|
184
|
+
phase: state.ctx.phase,
|
|
185
|
+
automatic: true,
|
|
186
|
+
});
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Check if we should end the phase.
|
|
190
|
+
const shouldEndPhase = ShouldEndPhase(state);
|
|
191
|
+
if (shouldEndPhase) {
|
|
192
|
+
events.push({
|
|
193
|
+
fn: EndPhase,
|
|
194
|
+
arg: shouldEndPhase,
|
|
195
|
+
turn: state.ctx.turn,
|
|
196
|
+
phase: state.ctx.phase,
|
|
197
|
+
automatic: true,
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// Check if we should end the turn.
|
|
202
|
+
if ([OnMove, UpdateStage, UpdateActivePlayers].includes(fn)) {
|
|
203
|
+
const shouldEndTurn = ShouldEndTurn(state);
|
|
204
|
+
if (shouldEndTurn) {
|
|
205
|
+
events.push({
|
|
206
|
+
fn: EndTurn,
|
|
207
|
+
arg: shouldEndTurn,
|
|
208
|
+
turn: state.ctx.turn,
|
|
209
|
+
phase: state.ctx.phase,
|
|
210
|
+
automatic: true,
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
events.push(...next);
|
|
216
|
+
}
|
|
217
|
+
return state;
|
|
218
|
+
}
|
|
219
|
+
///////////
|
|
220
|
+
// Start //
|
|
221
|
+
///////////
|
|
222
|
+
function StartGame(state, { next }) {
|
|
223
|
+
next.push({ fn: StartPhase });
|
|
224
|
+
return state;
|
|
225
|
+
}
|
|
226
|
+
function StartPhase(state, { next }) {
|
|
227
|
+
let { G, ctx } = state;
|
|
228
|
+
const phaseConfig = GetPhase(ctx);
|
|
229
|
+
// Run any phase setup code provided by the user.
|
|
230
|
+
G = phaseConfig.wrapped.onBegin(state);
|
|
231
|
+
next.push({ fn: StartTurn });
|
|
232
|
+
return { ...state, G, ctx };
|
|
233
|
+
}
|
|
234
|
+
function StartTurn(state, { currentPlayer }) {
|
|
235
|
+
let { ctx } = state;
|
|
236
|
+
const phaseConfig = GetPhase(ctx);
|
|
237
|
+
// Initialize the turn order state.
|
|
238
|
+
if (currentPlayer) {
|
|
239
|
+
ctx = { ...ctx, currentPlayer };
|
|
240
|
+
if (phaseConfig.turn.activePlayers) {
|
|
241
|
+
ctx = turnOrder.SetActivePlayers(ctx, phaseConfig.turn.activePlayers);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// This is only called at the beginning of the phase
|
|
246
|
+
// when there is no currentPlayer yet.
|
|
247
|
+
ctx = turnOrder.InitTurnOrderState(state, phaseConfig.turn);
|
|
248
|
+
}
|
|
249
|
+
const turn = ctx.turn + 1;
|
|
250
|
+
ctx = { ...ctx, turn, numMoves: 0, _prevActivePlayers: [] };
|
|
251
|
+
const G = phaseConfig.turn.wrapped.onBegin({ ...state, ctx });
|
|
252
|
+
return { ...state, G, ctx, _undo: [], _redo: [] };
|
|
253
|
+
}
|
|
254
|
+
////////////
|
|
255
|
+
// Update //
|
|
256
|
+
////////////
|
|
257
|
+
function UpdatePhase(state, { arg, next, phase }) {
|
|
258
|
+
const phaseConfig = GetPhase({ phase });
|
|
259
|
+
let { ctx } = state;
|
|
260
|
+
if (arg && arg.next) {
|
|
261
|
+
if (arg.next in phaseMap) {
|
|
262
|
+
ctx = { ...ctx, phase: arg.next };
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
turnOrder.error('invalid phase: ' + arg.next);
|
|
266
|
+
return state;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
ctx = { ...ctx, phase: phaseConfig.wrapped.next(state) || null };
|
|
271
|
+
}
|
|
272
|
+
state = { ...state, ctx };
|
|
273
|
+
// Start the new phase.
|
|
274
|
+
next.push({ fn: StartPhase });
|
|
275
|
+
return state;
|
|
276
|
+
}
|
|
277
|
+
function UpdateTurn(state, { arg, currentPlayer, next }) {
|
|
278
|
+
let { G, ctx } = state;
|
|
279
|
+
const phaseConfig = GetPhase(ctx);
|
|
280
|
+
// Update turn order state.
|
|
281
|
+
const { endPhase, ctx: newCtx } = turnOrder.UpdateTurnOrderState(state, currentPlayer, phaseConfig.turn, arg);
|
|
282
|
+
ctx = newCtx;
|
|
283
|
+
state = { ...state, G, ctx };
|
|
284
|
+
if (endPhase) {
|
|
285
|
+
next.push({ fn: EndPhase, turn: ctx.turn, phase: ctx.phase });
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
next.push({ fn: StartTurn, currentPlayer: ctx.currentPlayer });
|
|
289
|
+
}
|
|
290
|
+
return state;
|
|
291
|
+
}
|
|
292
|
+
function UpdateStage(state, { arg, playerID }) {
|
|
293
|
+
if (typeof arg === 'string' || arg === turnOrder.Stage.NULL) {
|
|
294
|
+
arg = { stage: arg };
|
|
295
|
+
}
|
|
296
|
+
if (typeof arg !== 'object')
|
|
297
|
+
return state;
|
|
298
|
+
// `arg` should be of type `StageArg`, loose typing as `any` here for historic reasons
|
|
299
|
+
// stages previously did not enforce minMoves, this behaviour is kept intentionally
|
|
300
|
+
turnOrder.supportDeprecatedMoveLimit(arg);
|
|
301
|
+
let { ctx } = state;
|
|
302
|
+
let { activePlayers, _activePlayersMinMoves, _activePlayersMaxMoves, _activePlayersNumMoves, } = ctx;
|
|
303
|
+
// Checking if stage is valid, even Stage.NULL
|
|
304
|
+
if (arg.stage !== undefined) {
|
|
305
|
+
if (activePlayers === null) {
|
|
306
|
+
activePlayers = {};
|
|
307
|
+
}
|
|
308
|
+
activePlayers[playerID] = arg.stage;
|
|
309
|
+
_activePlayersNumMoves[playerID] = 0;
|
|
310
|
+
if (arg.minMoves) {
|
|
311
|
+
if (_activePlayersMinMoves === null) {
|
|
312
|
+
_activePlayersMinMoves = {};
|
|
313
|
+
}
|
|
314
|
+
_activePlayersMinMoves[playerID] = arg.minMoves;
|
|
315
|
+
}
|
|
316
|
+
if (arg.maxMoves) {
|
|
317
|
+
if (_activePlayersMaxMoves === null) {
|
|
318
|
+
_activePlayersMaxMoves = {};
|
|
319
|
+
}
|
|
320
|
+
_activePlayersMaxMoves[playerID] = arg.maxMoves;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
ctx = {
|
|
324
|
+
...ctx,
|
|
325
|
+
activePlayers,
|
|
326
|
+
_activePlayersMinMoves,
|
|
327
|
+
_activePlayersMaxMoves,
|
|
328
|
+
_activePlayersNumMoves,
|
|
329
|
+
};
|
|
330
|
+
return { ...state, ctx };
|
|
331
|
+
}
|
|
332
|
+
function UpdateActivePlayers(state, { arg }) {
|
|
333
|
+
return { ...state, ctx: turnOrder.SetActivePlayers(state.ctx, arg) };
|
|
334
|
+
}
|
|
335
|
+
///////////////
|
|
336
|
+
// ShouldEnd //
|
|
337
|
+
///////////////
|
|
338
|
+
function ShouldEndGame(state) {
|
|
339
|
+
return wrapped.endIf(state);
|
|
340
|
+
}
|
|
341
|
+
function ShouldEndPhase(state) {
|
|
342
|
+
const phaseConfig = GetPhase(state.ctx);
|
|
343
|
+
return phaseConfig.wrapped.endIf(state);
|
|
344
|
+
}
|
|
345
|
+
function ShouldEndTurn(state) {
|
|
346
|
+
const phaseConfig = GetPhase(state.ctx);
|
|
347
|
+
// End the turn if the required number of moves has been made.
|
|
348
|
+
const currentPlayerMoves = state.ctx.numMoves || 0;
|
|
349
|
+
if (phaseConfig.turn.maxMoves &&
|
|
350
|
+
currentPlayerMoves >= phaseConfig.turn.maxMoves) {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
return phaseConfig.turn.wrapped.endIf(state);
|
|
354
|
+
}
|
|
355
|
+
/////////
|
|
356
|
+
// End //
|
|
357
|
+
/////////
|
|
358
|
+
function EndGame(state, { arg, phase }) {
|
|
359
|
+
state = EndPhase(state, { phase });
|
|
360
|
+
if (arg === undefined) {
|
|
361
|
+
arg = true;
|
|
362
|
+
}
|
|
363
|
+
state = { ...state, ctx: { ...state.ctx, gameover: arg } };
|
|
364
|
+
// Run game end hook.
|
|
365
|
+
const G = wrapped.onEnd(state);
|
|
366
|
+
return { ...state, G };
|
|
367
|
+
}
|
|
368
|
+
function EndPhase(state, { arg, next, turn: initialTurn, automatic }) {
|
|
369
|
+
// End the turn first.
|
|
370
|
+
state = EndTurn(state, { turn: initialTurn, force: true, automatic: true });
|
|
371
|
+
const { phase, turn } = state.ctx;
|
|
372
|
+
if (next) {
|
|
373
|
+
next.push({ fn: UpdatePhase, arg, phase });
|
|
374
|
+
}
|
|
375
|
+
// If we aren't in a phase, there is nothing else to do.
|
|
376
|
+
if (phase === null) {
|
|
377
|
+
return state;
|
|
378
|
+
}
|
|
379
|
+
// Run any cleanup code for the phase that is about to end.
|
|
380
|
+
const phaseConfig = GetPhase(state.ctx);
|
|
381
|
+
const G = phaseConfig.wrapped.onEnd(state);
|
|
382
|
+
// Reset the phase.
|
|
383
|
+
const ctx = { ...state.ctx, phase: null };
|
|
384
|
+
// Add log entry.
|
|
385
|
+
const action = turnOrder.gameEvent('endPhase', arg);
|
|
386
|
+
const { _stateID } = state;
|
|
387
|
+
const logEntry = { action, _stateID, turn, phase };
|
|
388
|
+
if (automatic)
|
|
389
|
+
logEntry.automatic = true;
|
|
390
|
+
const deltalog = [...(state.deltalog || []), logEntry];
|
|
391
|
+
return { ...state, G, ctx, deltalog };
|
|
392
|
+
}
|
|
393
|
+
function EndTurn(state, { arg, next, turn: initialTurn, force, automatic, playerID }) {
|
|
394
|
+
// This is not the turn that EndTurn was originally
|
|
395
|
+
// called for. The turn was probably ended some other way.
|
|
396
|
+
if (initialTurn !== state.ctx.turn) {
|
|
397
|
+
return state;
|
|
398
|
+
}
|
|
399
|
+
const { currentPlayer, numMoves, phase, turn } = state.ctx;
|
|
400
|
+
const phaseConfig = GetPhase(state.ctx);
|
|
401
|
+
// Prevent ending the turn if minMoves haven't been reached.
|
|
402
|
+
const currentPlayerMoves = numMoves || 0;
|
|
403
|
+
if (!force &&
|
|
404
|
+
phaseConfig.turn.minMoves &&
|
|
405
|
+
currentPlayerMoves < phaseConfig.turn.minMoves) {
|
|
406
|
+
turnOrder.info(`cannot end turn before making ${phaseConfig.turn.minMoves} moves`);
|
|
407
|
+
return state;
|
|
408
|
+
}
|
|
409
|
+
// Run turn-end triggers.
|
|
410
|
+
const G = phaseConfig.turn.wrapped.onEnd(state);
|
|
411
|
+
if (next) {
|
|
412
|
+
next.push({ fn: UpdateTurn, arg, currentPlayer });
|
|
413
|
+
}
|
|
414
|
+
// Reset activePlayers.
|
|
415
|
+
let ctx = { ...state.ctx, activePlayers: null };
|
|
416
|
+
// Remove player from playerOrder
|
|
417
|
+
if (arg && arg.remove) {
|
|
418
|
+
playerID = playerID || currentPlayer;
|
|
419
|
+
const playOrder = ctx.playOrder.filter((i) => i != playerID);
|
|
420
|
+
const playOrderPos = ctx.playOrderPos > playOrder.length - 1 ? 0 : ctx.playOrderPos;
|
|
421
|
+
ctx = { ...ctx, playOrder, playOrderPos };
|
|
422
|
+
if (playOrder.length === 0) {
|
|
423
|
+
next.push({ fn: EndPhase, turn, phase });
|
|
424
|
+
return state;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Create log entry.
|
|
428
|
+
const action = turnOrder.gameEvent('endTurn', arg);
|
|
429
|
+
const { _stateID } = state;
|
|
430
|
+
const logEntry = { action, _stateID, turn, phase };
|
|
431
|
+
if (automatic)
|
|
432
|
+
logEntry.automatic = true;
|
|
433
|
+
const deltalog = [...(state.deltalog || []), logEntry];
|
|
434
|
+
return { ...state, G, ctx, deltalog, _undo: [], _redo: [] };
|
|
435
|
+
}
|
|
436
|
+
function EndStage(state, { arg, next, automatic, playerID }) {
|
|
437
|
+
playerID = playerID || state.ctx.currentPlayer;
|
|
438
|
+
let { ctx, _stateID } = state;
|
|
439
|
+
let { activePlayers, _activePlayersNumMoves, _activePlayersMinMoves, _activePlayersMaxMoves, phase, turn, } = ctx;
|
|
440
|
+
const playerInStage = activePlayers !== null && playerID in activePlayers;
|
|
441
|
+
const phaseConfig = GetPhase(ctx);
|
|
442
|
+
if (!arg && playerInStage) {
|
|
443
|
+
const stage = phaseConfig.turn.stages[activePlayers[playerID]];
|
|
444
|
+
if (stage && stage.next) {
|
|
445
|
+
arg = stage.next;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Checking if arg is a valid stage, even Stage.NULL
|
|
449
|
+
if (next) {
|
|
450
|
+
next.push({ fn: UpdateStage, arg, playerID });
|
|
451
|
+
}
|
|
452
|
+
// If player isn’t in a stage, there is nothing else to do.
|
|
453
|
+
if (!playerInStage)
|
|
454
|
+
return state;
|
|
455
|
+
// Prevent ending the stage if minMoves haven't been reached.
|
|
456
|
+
const currentPlayerMoves = _activePlayersNumMoves[playerID] || 0;
|
|
457
|
+
if (_activePlayersMinMoves &&
|
|
458
|
+
_activePlayersMinMoves[playerID] &&
|
|
459
|
+
currentPlayerMoves < _activePlayersMinMoves[playerID]) {
|
|
460
|
+
turnOrder.info(`cannot end stage before making ${_activePlayersMinMoves[playerID]} moves`);
|
|
461
|
+
return state;
|
|
462
|
+
}
|
|
463
|
+
// Remove player from activePlayers.
|
|
464
|
+
activePlayers = { ...activePlayers };
|
|
465
|
+
delete activePlayers[playerID];
|
|
466
|
+
if (_activePlayersMinMoves) {
|
|
467
|
+
// Remove player from _activePlayersMinMoves.
|
|
468
|
+
_activePlayersMinMoves = { ..._activePlayersMinMoves };
|
|
469
|
+
delete _activePlayersMinMoves[playerID];
|
|
470
|
+
}
|
|
471
|
+
if (_activePlayersMaxMoves) {
|
|
472
|
+
// Remove player from _activePlayersMaxMoves.
|
|
473
|
+
_activePlayersMaxMoves = { ..._activePlayersMaxMoves };
|
|
474
|
+
delete _activePlayersMaxMoves[playerID];
|
|
475
|
+
}
|
|
476
|
+
ctx = turnOrder.UpdateActivePlayersOnceEmpty({
|
|
477
|
+
...ctx,
|
|
478
|
+
activePlayers,
|
|
479
|
+
_activePlayersMinMoves,
|
|
480
|
+
_activePlayersMaxMoves,
|
|
481
|
+
});
|
|
482
|
+
// Create log entry.
|
|
483
|
+
const action = turnOrder.gameEvent('endStage', arg);
|
|
484
|
+
const logEntry = { action, _stateID, turn, phase };
|
|
485
|
+
if (automatic)
|
|
486
|
+
logEntry.automatic = true;
|
|
487
|
+
const deltalog = [...(state.deltalog || []), logEntry];
|
|
488
|
+
return { ...state, ctx, deltalog };
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Retrieves the relevant move that can be played by playerID.
|
|
492
|
+
*
|
|
493
|
+
* If ctx.activePlayers is set (i.e. one or more players are in some stage),
|
|
494
|
+
* then it attempts to find the move inside the stages config for
|
|
495
|
+
* that turn. If the stage for a player is '', then the player is
|
|
496
|
+
* allowed to make a move (as determined by the phase config), but
|
|
497
|
+
* isn't restricted to a particular set as defined in the stage config.
|
|
498
|
+
*
|
|
499
|
+
* If not, it then looks for the move inside the phase.
|
|
500
|
+
*
|
|
501
|
+
* If it doesn't find the move there, it looks at the global move definition.
|
|
502
|
+
*
|
|
503
|
+
* @param {object} ctx
|
|
504
|
+
* @param {string} name
|
|
505
|
+
* @param {string} playerID
|
|
506
|
+
*/
|
|
507
|
+
function GetMove(ctx, name, playerID) {
|
|
508
|
+
const phaseConfig = GetPhase(ctx);
|
|
509
|
+
const stages = phaseConfig.turn.stages;
|
|
510
|
+
const { activePlayers } = ctx;
|
|
511
|
+
if (activePlayers &&
|
|
512
|
+
activePlayers[playerID] !== undefined &&
|
|
513
|
+
activePlayers[playerID] !== turnOrder.Stage.NULL &&
|
|
514
|
+
stages[activePlayers[playerID]] !== undefined &&
|
|
515
|
+
stages[activePlayers[playerID]].moves !== undefined) {
|
|
516
|
+
// Check if moves are defined for the player's stage.
|
|
517
|
+
const stage = stages[activePlayers[playerID]];
|
|
518
|
+
const moves = stage.moves;
|
|
519
|
+
if (name in moves) {
|
|
520
|
+
return moves[name];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else if (phaseConfig.moves) {
|
|
524
|
+
// Check if moves are defined for the current phase.
|
|
525
|
+
if (name in phaseConfig.moves) {
|
|
526
|
+
return phaseConfig.moves[name];
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else if (name in moves) {
|
|
530
|
+
// Check for the move globally.
|
|
531
|
+
return moves[name];
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
function ProcessMove(state, action) {
|
|
536
|
+
const { playerID, type } = action;
|
|
537
|
+
const { currentPlayer, activePlayers, _activePlayersMaxMoves } = state.ctx;
|
|
538
|
+
const move = GetMove(state.ctx, type, playerID);
|
|
539
|
+
const shouldCount = !move || typeof move === 'function' || move.noLimit !== true;
|
|
540
|
+
let { numMoves, _activePlayersNumMoves } = state.ctx;
|
|
541
|
+
if (shouldCount) {
|
|
542
|
+
if (playerID === currentPlayer)
|
|
543
|
+
numMoves++;
|
|
544
|
+
if (activePlayers)
|
|
545
|
+
_activePlayersNumMoves[playerID]++;
|
|
546
|
+
}
|
|
547
|
+
state = {
|
|
548
|
+
...state,
|
|
549
|
+
ctx: {
|
|
550
|
+
...state.ctx,
|
|
551
|
+
numMoves,
|
|
552
|
+
_activePlayersNumMoves,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
if (_activePlayersMaxMoves &&
|
|
556
|
+
_activePlayersNumMoves[playerID] >= _activePlayersMaxMoves[playerID]) {
|
|
557
|
+
state = EndStage(state, { playerID, automatic: true });
|
|
558
|
+
}
|
|
559
|
+
const phaseConfig = GetPhase(state.ctx);
|
|
560
|
+
const G = phaseConfig.turn.wrapped.onMove({ ...state, playerID });
|
|
561
|
+
state = { ...state, G };
|
|
562
|
+
const events = [{ fn: OnMove }];
|
|
563
|
+
return Process(state, events);
|
|
564
|
+
}
|
|
565
|
+
function SetStageEvent(state, playerID, arg) {
|
|
566
|
+
return Process(state, [{ fn: EndStage, arg, playerID }]);
|
|
567
|
+
}
|
|
568
|
+
function EndStageEvent(state, playerID) {
|
|
569
|
+
return Process(state, [{ fn: EndStage, playerID }]);
|
|
570
|
+
}
|
|
571
|
+
function SetActivePlayersEvent(state, _playerID, arg) {
|
|
572
|
+
return Process(state, [{ fn: UpdateActivePlayers, arg }]);
|
|
573
|
+
}
|
|
574
|
+
function SetPhaseEvent(state, _playerID, newPhase) {
|
|
575
|
+
return Process(state, [
|
|
576
|
+
{
|
|
577
|
+
fn: EndPhase,
|
|
578
|
+
phase: state.ctx.phase,
|
|
579
|
+
turn: state.ctx.turn,
|
|
580
|
+
arg: { next: newPhase },
|
|
581
|
+
},
|
|
582
|
+
]);
|
|
583
|
+
}
|
|
584
|
+
function EndPhaseEvent(state) {
|
|
585
|
+
return Process(state, [
|
|
586
|
+
{ fn: EndPhase, phase: state.ctx.phase, turn: state.ctx.turn },
|
|
587
|
+
]);
|
|
588
|
+
}
|
|
589
|
+
function EndTurnEvent(state, _playerID, arg) {
|
|
590
|
+
return Process(state, [
|
|
591
|
+
{ fn: EndTurn, turn: state.ctx.turn, phase: state.ctx.phase, arg },
|
|
592
|
+
]);
|
|
593
|
+
}
|
|
594
|
+
function PassEvent(state, _playerID, arg) {
|
|
595
|
+
return Process(state, [
|
|
596
|
+
{
|
|
597
|
+
fn: EndTurn,
|
|
598
|
+
turn: state.ctx.turn,
|
|
599
|
+
phase: state.ctx.phase,
|
|
600
|
+
force: true,
|
|
601
|
+
arg,
|
|
602
|
+
},
|
|
603
|
+
]);
|
|
604
|
+
}
|
|
605
|
+
function EndGameEvent(state, _playerID, arg) {
|
|
606
|
+
return Process(state, [
|
|
607
|
+
{ fn: EndGame, turn: state.ctx.turn, phase: state.ctx.phase, arg },
|
|
608
|
+
]);
|
|
609
|
+
}
|
|
610
|
+
const eventHandlers = {
|
|
611
|
+
endStage: EndStageEvent,
|
|
612
|
+
setStage: SetStageEvent,
|
|
613
|
+
endTurn: EndTurnEvent,
|
|
614
|
+
pass: PassEvent,
|
|
615
|
+
endPhase: EndPhaseEvent,
|
|
616
|
+
setPhase: SetPhaseEvent,
|
|
617
|
+
endGame: EndGameEvent,
|
|
618
|
+
setActivePlayers: SetActivePlayersEvent,
|
|
619
|
+
};
|
|
620
|
+
const enabledEventNames = [];
|
|
621
|
+
if (events.endTurn !== false) {
|
|
622
|
+
enabledEventNames.push('endTurn');
|
|
623
|
+
}
|
|
624
|
+
if (events.pass !== false) {
|
|
625
|
+
enabledEventNames.push('pass');
|
|
626
|
+
}
|
|
627
|
+
if (events.endPhase !== false) {
|
|
628
|
+
enabledEventNames.push('endPhase');
|
|
629
|
+
}
|
|
630
|
+
if (events.setPhase !== false) {
|
|
631
|
+
enabledEventNames.push('setPhase');
|
|
632
|
+
}
|
|
633
|
+
if (events.endGame !== false) {
|
|
634
|
+
enabledEventNames.push('endGame');
|
|
635
|
+
}
|
|
636
|
+
if (events.setActivePlayers !== false) {
|
|
637
|
+
enabledEventNames.push('setActivePlayers');
|
|
638
|
+
}
|
|
639
|
+
if (events.endStage !== false) {
|
|
640
|
+
enabledEventNames.push('endStage');
|
|
641
|
+
}
|
|
642
|
+
if (events.setStage !== false) {
|
|
643
|
+
enabledEventNames.push('setStage');
|
|
644
|
+
}
|
|
645
|
+
function ProcessEvent(state, action) {
|
|
646
|
+
const { type, playerID, args } = action.payload;
|
|
647
|
+
if (typeof eventHandlers[type] !== 'function')
|
|
648
|
+
return state;
|
|
649
|
+
return eventHandlers[type](state, playerID, ...(Array.isArray(args) ? args : [args]));
|
|
650
|
+
}
|
|
651
|
+
function IsPlayerActive(_G, ctx, playerID) {
|
|
652
|
+
if (ctx.activePlayers) {
|
|
653
|
+
return playerID in ctx.activePlayers;
|
|
654
|
+
}
|
|
655
|
+
return ctx.currentPlayer === playerID;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
ctx: (numPlayers) => ({
|
|
659
|
+
numPlayers,
|
|
660
|
+
turn: 0,
|
|
661
|
+
currentPlayer: '0',
|
|
662
|
+
playOrder: [...Array.from({ length: numPlayers })].map((_, i) => i + ''),
|
|
663
|
+
playOrderPos: 0,
|
|
664
|
+
phase: startingPhase,
|
|
665
|
+
activePlayers: null,
|
|
666
|
+
}),
|
|
667
|
+
init: (state) => {
|
|
668
|
+
return Process(state, [{ fn: StartGame }]);
|
|
669
|
+
},
|
|
670
|
+
isPlayerActive: IsPlayerActive,
|
|
671
|
+
eventHandlers,
|
|
672
|
+
eventNames: Object.keys(eventHandlers),
|
|
673
|
+
enabledEventNames,
|
|
674
|
+
moveMap,
|
|
675
|
+
moveNames: [...moveNames.values()],
|
|
676
|
+
processMove: ProcessMove,
|
|
677
|
+
processEvent: ProcessEvent,
|
|
678
|
+
getMove: GetMove,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/*
|
|
683
|
+
* Copyright 2017 The boardgame.io Authors
|
|
684
|
+
*
|
|
685
|
+
* Use of this source code is governed by a MIT-style
|
|
686
|
+
* license that can be found in the LICENSE file or at
|
|
687
|
+
* https://opensource.org/licenses/MIT.
|
|
688
|
+
*/
|
|
689
|
+
function IsProcessed(game) {
|
|
690
|
+
return game.processMove !== undefined;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Helper to generate the game move reducer. The returned
|
|
694
|
+
* reducer has the following signature:
|
|
695
|
+
*
|
|
696
|
+
* (G, action, ctx) => {}
|
|
697
|
+
*
|
|
698
|
+
* You can roll your own if you like, or use any Redux
|
|
699
|
+
* addon to generate such a reducer.
|
|
700
|
+
*
|
|
701
|
+
* The convention used in this framework is to
|
|
702
|
+
* have action.type contain the name of the move, and
|
|
703
|
+
* action.args contain any additional arguments as an
|
|
704
|
+
* Array.
|
|
705
|
+
*/
|
|
706
|
+
function ProcessGameConfig(game) {
|
|
707
|
+
// The Game() function has already been called on this
|
|
708
|
+
// config object, so just pass it through.
|
|
709
|
+
if (IsProcessed(game)) {
|
|
710
|
+
return game;
|
|
711
|
+
}
|
|
712
|
+
if (game.name === undefined)
|
|
713
|
+
game.name = 'default';
|
|
714
|
+
if (game.deltaState === undefined)
|
|
715
|
+
game.deltaState = false;
|
|
716
|
+
if (game.disableUndo === undefined)
|
|
717
|
+
game.disableUndo = false;
|
|
718
|
+
if (game.setup === undefined)
|
|
719
|
+
game.setup = () => ({});
|
|
720
|
+
if (game.moves === undefined)
|
|
721
|
+
game.moves = {};
|
|
722
|
+
if (game.playerView === undefined)
|
|
723
|
+
game.playerView = ({ G }) => G;
|
|
724
|
+
if (game.plugins === undefined)
|
|
725
|
+
game.plugins = [];
|
|
726
|
+
game.plugins.forEach((plugin) => {
|
|
727
|
+
if (plugin.name === undefined) {
|
|
728
|
+
throw new Error('Plugin missing name attribute');
|
|
729
|
+
}
|
|
730
|
+
if (plugin.name.includes(' ')) {
|
|
731
|
+
throw new Error(plugin.name + ': Plugin name must not include spaces');
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
if (game.name.includes(' ')) {
|
|
735
|
+
throw new Error(game.name + ': Game name must not include spaces');
|
|
736
|
+
}
|
|
737
|
+
const flow = Flow(game);
|
|
738
|
+
return {
|
|
739
|
+
...game,
|
|
740
|
+
flow,
|
|
741
|
+
moveNames: flow.moveNames,
|
|
742
|
+
pluginNames: game.plugins.map((p) => p.name),
|
|
743
|
+
processMove: (state, action) => {
|
|
744
|
+
let moveFn = flow.getMove(state.ctx, action.type, action.playerID);
|
|
745
|
+
if (IsLongFormMove(moveFn)) {
|
|
746
|
+
moveFn = moveFn.move;
|
|
747
|
+
}
|
|
748
|
+
if (moveFn instanceof Function) {
|
|
749
|
+
const fn = turnOrder.FnWrap(moveFn, turnOrder.GameMethod.MOVE, game.plugins);
|
|
750
|
+
let args = [];
|
|
751
|
+
if (action.args !== undefined) {
|
|
752
|
+
args = Array.isArray(action.args) ? action.args : [action.args];
|
|
753
|
+
}
|
|
754
|
+
const context = {
|
|
755
|
+
...turnOrder.GetAPIs(state),
|
|
756
|
+
G: state.G,
|
|
757
|
+
ctx: state.ctx,
|
|
758
|
+
playerID: action.playerID,
|
|
759
|
+
};
|
|
760
|
+
return fn(context, ...args);
|
|
761
|
+
}
|
|
762
|
+
turnOrder.error(`invalid move object: ${action.type}`);
|
|
763
|
+
return state.G;
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function IsLongFormMove(move) {
|
|
768
|
+
return move instanceof Object && move.move !== undefined;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/*
|
|
772
|
+
* Copyright 2017 The boardgame.io Authors
|
|
773
|
+
*
|
|
774
|
+
* Use of this source code is governed by a MIT-style
|
|
775
|
+
* license that can be found in the LICENSE file or at
|
|
776
|
+
* https://opensource.org/licenses/MIT.
|
|
777
|
+
*/
|
|
778
|
+
var UpdateErrorType;
|
|
779
|
+
(function (UpdateErrorType) {
|
|
780
|
+
// The action’s credentials were missing or invalid
|
|
781
|
+
UpdateErrorType["UnauthorizedAction"] = "update/unauthorized_action";
|
|
782
|
+
// The action’s matchID was not found
|
|
783
|
+
UpdateErrorType["MatchNotFound"] = "update/match_not_found";
|
|
784
|
+
// Could not apply Patch operation (rfc6902).
|
|
785
|
+
UpdateErrorType["PatchFailed"] = "update/patch_failed";
|
|
786
|
+
})(UpdateErrorType || (UpdateErrorType = {}));
|
|
787
|
+
var ActionErrorType;
|
|
788
|
+
(function (ActionErrorType) {
|
|
789
|
+
// The action contained a stale state ID
|
|
790
|
+
ActionErrorType["StaleStateId"] = "action/stale_state_id";
|
|
791
|
+
// The requested move is unknown or not currently available
|
|
792
|
+
ActionErrorType["UnavailableMove"] = "action/unavailable_move";
|
|
793
|
+
// The move declared it was invalid (INVALID_MOVE constant)
|
|
794
|
+
ActionErrorType["InvalidMove"] = "action/invalid_move";
|
|
795
|
+
// The player making the action is not currently active
|
|
796
|
+
ActionErrorType["InactivePlayer"] = "action/inactive_player";
|
|
797
|
+
// The game has finished
|
|
798
|
+
ActionErrorType["GameOver"] = "action/gameover";
|
|
799
|
+
// The requested action is disabled (e.g. undo/redo, events)
|
|
800
|
+
ActionErrorType["ActionDisabled"] = "action/action_disabled";
|
|
801
|
+
// The requested action is not currently possible
|
|
802
|
+
ActionErrorType["ActionInvalid"] = "action/action_invalid";
|
|
803
|
+
// The requested action was declared invalid by a plugin
|
|
804
|
+
ActionErrorType["PluginActionInvalid"] = "action/plugin_invalid";
|
|
805
|
+
})(ActionErrorType || (ActionErrorType = {}));
|
|
806
|
+
|
|
807
|
+
/*
|
|
808
|
+
* Copyright 2017 The boardgame.io Authors
|
|
809
|
+
*
|
|
810
|
+
* Use of this source code is governed by a MIT-style
|
|
811
|
+
* license that can be found in the LICENSE file or at
|
|
812
|
+
* https://opensource.org/licenses/MIT.
|
|
813
|
+
*/
|
|
814
|
+
/**
|
|
815
|
+
* Check if the payload for the passed action contains a playerID.
|
|
816
|
+
*/
|
|
817
|
+
const actionHasPlayerID = (action) => action.payload.playerID !== null && action.payload.playerID !== undefined;
|
|
818
|
+
/**
|
|
819
|
+
* Returns true if a move can be undone.
|
|
820
|
+
*/
|
|
821
|
+
const CanUndoMove = (G, ctx, move) => {
|
|
822
|
+
function HasUndoable(move) {
|
|
823
|
+
return move.undoable !== undefined;
|
|
824
|
+
}
|
|
825
|
+
function IsFunction(undoable) {
|
|
826
|
+
return undoable instanceof Function;
|
|
827
|
+
}
|
|
828
|
+
if (!HasUndoable(move)) {
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
if (IsFunction(move.undoable)) {
|
|
832
|
+
return move.undoable({ G, ctx });
|
|
833
|
+
}
|
|
834
|
+
return move.undoable;
|
|
835
|
+
};
|
|
836
|
+
/**
|
|
837
|
+
* Update the undo and redo stacks for a move or event.
|
|
838
|
+
*/
|
|
839
|
+
function updateUndoRedoState(state, opts) {
|
|
840
|
+
if (opts.game.disableUndo)
|
|
841
|
+
return state;
|
|
842
|
+
const undoEntry = {
|
|
843
|
+
G: state.G,
|
|
844
|
+
ctx: state.ctx,
|
|
845
|
+
plugins: state.plugins,
|
|
846
|
+
playerID: opts.action.payload.playerID || state.ctx.currentPlayer,
|
|
847
|
+
};
|
|
848
|
+
if (opts.action.type === 'MAKE_MOVE') {
|
|
849
|
+
undoEntry.moveType = opts.action.payload.type;
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
...state,
|
|
853
|
+
_undo: [...state._undo, undoEntry],
|
|
854
|
+
// Always reset redo stack when making a move or event
|
|
855
|
+
_redo: [],
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Process state, adding the initial deltalog for this action.
|
|
860
|
+
*/
|
|
861
|
+
function initializeDeltalog(state, action, move) {
|
|
862
|
+
// Create a log entry for this action.
|
|
863
|
+
const logEntry = {
|
|
864
|
+
action,
|
|
865
|
+
_stateID: state._stateID,
|
|
866
|
+
turn: state.ctx.turn,
|
|
867
|
+
phase: state.ctx.phase,
|
|
868
|
+
};
|
|
869
|
+
const pluginLogMetadata = state.plugins.log.data.metadata;
|
|
870
|
+
if (pluginLogMetadata !== undefined) {
|
|
871
|
+
logEntry.metadata = pluginLogMetadata;
|
|
872
|
+
}
|
|
873
|
+
if (typeof move === 'object' && move.redact === true) {
|
|
874
|
+
logEntry.redact = true;
|
|
875
|
+
}
|
|
876
|
+
else if (typeof move === 'object' && move.redact instanceof Function) {
|
|
877
|
+
logEntry.redact = move.redact({ G: state.G, ctx: state.ctx });
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
...state,
|
|
881
|
+
deltalog: [logEntry],
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Update plugin state after move/event & check if plugins consider the action to be valid.
|
|
886
|
+
* @param state Current version of state in the reducer.
|
|
887
|
+
* @param oldState State to revert to in case of error.
|
|
888
|
+
* @param pluginOpts Plugin configuration options.
|
|
889
|
+
* @returns Tuple of the new state updated after flushing plugins and the old
|
|
890
|
+
* state augmented with an error if a plugin declared the action invalid.
|
|
891
|
+
*/
|
|
892
|
+
function flushAndValidatePlugins(state, oldState, pluginOpts) {
|
|
893
|
+
const [newState, isInvalid] = turnOrder.FlushAndValidate(state, pluginOpts);
|
|
894
|
+
if (!isInvalid)
|
|
895
|
+
return [newState];
|
|
896
|
+
return [
|
|
897
|
+
newState,
|
|
898
|
+
WithError(oldState, ActionErrorType.PluginActionInvalid, isInvalid),
|
|
899
|
+
];
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* ExtractTransientsFromState
|
|
903
|
+
*
|
|
904
|
+
* Split out transients from the a TransientState
|
|
905
|
+
*/
|
|
906
|
+
function ExtractTransients(transientState) {
|
|
907
|
+
if (!transientState) {
|
|
908
|
+
// We preserve null for the state for legacy callers, but the transient
|
|
909
|
+
// field should be undefined if not present to be consistent with the
|
|
910
|
+
// code path below.
|
|
911
|
+
return [null, undefined];
|
|
912
|
+
}
|
|
913
|
+
const { transients, ...state } = transientState;
|
|
914
|
+
return [state, transients];
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* WithError
|
|
918
|
+
*
|
|
919
|
+
* Augment a State instance with transient error information.
|
|
920
|
+
*/
|
|
921
|
+
function WithError(state, errorType, payload) {
|
|
922
|
+
const error = {
|
|
923
|
+
type: errorType,
|
|
924
|
+
payload,
|
|
925
|
+
};
|
|
926
|
+
return {
|
|
927
|
+
...state,
|
|
928
|
+
transients: {
|
|
929
|
+
error,
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Middleware for processing TransientState associated with the reducer
|
|
935
|
+
* returned by CreateGameReducer.
|
|
936
|
+
* This should pretty much be used everywhere you want realistic state
|
|
937
|
+
* transitions and error handling.
|
|
938
|
+
*/
|
|
939
|
+
const TransientHandlingMiddleware = (store) => (next) => (action) => {
|
|
940
|
+
const result = next(action);
|
|
941
|
+
switch (action.type) {
|
|
942
|
+
case turnOrder.STRIP_TRANSIENTS: {
|
|
943
|
+
return result;
|
|
944
|
+
}
|
|
945
|
+
default: {
|
|
946
|
+
const [, transients] = ExtractTransients(store.getState());
|
|
947
|
+
if (typeof transients !== 'undefined') {
|
|
948
|
+
store.dispatch(turnOrder.stripTransients());
|
|
949
|
+
// Dev Note: If parent middleware needs to correlate the spawned
|
|
950
|
+
// StripTransients action to the triggering action, instrument here.
|
|
951
|
+
//
|
|
952
|
+
// This is a bit tricky; for more details, see:
|
|
953
|
+
// https://github.com/boardgameio/boardgame.io/pull/940#discussion_r636200648
|
|
954
|
+
return {
|
|
955
|
+
...result,
|
|
956
|
+
transients,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
return result;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
/**
|
|
964
|
+
* CreateGameReducer
|
|
965
|
+
*
|
|
966
|
+
* Creates the main game state reducer.
|
|
967
|
+
*/
|
|
968
|
+
function CreateGameReducer({ game, isClient, }) {
|
|
969
|
+
game = ProcessGameConfig(game);
|
|
970
|
+
/**
|
|
971
|
+
* GameReducer
|
|
972
|
+
*
|
|
973
|
+
* Redux reducer that maintains the overall game state.
|
|
974
|
+
* @param {object} state - The state before the action.
|
|
975
|
+
* @param {object} action - A Redux action.
|
|
976
|
+
*/
|
|
977
|
+
return (stateWithTransients = null, action) => {
|
|
978
|
+
let [state /*, transients */] = ExtractTransients(stateWithTransients);
|
|
979
|
+
switch (action.type) {
|
|
980
|
+
case turnOrder.STRIP_TRANSIENTS: {
|
|
981
|
+
// This action indicates that transient metadata in the state has been
|
|
982
|
+
// consumed and should now be stripped from the state..
|
|
983
|
+
return state;
|
|
984
|
+
}
|
|
985
|
+
case turnOrder.GAME_EVENT: {
|
|
986
|
+
state = { ...state, deltalog: [] };
|
|
987
|
+
// Process game events only on the server.
|
|
988
|
+
// These events like `endTurn` typically
|
|
989
|
+
// contain code that may rely on secret state
|
|
990
|
+
// and cannot be computed on the client.
|
|
991
|
+
if (isClient) {
|
|
992
|
+
return state;
|
|
993
|
+
}
|
|
994
|
+
// Disallow events once the game is over.
|
|
995
|
+
if (state.ctx.gameover !== undefined) {
|
|
996
|
+
turnOrder.error(`cannot call event after game end`);
|
|
997
|
+
return WithError(state, ActionErrorType.GameOver);
|
|
998
|
+
}
|
|
999
|
+
// Ignore the event if the player isn't active.
|
|
1000
|
+
if (actionHasPlayerID(action) &&
|
|
1001
|
+
!game.flow.isPlayerActive(state.G, state.ctx, action.payload.playerID)) {
|
|
1002
|
+
turnOrder.error(`disallowed event: ${action.payload.type}`);
|
|
1003
|
+
return WithError(state, ActionErrorType.InactivePlayer);
|
|
1004
|
+
}
|
|
1005
|
+
// Execute plugins.
|
|
1006
|
+
state = turnOrder.Enhance(state, {
|
|
1007
|
+
game,
|
|
1008
|
+
isClient: false,
|
|
1009
|
+
playerID: action.payload.playerID,
|
|
1010
|
+
});
|
|
1011
|
+
// Process event.
|
|
1012
|
+
let newState = game.flow.processEvent(state, action);
|
|
1013
|
+
// Execute plugins.
|
|
1014
|
+
let stateWithError;
|
|
1015
|
+
[newState, stateWithError] = flushAndValidatePlugins(newState, state, {
|
|
1016
|
+
game,
|
|
1017
|
+
isClient: false,
|
|
1018
|
+
});
|
|
1019
|
+
if (stateWithError)
|
|
1020
|
+
return stateWithError;
|
|
1021
|
+
// Update undo / redo state.
|
|
1022
|
+
newState = updateUndoRedoState(newState, { game, action });
|
|
1023
|
+
return { ...newState, _stateID: state._stateID + 1 };
|
|
1024
|
+
}
|
|
1025
|
+
case turnOrder.MAKE_MOVE: {
|
|
1026
|
+
const oldState = (state = { ...state, deltalog: [] });
|
|
1027
|
+
// Check whether the move is allowed at this time.
|
|
1028
|
+
const move = game.flow.getMove(state.ctx, action.payload.type, action.payload.playerID || state.ctx.currentPlayer);
|
|
1029
|
+
if (move === null) {
|
|
1030
|
+
turnOrder.error(`disallowed move: ${action.payload.type}`);
|
|
1031
|
+
return WithError(state, ActionErrorType.UnavailableMove);
|
|
1032
|
+
}
|
|
1033
|
+
// Don't run move on client if move says so.
|
|
1034
|
+
if (isClient && move.client === false) {
|
|
1035
|
+
return state;
|
|
1036
|
+
}
|
|
1037
|
+
// Disallow moves once the game is over.
|
|
1038
|
+
if (state.ctx.gameover !== undefined) {
|
|
1039
|
+
turnOrder.error(`cannot make move after game end`);
|
|
1040
|
+
return WithError(state, ActionErrorType.GameOver);
|
|
1041
|
+
}
|
|
1042
|
+
// Ignore the move if the player isn't active.
|
|
1043
|
+
if (actionHasPlayerID(action) &&
|
|
1044
|
+
!game.flow.isPlayerActive(state.G, state.ctx, action.payload.playerID)) {
|
|
1045
|
+
turnOrder.error(`disallowed move: ${action.payload.type}`);
|
|
1046
|
+
return WithError(state, ActionErrorType.InactivePlayer);
|
|
1047
|
+
}
|
|
1048
|
+
// Execute plugins.
|
|
1049
|
+
state = turnOrder.Enhance(state, {
|
|
1050
|
+
game,
|
|
1051
|
+
isClient,
|
|
1052
|
+
playerID: action.payload.playerID,
|
|
1053
|
+
});
|
|
1054
|
+
// Process the move.
|
|
1055
|
+
const G = game.processMove(state, action.payload);
|
|
1056
|
+
// The game declared the move as invalid.
|
|
1057
|
+
if (G === turnOrder.INVALID_MOVE) {
|
|
1058
|
+
turnOrder.error(`invalid move: ${action.payload.type} args: ${action.payload.args}`);
|
|
1059
|
+
// TODO(#723): Marshal a nice error payload with the processed move.
|
|
1060
|
+
return WithError(state, ActionErrorType.InvalidMove);
|
|
1061
|
+
}
|
|
1062
|
+
const newState = { ...state, G };
|
|
1063
|
+
// Some plugin indicated that it is not suitable to be
|
|
1064
|
+
// materialized on the client (and must wait for the server
|
|
1065
|
+
// response instead).
|
|
1066
|
+
if (isClient && turnOrder.NoClient(newState, { game })) {
|
|
1067
|
+
return state;
|
|
1068
|
+
}
|
|
1069
|
+
state = newState;
|
|
1070
|
+
// If we're on the client, just process the move
|
|
1071
|
+
// and no triggers in multiplayer mode.
|
|
1072
|
+
// These will be processed on the server, which
|
|
1073
|
+
// will send back a state update.
|
|
1074
|
+
if (isClient) {
|
|
1075
|
+
let stateWithError;
|
|
1076
|
+
[state, stateWithError] = flushAndValidatePlugins(state, oldState, {
|
|
1077
|
+
game,
|
|
1078
|
+
isClient: true,
|
|
1079
|
+
});
|
|
1080
|
+
if (stateWithError)
|
|
1081
|
+
return stateWithError;
|
|
1082
|
+
return {
|
|
1083
|
+
...state,
|
|
1084
|
+
_stateID: state._stateID + 1,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
// On the server, construct the deltalog.
|
|
1088
|
+
state = initializeDeltalog(state, action, move);
|
|
1089
|
+
// Allow the flow reducer to process any triggers that happen after moves.
|
|
1090
|
+
state = game.flow.processMove(state, action.payload);
|
|
1091
|
+
let stateWithError;
|
|
1092
|
+
[state, stateWithError] = flushAndValidatePlugins(state, oldState, {
|
|
1093
|
+
game,
|
|
1094
|
+
});
|
|
1095
|
+
if (stateWithError)
|
|
1096
|
+
return stateWithError;
|
|
1097
|
+
// Update undo / redo state.
|
|
1098
|
+
state = updateUndoRedoState(state, { game, action });
|
|
1099
|
+
return {
|
|
1100
|
+
...state,
|
|
1101
|
+
_stateID: state._stateID + 1,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
case turnOrder.RESET:
|
|
1105
|
+
case turnOrder.UPDATE:
|
|
1106
|
+
case turnOrder.SYNC: {
|
|
1107
|
+
return action.state;
|
|
1108
|
+
}
|
|
1109
|
+
case turnOrder.UNDO: {
|
|
1110
|
+
state = { ...state, deltalog: [] };
|
|
1111
|
+
if (game.disableUndo) {
|
|
1112
|
+
turnOrder.error('Undo is not enabled');
|
|
1113
|
+
return WithError(state, ActionErrorType.ActionDisabled);
|
|
1114
|
+
}
|
|
1115
|
+
const { G, ctx, _undo, _redo, _stateID } = state;
|
|
1116
|
+
if (_undo.length < 2) {
|
|
1117
|
+
turnOrder.error(`No moves to undo`);
|
|
1118
|
+
return WithError(state, ActionErrorType.ActionInvalid);
|
|
1119
|
+
}
|
|
1120
|
+
const last = _undo[_undo.length - 1];
|
|
1121
|
+
const restore = _undo[_undo.length - 2];
|
|
1122
|
+
// Only allow players to undo their own moves.
|
|
1123
|
+
if (actionHasPlayerID(action) &&
|
|
1124
|
+
action.payload.playerID !== last.playerID) {
|
|
1125
|
+
turnOrder.error(`Cannot undo other players' moves`);
|
|
1126
|
+
return WithError(state, ActionErrorType.ActionInvalid);
|
|
1127
|
+
}
|
|
1128
|
+
// If undoing a move, check it is undoable.
|
|
1129
|
+
if (last.moveType) {
|
|
1130
|
+
const lastMove = game.flow.getMove(restore.ctx, last.moveType, last.playerID);
|
|
1131
|
+
if (!CanUndoMove(G, ctx, lastMove)) {
|
|
1132
|
+
turnOrder.error(`Move cannot be undone`);
|
|
1133
|
+
return WithError(state, ActionErrorType.ActionInvalid);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
state = initializeDeltalog(state, action);
|
|
1137
|
+
return {
|
|
1138
|
+
...state,
|
|
1139
|
+
G: restore.G,
|
|
1140
|
+
ctx: restore.ctx,
|
|
1141
|
+
plugins: restore.plugins,
|
|
1142
|
+
_stateID: _stateID + 1,
|
|
1143
|
+
_undo: _undo.slice(0, -1),
|
|
1144
|
+
_redo: [last, ..._redo],
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
case turnOrder.REDO: {
|
|
1148
|
+
state = { ...state, deltalog: [] };
|
|
1149
|
+
if (game.disableUndo) {
|
|
1150
|
+
turnOrder.error('Redo is not enabled');
|
|
1151
|
+
return WithError(state, ActionErrorType.ActionDisabled);
|
|
1152
|
+
}
|
|
1153
|
+
const { _undo, _redo, _stateID } = state;
|
|
1154
|
+
if (_redo.length === 0) {
|
|
1155
|
+
turnOrder.error(`No moves to redo`);
|
|
1156
|
+
return WithError(state, ActionErrorType.ActionInvalid);
|
|
1157
|
+
}
|
|
1158
|
+
const first = _redo[0];
|
|
1159
|
+
// Only allow players to redo their own undos.
|
|
1160
|
+
if (actionHasPlayerID(action) &&
|
|
1161
|
+
action.payload.playerID !== first.playerID) {
|
|
1162
|
+
turnOrder.error(`Cannot redo other players' moves`);
|
|
1163
|
+
return WithError(state, ActionErrorType.ActionInvalid);
|
|
1164
|
+
}
|
|
1165
|
+
state = initializeDeltalog(state, action);
|
|
1166
|
+
return {
|
|
1167
|
+
...state,
|
|
1168
|
+
G: first.G,
|
|
1169
|
+
ctx: first.ctx,
|
|
1170
|
+
plugins: first.plugins,
|
|
1171
|
+
_stateID: _stateID + 1,
|
|
1172
|
+
_undo: [..._undo, first],
|
|
1173
|
+
_redo: _redo.slice(1),
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
case turnOrder.PLUGIN: {
|
|
1177
|
+
// TODO(#723): Expose error semantics to plugin processing.
|
|
1178
|
+
return turnOrder.ProcessAction(state, action, { game });
|
|
1179
|
+
}
|
|
1180
|
+
case turnOrder.PATCH: {
|
|
1181
|
+
const oldState = state;
|
|
1182
|
+
const newState = JSON.parse(JSON.stringify(oldState));
|
|
1183
|
+
const patchError = rfc6902.applyPatch(newState, action.patch);
|
|
1184
|
+
const hasError = patchError.some((entry) => entry !== null);
|
|
1185
|
+
if (hasError) {
|
|
1186
|
+
turnOrder.error(`Patch ${JSON.stringify(action.patch)} apply failed`);
|
|
1187
|
+
return WithError(oldState, UpdateErrorType.PatchFailed, patchError);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
return newState;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
default: {
|
|
1194
|
+
return state;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
exports.CreateGameReducer = CreateGameReducer;
|
|
1201
|
+
exports.IsLongFormMove = IsLongFormMove;
|
|
1202
|
+
exports.ProcessGameConfig = ProcessGameConfig;
|
|
1203
|
+
exports.TransientHandlingMiddleware = TransientHandlingMiddleware;
|